├── docs ├── .nojekyll ├── CNAME ├── assets │ └── dioxus │ │ ├── Chitchai_bg.wasm │ │ └── snippets │ │ ├── dioxus-web-54817add8ba334eb │ │ ├── inline0.js │ │ └── src │ │ │ └── eval.js │ │ └── dioxus-interpreter-js-603636eeca72cf05 │ │ ├── src │ │ └── common.js │ │ └── inline0.js ├── 404.html ├── index.html └── tailwind.css ├── src ├── prompt_engineer │ ├── mod.rs │ └── prompt_templates.rs ├── tailwind_input.css ├── components │ ├── left_sidebar │ │ ├── agent_profiles.rs │ │ ├── chat_history.rs │ │ └── icons.rs │ ├── mod.rs │ ├── chat │ │ ├── message_card.rs │ │ └── request_utils.rs │ ├── left_sidebar.rs │ ├── chat.rs │ └── setting_sidebar.rs ├── lib.rs ├── pages │ ├── mod.rs │ ├── announcements.rs │ ├── announcements.toml │ ├── app.rs │ └── agents.rs ├── utils │ ├── customization.rs │ ├── datetime.rs │ ├── storage.rs │ ├── settings.rs │ ├── auth.rs │ └── storage │ │ ├── conversion.rs │ │ └── schema.rs ├── main.rs ├── utils.rs ├── agents.rs └── chat.rs ├── .gitignore ├── images ├── bright_mode.png └── dark_mode.png ├── package.json ├── tailwind.config.js ├── default_assistants.toml ├── Dioxus.toml ├── Cargo.toml ├── README.md └── LICENSE /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | chitchai.reify.ing 2 | -------------------------------------------------------------------------------- /src/prompt_engineer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod prompt_templates; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | /node_modules 4 | Cargo.lock 5 | /dist -------------------------------------------------------------------------------- /src/tailwind_input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /images/bright_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifsheldon/chitchai/HEAD/images/bright_mode.png -------------------------------------------------------------------------------- /images/dark_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifsheldon/chitchai/HEAD/images/dark_mode.png -------------------------------------------------------------------------------- /docs/assets/dioxus/Chitchai_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifsheldon/chitchai/HEAD/docs/assets/dioxus/Chitchai_bg.wasm -------------------------------------------------------------------------------- /src/prompt_engineer/prompt_templates.rs: -------------------------------------------------------------------------------- 1 | pub const ASSISTANT_SYS_PROMPT_TEMPLATE: &str = r#"{{instructions}} 2 | {{name_instructions}} 3 | "#; -------------------------------------------------------------------------------- /src/components/left_sidebar/agent_profiles.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | pub fn AgentProfiles(cx: Scope) -> Element { 4 | todo!() 5 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | pub mod agents; 4 | pub mod components; 5 | pub mod pages; 6 | pub mod prompt_engineer; 7 | pub mod chat; 8 | pub mod utils; 9 | -------------------------------------------------------------------------------- /src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | pub use app::{App, Main}; 2 | pub use announcements::AnnouncementPage; 3 | pub use agents::Agents; 4 | 5 | pub mod app; 6 | pub mod announcements; 7 | pub mod agents; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@tailwindcss/typography": "^0.5.10", 4 | "tailwindcss": "^3.3.5" 5 | }, 6 | "dependencies": { 7 | "tailwind-scrollbar": "^3.0.5" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Components 2 | //! 3 | //! ## Reference: 4 | //! 1. https://www.langui.dev/components/prompt-containers#component-2 5 | //! 6 | 7 | pub use chat::*; 8 | pub use left_sidebar::*; 9 | pub use setting_sidebar::*; 10 | 11 | mod chat; 12 | mod setting_sidebar; 13 | mod left_sidebar; -------------------------------------------------------------------------------- /docs/assets/dioxus/snippets/dioxus-web-54817add8ba334eb/inline0.js: -------------------------------------------------------------------------------- 1 | 2 | export function get_form_data(form) { 3 | let values = new Map(); 4 | const formData = new FormData(form); 5 | 6 | for (let name of formData.keys()) { 7 | values.set(name, formData.getAll(name)); 8 | } 9 | 10 | return values; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/customization.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Debug, Clone, PartialEq, Serialize)] 4 | pub struct Customization { 5 | pub waiting_icons: Vec, 6 | } 7 | 8 | impl Default for Customization { 9 | fn default() -> Self { 10 | Self { 11 | waiting_icons: vec![".".to_string(), "..".to_string(), "...".to_string()] 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | mode: "all", 4 | content: [ 5 | // include all rust, html and css files in the src directory 6 | "./src/**/*.{rs,html,css}", 7 | // include all html files in the output (dist) directory 8 | "./dist/**/*.html", 9 | ], 10 | theme: { 11 | extend: {}, 12 | }, 13 | plugins: [ 14 | require('tailwind-scrollbar'), 15 | require('@tailwindcss/typography'), 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chitchai 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chitchai 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /default_assistants.toml: -------------------------------------------------------------------------------- 1 | [[agent_config]] 2 | name = "Alice" 3 | instructions = """You are a helpful assistant who specialized in programming. 4 | You are going to work with Bob, another assistant. 5 | You will receive requests from a user. If you think it's not your turn to reply or it's not your expertise, you can skip the request by replying `[NONE]`, which is totally fine.""" 6 | 7 | [[agent_config]] 8 | name = "Bob" 9 | instructions = """You are a helpful assistant who specialized in design. 10 | You are going to work with Alice, another assistant. 11 | You will receive requests from a user. If you think it's not your turn to reply or it's not your expertise, you can skip the request by replying `[NONE]`, which is totally fine.""" -------------------------------------------------------------------------------- /src/utils/datetime.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Local}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 5 | pub struct DatetimeString(pub String); 6 | 7 | impl DatetimeString { 8 | pub fn get_now() -> Self { 9 | Self(Local::now().to_rfc3339()) 10 | } 11 | } 12 | 13 | impl From> for DatetimeString { 14 | fn from(dt: DateTime) -> Self { 15 | Self(format!("{}", dt.to_rfc3339())) 16 | } 17 | } 18 | 19 | impl From for DatetimeString { 20 | fn from(s: String) -> Self { 21 | let parsed: DateTime = DateTime::parse_from_rfc3339(&s).unwrap().into(); 22 | Self(parsed.to_rfc3339()) 23 | } 24 | } -------------------------------------------------------------------------------- /Dioxus.toml: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | # App (Project) Name 4 | name = "Chitchai" 5 | 6 | # Dioxus App Default Platform 7 | # desktop, web, mobile, ssr 8 | default_platform = "web" 9 | 10 | # `build` & `serve` dist path 11 | out_dir = "dist" 12 | 13 | # resource (public) file folder 14 | asset_dir = "assets" 15 | 16 | [web.app] 17 | 18 | # HTML title tag content 19 | title = "Chitchai" 20 | 21 | [web.watcher] 22 | 23 | # when watcher trigger, regenerate the `index.html` 24 | reload_html = true 25 | 26 | # which files or dirs will be watcher monitoring 27 | watch_path = ["src", "assets", "default_assistants.toml"] 28 | 29 | # uncomment line below if using Router 30 | index_on_404 = true 31 | 32 | # include `assets` in web platform 33 | [web.resource] 34 | 35 | # CSS style file 36 | style = ["tailwind.css"] # tailwind.css that will be generated 37 | 38 | # Javascript code file 39 | script = [] 40 | 41 | [web.resource.dev] 42 | 43 | # serve: [dev-server] only 44 | 45 | # CSS style file 46 | style = [] 47 | 48 | # Javascript code file 49 | script = [] -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use dioxus::prelude::*; 4 | use dioxus_router::prelude::*; 5 | use log::Level; 6 | 7 | use chitchai::pages::*; 8 | 9 | #[derive(Routable, Clone)] 10 | enum Route { 11 | #[route("/")] 12 | Main {}, 13 | #[route("/announcements")] 14 | AnnouncementPage {}, 15 | #[route("/agents")] 16 | Agents {}, 17 | #[route("/:..route")] 18 | PageNotFound { route: Vec }, 19 | } 20 | 21 | 22 | #[inline_props] 23 | fn PageNotFound(cx: Scope, route: Vec) -> Element { 24 | render! { 25 | h1 { "Page not found" } 26 | p { "We are terribly sorry, but the page you requested doesn't exist." } 27 | pre { 28 | color: "red", 29 | "log:\nattemped to navigate to: {route:?}" 30 | } 31 | } 32 | } 33 | 34 | 35 | fn AppRouter(cx: Scope) -> Element { 36 | render! { 37 | Router:: {} 38 | } 39 | } 40 | 41 | fn main() { 42 | console_log::init_with_level(Level::Debug).unwrap(); 43 | dioxus_web::launch(AppRouter); 44 | } 45 | -------------------------------------------------------------------------------- /docs/assets/dioxus/snippets/dioxus-web-54817add8ba334eb/src/eval.js: -------------------------------------------------------------------------------- 1 | export class Dioxus { 2 | constructor(sendCallback, returnCallback) { 3 | this.sendCallback = sendCallback; 4 | this.returnCallback = returnCallback; 5 | this.promiseResolve = null; 6 | this.received = []; 7 | } 8 | 9 | // Receive message from Rust 10 | recv() { 11 | return new Promise((resolve, _reject) => { 12 | // If data already exists, resolve immediately 13 | let data = this.received.shift(); 14 | if (data) { 15 | resolve(data); 16 | return; 17 | } 18 | 19 | // Otherwise set a resolve callback 20 | this.promiseResolve = resolve; 21 | }); 22 | } 23 | 24 | // Send message to rust. 25 | send(data) { 26 | this.sendCallback(data); 27 | } 28 | 29 | // Internal rust send 30 | rustSend(data) { 31 | // If a promise is waiting for data, resolve it, and clear the resolve callback 32 | if (this.promiseResolve) { 33 | this.promiseResolve(data); 34 | this.promiseResolve = null; 35 | return; 36 | } 37 | 38 | // Otherwise add the data to a queue 39 | this.received.push(data); 40 | } 41 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chitchai" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | dioxus = "~0.4" 10 | dioxus-web = "~0.4" 11 | serde = { version = "1.0", features = ["derive"] } 12 | serde_json = "1.0" 13 | transprompt = { git = "https://github.com/ifsheldon/transprompt.git", version = "0.11", default-features = false, features = ["wasm"] } 14 | console_log = "1.0" 15 | log = "0.4" 16 | gloo-storage = "0.3" 17 | futures = "0.3" 18 | futures-util = "0.3" 19 | async-std = "1.12" 20 | chrono = { version = "0.4", features = ["serde", "unstable-locales"] } 21 | readonly = "0.2" 22 | dioxus-router = "0.4" 23 | toml = "0.8" 24 | dioxus_markdown = { git = "https://github.com/DioxusLabs/markdown.git", version = "0.2" } 25 | 26 | [dependencies.uuid] 27 | version = "1.5.0" 28 | features = [ 29 | "v4", # Lets you generate random UUIDs 30 | "fast-rng", # Use a faster (but still sufficiently random) RNG 31 | "serde", # Enables serialization/deserialization of UUIDs 32 | ] 33 | 34 | [profile.release] 35 | opt-level = "z" 36 | strip = true 37 | lto = true 38 | -------------------------------------------------------------------------------- /src/utils/storage.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub(crate) use schema::*; 6 | 7 | use crate::agents::{AgentConfig, AgentName}; 8 | use crate::chat::Chat; 9 | use crate::utils::auth::Auth; 10 | use crate::utils::customization::Customization; 11 | use crate::utils::settings::{GPTService, OpenAIModel}; 12 | 13 | pub(crate) mod schema; 14 | pub(crate) mod conversion; 15 | 16 | #[derive(Clone, Debug, PartialEq)] 17 | pub struct StoredStates { 18 | pub run_count: usize, 19 | pub customization: Customization, 20 | pub name_to_configs: HashMap, 21 | pub chats: Vec, 22 | pub auth: Option, 23 | pub selected_service: Option, 24 | pub openai_model: Option, 25 | } 26 | 27 | 28 | impl StoredStates { 29 | pub fn get_or_init() -> Self { 30 | RawStoredStates::get_or_init() 31 | } 32 | 33 | pub fn save(&self) { 34 | let saved_storage: RawStoredStates = self.clone().into(); 35 | saved_storage.save(); 36 | } 37 | } 38 | 39 | 40 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 41 | pub(crate) struct Announcements { 42 | pub(crate) announcement: Vec, 43 | } 44 | 45 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 46 | pub(crate) struct Announcement { 47 | pub(crate) title: String, 48 | pub(crate) date: String, 49 | pub(crate) author: String, 50 | pub(crate) content: String, 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/settings.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] 5 | pub enum GPTService { 6 | AzureOpenAI, 7 | OpenAI, 8 | } 9 | 10 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 11 | pub enum OpenAIModel { 12 | GPT35, 13 | GPT35_16k, 14 | GPT4, 15 | GPT4_32k, 16 | } 17 | 18 | impl OpenAIModel { 19 | pub fn all_models() -> &'static [OpenAIModel] { 20 | &[ 21 | OpenAIModel::GPT35, 22 | OpenAIModel::GPT35_16k, 23 | OpenAIModel::GPT4, 24 | OpenAIModel::GPT4_32k, 25 | ] 26 | } 27 | 28 | pub fn gpt35_models() -> &'static [OpenAIModel] { 29 | &[ 30 | OpenAIModel::GPT35, 31 | OpenAIModel::GPT35_16k, 32 | ] 33 | } 34 | 35 | pub fn gpt4_models() -> &'static [OpenAIModel] { 36 | &[ 37 | OpenAIModel::GPT4, 38 | OpenAIModel::GPT4_32k, 39 | ] 40 | } 41 | } 42 | 43 | impl Display for OpenAIModel { 44 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 45 | write!(f, "{}", match self { 46 | OpenAIModel::GPT35 => "gpt-3.5-turbo", 47 | OpenAIModel::GPT35_16k => "gpt-3.5-turbo-16k", 48 | OpenAIModel::GPT4 => "gpt-4", 49 | OpenAIModel::GPT4_32k => "gpt-4-32k", 50 | }) 51 | } 52 | } 53 | 54 | impl PartialEq for OpenAIModel { 55 | fn eq(&self, other: &str) -> bool { 56 | let other = other.trim().to_lowercase(); 57 | match self { 58 | OpenAIModel::GPT35 => other == "gpt-3.5-turbo", 59 | OpenAIModel::GPT35_16k => other == "gpt-3.5-turbo-16k", 60 | OpenAIModel::GPT4 => other == "gpt-4", 61 | OpenAIModel::GPT4_32k => other == "gpt-4-32k", 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/pages/announcements.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | use crate::agents::AgentName::{AssistantDefault, Named}; 4 | use crate::components::MessageCard; 5 | use crate::utils::{assistant_msg, user_msg}; 6 | use crate::utils::storage::Announcements; 7 | 8 | const ANNOUNCEMENTS: &str = include_str!("announcements.toml"); 9 | 10 | pub fn AnnouncementPage(cx: Scope) -> Element { 11 | let Announcements { mut announcement } = toml::from_str(ANNOUNCEMENTS).unwrap(); 12 | announcement.sort_by(|a, b| b.date.cmp(&a.date)); 13 | let messages = announcement 14 | .into_iter() 15 | .map(|a| { 16 | let command = format!("Please help me write an announcement with title `{}`", a.title); 17 | let announcement = format!(r#"## {} 18 | 19 | ### By {} 20 | 21 | #### {} 22 | 23 | {} 24 | "#, a.title, a.author, a.date, a.content); 25 | let command_msg = user_msg(command, Named(a.author.clone())); 26 | let announcement_msg = assistant_msg(announcement, AssistantDefault); 27 | [command_msg, announcement_msg] 28 | }) 29 | .flatten() 30 | .collect::>(); 31 | 32 | render! { 33 | div { 34 | class: "flex h-screen w-screen flex-col relative", 35 | div { 36 | class: "flex flex-col h-full space-y-6 bg-slate-200 text-sm leading-6 text-slate-900 shadow-sm dark:bg-slate-900 dark:text-slate-300 sm:text-base sm:leading-7", 37 | div { 38 | class: "overflow-auto max-h-[100vh] flex-grow dark:scrollbar dark:scrollbar-thumb-slate-700 dark:scrollbar-track-slate-900", 39 | messages 40 | .into_iter() 41 | .map(|msg| rsx! { 42 | MessageCard { 43 | chat_msg: msg 44 | } 45 | }) 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /docs/assets/dioxus/snippets/dioxus-interpreter-js-603636eeca72cf05/src/common.js: -------------------------------------------------------------------------------- 1 | const bool_attrs = { 2 | allowfullscreen: true, 3 | allowpaymentrequest: true, 4 | async: true, 5 | autofocus: true, 6 | autoplay: true, 7 | checked: true, 8 | controls: true, 9 | default: true, 10 | defer: true, 11 | disabled: true, 12 | formnovalidate: true, 13 | hidden: true, 14 | ismap: true, 15 | itemscope: true, 16 | loop: true, 17 | multiple: true, 18 | muted: true, 19 | nomodule: true, 20 | novalidate: true, 21 | open: true, 22 | playsinline: true, 23 | readonly: true, 24 | required: true, 25 | reversed: true, 26 | selected: true, 27 | truespeed: true, 28 | webkitdirectory: true, 29 | }; 30 | 31 | export function setAttributeInner(node, field, value, ns) { 32 | const name = field; 33 | if (ns === "style") { 34 | // ????? why do we need to do this 35 | if (node.style === undefined) { 36 | node.style = {}; 37 | } 38 | node.style[name] = value; 39 | } else if (ns != null && ns != undefined) { 40 | node.setAttributeNS(ns, name, value); 41 | } else { 42 | switch (name) { 43 | case "value": 44 | if (value !== node.value) { 45 | node.value = value; 46 | } 47 | break; 48 | case "initial_value": 49 | node.defaultValue = value; 50 | break; 51 | case "checked": 52 | node.checked = truthy(value); 53 | break; 54 | case "selected": 55 | node.selected = truthy(value); 56 | break; 57 | case "dangerous_inner_html": 58 | node.innerHTML = value; 59 | break; 60 | default: 61 | // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 62 | if (!truthy(value) && bool_attrs.hasOwnProperty(name)) { 63 | node.removeAttribute(name); 64 | } else { 65 | node.setAttribute(name, value); 66 | } 67 | } 68 | } 69 | } 70 | 71 | function truthy(val) { 72 | return val === "true" || val === true; 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/auth.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use transprompt::async_openai::config::{AzureConfig, OpenAIConfig}; 3 | 4 | #[non_exhaustive] 5 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] 6 | pub enum Auth { 7 | OpenAI { 8 | api_key: String, 9 | #[serde(skip_serializing_if = "Option::is_none")] 10 | org_id: Option, 11 | #[serde(skip_serializing_if = "Option::is_none")] 12 | api_base: Option, 13 | }, 14 | AzureOpenAI { 15 | api_version: String, 16 | deployment_id: String, 17 | api_base: String, 18 | api_key: String, 19 | }, 20 | } 21 | 22 | 23 | impl Into for Auth { 24 | fn into(self) -> AzureConfig { 25 | match self { 26 | Auth::AzureOpenAI { 27 | api_version, 28 | deployment_id, 29 | api_base, 30 | api_key, 31 | } => AzureConfig::new() 32 | .with_api_version(api_version) 33 | .with_deployment_id(deployment_id) 34 | .with_api_base(api_base) 35 | .with_api_key(api_key), 36 | _ => panic!("Cannot convert Auth to AzureConfig, Got {:?}", self), 37 | } 38 | } 39 | } 40 | 41 | impl Into for Auth { 42 | fn into(self) -> OpenAIConfig { 43 | match self { 44 | Auth::OpenAI { 45 | api_key, 46 | org_id, 47 | api_base, 48 | } => { 49 | let config = OpenAIConfig::default().with_api_key(api_key); 50 | let config = if let Some(org_id) = org_id { 51 | config.with_org_id(org_id) 52 | } else { 53 | config 54 | }; 55 | if let Some(api_base) = api_base { 56 | config.with_api_base(api_base) 57 | } else { 58 | config 59 | } 60 | } 61 | _ => panic!("Cannot convert Auth to OpenAIConfig, Got {:?}", self), 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/pages/announcements.toml: -------------------------------------------------------------------------------- 1 | [[announcement]] 2 | title = "Announcing Chitchai 0.1" 3 | date = "2023-11-05" 4 | author = "Feng Liang" 5 | content = """ We are pleased to announce the release of Chitchai 聊斋 0.1. This is the first release of Chitchai. 6 | Chitchai is a chat room for chatting with **multiple** agents. It's like group chats of [character.ai](https://character.ai), but without subscription. You need subscriptions from GPT providers, though, OpenAI and Azure OpenAI supported for now. 7 | 8 | Features: 9 | 1. Chat with multiple agents. Demo on chitchai.dev where you can chat with 2 default agents, a designer and a programmer. 10 | 2. Customize your own agents. As for now, by building Chitchai from source code, you can customize your own agents. We will provide a web interface for this in 0.2. 11 | 3. Chitchai is completely local. We don't need your data. Your chat will only be sent to GPT providers you trust. 12 | 4. Chitchai is open and free for all forever. 13 | 14 | Dev features: 15 | 1. Chitchai is built with Rust and Dioxus. Almost Rust except CSS. 16 | 2. Chitchai is wasm-based. It can be run in browser, desktop and mobile. 17 | 3. You can use it as a crate of a collection of chat components. For now you have to get it from git repo, but we will publish it to crates.io soon (when wasm support is upstreamed to `async-openai`). 18 | 19 | > How does it work? 20 | > 21 | > Compared to AutoGen, Chitchai is super simple thanks to the intelligence of GPT-4. 22 | > Each agent in a chat room internally has its own chat history, in which only the initial system prompt is different. 23 | > Messages in agent histories are denoted with agent/user names, and then magically GPT-4 correctly knows who is who and who says what (just like us humans). 24 | > This ability is remarkable but we humans take it for granted. However, GPT-3.5 or any LLMs like it does not have this ability (yet?). 25 | 26 | > A bit of research? 27 | > 28 | > Can we do AutoGen just by using aformentioned ability of GPT-4? That's a good question. I don't know. But I think it's worth a try. 29 | 30 | 31 | Please check the [Chitchai repo](https://github.com/ifsheldon/chitchai) for more details! And we always welcome your feedbacks as issues and PRs. 32 | """ -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use transprompt::async_openai::types::{ChatCompletionRequestAssistantMessage, ChatCompletionRequestMessage, ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage, ChatCompletionRequestUserMessageContent}; 3 | use transprompt::utils::llm::openai::ChatMsg; 4 | 5 | use crate::agents::AgentName; 6 | 7 | pub mod customization; 8 | pub mod storage; 9 | pub mod auth; 10 | pub mod settings; 11 | pub mod datetime; 12 | 13 | pub(crate) const EMPTY: String = String::new(); 14 | 15 | pub fn sys_msg(string: impl Into) -> ChatMsg { 16 | ChatMsg { 17 | msg: ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage { 18 | content: string.into(), 19 | role: Default::default(), 20 | name: None, 21 | }), 22 | metadata: None, 23 | } 24 | } 25 | 26 | pub fn user_msg(string: impl Into, name: AgentName) -> ChatMsg { 27 | let name = match name { 28 | AgentName::Named(name) => Some(name), 29 | AgentName::UserDefault => None, 30 | AgentName::AssistantDefault => { 31 | log::error!("Cannot use AssistantDefault as user name"); 32 | panic!() 33 | } 34 | }; 35 | ChatMsg { 36 | msg: ChatCompletionRequestMessage::User( 37 | ChatCompletionRequestUserMessage { 38 | content: ChatCompletionRequestUserMessageContent::Text(string.into()), 39 | role: Default::default(), 40 | name 41 | } 42 | ), 43 | metadata: None, 44 | } 45 | } 46 | 47 | pub fn assistant_msg(string: impl Into, name: AgentName) -> ChatMsg { 48 | let name = match name { 49 | AgentName::Named(name) => Some(name), 50 | AgentName::AssistantDefault => None, 51 | AgentName::UserDefault => { 52 | log::error!("Cannot use UserDefault as assistant name"); 53 | panic!() 54 | } 55 | }; 56 | ChatMsg { 57 | msg: ChatCompletionRequestMessage::Assistant( 58 | ChatCompletionRequestAssistantMessage{ 59 | content: Some(string.into()), 60 | role: Default::default(), 61 | name, 62 | tool_calls: None, 63 | function_call: None, 64 | } 65 | ), 66 | metadata: None, 67 | } 68 | } 69 | 70 | #[derive(Clone, Debug, PartialEq, Deserialize)] 71 | pub struct AgentInstructions { 72 | pub name: String, 73 | pub instructions: String, 74 | } 75 | 76 | #[derive(Clone, Debug, PartialEq, Deserialize)] 77 | pub struct Instructions { 78 | pub agent_config: Vec, 79 | } 80 | -------------------------------------------------------------------------------- /src/components/left_sidebar/chat_history.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use uuid::Uuid; 3 | 4 | use crate::components::LeftSidebarEvent; 5 | use crate::utils::datetime::DatetimeString; 6 | use crate::utils::storage::StoredStates; 7 | 8 | pub fn ChatHistorySidebar(cx: Scope) -> Element { 9 | let chat_event_handler = use_coroutine_handle::(cx).unwrap(); 10 | let chats: Vec<(String, DatetimeString, Uuid)> = use_shared_state::(cx) 11 | .unwrap() 12 | .read() 13 | .chats 14 | .iter() 15 | .map(|c| (c.topic.clone(), c.date.clone(), c.id)) 16 | .collect(); 17 | 18 | render! { 19 | div { 20 | class: "h-screen w-52 overflow-y-auto bg-slate-50 py-8 dark:bg-slate-900 sm:w-60", 21 | div { 22 | class: "flex items-start", 23 | h2 { 24 | class: "inline px-5 text-lg font-medium text-slate-800 dark:text-slate-200", 25 | "Chats" 26 | } 27 | span { 28 | class: "rounded-full bg-blue-600 px-2 py-1 text-xs text-slate-200", 29 | "{chats.len()}" 30 | } 31 | } 32 | div { 33 | class: "mx-2 mt-8 space-y-4", 34 | // chat list 35 | chats.into_iter().rev().map(|(title, date, id)| rsx!{ 36 | ChatHistoryItem { 37 | on_click: move |_| { 38 | chat_event_handler.send(LeftSidebarEvent::ChangeChat(id)) 39 | }, 40 | title: title, 41 | date: date.0, 42 | } 43 | }) 44 | } 45 | } 46 | } 47 | } 48 | 49 | #[derive(Props)] 50 | pub struct ChatHistoryItemProps<'a> { 51 | pub title: String, 52 | pub date: String, 53 | pub on_click: EventHandler<'a, MouseEvent>, 54 | } 55 | 56 | pub fn ChatHistoryItem<'a>(cx: Scope<'a, ChatHistoryItemProps>) -> Element<'a> { 57 | render! { 58 | button { 59 | onclick: |event| { 60 | cx.props.on_click.call(event); 61 | }, 62 | class: "flex w-full flex-col gap-y-2 rounded-lg px-3 py-2 text-left transition-colors duration-200 hover:bg-slate-200 focus:outline-none dark:hover:bg-slate-800", 63 | h1 { 64 | class: "text-sm font-medium capitalize text-slate-700 dark:text-slate-200", 65 | "{cx.props.title}" 66 | } 67 | p { 68 | class: "text-xs text-slate-500 dark:text-slate-400", 69 | "{cx.props.date}" 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/pages/app.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use futures_util::StreamExt; 3 | use transprompt::async_openai::Client; 4 | use transprompt::async_openai::config::{AzureConfig, OpenAIConfig}; 5 | use uuid::Uuid; 6 | 7 | use crate::components::{ChatContainer, LeftSidebar, SettingSidebar}; 8 | use crate::utils::auth::Auth; 9 | use crate::utils::storage::StoredStates; 10 | 11 | 12 | 13 | // Global states 14 | pub type AuthedClient = Option; 15 | 16 | pub struct ChatId(pub Uuid); 17 | 18 | pub struct StreamingReply(pub bool); 19 | 20 | pub fn Main(cx: Scope) -> Element { 21 | let mut stored_states = StoredStates::get_or_init(); 22 | stored_states.run_count += 1; 23 | stored_states.save(); 24 | log::info!("This is your {} time running ChitChai!", stored_states.run_count); 25 | render! { 26 | App { 27 | stored_states: stored_states 28 | } 29 | } 30 | } 31 | 32 | 33 | #[non_exhaustive] 34 | #[derive(Debug, Clone, PartialEq, Eq)] 35 | pub enum AppEvents { 36 | ToggleSettingsSidebar, 37 | } 38 | 39 | #[derive(Debug, Clone, Props, PartialEq)] 40 | pub struct AppProps { 41 | pub stored_states: StoredStates, 42 | } 43 | 44 | pub fn App(cx: Scope) -> Element { 45 | let stored_states = cx.props.stored_states.clone(); 46 | let last_chat_id = stored_states.chats.last().unwrap().id; 47 | let authed_client: AuthedClient = stored_states 48 | .auth 49 | .as_ref() 50 | .map(|auth| { 51 | match auth { 52 | Auth::OpenAI { .. } => Client::with_config::(auth.clone().into()), 53 | Auth::AzureOpenAI { .. } => Client::with_config::(auth.clone().into()), 54 | _ => unreachable!(), 55 | } 56 | }); 57 | let hide_settings_sidebar = stored_states.auth.is_some() && stored_states.selected_service.is_some(); 58 | // configure share states 59 | use_shared_state_provider(cx, || stored_states); 60 | use_shared_state_provider(cx, || authed_client); 61 | use_shared_state_provider(cx, || ChatId(last_chat_id)); 62 | use_shared_state_provider(cx, || StreamingReply(false)); 63 | let global = use_shared_state::(cx).unwrap(); 64 | let chat_id = use_shared_state::(cx).unwrap(); 65 | // configure local states 66 | let hide_setting_sidebar = use_state(cx, || hide_settings_sidebar); 67 | // configure event handler 68 | use_coroutine(cx, |mut rx| { 69 | let hide_setting_sidebar = hide_setting_sidebar.to_owned(); 70 | async move { 71 | while let Some(event) = rx.next().await { 72 | match event { 73 | AppEvents::ToggleSettingsSidebar => { 74 | hide_setting_sidebar.modify(|h| !(*h)); 75 | } 76 | _ => log::warn!("Unknown event: {:?}", event), 77 | } 78 | } 79 | } 80 | }); 81 | render! { 82 | div { 83 | class: "flex h-full w-full", 84 | LeftSidebar {} 85 | div { 86 | class: "flex-grow overflow-auto", 87 | ChatContainer {} 88 | } 89 | div { 90 | class: "w-1/6", 91 | hidden: *hide_setting_sidebar.get(), 92 | SettingSidebar {} 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/pages/agents.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | pub fn Agents(cx: Scope) -> Element { 4 | render! { 5 | div { 6 | AgentGrid {} 7 | } 8 | } 9 | } 10 | 11 | pub fn AgentGrid(cx: Scope) -> Element { 12 | render! { 13 | div { 14 | class: "bg-slate-800 min-h-screen p-8", 15 | div { 16 | class: "container mx-auto", 17 | div { 18 | class: "grid grid-cols-1 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-8", 19 | (0..8).map(|i| rsx! { 20 | AgentCard { 21 | idx: i, 22 | name: "Agent Name".to_string(), 23 | description: "Agent Description".to_string(), 24 | img_url: "https://dummyimage.com/128x128/354ea1/ffffff&text=A".to_string(), 25 | } 26 | }) 27 | AddAgentCard { 28 | on_click: move |event| log::info!("clicked adding agent!") 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | #[derive(Props)] 37 | pub struct AddAgentCardProps<'a> { 38 | pub on_click: EventHandler<'a, MouseEvent>, 39 | } 40 | 41 | pub fn AddAgentCard<'a>(cx: Scope<'a, AddAgentCardProps<'a>>) -> Element<'a> { 42 | render! { 43 | div { 44 | class: "flex justify-center items-center w-full h-full max-w-sm mx-auto rounded-3xl bg-white p-6 shadow-lg cursor-pointer \ 45 | hover:text-blue-500 text-gray-400 ring-1 ring-slate-300 \ 46 | dark:bg-slate-900 dark:text-slate-200 dark:ring-slate-300/20", 47 | onclick: move |event| cx.props.on_click.call(event), 48 | span { 49 | class: "text-6xl", 50 | "+" 51 | } 52 | } 53 | } 54 | } 55 | 56 | #[derive(Props, PartialEq, Debug)] 57 | pub struct AgentCardProps { 58 | idx: u16, 59 | name: String, 60 | description: String, 61 | img_url: String, 62 | } 63 | 64 | pub fn AgentCard(cx: Scope) -> Element { 65 | // 66 | render! { 67 | div { 68 | class: "flex w-full max-w-md flex-col rounded-3xl bg-slate-50 p-8 text-slate-900 ring-1 ring-slate-300 \ 69 | dark:bg-slate-900 dark:text-slate-200 dark:ring-slate-300/20", 70 | div { 71 | class: "flex justify-between items-center", 72 | h3 { 73 | class: "text-lg font-semibold leading-8", 74 | "{cx.props.name}" 75 | } 76 | img { 77 | class: "h-16 w-16 rounded-full object-cover", 78 | src: cx.props.img_url.as_str(), 79 | alt: cx.props.name.as_str(), 80 | } 81 | } 82 | div { 83 | class: "mt-4", 84 | p { 85 | class: "text-sm leading-6 text-slate-700 dark:text-slate-400", 86 | "{cx.props.description}" 87 | } 88 | } 89 | button { 90 | class: "mt-4 rounded-md bg-blue-600 text-white px-4 py-2 text-sm font-semibold leading-5 \ 91 | hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-opacity-50", 92 | // onclick: todo!(), 93 | "Edit" 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/components/chat/message_card.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_markdown::Markdown; 3 | use transprompt::async_openai::types::{ChatCompletionRequestMessage, ChatCompletionRequestUserMessageContent}; 4 | use transprompt::utils::llm::openai::ChatMsg; 5 | 6 | #[derive(Props, PartialEq, Clone, Debug)] 7 | pub struct MessageCardProps { 8 | chat_msg: ChatMsg, 9 | } 10 | 11 | pub fn MessageCard(cx: Scope) -> Element { 12 | let chat_msg = &cx.props.chat_msg; 13 | match &chat_msg.msg { 14 | ChatCompletionRequestMessage::System(sys_msg) => { 15 | let content = sys_msg.content.as_str(); 16 | render! { 17 | div { 18 | class: "flex flex-row-reverse items-start p-5", 19 | img { 20 | class: "ml-2 h-8 w-8 rounded-full", 21 | src: "https://dummyimage.com/128x128/354ea1/ffffff&text=S" 22 | } 23 | MarkdownTextBox { 24 | content: content, 25 | } 26 | } 27 | } 28 | } 29 | ChatCompletionRequestMessage::User(user_msg) => { 30 | let content = match &user_msg.content { 31 | ChatCompletionRequestUserMessageContent::Text(text) => text.as_str(), 32 | ChatCompletionRequestUserMessageContent::Array(_) => todo!() 33 | }; 34 | let name_char = user_msg.name.as_ref().map(|name| name.as_str().chars().next().unwrap()).unwrap_or('U'); 35 | render! { 36 | div { 37 | class: "flex flex-row-reverse items-start p-5", 38 | img { 39 | class: "ml-2 h-8 w-8 rounded-full", 40 | src: "https://dummyimage.com/128x128/354ea1/ffffff&text={name_char}" 41 | } 42 | MarkdownTextBox { 43 | content: content, 44 | } 45 | } 46 | } 47 | } 48 | ChatCompletionRequestMessage::Assistant(assistant_msg) => { 49 | let content = assistant_msg.content 50 | .as_ref() 51 | .expect("Assistant message content is missing; Should not happen as of now") 52 | .as_str(); 53 | let name_char = assistant_msg.name.as_ref() 54 | .map(|name| name.as_str().chars().next().unwrap()) 55 | .unwrap_or('A'); 56 | render! { 57 | div { 58 | class: "flex items-start p-5", 59 | img { 60 | class: "mr-2 h-8 w-8 rounded-full", 61 | src: "https://dummyimage.com/128x128/363536/ffffff&text={name_char}" 62 | } 63 | MarkdownTextBox { 64 | content: content, 65 | } 66 | } 67 | } 68 | } 69 | ChatCompletionRequestMessage::Tool(_) | ChatCompletionRequestMessage::Function(_) => todo!(), 70 | } 71 | } 72 | 73 | #[derive(Props, PartialEq, Clone, Debug)] 74 | pub struct MarkdownTextBoxProps<'a> { 75 | content: &'a str, 76 | } 77 | 78 | pub fn MarkdownTextBox<'a>(cx: Scope<'a, MarkdownTextBoxProps>) -> Element<'a> { 79 | let msg = cx.props.content; 80 | render! { 81 | div { 82 | class: "flex min-h-[85px] rounded-b-xl rounded-tl-xl bg-slate-50 px-4 dark:bg-slate-800 sm:min-h-0 sm:max-w-md md:max-w-2xl", 83 | article { 84 | class: "prose dark:prose-invert lg:prose-xl max-w-none", 85 | Markdown { 86 | content: "{msg}", 87 | } 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chitchai 聊斋 2 | [![website](https://badgen.net/badge/chitchai/reify.ing/blue)](https://chitchai.reify.ing) 3 | 4 | Chitchat with multiple AIs to get more insights! 5 | 6 | ![dark](./images/dark_mode.png) 7 | 8 | ![bright](./images/bright_mode.png) 9 | 10 | ## Run 11 | 12 | 0. You'll need Rust, `rustc` `wasm32-unknown-unknown` target and Dioxus CLI. 13 | 1. Install Rust: https://www.rust-lang.org/learn/get-started 14 | 2. Install wasm32-unknown-unknown target: `rustup target add wasm32-unknown-unknown` 15 | 3. Install Dioxus CLI: `cargo install dioxus-cli --locked` 16 | 1. Clone this repo 17 | 2. Run `dx serve` 18 | 19 | ### Service Configuration 20 | 21 | In the first run, you need to configure a GPT service provider, now OpenAI or Azure OpenAI. The settings panel will show 22 | up automatically. Enter your keys and save. That's all before you can start chatting. 23 | 24 | WARNING: 25 | Your API secrets will be stored in your browser's local storage. Please do NOT use `Chitchai` when using a shared 26 | computer. 27 | 28 | ## Configure Agents 29 | 30 | For now, to configure agents, you need to edit [`default_assistants.toml`](./default_assistants.toml) and then re-run 31 | the server. Each agent configuration has a field of `name` and `instructions`. `instructions` tells an agent what to or 32 | not to do. You can start from the template and customize yours. 33 | 34 | ## Algorithm 35 | 36 | The algorithm is super simple: 37 | 38 | 1. Give each agent a name and instructions 39 | 2. Create a different chat history for each agent 40 | 3. Forward chat histories to GPT-4 41 | * GPT-4 is intelligent enough to distinguish different agents by name and play different roles 42 | * GPT-3.5 could work sometimes but it often fails to distinguish different agents 43 | 44 | ### Costs 45 | 46 | If you have `N` agents, then basically the cost of each message is `N` times of the cost of a single message. 47 | 48 | ### Comparison with `AutoGen` 49 | 50 | `AutoGen` is comprised of some complicated algorithms and more intricate implementations (kudos), but in its essence, 51 | it's the same as `ChitChai`. 52 | 53 | ## Build 54 | 55 | 0. Run `npm install` 56 | 1. Run `npx tailwindcss -i src/tailwind_input.css -o ./assets/tailwind.css --watch` 57 | 2. Run `dx serve --hot-reload` 58 | 59 | ## TODOs 60 | 61 | Sorted by importance and priority: 62 | 63 | - [x] Hosting it on a website. chitchai.dev is on the way! 64 | - [ ] Add Support for OpenAI Threads and Assistants APIs 65 | - [ ] Add UI for manage agent profiles 66 | - [ ] Add UI for dynamically add agents to a chat 67 | - [ ] Add UIs for all sorts of warnings 68 | - [ ] Add better markdown support 69 | - [ ] Filter out `[NONE]` replies 70 | - [ ] Add UIs for user guide 71 | - [ ] i18n 72 | - [ ] Support MiniMax LLM 73 | - [ ] More UI refinements 74 | - [ ] Perhaps a bit more research? Can we do all the stuff `AutoGen` promises? 75 | 76 | ## Motivation 77 | 78 | I found `AutoGen` is quite amazing but the codebase is notoriously complicated for me. I want to build a simpler version 79 | of it. After experimenting a bit, I found GPT-4 is already super intelligent, enough to distinguish different agents 80 | only by name (i.e. the name field in messages that is often ignored by most LLM developers). 81 | 82 | Besides, I am also learning building frontends and UIs, so this is a perfect chance. 83 | 84 | (PS: I don't want to pay for character.ai for a functionality that can be easily implemented by myself) 85 | 86 | ## Contributing 87 | 88 | Contributions are always welcome! Please open an issue or submit a PR. Also see TODOs above. 89 | 90 | ## License 91 | 92 | The license is and will always be Apache 2.0. See [LICENSE](./LICENSE) for more details. Feel free to fork and use it. 93 | 94 | For UIs, we started ours with components from [Langui](langui.dev) which is licensed under MIT License. 95 | 96 | ## Acknowledgements 97 | 98 | Special thanks to GPT-4 and Github Copilot for helping out with the code. -------------------------------------------------------------------------------- /src/components/left_sidebar.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use futures_util::StreamExt; 3 | use uuid::Uuid; 4 | 5 | pub use agent_profiles::*; 6 | pub use chat_history::*; 7 | pub use icons::*; 8 | 9 | use crate::pages::app::{ChatId, StreamingReply}; 10 | use crate::chat::Chat; 11 | use crate::utils::storage::StoredStates; 12 | 13 | pub mod chat_history; 14 | pub mod icons; 15 | pub mod agent_profiles; 16 | 17 | #[non_exhaustive] 18 | #[derive(Debug, Clone, PartialEq, Eq)] 19 | pub enum LeftSidebarEvent { 20 | ChangeChat(Uuid), 21 | NewChat, 22 | EnableSecondary(SecondarySidebar), 23 | DisableSecondary(SecondarySidebar), 24 | } 25 | 26 | #[derive(Debug, Clone, PartialEq, Eq)] 27 | pub enum SecondarySidebar { 28 | History, 29 | Profile, 30 | None, 31 | } 32 | 33 | impl SecondarySidebar { 34 | pub fn is_none(&self) -> bool { 35 | matches!(self, Self::None) 36 | } 37 | } 38 | 39 | pub fn LeftSidebar(cx: Scope) -> Element { 40 | use_shared_state_provider(cx, || SecondarySidebar::None); 41 | let secondary_sidebar = use_shared_state::(cx).unwrap(); 42 | let showing_chat_id = use_shared_state::(cx).unwrap(); 43 | let streaming_reply = use_shared_state::(cx).unwrap(); 44 | let global = use_shared_state::(cx).unwrap(); 45 | use_coroutine(cx, |rx| event_handler(rx, secondary_sidebar.to_owned(), showing_chat_id.to_owned(), streaming_reply.to_owned(), global.to_owned())); 46 | render! { 47 | aside { 48 | class: "flex", 49 | IconSidebar {} 50 | match *secondary_sidebar.read() { 51 | SecondarySidebar::History => rsx! { 52 | ChatHistorySidebar {} 53 | }, 54 | SecondarySidebar::Profile => rsx! { 55 | AgentProfiles {} 56 | }, 57 | SecondarySidebar::None => rsx! { 58 | div {} 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | 66 | async fn event_handler(mut rx: UnboundedReceiver, 67 | secondary_sidebar: UseSharedState, 68 | showing_chat_id: UseSharedState, 69 | streaming_reply: UseSharedState, 70 | global: UseSharedState) { 71 | while let Some(event) = rx.next().await { 72 | match event { 73 | LeftSidebarEvent::EnableSecondary(secondary) => { 74 | *secondary_sidebar.write() = secondary; 75 | } 76 | LeftSidebarEvent::DisableSecondary(secondary) => { 77 | if *secondary_sidebar.read() == secondary { 78 | let mut secondary_sidebar = secondary_sidebar.write(); 79 | if *secondary_sidebar == secondary { 80 | *secondary_sidebar = SecondarySidebar::None; 81 | } 82 | } 83 | } 84 | LeftSidebarEvent::NewChat => { 85 | // if not streaming, create a new chat 86 | if !streaming_reply.read().0 { 87 | // if secondary sidebar is not history, change it to history 88 | if *secondary_sidebar.read() != SecondarySidebar::History { 89 | let mut secondary_sidebar = secondary_sidebar.write(); 90 | if *secondary_sidebar != SecondarySidebar::History { 91 | *secondary_sidebar = SecondarySidebar::History; 92 | } 93 | } 94 | let mut global = global.write(); 95 | let new_chat = Chat::default(); 96 | let new_chat_id = new_chat.id; 97 | global.chats.push(new_chat); 98 | global.save(); 99 | showing_chat_id.write().0 = new_chat_id; 100 | } 101 | } 102 | LeftSidebarEvent::ChangeChat(chat_id) => { 103 | if (!streaming_reply.read().0) && showing_chat_id.read().0 != chat_id { 104 | log::info!("Changing to Chat {}", chat_id); 105 | showing_chat_id.write().0 = chat_id; 106 | } 107 | } 108 | _ => log::warn!("Unknown event: {:?}", event), 109 | } 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /src/agents.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use transprompt::prompt::PromptTemplate; 3 | use uuid::Uuid; 4 | 5 | use crate::chat::{LinkedChatHistory, MessageManager}; 6 | use crate::prompt_engineer::prompt_templates::ASSISTANT_SYS_PROMPT_TEMPLATE; 7 | use crate::utils::{EMPTY, sys_msg}; 8 | 9 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 10 | pub enum AgentName { 11 | UserDefault, 12 | AssistantDefault, 13 | Named(String), 14 | } 15 | 16 | impl AgentName { 17 | pub fn assistant(name: Option>) -> Self { 18 | match name { 19 | Some(name) => Self::Named(name.into()), 20 | None => Self::AssistantDefault, 21 | } 22 | } 23 | 24 | pub fn user(name: Option>) -> Self { 25 | match name { 26 | Some(name) => Self::Named(name.into()), 27 | None => Self::UserDefault, 28 | } 29 | } 30 | } 31 | 32 | 33 | #[non_exhaustive] 34 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 35 | pub enum AgentType { 36 | User, 37 | Assistant { 38 | instructions: String, 39 | }, 40 | } 41 | 42 | 43 | impl AgentType { 44 | pub const fn str(&self) -> &'static str { 45 | match self { 46 | AgentType::User => "User", 47 | AgentType::Assistant { .. } => "Assistant", 48 | } 49 | } 50 | } 51 | 52 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 53 | pub struct AgentID { 54 | pub(crate) id: Uuid, 55 | } 56 | 57 | impl AgentID { 58 | pub fn new() -> Self { 59 | Self { 60 | id: Uuid::new_v4(), 61 | } 62 | } 63 | } 64 | 65 | #[derive(Clone, Debug, PartialEq, Eq)] 66 | pub struct AgentInstance { 67 | pub id: AgentID, 68 | pub config: AgentConfig, 69 | pub history: LinkedChatHistory, 70 | } 71 | 72 | impl AgentInstance { 73 | pub fn new(config: AgentConfig, history: LinkedChatHistory) -> Self { 74 | Self { 75 | id: AgentID::new(), 76 | config, 77 | history, 78 | } 79 | } 80 | 81 | pub fn get_name(&self) -> AgentName { 82 | self.config.name.clone() 83 | } 84 | 85 | pub fn default_assistant(name: AgentName, message_manager: &mut MessageManager) -> Self { 86 | let config = AgentConfig::new_assistant(name, "You are a helpful assistant.", EMPTY); 87 | let sys_prompt_id = message_manager.insert(sys_msg(config.simple_sys_prompt())); 88 | Self::new(config, vec![sys_prompt_id]) 89 | } 90 | 91 | pub fn default_user() -> Self { 92 | let config = AgentConfig::new_user(AgentName::UserDefault, EMPTY); 93 | Self::new(config, vec![]) 94 | } 95 | } 96 | 97 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 98 | pub struct AgentConfig { 99 | pub name: AgentName, 100 | pub description: String, 101 | pub agent_type: AgentType, 102 | } 103 | 104 | impl AgentConfig { 105 | pub fn new_user(name: AgentName, description: impl Into) -> Self { 106 | Self { 107 | name, 108 | description: description.into(), 109 | agent_type: AgentType::User, 110 | } 111 | } 112 | 113 | pub fn new_assistant(name: AgentName, 114 | instructions: impl Into, 115 | description: impl Into) -> Self { 116 | let instructions = instructions.into(); 117 | Self { 118 | name, 119 | description: description.into(), 120 | agent_type: AgentType::Assistant { instructions }, 121 | } 122 | } 123 | 124 | pub fn simple_sys_prompt(&self) -> String { 125 | match &self.agent_type { 126 | AgentType::User => EMPTY, 127 | AgentType::Assistant { instructions } => { 128 | PromptTemplate::new(ASSISTANT_SYS_PROMPT_TEMPLATE) 129 | .construct_prompt() 130 | .fill("name_instructions", match &self.name { 131 | AgentName::UserDefault => EMPTY, 132 | AgentName::AssistantDefault => EMPTY, 133 | AgentName::Named(name) => format!("Your name is {}.", name), 134 | }) 135 | .fill("instructions", instructions.clone()) 136 | .complete() 137 | .expect("Failed to complete sys_prompt") 138 | } 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /src/chat.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use transprompt::utils::llm::openai::ChatMsg; 4 | use uuid::Uuid; 5 | 6 | use crate::agents::{AgentConfig, AgentID, AgentInstance, AgentName, AgentType}; 7 | use crate::utils::{Instructions, sys_msg}; 8 | use crate::utils::datetime::DatetimeString; 9 | 10 | pub type LinkedChatHistory = Vec; 11 | 12 | #[derive(Clone, Copy, Hash, PartialEq, Debug, Eq)] 13 | pub struct MessageID(pub(crate) Uuid); 14 | 15 | impl MessageID { 16 | pub fn new() -> Self { 17 | Self(Uuid::new_v4()) 18 | } 19 | } 20 | 21 | #[derive(Clone, Debug, PartialEq, Default)] 22 | pub struct MessageManager { 23 | pub(crate) messages: HashMap, 24 | } 25 | 26 | impl MessageManager { 27 | pub fn insert(&mut self, msg: ChatMsg) -> MessageID { 28 | let id = MessageID::new(); 29 | self.messages.insert(id.clone(), msg); 30 | id 31 | } 32 | 33 | pub fn remove(&mut self, id: &MessageID) -> Option { 34 | self.messages.remove(id) 35 | } 36 | 37 | pub fn get(&self, id: &MessageID) -> Option<&ChatMsg> { 38 | self.messages.get(id) 39 | } 40 | 41 | pub fn get_mut(&mut self, id: &MessageID) -> Option<&mut ChatMsg> { 42 | self.messages.get_mut(id) 43 | } 44 | 45 | pub fn update(&mut self, id: &MessageID, msg: ChatMsg) -> Option { 46 | self.messages.insert(id.clone(), msg) 47 | } 48 | } 49 | 50 | 51 | #[derive(Debug, PartialEq)] 52 | pub struct Chat { 53 | pub(crate) id: Uuid, 54 | pub message_manager: MessageManager, 55 | pub topic: String, 56 | pub date: DatetimeString, 57 | pub agents: HashMap, 58 | } 59 | 60 | impl Chat { 61 | pub fn id(&self) -> Uuid { 62 | self.id 63 | } 64 | 65 | pub fn default_chat_and_configs() -> (Self, HashMap) { 66 | let Instructions { agent_config: configs } = toml::from_str(include_str!("../default_assistants.toml")).unwrap(); 67 | let mut name_to_configs = HashMap::new(); 68 | let mut message_manager = MessageManager::default(); 69 | let mut agents = HashMap::new(); 70 | for agent_instructions in configs { 71 | let agent_name = AgentName::Named(agent_instructions.name.clone()); 72 | let agent_config = AgentConfig::new_assistant( 73 | agent_name.clone(), 74 | agent_instructions.instructions.clone(), 75 | "", 76 | ); 77 | let sys_prompt = agent_config.simple_sys_prompt(); 78 | let sys_prompt_id = message_manager.insert(sys_msg(sys_prompt)); 79 | let agent = AgentInstance::new(agent_config, vec![sys_prompt_id]); 80 | name_to_configs.insert(agent_name, agent.config.clone()); 81 | agents.insert(agent.id, agent); 82 | } 83 | // init a user whose history is empty and will be displayed by default 84 | let user = AgentInstance::default_user(); 85 | name_to_configs.insert(user.get_name(), user.config.clone()); 86 | agents.insert(user.id, user); 87 | let chat = Self { 88 | id: Uuid::new_v4(), 89 | message_manager, 90 | topic: "New Chat".to_string(), 91 | date: DatetimeString::get_now(), 92 | agents, 93 | }; 94 | (chat, name_to_configs) 95 | } 96 | 97 | pub fn default() -> Self { 98 | let (chat, _) = Self::default_chat_and_configs(); 99 | chat 100 | } 101 | 102 | pub fn user_agent_ids>(&self) -> B { 103 | self 104 | .agents 105 | .iter() 106 | .filter_map(|(id, agent)| { 107 | if agent.config.agent_type == AgentType::User { 108 | Some(id.clone()) 109 | } else { 110 | None 111 | } 112 | }) 113 | .collect() 114 | } 115 | 116 | pub fn assistant_agent_ids>(&self) -> B { 117 | self 118 | .agents 119 | .iter() 120 | .filter_map(|(id, agent)| { 121 | if let AgentType::Assistant { .. } = agent.config.agent_type { 122 | Some(id.clone()) 123 | } else { 124 | None 125 | } 126 | }) 127 | .collect() 128 | } 129 | 130 | pub fn agent_ids(&self) -> Vec { 131 | self.agents.keys().cloned().collect() 132 | } 133 | } 134 | 135 | impl Clone for Chat { 136 | fn clone(&self) -> Self { 137 | Self { 138 | id: Uuid::new_v4(), 139 | message_manager: self.message_manager.clone(), 140 | topic: self.topic.clone(), 141 | date: DatetimeString::get_now(), 142 | agents: self.agents.clone(), 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/utils/storage/conversion.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use uuid::Uuid; 4 | 5 | use crate::agents::{AgentConfig, AgentID, AgentInstance, AgentName}; 6 | use crate::chat::{Chat, MessageID, MessageManager}; 7 | use crate::utils::storage::StoredStates; 8 | 9 | use super::schema::*; 10 | 11 | impl Into for RawStoredStates { 12 | fn into(self) -> StoredStates { 13 | let RawStoredStates { 14 | raw_app_settings, 15 | raw_chats, 16 | raw_agent_configs, 17 | } = self; 18 | let RawAppSettings { 19 | run_count, 20 | customization, 21 | auth, 22 | selected_service, 23 | openai_model, 24 | } = raw_app_settings; 25 | let name_to_configs = raw_agent_configs.name_to_configs.into_iter().map(|(k, v)| (k.into(), v)).collect(); 26 | let chats = raw_chats.chats.into_iter().map(|c| c.into_chat(&name_to_configs)).collect(); 27 | StoredStates { 28 | run_count, 29 | customization, 30 | name_to_configs, 31 | chats, 32 | auth, 33 | selected_service, 34 | openai_model, 35 | } 36 | } 37 | } 38 | 39 | impl From for RawStoredStates { 40 | fn from(value: StoredStates) -> Self { 41 | let StoredStates { 42 | run_count, 43 | customization, 44 | name_to_configs, 45 | chats, 46 | auth, 47 | selected_service, 48 | openai_model 49 | } = value; 50 | let raw_app_settings = RawAppSettings { 51 | run_count, 52 | customization, 53 | auth, 54 | selected_service, 55 | openai_model, 56 | }; 57 | let raw_agent_configs = RawAgentConfigs { 58 | name_to_configs: name_to_configs.into_iter().map(|(k, v)| (k.into(), v)).collect(), 59 | }; 60 | let raw_chats = RawChats { 61 | chats: chats.into_iter().map(|c| c.into()).collect(), 62 | }; 63 | Self { 64 | raw_app_settings, 65 | raw_chats, 66 | raw_agent_configs, 67 | } 68 | } 69 | } 70 | 71 | 72 | impl From for RawChat { 73 | fn from(value: Chat) -> Self { 74 | let Chat { 75 | id, message_manager, topic, date, agents 76 | } = value; 77 | let agents = agents.into_iter().map(|(k, v)| (k.into(), v.into())).collect(); 78 | let messages = message_manager.messages.into_iter().map(|(k, v)| (k.into(), v)).collect(); 79 | Self { 80 | id, 81 | messages, 82 | topic, 83 | date, 84 | agents, 85 | } 86 | } 87 | } 88 | 89 | impl RawChat { 90 | pub fn into_chat(self, name_to_configs: &HashMap) -> Chat { 91 | let RawChat { 92 | id, messages, topic, date, agents 93 | } = self; 94 | let agents = agents 95 | .into_iter() 96 | .map(|(k, v)| (k.into(), v.into_agent_instance(name_to_configs))) 97 | .collect(); 98 | let messages = messages.into_iter().map(|(k, v)| (k.into(), v)).collect(); 99 | 100 | Chat { 101 | id, 102 | message_manager: MessageManager { 103 | messages, 104 | }, 105 | topic, 106 | date, 107 | agents, 108 | } 109 | } 110 | } 111 | 112 | 113 | impl Into for AgentID { 114 | fn into(self) -> UUIDKey { 115 | self.id.to_string() 116 | } 117 | } 118 | 119 | impl From for AgentID { 120 | fn from(s: UUIDKey) -> Self { 121 | let id = Uuid::parse_str(&s).expect("Failed to parse AgentId from String"); 122 | Self { 123 | id, 124 | } 125 | } 126 | } 127 | 128 | impl Into for MessageID { 129 | fn into(self) -> RawMessageID { 130 | self.0.to_string() 131 | } 132 | } 133 | 134 | impl From for MessageID { 135 | fn from(s: RawMessageID) -> Self { 136 | let id = Uuid::parse_str(&s).expect("Failed to parse MessageId from String"); 137 | Self(id) 138 | } 139 | } 140 | 141 | impl Into for AgentInstance { 142 | fn into(self) -> RawAgentInstance { 143 | let AgentInstance { id, config, history } = self; 144 | let AgentConfig { name, .. } = config; 145 | let history = history.into_iter().map(|id| id.into()).collect(); 146 | RawAgentInstance { 147 | id, 148 | name, 149 | history, 150 | } 151 | } 152 | } 153 | 154 | impl RawAgentInstance { 155 | pub fn into_agent_instance(self, name_to_configs: &HashMap) -> AgentInstance { 156 | let RawAgentInstance { id, name, history } = self; 157 | let config = match name_to_configs.get(&name) { 158 | Some(config) => config, 159 | None => { 160 | log::warn!("AgentConfig not found for name: {:?}", name); 161 | unreachable!("AgentConfig not found for name: {:?}", name); 162 | } 163 | }; 164 | let history = history.into_iter().map(|id| id.into()).collect(); 165 | AgentInstance { 166 | id, 167 | config: config.clone(), 168 | history, 169 | } 170 | } 171 | } 172 | 173 | impl Into for AgentName { 174 | fn into(self) -> RawAgentName { 175 | match self { 176 | AgentName::Named(name) => name, 177 | AgentName::UserDefault => "_USER".to_string(), 178 | AgentName::AssistantDefault => "_ASSISTANT".to_string(), 179 | } 180 | } 181 | } 182 | 183 | impl From for AgentName { 184 | fn from(s: RawAgentName) -> Self { 185 | match s.as_str() { 186 | "_USER" => Self::UserDefault, 187 | "_ASSISTANT" => Self::AssistantDefault, 188 | _ => Self::Named(s), 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/utils/storage/schema.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use gloo_storage::{LocalStorage, Storage}; 4 | use serde::{Deserialize, Serialize}; 5 | use serde::de::DeserializeOwned; 6 | use transprompt::utils::llm::openai::ChatMsg; 7 | use uuid::Uuid; 8 | 9 | use crate::agents::{AgentConfig, AgentID, AgentName}; 10 | use crate::chat::Chat; 11 | use crate::utils::auth::Auth; 12 | use crate::utils::customization::Customization; 13 | use crate::utils::datetime::DatetimeString; 14 | use crate::utils::settings::{GPTService, OpenAIModel}; 15 | use crate::utils::storage::StoredStates; 16 | 17 | pub(crate) type UUIDKey = String; 18 | pub(crate) type UUIDString = String; 19 | 20 | pub(crate) type RawAgentID = UUIDKey; 21 | pub(crate) type RawMessageID = UUIDKey; 22 | pub(crate) type RawLinkedChatHistory = Vec; 23 | pub(crate) type RawAgentName = String; 24 | 25 | #[derive(Serialize, Deserialize, Clone)] 26 | pub(crate) struct RawChat { 27 | pub(crate) id: Uuid, 28 | pub messages: HashMap, 29 | pub topic: String, 30 | pub date: DatetimeString, 31 | pub agents: HashMap, 32 | } 33 | 34 | #[derive(Serialize, Deserialize, Clone)] 35 | pub(crate) struct RawAgentInstance { 36 | pub id: AgentID, 37 | pub name: AgentName, 38 | pub history: RawLinkedChatHistory, 39 | } 40 | 41 | pub(crate) trait StoredState: Serialize + DeserializeOwned { 42 | const STORE_KEY: &'static str; 43 | fn get_or_init() -> Self; 44 | fn save(self) { 45 | match LocalStorage::set(Self::STORE_KEY, self) { 46 | Ok(_) => log::info!("Saved StoredState with key {}", Self::STORE_KEY), 47 | Err(e) => log::error!("Error when saving StoredState with key {}: {}", Self::STORE_KEY, e), 48 | } 49 | } 50 | } 51 | 52 | #[derive(Serialize, Deserialize, Clone)] 53 | pub(crate) struct RawAppSettings { 54 | pub run_count: usize, 55 | pub customization: Customization, 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | pub auth: Option, 58 | #[serde(skip_serializing_if = "Option::is_none")] 59 | pub selected_service: Option, 60 | #[serde(skip_serializing_if = "Option::is_none")] 61 | pub openai_model: Option, 62 | } 63 | 64 | impl StoredState for RawAppSettings { 65 | const STORE_KEY: &'static str = "chitchai_settings"; 66 | 67 | fn get_or_init() -> Self { 68 | match LocalStorage::get::(Self::STORE_KEY) { 69 | Ok(settings) => settings, 70 | Err(e) => { 71 | log::error!("error on init RawAppSettings: {}", e); 72 | let raw_app_settings = RawAppSettings { 73 | run_count: 0, 74 | customization: Default::default(), 75 | auth: None, 76 | selected_service: None, 77 | openai_model: None, 78 | }; 79 | raw_app_settings.clone().save(); 80 | raw_app_settings 81 | } 82 | } 83 | } 84 | } 85 | 86 | #[derive(Serialize, Deserialize, Clone)] 87 | pub(crate) struct RawChats { 88 | pub chats: Vec, 89 | } 90 | 91 | impl StoredState for RawChats { 92 | const STORE_KEY: &'static str = "chitchai_chats"; 93 | fn get_or_init() -> Self { 94 | match LocalStorage::get::(Self::STORE_KEY) { 95 | Ok(value) => value, 96 | Err(e) => { 97 | log::error!("error on init RawChats: {}", e); 98 | let (mut default_chat, _name_to_configs) = Chat::default_chat_and_configs(); 99 | default_chat.topic = "Default Chat".to_string(); 100 | let raw_chats = vec![default_chat.into()]; 101 | let raw_chats = RawChats { chats: raw_chats }; 102 | raw_chats.clone().save(); 103 | raw_chats 104 | } 105 | } 106 | } 107 | } 108 | 109 | #[derive(Serialize, Deserialize, Clone)] 110 | pub(crate) struct RawAgentConfigs { 111 | pub name_to_configs: HashMap, 112 | } 113 | 114 | impl StoredState for RawAgentConfigs { 115 | const STORE_KEY: &'static str = "chitchai_agent_configs"; 116 | fn get_or_init() -> Self { 117 | match LocalStorage::get::(Self::STORE_KEY) { 118 | Ok(configs) => configs, 119 | Err(e) => { 120 | log::error!("error on init RawAgentConfigs: {}", e); 121 | let (_default_chat, name_to_configs) = Chat::default_chat_and_configs(); 122 | let name_to_configs = name_to_configs.into_iter().map(|(k, v)| (k.into(), v)).collect(); 123 | let raw_agent_configs = RawAgentConfigs { name_to_configs }; 124 | raw_agent_configs.clone().save(); 125 | raw_agent_configs 126 | } 127 | } 128 | } 129 | } 130 | 131 | 132 | pub(crate) struct RawStoredStates { 133 | pub raw_app_settings: RawAppSettings, 134 | pub raw_chats: RawChats, 135 | pub raw_agent_configs: RawAgentConfigs, 136 | } 137 | 138 | impl RawStoredStates { 139 | pub fn get_or_init() -> StoredStates { 140 | let raw_app_settings = RawAppSettings::get_or_init(); 141 | let raw_chats = RawChats::get_or_init(); 142 | let raw_agent_configs = RawAgentConfigs::get_or_init(); 143 | let name_to_configs = raw_agent_configs.name_to_configs.into_iter().map(|(k, v)| (k.into(), v)).collect(); 144 | let chats = raw_chats.chats.into_iter().map(|c| c.into_chat(&name_to_configs)).collect(); 145 | let RawAppSettings { 146 | run_count, 147 | customization, 148 | auth, 149 | selected_service, 150 | openai_model, 151 | } = raw_app_settings; 152 | StoredStates { 153 | run_count, 154 | customization, 155 | name_to_configs, 156 | chats, 157 | auth, 158 | selected_service, 159 | openai_model, 160 | } 161 | } 162 | 163 | pub fn save(self) { 164 | let RawStoredStates { 165 | raw_app_settings, 166 | raw_chats, 167 | raw_agent_configs, 168 | } = self; 169 | raw_app_settings.save(); 170 | raw_chats.save(); 171 | raw_agent_configs.save(); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/components/chat.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use std::time::Duration; 3 | 4 | use async_std::task::sleep; 5 | use dioxus::prelude::*; 6 | 7 | pub use message_card::*; 8 | 9 | use crate::agents::AgentID; 10 | use crate::pages::app::{AuthedClient, ChatId, StreamingReply}; 11 | use crate::chat::Chat; 12 | use crate::components::chat::request_utils::{find_chat_idx_by_id, handle_request}; 13 | use crate::utils::storage::StoredStates; 14 | 15 | mod request_utils; 16 | pub mod message_card; 17 | 18 | struct Request(String); 19 | 20 | 21 | pub fn ChatContainer(cx: Scope) -> Element { 22 | let stored_states = use_shared_state::(cx).unwrap(); 23 | let authed_client = use_shared_state::(cx).unwrap(); 24 | let streaming_reply = use_shared_state::(cx).unwrap(); 25 | let chat_id = use_shared_state::(cx).unwrap(); 26 | // request handler 27 | use_coroutine(cx, |rx| 28 | handle_request(rx, 29 | chat_id.to_owned(), 30 | stored_states.to_owned(), 31 | authed_client.to_owned(), 32 | streaming_reply.to_owned()), 33 | ); 34 | // get data 35 | let stored_states = stored_states.read(); 36 | let chat_idx = find_chat_idx_by_id(&stored_states.chats, &chat_id.read().0); 37 | let chat: &Chat = &stored_states.chats[chat_idx]; 38 | let user_agent_id: Vec = chat.user_agent_ids(); 39 | assert_eq!(user_agent_id.len(), 1, "user_agents.len() == 1"); // TODO: support multiple user agents 40 | let user_agent = chat.agents.get(&user_agent_id[0]).unwrap(); 41 | let history = &user_agent.history; 42 | render! { 43 | div { 44 | class: "flex h-full w-full flex-col relative", 45 | div { 46 | class: "flex flex-col h-full space-y-6 bg-slate-200 text-sm leading-6 text-slate-900 shadow-sm dark:bg-slate-900 dark:text-slate-300 sm:text-base sm:leading-7", 47 | div { 48 | class: "overflow-auto max-h-[90vh] flex-grow dark:scrollbar dark:scrollbar-thumb-slate-700 dark:scrollbar-track-slate-900", 49 | history 50 | .iter() 51 | .map(|msg_id| { 52 | let msg = chat.message_manager.get(msg_id).unwrap(); 53 | rsx! { 54 | MessageCard { 55 | chat_msg: msg.clone() 56 | } 57 | } 58 | }) 59 | } 60 | ChatMessageInput { 61 | disable_submit: streaming_reply.read().0 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | 69 | #[inline_props] 70 | pub fn ChatMessageInput(cx: Scope, disable_submit: bool) -> Element { 71 | const TEXTAREA_ID: &str = "chat-input"; 72 | let customization = &use_shared_state::(cx).unwrap().read().customization; 73 | let tick = use_state(cx, || 0_usize); 74 | // configure timer 75 | use_coroutine(cx, |_: UnboundedReceiver<()>| { 76 | let tick = tick.to_owned(); 77 | async move { 78 | loop { 79 | sleep(Duration::from_millis(500)).await; 80 | tick.modify(|tick| tick.wrapping_add(1)); 81 | } 82 | } 83 | }); 84 | let request_sender: &Coroutine = use_coroutine_handle(cx).unwrap(); 85 | let input_value = use_state(cx, || { 86 | let empty_form = FormData { 87 | value: String::new(), 88 | values: Default::default(), 89 | files: None, 90 | }; 91 | Rc::new(empty_form) 92 | }); 93 | // TODO: try not to use js to clear textarea 94 | let create_eval = use_eval(cx); 95 | let clear_textarea = use_future(cx, (), |_| { 96 | let create_eval = create_eval.to_owned(); 97 | let clear_js = format!("document.getElementById('{}').value = '';", TEXTAREA_ID); 98 | async move { 99 | let result = create_eval(clear_js.as_str()) 100 | .unwrap() 101 | .join() 102 | .await; 103 | match result { 104 | Ok(_) => log::info!("clear_textarea"), 105 | Err(e) => log::error!("clear_textarea error: {:?}", e), 106 | } 107 | } 108 | }); 109 | 110 | render! { 111 | form { 112 | class: "mt-2 absolute bottom-0 w-full p-5", 113 | id: "chat-form", 114 | onsubmit: move |_| { 115 | log::info!("onsubmit {}", &input_value.get().value); 116 | request_sender.send(Request(input_value.get().value.clone())); 117 | clear_textarea.restart(); 118 | }, 119 | label { 120 | r#for: "{TEXTAREA_ID}", 121 | class: "sr-only", 122 | "Enter your prompt" 123 | } 124 | div { 125 | class: "relative", 126 | textarea { 127 | oninput: move |event| input_value.set(event.data), 128 | id: "chat-input", 129 | form: "chat-form", 130 | class: "block w-full resize-none rounded-xl border-none bg-slate-200 p-4 pl-10 pr-20 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:bg-slate-900 dark:text-slate-200 dark:placeholder-slate-400 dark:focus:ring-blue-600 sm:text-base", 131 | placeholder: "Enter your prompt", 132 | rows: "2", 133 | required: true, 134 | } 135 | button { 136 | r#type: "submit", 137 | disabled: *disable_submit, 138 | class: "absolute bottom-2 right-2.5 rounded-lg bg-blue-700 px-4 py-2 text-sm font-medium text-slate-200 hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 sm:text-base", 139 | if *disable_submit { 140 | customization.waiting_icons[*tick.get() % customization.waiting_icons.len()].as_str() 141 | } else { 142 | "Send" 143 | } 144 | span { 145 | class: "sr-only", 146 | "Send message" 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/components/chat/request_utils.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use dioxus::prelude::*; 4 | use futures::future::join_all; 5 | use futures_util::StreamExt; 6 | use transprompt::async_openai::types::{ChatCompletionRequestMessage, CreateChatCompletionRequestArgs}; 7 | use uuid::Uuid; 8 | 9 | use crate::agents::AgentID; 10 | use crate::pages::app::{AuthedClient, ChatId, StreamingReply}; 11 | use crate::chat::{Chat, LinkedChatHistory, MessageID, MessageManager}; 12 | use crate::components::chat::Request; 13 | use crate::utils::{assistant_msg, EMPTY, user_msg}; 14 | use crate::utils::storage::StoredStates; 15 | 16 | pub(super) fn find_chat_idx_by_id(chats: &Vec, id: &Uuid) -> usize { 17 | for (idx, c) in chats.iter().enumerate() { 18 | if c.id.eq(id) { 19 | return idx; 20 | } 21 | } 22 | unreachable!("Cannot find a chat, should not be since deleting is not implemented yet") 23 | } 24 | 25 | 26 | #[inline] 27 | fn map_chat_messages(chat_msgs: &LinkedChatHistory, 28 | message_manager: &MessageManager) -> Vec { 29 | chat_msgs 30 | .iter() 31 | .map(|msg_id| message_manager.get(msg_id).unwrap().msg.clone()) 32 | .collect() 33 | } 34 | 35 | #[inline] 36 | fn push_history(chat: &mut Chat, 37 | agent_id: &AgentID, 38 | msg_id: MessageID) { 39 | chat 40 | .agents 41 | .get_mut(agent_id) 42 | .unwrap() 43 | .history 44 | .push(msg_id) 45 | } 46 | 47 | #[inline] 48 | fn linearize_replies(mut replies: Vec<(AgentID, MessageID, usize)>) -> LinkedChatHistory { 49 | replies.sort_by(|(_, _, ord1), (_, _, ord2)| ord1.cmp(ord2)); 50 | replies 51 | .into_iter() 52 | .map(|(_agent_id, msg_id, _)| msg_id) 53 | .collect() 54 | } 55 | 56 | async fn post_agent_request(assistant_id: AgentID, 57 | user_agent_id: AgentID, 58 | chat_idx: usize, 59 | authed_client: UseSharedState, 60 | order: Arc>, 61 | global: UseSharedState) -> (AgentID, MessageID, usize) { 62 | let mut global_mut = global.write(); 63 | let chat = &global_mut.chats[chat_idx]; 64 | // get the context to send to AI 65 | let agent = chat.agents.get(&assistant_id).unwrap(); 66 | let messages_to_send = map_chat_messages(&agent.history, &chat.message_manager); 67 | let agent_name = agent.get_name(); 68 | // update history, inserting assistant reply that is empty initially 69 | let chat = &mut global_mut.chats[chat_idx]; 70 | let assistant_reply_id = chat.message_manager.insert(assistant_msg(EMPTY, agent_name)); 71 | push_history(chat, &assistant_id, assistant_reply_id); 72 | push_history(chat, &user_agent_id, assistant_reply_id); 73 | // drop write lock before await point 74 | drop(global_mut); 75 | // send request, returning a stream 76 | let mut stream = authed_client 77 | .read() 78 | .as_ref() 79 | .unwrap() 80 | .chat() 81 | .create_stream(CreateChatCompletionRequestArgs::default() 82 | .model("gpt-3.5-turbo-0613") // TODO: use model when it's OpenAI Service 83 | .messages(messages_to_send) 84 | .build() 85 | .expect("creating request failed")) 86 | .await 87 | .expect("creating stream failed"); 88 | while let Some(chunk) = stream.next().await { 89 | match chunk { 90 | Ok(response) => { 91 | if response.choices.is_empty() { 92 | // azure openai service returns empty response on first call 93 | continue; 94 | } 95 | let mut global_mut = global.write(); 96 | let assistant_reply_msg = global_mut 97 | .chats[chat_idx] 98 | .message_manager 99 | .get_mut(&assistant_reply_id) 100 | .unwrap(); 101 | assistant_reply_msg.merge_delta(&response.choices[0].delta); 102 | } 103 | Err(e) => log::error!("OpenAI Error: {:?}", e), 104 | } 105 | } 106 | let mut order = order.lock().unwrap(); 107 | let got_order = *order; 108 | *order += 1; 109 | (assistant_id, assistant_reply_id, got_order) 110 | } 111 | 112 | 113 | pub(super) async fn handle_request(mut rx: UnboundedReceiver, 114 | chat_id: UseSharedState, 115 | global: UseSharedState, 116 | authed_client: UseSharedState, 117 | streaming_reply: UseSharedState) { 118 | while let Some(Request(request)) = rx.next().await { 119 | let chat_id = chat_id.read().0; 120 | log::info!("chat id = {}", chat_id); 121 | if authed_client.read().is_none() { 122 | // TODO: handle this error and make a toast to notify user 123 | log::error!("authed_client is None"); 124 | continue; 125 | } 126 | log::info!("request_handler {}", request); 127 | let mut global_mut = global.write(); 128 | let chat_idx = find_chat_idx_by_id(&global_mut.chats, &chat_id); 129 | let chat = &global_mut.chats[chat_idx]; 130 | let user_agent_ids: Vec = chat.user_agent_ids(); 131 | assert_eq!(user_agent_ids.len(), 1, "user_agent_ids.len() == 1"); // TODO: support multiple user agents 132 | let user_agent_id = user_agent_ids[0]; 133 | let user_agent = chat.agents.get(&user_agent_id).unwrap(); 134 | let assistant_agent_ids: Vec = chat.assistant_agent_ids(); 135 | // create user message and register them to chat manager 136 | let user_query = user_msg(request.as_str(), user_agent.get_name()); 137 | let user_msg_id = global_mut.chats[chat_idx].message_manager.insert(user_query.clone()); 138 | // update history, inserting user request 139 | global_mut 140 | .chats[chat_idx] 141 | .agents 142 | .iter_mut() 143 | .for_each(|(_, agent)| agent.history.push(user_msg_id)); 144 | global_mut.save(); 145 | // drop write lock before await point 146 | drop(global_mut); 147 | streaming_reply.write().0 = true; 148 | let order = Arc::new(Mutex::new(0_usize)); 149 | let results = join_all( 150 | assistant_agent_ids 151 | .iter() 152 | .map(|assistant_id| post_agent_request(*assistant_id, user_agent_id, chat_idx, authed_client.to_owned(), order.clone(), global.to_owned())) 153 | ).await; 154 | let replies = linearize_replies(results); 155 | // add replies to history of each assistant 156 | let mut global_mut = global.write(); 157 | let chat = &mut global_mut.chats[chat_idx]; 158 | assistant_agent_ids 159 | .iter() 160 | .for_each(|agent_id| { 161 | for msg_id in replies.iter() { 162 | push_history(chat, agent_id, *msg_id); 163 | } 164 | }); 165 | drop(global_mut); 166 | // stage assistant reply into local storage 167 | global.read().save(); 168 | streaming_reply.write().0 = false; 169 | } 170 | log::error!("request_handler exited"); 171 | } -------------------------------------------------------------------------------- /src/components/left_sidebar/icons.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | use crate::pages::app::AppEvents; 4 | use crate::components::{LeftSidebarEvent, SecondarySidebar}; 5 | 6 | pub fn IconSidebar(cx: Scope) -> Element { 7 | let now_active_secondary = use_shared_state::(cx).unwrap().read(); 8 | let conversation_list_button_active = matches!(*now_active_secondary, SecondarySidebar::History); 9 | let user_profile_button_active = matches!(*now_active_secondary, SecondarySidebar::Profile); 10 | 11 | render! { 12 | div { 13 | class: "flex h-screen w-12 flex-col items-center space-y-8 border-r border-slate-300 bg-slate-50 py-8 dark:border-slate-700 dark:bg-slate-900 sm:w-16", 14 | Logo {}, 15 | NewConversationButton { 16 | activatable: false, 17 | active: false, 18 | }, 19 | ConversationListButton { 20 | activatable: true, 21 | active: conversation_list_button_active, 22 | }, 23 | DiscoverButton { 24 | activatable: false, 25 | active: false, 26 | }, 27 | UserProfileButton { 28 | activatable: true, 29 | active: user_profile_button_active, 30 | }, 31 | SettingsButton { 32 | activatable: false, 33 | active: false, 34 | }, 35 | } 36 | } 37 | } 38 | 39 | 40 | pub fn Logo(cx: Scope) -> Element { 41 | render! { 42 | a { 43 | href: "https://github.com/ifsheldon/chitchai", 44 | target: "_blank", 45 | rel: "noopener noreferrer", 46 | class: "mb-1", 47 | svg { 48 | xmlns: "http://www.w3.org/2000/svg", 49 | class: "h-7 w-7 text-blue-600", 50 | fill: "currentColor", 51 | stroke_width: "1", 52 | view_box: "0 0 24 24", 53 | path { 54 | d: "M20.553 3.105l-6 3C11.225 7.77 9.274 9.953 8.755 12.6c-.738 3.751 1.992 7.958 2.861 8.321A.985.985 0 0012 21c6.682 0 11-3.532 11-9 0-6.691-.9-8.318-1.293-8.707a1 1 0 00-1.154-.188zm-7.6 15.86a8.594 8.594 0 015.44-8.046 1 1 0 10-.788-1.838 10.363 10.363 0 00-6.393 7.667 6.59 6.59 0 01-.494-3.777c.4-2 1.989-3.706 4.728-5.076l5.03-2.515A29.2 29.2 0 0121 12c0 4.063-3.06 6.67-8.046 6.965zM3.523 5.38A29.2 29.2 0 003 12a6.386 6.386 0 004.366 6.212 1 1 0 11-.732 1.861A8.377 8.377 0 011 12c0-6.691.9-8.318 1.293-8.707a1 1 0 011.154-.188l6 3A1 1 0 018.553 7.9z", 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | 62 | #[derive(PartialEq, Props, Clone)] 63 | pub struct ButtonProps { 64 | activatable: bool, 65 | active: bool, 66 | } 67 | 68 | #[derive(Props)] 69 | pub struct RawButtonProps<'a> { 70 | button_props: &'a ButtonProps, 71 | on_click: Option>, 72 | on_click_active: Option>, 73 | on_click_inactive: Option>, 74 | children: Element<'a>, 75 | } 76 | 77 | pub fn RawButton<'a>(cx: Scope<'a, RawButtonProps<'a>>) -> Element<'a> { 78 | const BUTTON_INACTIVE_STYLE: &str = "rounded-lg p-1.5 text-slate-500 transition-colors duration-200 hover:bg-slate-200 focus:outline-none dark:text-slate-400 dark:hover:bg-slate-800"; 79 | const BUTTON_ACTIVE_STYLE: &str = "rounded-lg bg-blue-100 p-1.5 text-blue-600 transition-colors duration-200 dark:bg-slate-800 dark:text-blue-600"; 80 | let activatable = cx.props.button_props.activatable; 81 | let active = cx.props.button_props.active; 82 | render! { 83 | a { 84 | href: "#", 85 | class: if activatable && active {BUTTON_ACTIVE_STYLE} else {BUTTON_INACTIVE_STYLE}, 86 | onclick: move |event| { 87 | if activatable { 88 | match (active, &cx.props.on_click_active, &cx.props.on_click_inactive) { 89 | (true, Some(on_click_active), _) => on_click_active.call(event), 90 | (false, _, Some(on_click_inactive)) => on_click_inactive.call(event), 91 | _ => {} 92 | } 93 | } else { 94 | if let Some(on_click) = &cx.props.on_click { 95 | on_click.call(event) 96 | } 97 | } 98 | }, 99 | &cx.props.children 100 | } 101 | } 102 | } 103 | 104 | pub fn NewConversationButton(cx: Scope) -> Element { 105 | let chat_sidebar_event_handler = use_coroutine_handle::(cx).unwrap(); 106 | render! { 107 | RawButton { 108 | button_props: &cx.props, 109 | on_click: move |_| chat_sidebar_event_handler.send(LeftSidebarEvent::NewChat), 110 | svg { 111 | xmlns: "http://www.w3.org/2000/svg", 112 | class: "h-6 w-6", 113 | view_box: "0 0 24 24", 114 | stroke_width: "2", 115 | stroke: "currentColor", 116 | fill: "none", 117 | stroke_linecap: "round", 118 | stroke_linejoin: "round", 119 | path { 120 | stroke: "none", 121 | d: "M0 0h24v24H0z", 122 | fill: "none", 123 | } 124 | path { 125 | d: "M8 9h8", 126 | } 127 | path { 128 | d: "M8 13h6", 129 | } 130 | path { 131 | d: "M12.01 18.594l-4.01 2.406v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v5.5", 132 | } 133 | path { 134 | d: "M16 19h6", 135 | } 136 | path { 137 | d: "M19 16v6", 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | pub fn ConversationListButton(cx: Scope) -> Element { 145 | let chat_sidebar_event_handler = use_coroutine_handle::(cx).unwrap(); 146 | render! { 147 | RawButton { 148 | on_click_active: move |_| chat_sidebar_event_handler.send(LeftSidebarEvent::DisableSecondary(SecondarySidebar::History)), 149 | on_click_inactive: move |_| chat_sidebar_event_handler.send(LeftSidebarEvent::EnableSecondary(SecondarySidebar::History)), 150 | button_props: &cx.props, 151 | svg { 152 | xmlns: "http://www.w3.org/2000/svg", 153 | class: "h-6 w-6", 154 | view_box: "0 0 24 24", 155 | stroke_width: "2", 156 | stroke: "currentColor", 157 | fill: "none", 158 | stroke_linecap: "round", 159 | stroke_linejoin: "round", 160 | path { 161 | stroke: "none", 162 | d: "M0 0h24v24H0z", 163 | fill: "none", 164 | } 165 | path { 166 | d: "M21 14l-3 -3h-7a1 1 0 0 1 -1 -1v-6a1 1 0 0 1 1 -1h9a1 1 0 0 1 1 1v10", 167 | } 168 | path { 169 | d: "M14 15v2a1 1 0 0 1 -1 1h-7l-3 3v-10a1 1 0 0 1 1 -1h2", 170 | } 171 | } 172 | } 173 | } 174 | } 175 | 176 | 177 | pub fn DiscoverButton(cx: Scope) -> Element { 178 | // let chat_sidebar_event_handler = use_coroutine_handle::(cx).unwrap(); 179 | render! { 180 | RawButton { 181 | button_props: &cx.props, 182 | svg { 183 | xmlns: "http://www.w3.org/2000/svg", 184 | class: "h-6 w-6", 185 | view_box: "0 0 24 24", 186 | stroke_width: "2", 187 | stroke: "currentColor", 188 | fill: "none", 189 | stroke_linecap: "round", 190 | stroke_linejoin: "round", 191 | path { 192 | stroke: "none", 193 | d: "M0 0h24v24H0z", 194 | fill: "none", 195 | } 196 | path { 197 | d: "M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0", 198 | } 199 | path { 200 | d: "M21 21l-6 -6", 201 | } 202 | } 203 | } 204 | } 205 | } 206 | 207 | 208 | pub fn UserProfileButton(cx: Scope) -> Element { 209 | // let chat_sidebar_event_handler = use_coroutine_handle::(cx).unwrap(); 210 | render! { 211 | RawButton { 212 | // TODO: enble this after implementing profile sidebar 213 | // on_click_active: move |_| chat_sidebar_event_handler.send(LeftSidebarEvent::EnableSecondary(SecondarySidebar::Profile)), 214 | // on_click_inactive: move |_| chat_sidebar_event_handler.send(LeftSidebarEvent::DisableSecondary(SecondarySidebar::Profile)), 215 | button_props: &cx.props, 216 | svg { 217 | xmlns: "http://www.w3.org/2000/svg", 218 | class: "h-6 w-6", 219 | view_box: "0 0 24 24", 220 | stroke_width: "2", 221 | stroke: "currentColor", 222 | fill: "none", 223 | stroke_linecap: "round", 224 | stroke_linejoin: "round", 225 | path { 226 | stroke: "none", 227 | d: "M0 0h24v24H0z", 228 | fill: "none", 229 | } 230 | path { 231 | d: "M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0", 232 | } 233 | path { 234 | d: "M12 10m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0", 235 | } 236 | path { 237 | d: "M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855", 238 | } 239 | } 240 | } 241 | } 242 | } 243 | 244 | pub fn SettingsButton(cx: Scope) -> Element { 245 | let app_event_handler = use_coroutine_handle::(cx).unwrap(); 246 | render! { 247 | RawButton { 248 | button_props: &cx.props, 249 | on_click: |_| app_event_handler.send(AppEvents::ToggleSettingsSidebar), 250 | svg { 251 | xmlns: "http://www.w3.org/2000/svg", 252 | class: "h-6 w-6", 253 | view_box: "0 0 24 24", 254 | stroke_width: "2", 255 | stroke: "currentColor", 256 | fill: "none", 257 | stroke_linecap: "round", 258 | stroke_linejoin: "round", 259 | path { 260 | stroke: "none", 261 | d: "M0 0h24v24H0z", 262 | fill: "none", 263 | } 264 | path { 265 | d: "M19.875 6.27a2.225 2.225 0 0 1 1.125 1.948v7.284c0 .809 -.443 1.555 -1.158 1.948l-6.75 4.27a2.269 2.269 0 0 1 -2.184 0l-6.75 -4.27a2.225 2.225 0 0 1 -1.158 -1.948v-7.285c0 -.809 .443 -1.554 1.158 -1.947l6.75 -3.98a2.33 2.33 0 0 1 2.25 0l6.75 3.98h-.033z", 266 | } 267 | path { 268 | d: "M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0", 269 | } 270 | } 271 | } 272 | } 273 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /docs/assets/dioxus/snippets/dioxus-interpreter-js-603636eeca72cf05/inline0.js: -------------------------------------------------------------------------------- 1 | let m,p,ls,d,t,op,i,e,z,metaflags; 2 | 3 | class ListenerMap { 4 | constructor(root) { 5 | // bubbling events can listen at the root element 6 | this.global = {}; 7 | // non bubbling events listen at the element the listener was created at 8 | this.local = {}; 9 | this.root = null; 10 | this.handler = null; 11 | } 12 | 13 | create(event_name, element, bubbles) { 14 | if (bubbles) { 15 | if (this.global[event_name] === undefined) { 16 | this.global[event_name] = {}; 17 | this.global[event_name].active = 1; 18 | this.root.addEventListener(event_name, this.handler); 19 | } else { 20 | this.global[event_name].active++; 21 | } 22 | } 23 | else { 24 | const id = element.getAttribute("data-dioxus-id"); 25 | if (!this.local[id]) { 26 | this.local[id] = {}; 27 | } 28 | element.addEventListener(event_name, this.handler); 29 | } 30 | } 31 | 32 | remove(element, event_name, bubbles) { 33 | if (bubbles) { 34 | this.global[event_name].active--; 35 | if (this.global[event_name].active === 0) { 36 | this.root.removeEventListener(event_name, this.global[event_name].callback); 37 | delete this.global[event_name]; 38 | } 39 | } 40 | else { 41 | const id = element.getAttribute("data-dioxus-id"); 42 | delete this.local[id][event_name]; 43 | if (this.local[id].length === 0) { 44 | delete this.local[id]; 45 | } 46 | element.removeEventListener(event_name, this.handler); 47 | } 48 | } 49 | 50 | removeAllNonBubbling(element) { 51 | const id = element.getAttribute("data-dioxus-id"); 52 | delete this.local[id]; 53 | } 54 | } 55 | function SetAttributeInner(node, field, value, ns) { 56 | const name = field; 57 | if (ns === "style") { 58 | // ????? why do we need to do this 59 | if (node.style === undefined) { 60 | node.style = {}; 61 | } 62 | node.style[name] = value; 63 | } else if (ns !== null && ns !== undefined && ns !== "") { 64 | node.setAttributeNS(ns, name, value); 65 | } else { 66 | switch (name) { 67 | case "value": 68 | if (value !== node.value) { 69 | node.value = value; 70 | } 71 | break; 72 | case "initial_value": 73 | node.defaultValue = value; 74 | break; 75 | case "checked": 76 | node.checked = truthy(value); 77 | break; 78 | case "selected": 79 | node.selected = truthy(value); 80 | break; 81 | case "dangerous_inner_html": 82 | node.innerHTML = value; 83 | break; 84 | default: 85 | // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 86 | if (!truthy(value) && bool_attrs.hasOwnProperty(name)) { 87 | node.removeAttribute(name); 88 | } else { 89 | node.setAttribute(name, value); 90 | } 91 | } 92 | } 93 | } 94 | function LoadChild(ptr, len) { 95 | // iterate through each number and get that child 96 | node = stack[stack.length - 1]; 97 | ptr_end = ptr + len; 98 | for (; ptr < ptr_end; ptr++) { 99 | end = m.getUint8(ptr); 100 | for (node = node.firstChild; end > 0; end--) { 101 | node = node.nextSibling; 102 | } 103 | } 104 | return node; 105 | } 106 | const listeners = new ListenerMap(); 107 | let nodes = []; 108 | let stack = []; 109 | let root; 110 | const templates = {}; 111 | let node, els, end, ptr_end, k; 112 | export function save_template(nodes, tmpl_id) { 113 | templates[tmpl_id] = nodes; 114 | } 115 | export function set_node(id, node) { 116 | nodes[id] = node; 117 | } 118 | export function get_node(id) { 119 | return nodes[id]; 120 | } 121 | export function initilize(root, handler) { 122 | listeners.handler = handler; 123 | nodes = [root]; 124 | stack = [root]; 125 | listeners.root = root; 126 | } 127 | function AppendChildren(id, many){ 128 | root = nodes[id]; 129 | els = stack.splice(stack.length-many); 130 | for (k = 0; k < many; k++) { 131 | root.appendChild(els[k]); 132 | } 133 | } 134 | const bool_attrs = { 135 | allowfullscreen: true, 136 | allowpaymentrequest: true, 137 | async: true, 138 | autofocus: true, 139 | autoplay: true, 140 | checked: true, 141 | controls: true, 142 | default: true, 143 | defer: true, 144 | disabled: true, 145 | formnovalidate: true, 146 | hidden: true, 147 | ismap: true, 148 | itemscope: true, 149 | loop: true, 150 | multiple: true, 151 | muted: true, 152 | nomodule: true, 153 | novalidate: true, 154 | open: true, 155 | playsinline: true, 156 | readonly: true, 157 | required: true, 158 | reversed: true, 159 | selected: true, 160 | truespeed: true, 161 | webkitdirectory: true, 162 | }; 163 | function truthy(val) { 164 | return val === "true" || val === true; 165 | } 166 | const attr = []; 167 | let attr_tmp1, attr_tmp2; 168 | function get_attr() { 169 | attr_tmp2 = u8buf[u8bufp++]; 170 | if(attr_tmp2 & 128){ 171 | attr_tmp1=s.substring(sp,sp+=u8buf[u8bufp++]); 172 | attr[attr_tmp2&4294967167]=attr_tmp1; 173 | return attr_tmp1; 174 | } 175 | else{ 176 | return attr[attr_tmp2&4294967167]; 177 | } 178 | }const evt = []; 179 | let evt_tmp1, evt_tmp2; 180 | function get_evt() { 181 | evt_tmp2 = u8buf[u8bufp++]; 182 | if(evt_tmp2 & 128){ 183 | evt_tmp1=s.substring(sp,sp+=u8buf[u8bufp++]); 184 | evt[evt_tmp2&4294967167]=evt_tmp1; 185 | return evt_tmp1; 186 | } 187 | else{ 188 | return evt[evt_tmp2&4294967167]; 189 | } 190 | }const ns_cache = []; 191 | let ns_cache_tmp1, ns_cache_tmp2; 192 | function get_ns_cache() { 193 | ns_cache_tmp2 = u8buf[u8bufp++]; 194 | if(ns_cache_tmp2 & 128){ 195 | ns_cache_tmp1=s.substring(sp,sp+=u8buf[u8bufp++]); 196 | ns_cache[ns_cache_tmp2&4294967167]=ns_cache_tmp1; 197 | return ns_cache_tmp1; 198 | } 199 | else{ 200 | return ns_cache[ns_cache_tmp2&4294967167]; 201 | } 202 | }let s = "";let lsp,sp,sl; let c = new TextDecoder();let u32buf,u32bufp;let u8buf,u8bufp; 203 | let id,bubbles,ns,event_name,value,field,len,ptr; 204 | export function create(r){ 205 | d=r; 206 | } 207 | export function update_memory(b){ 208 | m=new DataView(b.buffer) 209 | } 210 | export function run(){ 211 | metaflags=m.getUint32(d,true); 212 | if((metaflags>>>12)&1){ 213 | ls=m.getUint32(d+12*4,true); 214 | } 215 | p=ls; 216 | if (metaflags&1){ 217 | lsp = m.getUint32(d+1*4,true); 218 | } 219 | if ((metaflags>>>2)&1) { 220 | sl = m.getUint32(d+2*4,true); 221 | if ((metaflags>>>1)&1) { 222 | sp = lsp; 223 | s = ""; 224 | e = sp + ((sl / 4) | 0) * 4; 225 | while (sp < e) { 226 | t = m.getUint32(sp, true); 227 | s += String.fromCharCode( 228 | t & 255, 229 | (t & 65280) >> 8, 230 | (t & 16711680) >> 16, 231 | t >> 24 232 | ); 233 | sp += 4; 234 | } 235 | while (sp < lsp + sl) { 236 | s += String.fromCharCode(m.getUint8(sp++)); 237 | } 238 | } else { 239 | s = c.decode(new DataView(m.buffer, lsp, sl)); 240 | } 241 | } 242 | sp=0;if ((metaflags>>>3)&1){ 243 | u32buf=new Uint32Array(m.buffer,m.getUint32(d+3*4,true)) 244 | } 245 | u32bufp=0;if ((metaflags>>>5)&1){ 246 | u8buf=new Uint8Array(m.buffer,m.getUint32(d+5*4,true)) 247 | } 248 | u8bufp=0; 249 | for(;;){ 250 | op=m.getUint32(p,true); 251 | p+=4; 252 | z=0; 253 | while(z++<4){ 254 | switch(op&255){ 255 | case 0:{AppendChildren(root, stack.length-1);}break;case 1:{stack.push(nodes[u32buf[u32bufp++]]);}break;case 2:{AppendChildren(u32buf[u32bufp++], u32buf[u32bufp++]);}break;case 3:{stack.pop();}break;case 4:{root = nodes[u32buf[u32bufp++]]; els = stack.splice(stack.length-u32buf[u32bufp++]); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}break;case 5:{nodes[u32buf[u32bufp++]].after(...stack.splice(stack.length-u32buf[u32bufp++]));}break;case 6:{nodes[u32buf[u32bufp++]].before(...stack.splice(stack.length-u32buf[u32bufp++]));}break;case 7:{node = nodes[u32buf[u32bufp++]]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}break;case 8:{stack.push(document.createTextNode(s.substring(sp,sp+=u32buf[u32bufp++])));}break;case 9:{node = document.createTextNode(s.substring(sp,sp+=u32buf[u32bufp++])); nodes[u32buf[u32bufp++]] = node; stack.push(node);}break;case 10:{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[u32buf[u32bufp++]] = node;}break;case 11:event_name=get_evt();id=u32buf[u32bufp++];bubbles=u8buf[u8bufp++];node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `${id}`); listeners.create(event_name, node, bubbles);break;case 12:{node = nodes[u32buf[u32bufp++]]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, get_evt(), u8buf[u8bufp++]);}break;case 13:{nodes[u32buf[u32bufp++]].textContent = s.substring(sp,sp+=u32buf[u32bufp++]);}break;case 14:{node = nodes[u32buf[u32bufp++]]; SetAttributeInner(node, get_attr(), s.substring(sp,sp+=u32buf[u32bufp++]), get_ns_cache());}break;case 15:id=u32buf[u32bufp++];field=get_attr();ns=get_ns_cache();{ 256 | node = nodes[id]; 257 | if (!ns) { 258 | switch (field) { 259 | case "value": 260 | node.value = ""; 261 | break; 262 | case "checked": 263 | node.checked = false; 264 | break; 265 | case "selected": 266 | node.selected = false; 267 | break; 268 | case "dangerous_inner_html": 269 | node.innerHTML = ""; 270 | break; 271 | default: 272 | node.removeAttribute(field); 273 | break; 274 | } 275 | } else if (ns == "style") { 276 | node.style.removeProperty(name); 277 | } else { 278 | node.removeAttributeNS(ns, field); 279 | } 280 | }break;case 16:{nodes[u32buf[u32bufp++]] = LoadChild(u32buf[u32bufp++], u8buf[u8bufp++]);}break;case 17:ptr=u32buf[u32bufp++];len=u8buf[u8bufp++];value=s.substring(sp,sp+=u32buf[u32bufp++]);id=u32buf[u32bufp++];{ 281 | node = LoadChild(ptr, len); 282 | if (node.nodeType == Node.TEXT_NODE) { 283 | node.textContent = value; 284 | } else { 285 | let text = document.createTextNode(value); 286 | node.replaceWith(text); 287 | node = text; 288 | } 289 | nodes[id] = node; 290 | }break;case 18:{els = stack.splice(stack.length - u32buf[u32bufp++]); node = LoadChild(u32buf[u32bufp++], u8buf[u8bufp++]); node.replaceWith(...els);}break;case 19:{node = templates[u32buf[u32bufp++]][u32buf[u32bufp++]].cloneNode(true); nodes[u32buf[u32bufp++]] = node; stack.push(node);}break;case 20:return true; 291 | } 292 | op>>>=8; 293 | } 294 | } 295 | } -------------------------------------------------------------------------------- /src/components/setting_sidebar.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use futures_util::StreamExt; 3 | use transprompt::async_openai::Client; 4 | use transprompt::async_openai::config::{AzureConfig, OpenAIConfig}; 5 | 6 | use crate::pages::app::{AppEvents, AuthedClient}; 7 | use crate::utils::auth::Auth; 8 | use crate::utils::settings::{GPTService, OpenAIModel}; 9 | use crate::utils::storage::StoredStates; 10 | 11 | const API_KEY: &str = "api-key"; 12 | const API_BASE: &str = "base-url"; 13 | const ORG_ID: &str = "org-id"; 14 | const API_VERSION: &str = "api-version"; 15 | const DEPLOYMENT_ID: &str = "deployment-id"; 16 | 17 | #[derive(Debug, Clone, PartialEq)] 18 | enum SettingEvent { 19 | SetGroupChat(bool), 20 | SelectService(Option), 21 | SaveServiceConfig(Option), 22 | } 23 | 24 | 25 | #[derive(Debug, Clone, PartialEq, Default)] 26 | struct ServiceSettings { 27 | api_key: Option, 28 | api_base: Option, 29 | org_id: Option, 30 | api_version: Option, 31 | deployment_id: Option, 32 | openai_model: Option, 33 | } 34 | 35 | async fn setting_event_handler(mut rx: UnboundedReceiver, 36 | enable_group_chat: UseState, 37 | authed_client: UseSharedState, 38 | service_settings: UseSharedState, 39 | global: UseSharedState) { 40 | while let Some(event) = rx.next().await { 41 | log::info!("setting_event_handler {:?}", event); 42 | match event { 43 | SettingEvent::SetGroupChat(s) => enable_group_chat.set(s), 44 | SettingEvent::SelectService(service) => { 45 | match service.as_ref() { 46 | None => *service_settings.write() = ServiceSettings::default(), 47 | Some(s) => { 48 | if *s == GPTService::AzureOpenAI { 49 | service_settings.write().openai_model = None; 50 | } 51 | } 52 | }; 53 | log::info!("Selected service: {:?}", service); 54 | global.write().selected_service = service; 55 | } 56 | SettingEvent::SaveServiceConfig(openai_model) => { 57 | let gpt_service = global.read().selected_service.clone(); 58 | log::info!("Saving service configs for {:?}", gpt_service); 59 | match gpt_service { 60 | None => unreachable!(), 61 | Some(gpt_service) => { 62 | let service_settings = service_settings.read(); 63 | // check fields first 64 | match gpt_service { 65 | GPTService::AzureOpenAI => { 66 | if service_settings.api_key.is_none() { 67 | log::error!("API Key is required"); 68 | continue; 69 | } 70 | if service_settings.api_base.is_none() { 71 | log::error!("API Base is required"); 72 | continue; 73 | } 74 | if service_settings.deployment_id.is_none() { 75 | log::error!("Deployment ID is required"); 76 | continue; 77 | } 78 | if service_settings.api_version.is_none() { 79 | log::error!("API Version is required"); 80 | continue; 81 | } 82 | } 83 | GPTService::OpenAI => { 84 | if service_settings.api_key.is_none() { 85 | log::error!("API Key is required"); 86 | continue; 87 | } 88 | if openai_model.is_none() { 89 | log::error!("Model is required"); 90 | continue; 91 | } 92 | } 93 | } 94 | // save configs 95 | let (new_auth, new_authed_client): (Auth, Client) = match gpt_service { 96 | GPTService::AzureOpenAI => { 97 | let auth = Auth::AzureOpenAI { 98 | api_version: service_settings.api_version.to_owned().unwrap(), 99 | deployment_id: service_settings.deployment_id.to_owned().unwrap(), 100 | api_base: service_settings.api_base.to_owned().unwrap(), 101 | api_key: service_settings.api_key.to_owned().unwrap(), 102 | }; 103 | let client = Client::with_config::(auth.clone().into()); 104 | (auth, client) 105 | } 106 | GPTService::OpenAI => { 107 | let auth = Auth::OpenAI { 108 | api_key: service_settings.api_key.to_owned().unwrap(), 109 | org_id: service_settings.org_id.to_owned(), 110 | api_base: service_settings.api_base.to_owned(), 111 | }; 112 | let client = Client::with_config::(auth.clone().into()); 113 | (auth, client) 114 | } 115 | }; 116 | let mut global = global.write(); 117 | global.openai_model = openai_model; 118 | global.auth.replace(new_auth); 119 | authed_client.write().replace(new_authed_client); 120 | global.save(); 121 | // TODO: remove this after testing 122 | log::info!("Saved new auth: {:?}", global.auth); 123 | } 124 | } 125 | } 126 | } 127 | } 128 | log::error!("setting_event_handler exited"); 129 | } 130 | 131 | 132 | pub fn SettingSidebar(cx: Scope) -> Element { 133 | // get global states 134 | let global = use_shared_state::(cx).unwrap(); 135 | let authed_client = use_shared_state::(cx).unwrap(); 136 | // setup local states 137 | let enable_group_chat = use_state(cx, || false); 138 | // setup shared states 139 | use_shared_state_provider(cx, ServiceSettings::default); 140 | let service_settings = use_shared_state::(cx).unwrap(); 141 | use_coroutine(cx, |rx| setting_event_handler(rx, 142 | enable_group_chat.to_owned(), 143 | authed_client.to_owned(), 144 | service_settings.to_owned(), 145 | global.to_owned())); 146 | render! { 147 | aside { 148 | class: "flex", 149 | div { 150 | class: "relative h-screen w-full overflow-y-auto border-l border-slate-300 bg-slate-50 py-8 dark:border-slate-700 dark:bg-slate-900 sm:w-full", 151 | div { 152 | class: "mb-4 flex items-center gap-x-2 px-2 text-slate-800 dark:text-slate-200", 153 | CloseSettingButton {}, 154 | h2 { 155 | class: "text-lg font-medium", 156 | "Settings" 157 | } 158 | } 159 | ToggleGroupChat {} 160 | ServiceConfigs { 161 | gpt_service: global.read().selected_service.clone(), 162 | enable_group_chat: *enable_group_chat.get(), 163 | } 164 | ModelParameters {} 165 | } 166 | } 167 | } 168 | } 169 | 170 | fn CloseSettingButton(cx: Scope) -> Element { 171 | let app_event_handler = use_coroutine_handle::(cx).unwrap(); 172 | render! { 173 | button { 174 | class: "inline-flex rounded-lg p-1 hover:bg-slate-700", 175 | onclick: |_| app_event_handler.send(AppEvents::ToggleSettingsSidebar), 176 | svg { 177 | xmlns: "http://www.w3.org/2000/svg", 178 | class: "h-6 w-6", 179 | stroke_width: "2", 180 | stroke: "currentColor", 181 | fill: "none", 182 | stroke_linecap: "round", 183 | stroke_linejoin: "round", 184 | path { 185 | stroke: "none", 186 | d: "M0 0h24v24H0z", 187 | fill: "none", 188 | } 189 | path { 190 | d: "M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z", 191 | } 192 | path { 193 | d: "M9 4v16", 194 | } 195 | path { 196 | d: "M14 10l2 2l-2 2", 197 | } 198 | } 199 | span { 200 | class: "sr-only", 201 | "Close settings sidebar" 202 | } 203 | } 204 | } 205 | } 206 | 207 | fn SelectServiceSection(cx: Scope) -> Element { 208 | const NULL_OPTION: &str = "Select AI Provider"; 209 | let setting_event_handler = use_coroutine_handle::(cx).unwrap(); 210 | 211 | render! { 212 | div { 213 | class: "px-2 py-4 text-slate-800 dark:text-slate-200", 214 | select { 215 | name: "select-service", 216 | id: "select-service", 217 | onchange: |select| { 218 | let value = select.data.value.as_str(); 219 | match value { 220 | "AzureOpenAI" => setting_event_handler.send(SettingEvent::SelectService(Some(GPTService::AzureOpenAI))), 221 | "OpenAI" => setting_event_handler.send(SettingEvent::SelectService(Some(GPTService::OpenAI))), 222 | NULL_OPTION => setting_event_handler.send(SettingEvent::SelectService(None)), 223 | _ => log::error!("Unknown select-service value: {}", value), 224 | } 225 | }, 226 | class: "mt-2 w-full cursor-pointer rounded-lg border-r-4 border-transparent bg-slate-200 py-3 pl-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-600 dark:bg-slate-800", 227 | option { 228 | value: "", 229 | "{NULL_OPTION}" 230 | } 231 | option { 232 | value: "AzureOpenAI", 233 | "Azure OpenAI" 234 | } 235 | option { 236 | value: "OpenAI", 237 | "OpenAI" 238 | } 239 | } 240 | } 241 | } 242 | } 243 | 244 | pub fn ToggleGroupChat(cx: Scope) -> Element { 245 | let setting_event_handler = use_coroutine_handle::(cx).unwrap(); 246 | render! { 247 | div { 248 | class: "px-2 py-4", 249 | label { 250 | class: "relative flex cursor-pointer items-center", 251 | input { 252 | r#type: "checkbox", 253 | onchange: |e| { 254 | let value = e.data.value.as_str(); 255 | match value { 256 | "true" => setting_event_handler.send(SettingEvent::SetGroupChat(true)), 257 | "false" => setting_event_handler.send(SettingEvent::SetGroupChat(false)), 258 | _ => log::error!("Unknown toggle value: {}", value), 259 | } 260 | }, 261 | value: "", 262 | class: "peer sr-only", 263 | } 264 | div { 265 | class: "peer h-6 w-11 rounded-full bg-slate-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-slate-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300 dark:border-slate-600 dark:bg-slate-700 dark:peer-focus:ring-blue-800", 266 | } 267 | span { 268 | class: "ml-3 text-sm font-medium text-slate-800 dark:text-slate-200", 269 | "Enable Group Chat" 270 | } 271 | } 272 | } 273 | } 274 | } 275 | 276 | #[derive(Props, PartialEq)] 277 | struct ServiceConfigsProps { 278 | #[props(! optional)] 279 | gpt_service: Option, 280 | enable_group_chat: bool, 281 | } 282 | 283 | enum ServiceEvent { 284 | SaveConfigs, 285 | SelectOpenAIModel(Option), 286 | } 287 | 288 | 289 | fn ServiceConfigs(cx: Scope) -> Element { 290 | // TODO: when the component is opened, display the stored configs if any 291 | let service_settings = use_shared_state::(cx).unwrap(); 292 | let setting_event_handler = use_coroutine_handle::(cx).unwrap(); 293 | let service_event_handler = use_coroutine(cx, |mut rx| { 294 | let service_settings = service_settings.to_owned(); 295 | let setting_event_handler = setting_event_handler.to_owned(); 296 | async move { 297 | while let Some(event) = rx.next().await { 298 | match event { 299 | ServiceEvent::SaveConfigs => setting_event_handler.send(SettingEvent::SaveServiceConfig(service_settings.read().openai_model.clone())), 300 | ServiceEvent::SelectOpenAIModel(model) => service_settings.write().openai_model = model, 301 | } 302 | } 303 | } 304 | }); 305 | render! { 306 | div { 307 | class: "my-4 border-t border-slate-300 px-2 py-4 text-slate-800 dark:border-slate-700 dark:text-slate-200", 308 | label { 309 | class: "px-2 text-xs uppercase text-slate-500 dark:text-slate-400", 310 | "Service Configurations" 311 | } 312 | SelectServiceSection {} 313 | if let Some(gpt_service) = cx.props.gpt_service { 314 | rsx! { 315 | SecretInputs { 316 | gpt_service: gpt_service, 317 | } 318 | if gpt_service == GPTService::OpenAI { 319 | rsx! { 320 | SelectOpenAIModel { 321 | enable_group_chat: cx.props.enable_group_chat, 322 | } 323 | } 324 | } 325 | button { 326 | r#type: "button", 327 | class: "mt-4 block w-full rounded-lg bg-slate-200 p-2.5 text-xs font-semibold hover:bg-blue-600 hover:text-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:bg-slate-800 dark:hover:bg-blue-600", 328 | onclick: |_| { 329 | service_event_handler.send(ServiceEvent::SaveConfigs) 330 | }, 331 | "Save Configs" 332 | } 333 | } 334 | } 335 | } 336 | } 337 | } 338 | 339 | #[derive(Props, PartialEq)] 340 | struct SecretInputsProps { 341 | gpt_service: GPTService, 342 | } 343 | 344 | fn SecretInputs(cx: Scope) -> Element { 345 | let service_settings = use_shared_state::(cx).unwrap(); 346 | const LABEL_STYLE: &str = "mb-2 mt-4 block px-2 text-sm font-medium"; 347 | const INPUT_STYLE: &str = "block w-full rounded-lg bg-slate-200 p-2.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-600 dark:bg-slate-800 dark:placeholder-slate-400 dark:focus:ring-blue-600"; 348 | match cx.props.gpt_service { 349 | GPTService::OpenAI => render! { 350 | div { 351 | // API Key 352 | label { 353 | r#for: "{API_KEY}", 354 | class: "{LABEL_STYLE}", 355 | "API Key" 356 | } 357 | input { 358 | r#type: "password", 359 | id: "{API_KEY}", 360 | class: "{INPUT_STYLE}", 361 | onchange: |c| { 362 | let value = &c.data.value; 363 | if value.is_empty() { 364 | service_settings.write().api_key = None; 365 | } else { 366 | service_settings.write().api_key = Some(value.to_string()); 367 | } 368 | }, 369 | placeholder: "Required" 370 | } 371 | // Base URL 372 | label { 373 | r#for: "{API_BASE}", 374 | class: "{LABEL_STYLE}", 375 | "Base URL / API Base (Optional)" 376 | } 377 | input { 378 | r#type: "url", 379 | id: "{API_BASE}", 380 | class: "{INPUT_STYLE}", 381 | onchange: |c| { 382 | let value = &c.data.value; 383 | if value.is_empty() { 384 | service_settings.write().api_base = None; 385 | } else { 386 | service_settings.write().api_base = Some(value.to_string()); 387 | } 388 | }, 389 | placeholder: "https://api.openai.com", 390 | } 391 | // Org ID 392 | label { 393 | r#for: "{ORG_ID}", 394 | class: "{LABEL_STYLE}", 395 | "Org ID (Optional)" 396 | } 397 | input { 398 | r#type: "text", 399 | id: "{ORG_ID}", 400 | class: "{INPUT_STYLE}", 401 | onchange: |c| { 402 | let value = &c.data.value; 403 | if value.is_empty() { 404 | service_settings.write().org_id = None; 405 | } else { 406 | service_settings.write().org_id = Some(value.to_string()); 407 | } 408 | }, 409 | } 410 | } 411 | }, 412 | GPTService::AzureOpenAI => render! { 413 | div { 414 | // API Key 415 | label { 416 | r#for: "{API_KEY}", 417 | class: "{LABEL_STYLE}", 418 | "API Key" 419 | } 420 | input { 421 | r#type: "password", 422 | id: "{API_KEY}", 423 | class: "{INPUT_STYLE}", 424 | placeholder: "Required", 425 | onchange: |c| { 426 | let value = &c.data.value; 427 | if value.is_empty() { 428 | service_settings.write().api_key = None; 429 | } else { 430 | service_settings.write().api_key = Some(value.to_string()); 431 | } 432 | }, 433 | } 434 | // Base URL 435 | label { 436 | r#for: "{API_BASE}", 437 | class: "{LABEL_STYLE}", 438 | "Base URL / API Base" 439 | } 440 | input { 441 | r#type: "url", 442 | id: "{API_BASE}", 443 | class: "{INPUT_STYLE}", 444 | placeholder: "Required", 445 | onchange: |c| { 446 | let value = &c.data.value; 447 | if value.is_empty() { 448 | service_settings.write().api_base = None; 449 | } else { 450 | service_settings.write().api_base = Some(value.to_string()); 451 | } 452 | }, 453 | } 454 | // Deployment ID 455 | label { 456 | r#for: "{DEPLOYMENT_ID}", 457 | class: "{LABEL_STYLE}", 458 | "Deployment ID" 459 | } 460 | input { 461 | r#type: "text", 462 | id: "{DEPLOYMENT_ID}", 463 | class: "{INPUT_STYLE}", 464 | placeholder: "Required", 465 | onchange: |c| { 466 | let value = &c.data.value; 467 | if value.is_empty() { 468 | service_settings.write().deployment_id = None; 469 | } else { 470 | service_settings.write().deployment_id = Some(value.to_string()); 471 | } 472 | }, 473 | } 474 | // API Version 475 | label { 476 | r#for: "{API_VERSION}", 477 | class: "{LABEL_STYLE}", 478 | "API Version" 479 | } 480 | input { 481 | r#type: "text", 482 | id: "{API_VERSION}", 483 | class: "{INPUT_STYLE}", 484 | placeholder: "Required", 485 | onchange: |c| { 486 | let value = &c.data.value; 487 | if value.is_empty() { 488 | service_settings.write().api_version = None; 489 | } else { 490 | service_settings.write().api_version = Some(value.to_string()); 491 | } 492 | }, 493 | } 494 | } 495 | } 496 | } 497 | } 498 | 499 | #[inline_props] 500 | fn SelectOpenAIModel(cx: Scope, enable_group_chat: bool) -> Element { 501 | const NULL_OPTION: &str = "Select a model"; 502 | let service_event_handler = use_coroutine_handle::(cx).unwrap(); 503 | let usable_models = if *enable_group_chat { 504 | OpenAIModel::gpt4_models() 505 | } else { 506 | OpenAIModel::all_models() 507 | }; 508 | render! { 509 | div { 510 | label { 511 | r#for: "select-model", 512 | class: "mb-2 mt-4 block px-2 text-sm font-medium", 513 | "Model" 514 | } 515 | select { 516 | name: "select-model", 517 | onchange: |change|{ 518 | let model = change.data.value.as_str(); 519 | if model == NULL_OPTION { 520 | service_event_handler.send(ServiceEvent::SelectOpenAIModel(None)); 521 | } else { 522 | match usable_models.iter().find(|m| (*m).eq(model)).cloned() { 523 | Some(m) => service_event_handler.send(ServiceEvent::SelectOpenAIModel(Some(m))), 524 | None => log::error!("Unknown model: {}", model), 525 | } 526 | } 527 | }, 528 | id: "select-model", 529 | class: "block w-full cursor-pointer rounded-lg border-r-4 border-transparent bg-slate-200 py-3 pl-1 text-xs focus:outline-none focus:ring-2 focus:ring-blue-600 dark:bg-slate-800 dark:placeholder-slate-400 dark:focus:ring-blue-600", 530 | option { 531 | value: "", 532 | "{NULL_OPTION}" 533 | } 534 | usable_models.iter().map(|model| rsx! { 535 | option { 536 | value: "{model}", 537 | "{model}" 538 | } 539 | }) 540 | } 541 | } 542 | } 543 | } 544 | 545 | fn ModelParameters(cx: Scope) -> Element { 546 | const LABEL_STYLE: &str = "mb-2 mt-4 block px-2 text-sm font-medium"; 547 | const INPUT_STYLE: &str = "block w-full rounded-lg bg-slate-200 p-2.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-600 dark:bg-slate-800 dark:placeholder-slate-400 dark:focus:ring-blue-600"; 548 | render! { 549 | div { 550 | class: "my-4 border-t border-slate-300 px-2 py-4 text-slate-800 dark:border-slate-700 dark:text-slate-200", 551 | label { 552 | class: "px-2 text-xs uppercase text-slate-500 dark:text-slate-400", 553 | "Model Configurations" 554 | } 555 | label { 556 | r#for: "max-tokens", 557 | class: "{LABEL_STYLE}", 558 | "Max tokens" 559 | } 560 | input { 561 | r#type: "number", 562 | id: "max-tokens", 563 | class: "{INPUT_STYLE}", 564 | placeholder: "2048", 565 | } 566 | label { 567 | r#for: "model-temperature", 568 | class: "{LABEL_STYLE}", 569 | "Temperature" 570 | } 571 | input { 572 | r#type: "number", 573 | id: "model-temperature", 574 | class: "{INPUT_STYLE}", 575 | placeholder: "0.7", 576 | } 577 | button { 578 | r#type: "button", 579 | class: "mt-4 block w-full rounded-lg bg-slate-200 p-2.5 text-xs font-semibold hover:bg-blue-600 hover:text-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:bg-slate-800 dark:hover:bg-blue-600", 580 | "Save Parameters" 581 | } 582 | } 583 | } 584 | } -------------------------------------------------------------------------------- /docs/tailwind.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | 6. Use the user's configured `sans` font-variation-settings by default. 35 | */ 36 | 37 | html { 38 | line-height: 1.5; 39 | /* 1 */ 40 | -webkit-text-size-adjust: 100%; 41 | /* 2 */ 42 | -moz-tab-size: 4; 43 | /* 3 */ 44 | -o-tab-size: 4; 45 | tab-size: 4; 46 | /* 3 */ 47 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 48 | /* 4 */ 49 | font-feature-settings: normal; 50 | /* 5 */ 51 | font-variation-settings: normal; 52 | /* 6 */ 53 | } 54 | 55 | /* 56 | 1. Remove the margin in all browsers. 57 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 58 | */ 59 | 60 | body { 61 | margin: 0; 62 | /* 1 */ 63 | line-height: inherit; 64 | /* 2 */ 65 | } 66 | 67 | /* 68 | 1. Add the correct height in Firefox. 69 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 70 | 3. Ensure horizontal rules are visible by default. 71 | */ 72 | 73 | hr { 74 | height: 0; 75 | /* 1 */ 76 | color: inherit; 77 | /* 2 */ 78 | border-top-width: 1px; 79 | /* 3 */ 80 | } 81 | 82 | /* 83 | Add the correct text decoration in Chrome, Edge, and Safari. 84 | */ 85 | 86 | abbr:where([title]) { 87 | -webkit-text-decoration: underline dotted; 88 | text-decoration: underline dotted; 89 | } 90 | 91 | /* 92 | Remove the default font size and weight for headings. 93 | */ 94 | 95 | h1, 96 | h2, 97 | h3, 98 | h4, 99 | h5, 100 | h6 { 101 | font-size: inherit; 102 | font-weight: inherit; 103 | } 104 | 105 | /* 106 | Reset links to optimize for opt-in styling instead of opt-out. 107 | */ 108 | 109 | a { 110 | color: inherit; 111 | text-decoration: inherit; 112 | } 113 | 114 | /* 115 | Add the correct font weight in Edge and Safari. 116 | */ 117 | 118 | b, 119 | strong { 120 | font-weight: bolder; 121 | } 122 | 123 | /* 124 | 1. Use the user's configured `mono` font family by default. 125 | 2. Correct the odd `em` font sizing in all browsers. 126 | */ 127 | 128 | code, 129 | kbd, 130 | samp, 131 | pre { 132 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 133 | /* 1 */ 134 | font-size: 1em; 135 | /* 2 */ 136 | } 137 | 138 | /* 139 | Add the correct font size in all browsers. 140 | */ 141 | 142 | small { 143 | font-size: 80%; 144 | } 145 | 146 | /* 147 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 148 | */ 149 | 150 | sub, 151 | sup { 152 | font-size: 75%; 153 | line-height: 0; 154 | position: relative; 155 | vertical-align: baseline; 156 | } 157 | 158 | sub { 159 | bottom: -0.25em; 160 | } 161 | 162 | sup { 163 | top: -0.5em; 164 | } 165 | 166 | /* 167 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 168 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 169 | 3. Remove gaps between table borders by default. 170 | */ 171 | 172 | table { 173 | text-indent: 0; 174 | /* 1 */ 175 | border-color: inherit; 176 | /* 2 */ 177 | border-collapse: collapse; 178 | /* 3 */ 179 | } 180 | 181 | /* 182 | 1. Change the font styles in all browsers. 183 | 2. Remove the margin in Firefox and Safari. 184 | 3. Remove default padding in all browsers. 185 | */ 186 | 187 | button, 188 | input, 189 | optgroup, 190 | select, 191 | textarea { 192 | font-family: inherit; 193 | /* 1 */ 194 | font-feature-settings: inherit; 195 | /* 1 */ 196 | font-variation-settings: inherit; 197 | /* 1 */ 198 | font-size: 100%; 199 | /* 1 */ 200 | font-weight: inherit; 201 | /* 1 */ 202 | line-height: inherit; 203 | /* 1 */ 204 | color: inherit; 205 | /* 1 */ 206 | margin: 0; 207 | /* 2 */ 208 | padding: 0; 209 | /* 3 */ 210 | } 211 | 212 | /* 213 | Remove the inheritance of text transform in Edge and Firefox. 214 | */ 215 | 216 | button, 217 | select { 218 | text-transform: none; 219 | } 220 | 221 | /* 222 | 1. Correct the inability to style clickable types in iOS and Safari. 223 | 2. Remove default button styles. 224 | */ 225 | 226 | button, 227 | [type='button'], 228 | [type='reset'], 229 | [type='submit'] { 230 | -webkit-appearance: button; 231 | /* 1 */ 232 | background-color: transparent; 233 | /* 2 */ 234 | background-image: none; 235 | /* 2 */ 236 | } 237 | 238 | /* 239 | Use the modern Firefox focus style for all focusable elements. 240 | */ 241 | 242 | :-moz-focusring { 243 | outline: auto; 244 | } 245 | 246 | /* 247 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 248 | */ 249 | 250 | :-moz-ui-invalid { 251 | box-shadow: none; 252 | } 253 | 254 | /* 255 | Add the correct vertical alignment in Chrome and Firefox. 256 | */ 257 | 258 | progress { 259 | vertical-align: baseline; 260 | } 261 | 262 | /* 263 | Correct the cursor style of increment and decrement buttons in Safari. 264 | */ 265 | 266 | ::-webkit-inner-spin-button, 267 | ::-webkit-outer-spin-button { 268 | height: auto; 269 | } 270 | 271 | /* 272 | 1. Correct the odd appearance in Chrome and Safari. 273 | 2. Correct the outline style in Safari. 274 | */ 275 | 276 | [type='search'] { 277 | -webkit-appearance: textfield; 278 | /* 1 */ 279 | outline-offset: -2px; 280 | /* 2 */ 281 | } 282 | 283 | /* 284 | Remove the inner padding in Chrome and Safari on macOS. 285 | */ 286 | 287 | ::-webkit-search-decoration { 288 | -webkit-appearance: none; 289 | } 290 | 291 | /* 292 | 1. Correct the inability to style clickable types in iOS and Safari. 293 | 2. Change font properties to `inherit` in Safari. 294 | */ 295 | 296 | ::-webkit-file-upload-button { 297 | -webkit-appearance: button; 298 | /* 1 */ 299 | font: inherit; 300 | /* 2 */ 301 | } 302 | 303 | /* 304 | Add the correct display in Chrome and Safari. 305 | */ 306 | 307 | summary { 308 | display: list-item; 309 | } 310 | 311 | /* 312 | Removes the default spacing and border for appropriate elements. 313 | */ 314 | 315 | blockquote, 316 | dl, 317 | dd, 318 | h1, 319 | h2, 320 | h3, 321 | h4, 322 | h5, 323 | h6, 324 | hr, 325 | figure, 326 | p, 327 | pre { 328 | margin: 0; 329 | } 330 | 331 | fieldset { 332 | margin: 0; 333 | padding: 0; 334 | } 335 | 336 | legend { 337 | padding: 0; 338 | } 339 | 340 | ol, 341 | ul, 342 | menu { 343 | list-style: none; 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | /* 349 | Reset default styling for dialogs. 350 | */ 351 | 352 | dialog { 353 | padding: 0; 354 | } 355 | 356 | /* 357 | Prevent resizing textareas horizontally by default. 358 | */ 359 | 360 | textarea { 361 | resize: vertical; 362 | } 363 | 364 | /* 365 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 366 | 2. Set the default placeholder color to the user's configured gray 400 color. 367 | */ 368 | 369 | input::-moz-placeholder, textarea::-moz-placeholder { 370 | opacity: 1; 371 | /* 1 */ 372 | color: #9ca3af; 373 | /* 2 */ 374 | } 375 | 376 | input::placeholder, 377 | textarea::placeholder { 378 | opacity: 1; 379 | /* 1 */ 380 | color: #9ca3af; 381 | /* 2 */ 382 | } 383 | 384 | /* 385 | Set the default cursor for buttons. 386 | */ 387 | 388 | button, 389 | [role="button"] { 390 | cursor: pointer; 391 | } 392 | 393 | /* 394 | Make sure disabled buttons don't get the pointer cursor. 395 | */ 396 | 397 | :disabled { 398 | cursor: default; 399 | } 400 | 401 | /* 402 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 403 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 404 | This can trigger a poorly considered lint error in some tools but is included by design. 405 | */ 406 | 407 | img, 408 | svg, 409 | video, 410 | canvas, 411 | audio, 412 | iframe, 413 | embed, 414 | object { 415 | display: block; 416 | /* 1 */ 417 | vertical-align: middle; 418 | /* 2 */ 419 | } 420 | 421 | /* 422 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 423 | */ 424 | 425 | img, 426 | video { 427 | max-width: 100%; 428 | height: auto; 429 | } 430 | 431 | /* Make elements with the HTML hidden attribute stay hidden by default */ 432 | 433 | [hidden] { 434 | display: none; 435 | } 436 | 437 | * { 438 | scrollbar-color: initial; 439 | scrollbar-width: initial; 440 | } 441 | 442 | *, ::before, ::after { 443 | --tw-border-spacing-x: 0; 444 | --tw-border-spacing-y: 0; 445 | --tw-translate-x: 0; 446 | --tw-translate-y: 0; 447 | --tw-rotate: 0; 448 | --tw-skew-x: 0; 449 | --tw-skew-y: 0; 450 | --tw-scale-x: 1; 451 | --tw-scale-y: 1; 452 | --tw-pan-x: ; 453 | --tw-pan-y: ; 454 | --tw-pinch-zoom: ; 455 | --tw-scroll-snap-strictness: proximity; 456 | --tw-gradient-from-position: ; 457 | --tw-gradient-via-position: ; 458 | --tw-gradient-to-position: ; 459 | --tw-ordinal: ; 460 | --tw-slashed-zero: ; 461 | --tw-numeric-figure: ; 462 | --tw-numeric-spacing: ; 463 | --tw-numeric-fraction: ; 464 | --tw-ring-inset: ; 465 | --tw-ring-offset-width: 0px; 466 | --tw-ring-offset-color: #fff; 467 | --tw-ring-color: rgb(59 130 246 / 0.5); 468 | --tw-ring-offset-shadow: 0 0 #0000; 469 | --tw-ring-shadow: 0 0 #0000; 470 | --tw-shadow: 0 0 #0000; 471 | --tw-shadow-colored: 0 0 #0000; 472 | --tw-blur: ; 473 | --tw-brightness: ; 474 | --tw-contrast: ; 475 | --tw-grayscale: ; 476 | --tw-hue-rotate: ; 477 | --tw-invert: ; 478 | --tw-saturate: ; 479 | --tw-sepia: ; 480 | --tw-drop-shadow: ; 481 | --tw-backdrop-blur: ; 482 | --tw-backdrop-brightness: ; 483 | --tw-backdrop-contrast: ; 484 | --tw-backdrop-grayscale: ; 485 | --tw-backdrop-hue-rotate: ; 486 | --tw-backdrop-invert: ; 487 | --tw-backdrop-opacity: ; 488 | --tw-backdrop-saturate: ; 489 | --tw-backdrop-sepia: ; 490 | } 491 | 492 | ::backdrop { 493 | --tw-border-spacing-x: 0; 494 | --tw-border-spacing-y: 0; 495 | --tw-translate-x: 0; 496 | --tw-translate-y: 0; 497 | --tw-rotate: 0; 498 | --tw-skew-x: 0; 499 | --tw-skew-y: 0; 500 | --tw-scale-x: 1; 501 | --tw-scale-y: 1; 502 | --tw-pan-x: ; 503 | --tw-pan-y: ; 504 | --tw-pinch-zoom: ; 505 | --tw-scroll-snap-strictness: proximity; 506 | --tw-gradient-from-position: ; 507 | --tw-gradient-via-position: ; 508 | --tw-gradient-to-position: ; 509 | --tw-ordinal: ; 510 | --tw-slashed-zero: ; 511 | --tw-numeric-figure: ; 512 | --tw-numeric-spacing: ; 513 | --tw-numeric-fraction: ; 514 | --tw-ring-inset: ; 515 | --tw-ring-offset-width: 0px; 516 | --tw-ring-offset-color: #fff; 517 | --tw-ring-color: rgb(59 130 246 / 0.5); 518 | --tw-ring-offset-shadow: 0 0 #0000; 519 | --tw-ring-shadow: 0 0 #0000; 520 | --tw-shadow: 0 0 #0000; 521 | --tw-shadow-colored: 0 0 #0000; 522 | --tw-blur: ; 523 | --tw-brightness: ; 524 | --tw-contrast: ; 525 | --tw-grayscale: ; 526 | --tw-hue-rotate: ; 527 | --tw-invert: ; 528 | --tw-saturate: ; 529 | --tw-sepia: ; 530 | --tw-drop-shadow: ; 531 | --tw-backdrop-blur: ; 532 | --tw-backdrop-brightness: ; 533 | --tw-backdrop-contrast: ; 534 | --tw-backdrop-grayscale: ; 535 | --tw-backdrop-hue-rotate: ; 536 | --tw-backdrop-invert: ; 537 | --tw-backdrop-opacity: ; 538 | --tw-backdrop-saturate: ; 539 | --tw-backdrop-sepia: ; 540 | } 541 | 542 | .prose { 543 | color: var(--tw-prose-body); 544 | max-width: 65ch; 545 | } 546 | 547 | .prose :where(p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 548 | margin-top: 1.25em; 549 | margin-bottom: 1.25em; 550 | } 551 | 552 | .prose :where([class~="lead"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 553 | color: var(--tw-prose-lead); 554 | font-size: 1.25em; 555 | line-height: 1.6; 556 | margin-top: 1.2em; 557 | margin-bottom: 1.2em; 558 | } 559 | 560 | .prose :where(a):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 561 | color: var(--tw-prose-links); 562 | text-decoration: underline; 563 | font-weight: 500; 564 | } 565 | 566 | .prose :where(strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 567 | color: var(--tw-prose-bold); 568 | font-weight: 600; 569 | } 570 | 571 | .prose :where(a strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 572 | color: inherit; 573 | } 574 | 575 | .prose :where(blockquote strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 576 | color: inherit; 577 | } 578 | 579 | .prose :where(thead th strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 580 | color: inherit; 581 | } 582 | 583 | .prose :where(ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 584 | list-style-type: decimal; 585 | margin-top: 1.25em; 586 | margin-bottom: 1.25em; 587 | padding-left: 1.625em; 588 | } 589 | 590 | .prose :where(ol[type="A"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 591 | list-style-type: upper-alpha; 592 | } 593 | 594 | .prose :where(ol[type="a"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 595 | list-style-type: lower-alpha; 596 | } 597 | 598 | .prose :where(ol[type="A" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 599 | list-style-type: upper-alpha; 600 | } 601 | 602 | .prose :where(ol[type="a" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 603 | list-style-type: lower-alpha; 604 | } 605 | 606 | .prose :where(ol[type="I"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 607 | list-style-type: upper-roman; 608 | } 609 | 610 | .prose :where(ol[type="i"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 611 | list-style-type: lower-roman; 612 | } 613 | 614 | .prose :where(ol[type="I" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 615 | list-style-type: upper-roman; 616 | } 617 | 618 | .prose :where(ol[type="i" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 619 | list-style-type: lower-roman; 620 | } 621 | 622 | .prose :where(ol[type="1"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 623 | list-style-type: decimal; 624 | } 625 | 626 | .prose :where(ul):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 627 | list-style-type: disc; 628 | margin-top: 1.25em; 629 | margin-bottom: 1.25em; 630 | padding-left: 1.625em; 631 | } 632 | 633 | .prose :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker { 634 | font-weight: 400; 635 | color: var(--tw-prose-counters); 636 | } 637 | 638 | .prose :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker { 639 | color: var(--tw-prose-bullets); 640 | } 641 | 642 | .prose :where(dt):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 643 | color: var(--tw-prose-headings); 644 | font-weight: 600; 645 | margin-top: 1.25em; 646 | } 647 | 648 | .prose :where(hr):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 649 | border-color: var(--tw-prose-hr); 650 | border-top-width: 1px; 651 | margin-top: 3em; 652 | margin-bottom: 3em; 653 | } 654 | 655 | .prose :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 656 | font-weight: 500; 657 | font-style: italic; 658 | color: var(--tw-prose-quotes); 659 | border-left-width: 0.25rem; 660 | border-left-color: var(--tw-prose-quote-borders); 661 | quotes: "\201C""\201D""\2018""\2019"; 662 | margin-top: 1.6em; 663 | margin-bottom: 1.6em; 664 | padding-left: 1em; 665 | } 666 | 667 | .prose :where(blockquote p:first-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { 668 | content: open-quote; 669 | } 670 | 671 | .prose :where(blockquote p:last-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { 672 | content: close-quote; 673 | } 674 | 675 | .prose :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 676 | color: var(--tw-prose-headings); 677 | font-weight: 800; 678 | font-size: 2.25em; 679 | margin-top: 0; 680 | margin-bottom: 0.8888889em; 681 | line-height: 1.1111111; 682 | } 683 | 684 | .prose :where(h1 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 685 | font-weight: 900; 686 | color: inherit; 687 | } 688 | 689 | .prose :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 690 | color: var(--tw-prose-headings); 691 | font-weight: 700; 692 | font-size: 1.5em; 693 | margin-top: 2em; 694 | margin-bottom: 1em; 695 | line-height: 1.3333333; 696 | } 697 | 698 | .prose :where(h2 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 699 | font-weight: 800; 700 | color: inherit; 701 | } 702 | 703 | .prose :where(h3):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 704 | color: var(--tw-prose-headings); 705 | font-weight: 600; 706 | font-size: 1.25em; 707 | margin-top: 1.6em; 708 | margin-bottom: 0.6em; 709 | line-height: 1.6; 710 | } 711 | 712 | .prose :where(h3 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 713 | font-weight: 700; 714 | color: inherit; 715 | } 716 | 717 | .prose :where(h4):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 718 | color: var(--tw-prose-headings); 719 | font-weight: 600; 720 | margin-top: 1.5em; 721 | margin-bottom: 0.5em; 722 | line-height: 1.5; 723 | } 724 | 725 | .prose :where(h4 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 726 | font-weight: 700; 727 | color: inherit; 728 | } 729 | 730 | .prose :where(img):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 731 | margin-top: 2em; 732 | margin-bottom: 2em; 733 | } 734 | 735 | .prose :where(picture):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 736 | display: block; 737 | margin-top: 2em; 738 | margin-bottom: 2em; 739 | } 740 | 741 | .prose :where(kbd):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 742 | font-weight: 500; 743 | font-family: inherit; 744 | color: var(--tw-prose-kbd); 745 | box-shadow: 0 0 0 1px rgb(var(--tw-prose-kbd-shadows) / 10%), 0 3px 0 rgb(var(--tw-prose-kbd-shadows) / 10%); 746 | font-size: 0.875em; 747 | border-radius: 0.3125rem; 748 | padding-top: 0.1875em; 749 | padding-right: 0.375em; 750 | padding-bottom: 0.1875em; 751 | padding-left: 0.375em; 752 | } 753 | 754 | .prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 755 | color: var(--tw-prose-code); 756 | font-weight: 600; 757 | font-size: 0.875em; 758 | } 759 | 760 | .prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { 761 | content: "`"; 762 | } 763 | 764 | .prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { 765 | content: "`"; 766 | } 767 | 768 | .prose :where(a code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 769 | color: inherit; 770 | } 771 | 772 | .prose :where(h1 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 773 | color: inherit; 774 | } 775 | 776 | .prose :where(h2 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 777 | color: inherit; 778 | font-size: 0.875em; 779 | } 780 | 781 | .prose :where(h3 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 782 | color: inherit; 783 | font-size: 0.9em; 784 | } 785 | 786 | .prose :where(h4 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 787 | color: inherit; 788 | } 789 | 790 | .prose :where(blockquote code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 791 | color: inherit; 792 | } 793 | 794 | .prose :where(thead th code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 795 | color: inherit; 796 | } 797 | 798 | .prose :where(pre):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 799 | color: var(--tw-prose-pre-code); 800 | background-color: var(--tw-prose-pre-bg); 801 | overflow-x: auto; 802 | font-weight: 400; 803 | font-size: 0.875em; 804 | line-height: 1.7142857; 805 | margin-top: 1.7142857em; 806 | margin-bottom: 1.7142857em; 807 | border-radius: 0.375rem; 808 | padding-top: 0.8571429em; 809 | padding-right: 1.1428571em; 810 | padding-bottom: 0.8571429em; 811 | padding-left: 1.1428571em; 812 | } 813 | 814 | .prose :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 815 | background-color: transparent; 816 | border-width: 0; 817 | border-radius: 0; 818 | padding: 0; 819 | font-weight: inherit; 820 | color: inherit; 821 | font-size: inherit; 822 | font-family: inherit; 823 | line-height: inherit; 824 | } 825 | 826 | .prose :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { 827 | content: none; 828 | } 829 | 830 | .prose :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { 831 | content: none; 832 | } 833 | 834 | .prose :where(table):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 835 | width: 100%; 836 | table-layout: auto; 837 | text-align: left; 838 | margin-top: 2em; 839 | margin-bottom: 2em; 840 | font-size: 0.875em; 841 | line-height: 1.7142857; 842 | } 843 | 844 | .prose :where(thead):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 845 | border-bottom-width: 1px; 846 | border-bottom-color: var(--tw-prose-th-borders); 847 | } 848 | 849 | .prose :where(thead th):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 850 | color: var(--tw-prose-headings); 851 | font-weight: 600; 852 | vertical-align: bottom; 853 | padding-right: 0.5714286em; 854 | padding-bottom: 0.5714286em; 855 | padding-left: 0.5714286em; 856 | } 857 | 858 | .prose :where(tbody tr):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 859 | border-bottom-width: 1px; 860 | border-bottom-color: var(--tw-prose-td-borders); 861 | } 862 | 863 | .prose :where(tbody tr:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 864 | border-bottom-width: 0; 865 | } 866 | 867 | .prose :where(tbody td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 868 | vertical-align: baseline; 869 | } 870 | 871 | .prose :where(tfoot):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 872 | border-top-width: 1px; 873 | border-top-color: var(--tw-prose-th-borders); 874 | } 875 | 876 | .prose :where(tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 877 | vertical-align: top; 878 | } 879 | 880 | .prose :where(figure > *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 881 | margin-top: 0; 882 | margin-bottom: 0; 883 | } 884 | 885 | .prose :where(figcaption):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 886 | color: var(--tw-prose-captions); 887 | font-size: 0.875em; 888 | line-height: 1.4285714; 889 | margin-top: 0.8571429em; 890 | } 891 | 892 | .prose { 893 | --tw-prose-body: #374151; 894 | --tw-prose-headings: #111827; 895 | --tw-prose-lead: #4b5563; 896 | --tw-prose-links: #111827; 897 | --tw-prose-bold: #111827; 898 | --tw-prose-counters: #6b7280; 899 | --tw-prose-bullets: #d1d5db; 900 | --tw-prose-hr: #e5e7eb; 901 | --tw-prose-quotes: #111827; 902 | --tw-prose-quote-borders: #e5e7eb; 903 | --tw-prose-captions: #6b7280; 904 | --tw-prose-kbd: #111827; 905 | --tw-prose-kbd-shadows: 17 24 39; 906 | --tw-prose-code: #111827; 907 | --tw-prose-pre-code: #e5e7eb; 908 | --tw-prose-pre-bg: #1f2937; 909 | --tw-prose-th-borders: #d1d5db; 910 | --tw-prose-td-borders: #e5e7eb; 911 | --tw-prose-invert-body: #d1d5db; 912 | --tw-prose-invert-headings: #fff; 913 | --tw-prose-invert-lead: #9ca3af; 914 | --tw-prose-invert-links: #fff; 915 | --tw-prose-invert-bold: #fff; 916 | --tw-prose-invert-counters: #9ca3af; 917 | --tw-prose-invert-bullets: #4b5563; 918 | --tw-prose-invert-hr: #374151; 919 | --tw-prose-invert-quotes: #f3f4f6; 920 | --tw-prose-invert-quote-borders: #374151; 921 | --tw-prose-invert-captions: #9ca3af; 922 | --tw-prose-invert-kbd: #fff; 923 | --tw-prose-invert-kbd-shadows: 255 255 255; 924 | --tw-prose-invert-code: #fff; 925 | --tw-prose-invert-pre-code: #d1d5db; 926 | --tw-prose-invert-pre-bg: rgb(0 0 0 / 50%); 927 | --tw-prose-invert-th-borders: #4b5563; 928 | --tw-prose-invert-td-borders: #374151; 929 | font-size: 1rem; 930 | line-height: 1.75; 931 | } 932 | 933 | .prose :where(picture > img):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 934 | margin-top: 0; 935 | margin-bottom: 0; 936 | } 937 | 938 | .prose :where(video):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 939 | margin-top: 2em; 940 | margin-bottom: 2em; 941 | } 942 | 943 | .prose :where(li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 944 | margin-top: 0.5em; 945 | margin-bottom: 0.5em; 946 | } 947 | 948 | .prose :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 949 | padding-left: 0.375em; 950 | } 951 | 952 | .prose :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 953 | padding-left: 0.375em; 954 | } 955 | 956 | .prose :where(.prose > ul > li p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 957 | margin-top: 0.75em; 958 | margin-bottom: 0.75em; 959 | } 960 | 961 | .prose :where(.prose > ul > li > *:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 962 | margin-top: 1.25em; 963 | } 964 | 965 | .prose :where(.prose > ul > li > *:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 966 | margin-bottom: 1.25em; 967 | } 968 | 969 | .prose :where(.prose > ol > li > *:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 970 | margin-top: 1.25em; 971 | } 972 | 973 | .prose :where(.prose > ol > li > *:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 974 | margin-bottom: 1.25em; 975 | } 976 | 977 | .prose :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 978 | margin-top: 0.75em; 979 | margin-bottom: 0.75em; 980 | } 981 | 982 | .prose :where(dl):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 983 | margin-top: 1.25em; 984 | margin-bottom: 1.25em; 985 | } 986 | 987 | .prose :where(dd):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 988 | margin-top: 0.5em; 989 | padding-left: 1.625em; 990 | } 991 | 992 | .prose :where(hr + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 993 | margin-top: 0; 994 | } 995 | 996 | .prose :where(h2 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 997 | margin-top: 0; 998 | } 999 | 1000 | .prose :where(h3 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1001 | margin-top: 0; 1002 | } 1003 | 1004 | .prose :where(h4 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1005 | margin-top: 0; 1006 | } 1007 | 1008 | .prose :where(thead th:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1009 | padding-left: 0; 1010 | } 1011 | 1012 | .prose :where(thead th:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1013 | padding-right: 0; 1014 | } 1015 | 1016 | .prose :where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1017 | padding-top: 0.5714286em; 1018 | padding-right: 0.5714286em; 1019 | padding-bottom: 0.5714286em; 1020 | padding-left: 0.5714286em; 1021 | } 1022 | 1023 | .prose :where(tbody td:first-child, tfoot td:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1024 | padding-left: 0; 1025 | } 1026 | 1027 | .prose :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1028 | padding-right: 0; 1029 | } 1030 | 1031 | .prose :where(figure):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1032 | margin-top: 2em; 1033 | margin-bottom: 2em; 1034 | } 1035 | 1036 | .prose :where(.prose > :first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1037 | margin-top: 0; 1038 | } 1039 | 1040 | .prose :where(.prose > :last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1041 | margin-bottom: 0; 1042 | } 1043 | 1044 | .\!prose-invert { 1045 | --tw-prose-body: var(--tw-prose-invert-body) !important; 1046 | --tw-prose-headings: var(--tw-prose-invert-headings) !important; 1047 | --tw-prose-lead: var(--tw-prose-invert-lead) !important; 1048 | --tw-prose-links: var(--tw-prose-invert-links) !important; 1049 | --tw-prose-bold: var(--tw-prose-invert-bold) !important; 1050 | --tw-prose-counters: var(--tw-prose-invert-counters) !important; 1051 | --tw-prose-bullets: var(--tw-prose-invert-bullets) !important; 1052 | --tw-prose-hr: var(--tw-prose-invert-hr) !important; 1053 | --tw-prose-quotes: var(--tw-prose-invert-quotes) !important; 1054 | --tw-prose-quote-borders: var(--tw-prose-invert-quote-borders) !important; 1055 | --tw-prose-captions: var(--tw-prose-invert-captions) !important; 1056 | --tw-prose-kbd: var(--tw-prose-invert-kbd) !important; 1057 | --tw-prose-kbd-shadows: var(--tw-prose-invert-kbd-shadows) !important; 1058 | --tw-prose-code: var(--tw-prose-invert-code) !important; 1059 | --tw-prose-pre-code: var(--tw-prose-invert-pre-code) !important; 1060 | --tw-prose-pre-bg: var(--tw-prose-invert-pre-bg) !important; 1061 | --tw-prose-th-borders: var(--tw-prose-invert-th-borders) !important; 1062 | --tw-prose-td-borders: var(--tw-prose-invert-td-borders) !important; 1063 | } 1064 | 1065 | .prose-invert { 1066 | --tw-prose-body: var(--tw-prose-invert-body); 1067 | --tw-prose-headings: var(--tw-prose-invert-headings); 1068 | --tw-prose-lead: var(--tw-prose-invert-lead); 1069 | --tw-prose-links: var(--tw-prose-invert-links); 1070 | --tw-prose-bold: var(--tw-prose-invert-bold); 1071 | --tw-prose-counters: var(--tw-prose-invert-counters); 1072 | --tw-prose-bullets: var(--tw-prose-invert-bullets); 1073 | --tw-prose-hr: var(--tw-prose-invert-hr); 1074 | --tw-prose-quotes: var(--tw-prose-invert-quotes); 1075 | --tw-prose-quote-borders: var(--tw-prose-invert-quote-borders); 1076 | --tw-prose-captions: var(--tw-prose-invert-captions); 1077 | --tw-prose-kbd: var(--tw-prose-invert-kbd); 1078 | --tw-prose-kbd-shadows: var(--tw-prose-invert-kbd-shadows); 1079 | --tw-prose-code: var(--tw-prose-invert-code); 1080 | --tw-prose-pre-code: var(--tw-prose-invert-pre-code); 1081 | --tw-prose-pre-bg: var(--tw-prose-invert-pre-bg); 1082 | --tw-prose-th-borders: var(--tw-prose-invert-th-borders); 1083 | --tw-prose-td-borders: var(--tw-prose-invert-td-borders); 1084 | } 1085 | 1086 | .sr-only { 1087 | position: absolute; 1088 | width: 1px; 1089 | height: 1px; 1090 | padding: 0; 1091 | margin: -1px; 1092 | overflow: hidden; 1093 | clip: rect(0, 0, 0, 0); 1094 | white-space: nowrap; 1095 | border-width: 0; 1096 | } 1097 | 1098 | .static { 1099 | position: static; 1100 | } 1101 | 1102 | .absolute { 1103 | position: absolute; 1104 | } 1105 | 1106 | .relative { 1107 | position: relative; 1108 | } 1109 | 1110 | .bottom-0 { 1111 | bottom: 0px; 1112 | } 1113 | 1114 | .bottom-2 { 1115 | bottom: 0.5rem; 1116 | } 1117 | 1118 | .right-2 { 1119 | right: 0.5rem; 1120 | } 1121 | 1122 | .right-2\.5 { 1123 | right: 0.625rem; 1124 | } 1125 | 1126 | .mx-2 { 1127 | margin-left: 0.5rem; 1128 | margin-right: 0.5rem; 1129 | } 1130 | 1131 | .my-4 { 1132 | margin-top: 1rem; 1133 | margin-bottom: 1rem; 1134 | } 1135 | 1136 | .mb-1 { 1137 | margin-bottom: 0.25rem; 1138 | } 1139 | 1140 | .mb-2 { 1141 | margin-bottom: 0.5rem; 1142 | } 1143 | 1144 | .mb-4 { 1145 | margin-bottom: 1rem; 1146 | } 1147 | 1148 | .ml-2 { 1149 | margin-left: 0.5rem; 1150 | } 1151 | 1152 | .ml-3 { 1153 | margin-left: 0.75rem; 1154 | } 1155 | 1156 | .mr-2 { 1157 | margin-right: 0.5rem; 1158 | } 1159 | 1160 | .mt-2 { 1161 | margin-top: 0.5rem; 1162 | } 1163 | 1164 | .mt-4 { 1165 | margin-top: 1rem; 1166 | } 1167 | 1168 | .mt-8 { 1169 | margin-top: 2rem; 1170 | } 1171 | 1172 | .block { 1173 | display: block; 1174 | } 1175 | 1176 | .inline { 1177 | display: inline; 1178 | } 1179 | 1180 | .flex { 1181 | display: flex; 1182 | } 1183 | 1184 | .inline-flex { 1185 | display: inline-flex; 1186 | } 1187 | 1188 | .hidden { 1189 | display: none; 1190 | } 1191 | 1192 | .h-6 { 1193 | height: 1.5rem; 1194 | } 1195 | 1196 | .h-7 { 1197 | height: 1.75rem; 1198 | } 1199 | 1200 | .h-8 { 1201 | height: 2rem; 1202 | } 1203 | 1204 | .h-full { 1205 | height: 100%; 1206 | } 1207 | 1208 | .h-screen { 1209 | height: 100vh; 1210 | } 1211 | 1212 | .max-h-\[100vh\] { 1213 | max-height: 100vh; 1214 | } 1215 | 1216 | .max-h-\[90vh\] { 1217 | max-height: 90vh; 1218 | } 1219 | 1220 | .min-h-\[85px\] { 1221 | min-height: 85px; 1222 | } 1223 | 1224 | .w-1\/6 { 1225 | width: 16.666667%; 1226 | } 1227 | 1228 | .w-11 { 1229 | width: 2.75rem; 1230 | } 1231 | 1232 | .w-12 { 1233 | width: 3rem; 1234 | } 1235 | 1236 | .w-52 { 1237 | width: 13rem; 1238 | } 1239 | 1240 | .w-6 { 1241 | width: 1.5rem; 1242 | } 1243 | 1244 | .w-7 { 1245 | width: 1.75rem; 1246 | } 1247 | 1248 | .w-8 { 1249 | width: 2rem; 1250 | } 1251 | 1252 | .w-full { 1253 | width: 100%; 1254 | } 1255 | 1256 | .w-screen { 1257 | width: 100vw; 1258 | } 1259 | 1260 | .max-w-none { 1261 | max-width: none; 1262 | } 1263 | 1264 | .flex-grow { 1265 | flex-grow: 1; 1266 | } 1267 | 1268 | .cursor-pointer { 1269 | cursor: pointer; 1270 | } 1271 | 1272 | .resize-none { 1273 | resize: none; 1274 | } 1275 | 1276 | .flex-row-reverse { 1277 | flex-direction: row-reverse; 1278 | } 1279 | 1280 | .flex-col { 1281 | flex-direction: column; 1282 | } 1283 | 1284 | .items-start { 1285 | align-items: flex-start; 1286 | } 1287 | 1288 | .items-center { 1289 | align-items: center; 1290 | } 1291 | 1292 | .gap-x-2 { 1293 | -moz-column-gap: 0.5rem; 1294 | column-gap: 0.5rem; 1295 | } 1296 | 1297 | .gap-y-2 { 1298 | row-gap: 0.5rem; 1299 | } 1300 | 1301 | .space-y-4 > :not([hidden]) ~ :not([hidden]) { 1302 | --tw-space-y-reverse: 0; 1303 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); 1304 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); 1305 | } 1306 | 1307 | .space-y-6 > :not([hidden]) ~ :not([hidden]) { 1308 | --tw-space-y-reverse: 0; 1309 | margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); 1310 | margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); 1311 | } 1312 | 1313 | .space-y-8 > :not([hidden]) ~ :not([hidden]) { 1314 | --tw-space-y-reverse: 0; 1315 | margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse))); 1316 | margin-bottom: calc(2rem * var(--tw-space-y-reverse)); 1317 | } 1318 | 1319 | .overflow-auto { 1320 | overflow: auto; 1321 | } 1322 | 1323 | .overflow-y-auto { 1324 | overflow-y: auto; 1325 | } 1326 | 1327 | .rounded-full { 1328 | border-radius: 9999px; 1329 | } 1330 | 1331 | .rounded-lg { 1332 | border-radius: 0.5rem; 1333 | } 1334 | 1335 | .rounded-xl { 1336 | border-radius: 0.75rem; 1337 | } 1338 | 1339 | .rounded-b-xl { 1340 | border-bottom-right-radius: 0.75rem; 1341 | border-bottom-left-radius: 0.75rem; 1342 | } 1343 | 1344 | .rounded-tl-xl { 1345 | border-top-left-radius: 0.75rem; 1346 | } 1347 | 1348 | .border-l { 1349 | border-left-width: 1px; 1350 | } 1351 | 1352 | .border-r { 1353 | border-right-width: 1px; 1354 | } 1355 | 1356 | .border-r-4 { 1357 | border-right-width: 4px; 1358 | } 1359 | 1360 | .border-t { 1361 | border-top-width: 1px; 1362 | } 1363 | 1364 | .border-none { 1365 | border-style: none; 1366 | } 1367 | 1368 | .border-slate-300 { 1369 | --tw-border-opacity: 1; 1370 | border-color: rgb(203 213 225 / var(--tw-border-opacity)); 1371 | } 1372 | 1373 | .border-transparent { 1374 | border-color: transparent; 1375 | } 1376 | 1377 | .bg-blue-100 { 1378 | --tw-bg-opacity: 1; 1379 | background-color: rgb(219 234 254 / var(--tw-bg-opacity)); 1380 | } 1381 | 1382 | .bg-blue-600 { 1383 | --tw-bg-opacity: 1; 1384 | background-color: rgb(37 99 235 / var(--tw-bg-opacity)); 1385 | } 1386 | 1387 | .bg-blue-700 { 1388 | --tw-bg-opacity: 1; 1389 | background-color: rgb(29 78 216 / var(--tw-bg-opacity)); 1390 | } 1391 | 1392 | .bg-slate-200 { 1393 | --tw-bg-opacity: 1; 1394 | background-color: rgb(226 232 240 / var(--tw-bg-opacity)); 1395 | } 1396 | 1397 | .bg-slate-50 { 1398 | --tw-bg-opacity: 1; 1399 | background-color: rgb(248 250 252 / var(--tw-bg-opacity)); 1400 | } 1401 | 1402 | .p-1 { 1403 | padding: 0.25rem; 1404 | } 1405 | 1406 | .p-1\.5 { 1407 | padding: 0.375rem; 1408 | } 1409 | 1410 | .p-2 { 1411 | padding: 0.5rem; 1412 | } 1413 | 1414 | .p-2\.5 { 1415 | padding: 0.625rem; 1416 | } 1417 | 1418 | .p-4 { 1419 | padding: 1rem; 1420 | } 1421 | 1422 | .p-5 { 1423 | padding: 1.25rem; 1424 | } 1425 | 1426 | .px-2 { 1427 | padding-left: 0.5rem; 1428 | padding-right: 0.5rem; 1429 | } 1430 | 1431 | .px-3 { 1432 | padding-left: 0.75rem; 1433 | padding-right: 0.75rem; 1434 | } 1435 | 1436 | .px-4 { 1437 | padding-left: 1rem; 1438 | padding-right: 1rem; 1439 | } 1440 | 1441 | .px-5 { 1442 | padding-left: 1.25rem; 1443 | padding-right: 1.25rem; 1444 | } 1445 | 1446 | .py-1 { 1447 | padding-top: 0.25rem; 1448 | padding-bottom: 0.25rem; 1449 | } 1450 | 1451 | .py-2 { 1452 | padding-top: 0.5rem; 1453 | padding-bottom: 0.5rem; 1454 | } 1455 | 1456 | .py-3 { 1457 | padding-top: 0.75rem; 1458 | padding-bottom: 0.75rem; 1459 | } 1460 | 1461 | .py-4 { 1462 | padding-top: 1rem; 1463 | padding-bottom: 1rem; 1464 | } 1465 | 1466 | .py-8 { 1467 | padding-top: 2rem; 1468 | padding-bottom: 2rem; 1469 | } 1470 | 1471 | .pl-1 { 1472 | padding-left: 0.25rem; 1473 | } 1474 | 1475 | .pl-10 { 1476 | padding-left: 2.5rem; 1477 | } 1478 | 1479 | .pr-20 { 1480 | padding-right: 5rem; 1481 | } 1482 | 1483 | .text-left { 1484 | text-align: left; 1485 | } 1486 | 1487 | .text-lg { 1488 | font-size: 1.125rem; 1489 | line-height: 1.75rem; 1490 | } 1491 | 1492 | .text-sm { 1493 | font-size: 0.875rem; 1494 | line-height: 1.25rem; 1495 | } 1496 | 1497 | .text-xs { 1498 | font-size: 0.75rem; 1499 | line-height: 1rem; 1500 | } 1501 | 1502 | .font-medium { 1503 | font-weight: 500; 1504 | } 1505 | 1506 | .font-semibold { 1507 | font-weight: 600; 1508 | } 1509 | 1510 | .uppercase { 1511 | text-transform: uppercase; 1512 | } 1513 | 1514 | .capitalize { 1515 | text-transform: capitalize; 1516 | } 1517 | 1518 | .leading-6 { 1519 | line-height: 1.5rem; 1520 | } 1521 | 1522 | .text-blue-600 { 1523 | --tw-text-opacity: 1; 1524 | color: rgb(37 99 235 / var(--tw-text-opacity)); 1525 | } 1526 | 1527 | .text-slate-200 { 1528 | --tw-text-opacity: 1; 1529 | color: rgb(226 232 240 / var(--tw-text-opacity)); 1530 | } 1531 | 1532 | .text-slate-500 { 1533 | --tw-text-opacity: 1; 1534 | color: rgb(100 116 139 / var(--tw-text-opacity)); 1535 | } 1536 | 1537 | .text-slate-700 { 1538 | --tw-text-opacity: 1; 1539 | color: rgb(51 65 85 / var(--tw-text-opacity)); 1540 | } 1541 | 1542 | .text-slate-800 { 1543 | --tw-text-opacity: 1; 1544 | color: rgb(30 41 59 / var(--tw-text-opacity)); 1545 | } 1546 | 1547 | .text-slate-900 { 1548 | --tw-text-opacity: 1; 1549 | color: rgb(15 23 42 / var(--tw-text-opacity)); 1550 | } 1551 | 1552 | .shadow-sm { 1553 | --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 1554 | --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); 1555 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1556 | } 1557 | 1558 | .transition-colors { 1559 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; 1560 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1561 | transition-duration: 150ms; 1562 | } 1563 | 1564 | .duration-200 { 1565 | transition-duration: 200ms; 1566 | } 1567 | 1568 | @media (prefers-color-scheme: dark) { 1569 | .dark\:\!prose-invert { 1570 | --tw-prose-body: var(--tw-prose-invert-body) !important; 1571 | --tw-prose-headings: var(--tw-prose-invert-headings) !important; 1572 | --tw-prose-lead: var(--tw-prose-invert-lead) !important; 1573 | --tw-prose-links: var(--tw-prose-invert-links) !important; 1574 | --tw-prose-bold: var(--tw-prose-invert-bold) !important; 1575 | --tw-prose-counters: var(--tw-prose-invert-counters) !important; 1576 | --tw-prose-bullets: var(--tw-prose-invert-bullets) !important; 1577 | --tw-prose-hr: var(--tw-prose-invert-hr) !important; 1578 | --tw-prose-quotes: var(--tw-prose-invert-quotes) !important; 1579 | --tw-prose-quote-borders: var(--tw-prose-invert-quote-borders) !important; 1580 | --tw-prose-captions: var(--tw-prose-invert-captions) !important; 1581 | --tw-prose-kbd: var(--tw-prose-invert-kbd) !important; 1582 | --tw-prose-kbd-shadows: var(--tw-prose-invert-kbd-shadows) !important; 1583 | --tw-prose-code: var(--tw-prose-invert-code) !important; 1584 | --tw-prose-pre-code: var(--tw-prose-invert-pre-code) !important; 1585 | --tw-prose-pre-bg: var(--tw-prose-invert-pre-bg) !important; 1586 | --tw-prose-th-borders: var(--tw-prose-invert-th-borders) !important; 1587 | --tw-prose-td-borders: var(--tw-prose-invert-td-borders) !important; 1588 | } 1589 | 1590 | .dark\:prose-invert { 1591 | --tw-prose-body: var(--tw-prose-invert-body); 1592 | --tw-prose-headings: var(--tw-prose-invert-headings); 1593 | --tw-prose-lead: var(--tw-prose-invert-lead); 1594 | --tw-prose-links: var(--tw-prose-invert-links); 1595 | --tw-prose-bold: var(--tw-prose-invert-bold); 1596 | --tw-prose-counters: var(--tw-prose-invert-counters); 1597 | --tw-prose-bullets: var(--tw-prose-invert-bullets); 1598 | --tw-prose-hr: var(--tw-prose-invert-hr); 1599 | --tw-prose-quotes: var(--tw-prose-invert-quotes); 1600 | --tw-prose-quote-borders: var(--tw-prose-invert-quote-borders); 1601 | --tw-prose-captions: var(--tw-prose-invert-captions); 1602 | --tw-prose-kbd: var(--tw-prose-invert-kbd); 1603 | --tw-prose-kbd-shadows: var(--tw-prose-invert-kbd-shadows); 1604 | --tw-prose-code: var(--tw-prose-invert-code); 1605 | --tw-prose-pre-code: var(--tw-prose-invert-pre-code); 1606 | --tw-prose-pre-bg: var(--tw-prose-invert-pre-bg); 1607 | --tw-prose-th-borders: var(--tw-prose-invert-th-borders); 1608 | --tw-prose-td-borders: var(--tw-prose-invert-td-borders); 1609 | } 1610 | } 1611 | 1612 | @media (min-width: 1024px) { 1613 | .lg\:prose-xl { 1614 | font-size: 1.25rem; 1615 | line-height: 1.8; 1616 | } 1617 | 1618 | .lg\:prose-xl :where(p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1619 | margin-top: 1.2em; 1620 | margin-bottom: 1.2em; 1621 | } 1622 | 1623 | .lg\:prose-xl :where([class~="lead"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1624 | font-size: 1.2em; 1625 | line-height: 1.5; 1626 | margin-top: 1em; 1627 | margin-bottom: 1em; 1628 | } 1629 | 1630 | .lg\:prose-xl :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1631 | margin-top: 1.6em; 1632 | margin-bottom: 1.6em; 1633 | padding-left: 1.0666667em; 1634 | } 1635 | 1636 | .lg\:prose-xl :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1637 | font-size: 2.8em; 1638 | margin-top: 0; 1639 | margin-bottom: 0.8571429em; 1640 | line-height: 1; 1641 | } 1642 | 1643 | .lg\:prose-xl :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1644 | font-size: 1.8em; 1645 | margin-top: 1.5555556em; 1646 | margin-bottom: 0.8888889em; 1647 | line-height: 1.1111111; 1648 | } 1649 | 1650 | .lg\:prose-xl :where(h3):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1651 | font-size: 1.5em; 1652 | margin-top: 1.6em; 1653 | margin-bottom: 0.6666667em; 1654 | line-height: 1.3333333; 1655 | } 1656 | 1657 | .lg\:prose-xl :where(h4):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1658 | margin-top: 1.8em; 1659 | margin-bottom: 0.6em; 1660 | line-height: 1.6; 1661 | } 1662 | 1663 | .lg\:prose-xl :where(img):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1664 | margin-top: 2em; 1665 | margin-bottom: 2em; 1666 | } 1667 | 1668 | .lg\:prose-xl :where(picture):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1669 | margin-top: 2em; 1670 | margin-bottom: 2em; 1671 | } 1672 | 1673 | .lg\:prose-xl :where(picture > img):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1674 | margin-top: 0; 1675 | margin-bottom: 0; 1676 | } 1677 | 1678 | .lg\:prose-xl :where(video):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1679 | margin-top: 2em; 1680 | margin-bottom: 2em; 1681 | } 1682 | 1683 | .lg\:prose-xl :where(kbd):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1684 | font-size: 0.9em; 1685 | border-radius: 0.3125rem; 1686 | padding-top: 0.25em; 1687 | padding-right: 0.4em; 1688 | padding-bottom: 0.25em; 1689 | padding-left: 0.4em; 1690 | } 1691 | 1692 | .lg\:prose-xl :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1693 | font-size: 0.9em; 1694 | } 1695 | 1696 | .lg\:prose-xl :where(h2 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1697 | font-size: 0.8611111em; 1698 | } 1699 | 1700 | .lg\:prose-xl :where(h3 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1701 | font-size: 0.9em; 1702 | } 1703 | 1704 | .lg\:prose-xl :where(pre):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1705 | font-size: 0.9em; 1706 | line-height: 1.7777778; 1707 | margin-top: 2em; 1708 | margin-bottom: 2em; 1709 | border-radius: 0.5rem; 1710 | padding-top: 1.1111111em; 1711 | padding-right: 1.3333333em; 1712 | padding-bottom: 1.1111111em; 1713 | padding-left: 1.3333333em; 1714 | } 1715 | 1716 | .lg\:prose-xl :where(ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1717 | margin-top: 1.2em; 1718 | margin-bottom: 1.2em; 1719 | padding-left: 1.6em; 1720 | } 1721 | 1722 | .lg\:prose-xl :where(ul):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1723 | margin-top: 1.2em; 1724 | margin-bottom: 1.2em; 1725 | padding-left: 1.6em; 1726 | } 1727 | 1728 | .lg\:prose-xl :where(li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1729 | margin-top: 0.6em; 1730 | margin-bottom: 0.6em; 1731 | } 1732 | 1733 | .lg\:prose-xl :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1734 | padding-left: 0.4em; 1735 | } 1736 | 1737 | .lg\:prose-xl :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1738 | padding-left: 0.4em; 1739 | } 1740 | 1741 | .lg\:prose-xl :where(.lg\:prose-xl > ul > li p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1742 | margin-top: 0.8em; 1743 | margin-bottom: 0.8em; 1744 | } 1745 | 1746 | .lg\:prose-xl :where(.lg\:prose-xl > ul > li > *:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1747 | margin-top: 1.2em; 1748 | } 1749 | 1750 | .lg\:prose-xl :where(.lg\:prose-xl > ul > li > *:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1751 | margin-bottom: 1.2em; 1752 | } 1753 | 1754 | .lg\:prose-xl :where(.lg\:prose-xl > ol > li > *:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1755 | margin-top: 1.2em; 1756 | } 1757 | 1758 | .lg\:prose-xl :where(.lg\:prose-xl > ol > li > *:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1759 | margin-bottom: 1.2em; 1760 | } 1761 | 1762 | .lg\:prose-xl :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1763 | margin-top: 0.8em; 1764 | margin-bottom: 0.8em; 1765 | } 1766 | 1767 | .lg\:prose-xl :where(dl):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1768 | margin-top: 1.2em; 1769 | margin-bottom: 1.2em; 1770 | } 1771 | 1772 | .lg\:prose-xl :where(dt):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1773 | margin-top: 1.2em; 1774 | } 1775 | 1776 | .lg\:prose-xl :where(dd):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1777 | margin-top: 0.6em; 1778 | padding-left: 1.6em; 1779 | } 1780 | 1781 | .lg\:prose-xl :where(hr):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1782 | margin-top: 2.8em; 1783 | margin-bottom: 2.8em; 1784 | } 1785 | 1786 | .lg\:prose-xl :where(hr + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1787 | margin-top: 0; 1788 | } 1789 | 1790 | .lg\:prose-xl :where(h2 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1791 | margin-top: 0; 1792 | } 1793 | 1794 | .lg\:prose-xl :where(h3 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1795 | margin-top: 0; 1796 | } 1797 | 1798 | .lg\:prose-xl :where(h4 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1799 | margin-top: 0; 1800 | } 1801 | 1802 | .lg\:prose-xl :where(table):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1803 | font-size: 0.9em; 1804 | line-height: 1.5555556; 1805 | } 1806 | 1807 | .lg\:prose-xl :where(thead th):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1808 | padding-right: 0.6666667em; 1809 | padding-bottom: 0.8888889em; 1810 | padding-left: 0.6666667em; 1811 | } 1812 | 1813 | .lg\:prose-xl :where(thead th:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1814 | padding-left: 0; 1815 | } 1816 | 1817 | .lg\:prose-xl :where(thead th:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1818 | padding-right: 0; 1819 | } 1820 | 1821 | .lg\:prose-xl :where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1822 | padding-top: 0.8888889em; 1823 | padding-right: 0.6666667em; 1824 | padding-bottom: 0.8888889em; 1825 | padding-left: 0.6666667em; 1826 | } 1827 | 1828 | .lg\:prose-xl :where(tbody td:first-child, tfoot td:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1829 | padding-left: 0; 1830 | } 1831 | 1832 | .lg\:prose-xl :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1833 | padding-right: 0; 1834 | } 1835 | 1836 | .lg\:prose-xl :where(figure):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1837 | margin-top: 2em; 1838 | margin-bottom: 2em; 1839 | } 1840 | 1841 | .lg\:prose-xl :where(figure > *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1842 | margin-top: 0; 1843 | margin-bottom: 0; 1844 | } 1845 | 1846 | .lg\:prose-xl :where(figcaption):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1847 | font-size: 0.9em; 1848 | line-height: 1.5555556; 1849 | margin-top: 1em; 1850 | } 1851 | 1852 | .lg\:prose-xl :where(.lg\:prose-xl > :first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1853 | margin-top: 0; 1854 | } 1855 | 1856 | .lg\:prose-xl :where(.lg\:prose-xl > :last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { 1857 | margin-bottom: 0; 1858 | } 1859 | } 1860 | 1861 | .after\:absolute::after { 1862 | content: var(--tw-content); 1863 | position: absolute; 1864 | } 1865 | 1866 | .after\:left-\[2px\]::after { 1867 | content: var(--tw-content); 1868 | left: 2px; 1869 | } 1870 | 1871 | .after\:top-\[2px\]::after { 1872 | content: var(--tw-content); 1873 | top: 2px; 1874 | } 1875 | 1876 | .after\:h-5::after { 1877 | content: var(--tw-content); 1878 | height: 1.25rem; 1879 | } 1880 | 1881 | .after\:w-5::after { 1882 | content: var(--tw-content); 1883 | width: 1.25rem; 1884 | } 1885 | 1886 | .after\:rounded-full::after { 1887 | content: var(--tw-content); 1888 | border-radius: 9999px; 1889 | } 1890 | 1891 | .after\:border::after { 1892 | content: var(--tw-content); 1893 | border-width: 1px; 1894 | } 1895 | 1896 | .after\:border-slate-300::after { 1897 | content: var(--tw-content); 1898 | --tw-border-opacity: 1; 1899 | border-color: rgb(203 213 225 / var(--tw-border-opacity)); 1900 | } 1901 | 1902 | .after\:bg-white::after { 1903 | content: var(--tw-content); 1904 | --tw-bg-opacity: 1; 1905 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 1906 | } 1907 | 1908 | .after\:transition-all::after { 1909 | content: var(--tw-content); 1910 | transition-property: all; 1911 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1912 | transition-duration: 150ms; 1913 | } 1914 | 1915 | .after\:content-\[\'\'\]::after { 1916 | --tw-content: ''; 1917 | content: var(--tw-content); 1918 | } 1919 | 1920 | .peer:checked ~ .peer-checked\:bg-blue-600 { 1921 | --tw-bg-opacity: 1; 1922 | background-color: rgb(37 99 235 / var(--tw-bg-opacity)); 1923 | } 1924 | 1925 | .peer:checked ~ .peer-checked\:after\:translate-x-full::after { 1926 | content: var(--tw-content); 1927 | --tw-translate-x: 100%; 1928 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 1929 | } 1930 | 1931 | .peer:checked ~ .peer-checked\:after\:border-white::after { 1932 | content: var(--tw-content); 1933 | --tw-border-opacity: 1; 1934 | border-color: rgb(255 255 255 / var(--tw-border-opacity)); 1935 | } 1936 | 1937 | .peer:focus ~ .peer-focus\:outline-none { 1938 | outline: 2px solid transparent; 1939 | outline-offset: 2px; 1940 | } 1941 | 1942 | .peer:focus ~ .peer-focus\:ring-2 { 1943 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1944 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1945 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1946 | } 1947 | 1948 | .peer:focus ~ .peer-focus\:ring-blue-300 { 1949 | --tw-ring-opacity: 1; 1950 | --tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity)); 1951 | } 1952 | 1953 | .hover\:bg-blue-600:hover { 1954 | --tw-bg-opacity: 1; 1955 | background-color: rgb(37 99 235 / var(--tw-bg-opacity)); 1956 | } 1957 | 1958 | .hover\:bg-blue-800:hover { 1959 | --tw-bg-opacity: 1; 1960 | background-color: rgb(30 64 175 / var(--tw-bg-opacity)); 1961 | } 1962 | 1963 | .hover\:bg-slate-200:hover { 1964 | --tw-bg-opacity: 1; 1965 | background-color: rgb(226 232 240 / var(--tw-bg-opacity)); 1966 | } 1967 | 1968 | .hover\:bg-slate-700:hover { 1969 | --tw-bg-opacity: 1; 1970 | background-color: rgb(51 65 85 / var(--tw-bg-opacity)); 1971 | } 1972 | 1973 | .hover\:text-slate-200:hover { 1974 | --tw-text-opacity: 1; 1975 | color: rgb(226 232 240 / var(--tw-text-opacity)); 1976 | } 1977 | 1978 | .focus\:outline-none:focus { 1979 | outline: 2px solid transparent; 1980 | outline-offset: 2px; 1981 | } 1982 | 1983 | .focus\:ring-2:focus { 1984 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1985 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1986 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1987 | } 1988 | 1989 | .focus\:ring-4:focus { 1990 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1991 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1992 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1993 | } 1994 | 1995 | .focus\:ring-blue-300:focus { 1996 | --tw-ring-opacity: 1; 1997 | --tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity)); 1998 | } 1999 | 2000 | .focus\:ring-blue-600:focus { 2001 | --tw-ring-opacity: 1; 2002 | --tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity)); 2003 | } 2004 | 2005 | @media (prefers-color-scheme: dark) { 2006 | .dark\:border-slate-600 { 2007 | --tw-border-opacity: 1; 2008 | border-color: rgb(71 85 105 / var(--tw-border-opacity)); 2009 | } 2010 | 2011 | .dark\:border-slate-700 { 2012 | --tw-border-opacity: 1; 2013 | border-color: rgb(51 65 85 / var(--tw-border-opacity)); 2014 | } 2015 | 2016 | .dark\:bg-blue-600 { 2017 | --tw-bg-opacity: 1; 2018 | background-color: rgb(37 99 235 / var(--tw-bg-opacity)); 2019 | } 2020 | 2021 | .dark\:bg-slate-700 { 2022 | --tw-bg-opacity: 1; 2023 | background-color: rgb(51 65 85 / var(--tw-bg-opacity)); 2024 | } 2025 | 2026 | .dark\:bg-slate-800 { 2027 | --tw-bg-opacity: 1; 2028 | background-color: rgb(30 41 59 / var(--tw-bg-opacity)); 2029 | } 2030 | 2031 | .dark\:bg-slate-900 { 2032 | --tw-bg-opacity: 1; 2033 | background-color: rgb(15 23 42 / var(--tw-bg-opacity)); 2034 | } 2035 | 2036 | .dark\:text-blue-600 { 2037 | --tw-text-opacity: 1; 2038 | color: rgb(37 99 235 / var(--tw-text-opacity)); 2039 | } 2040 | 2041 | .dark\:text-slate-200 { 2042 | --tw-text-opacity: 1; 2043 | color: rgb(226 232 240 / var(--tw-text-opacity)); 2044 | } 2045 | 2046 | .dark\:text-slate-300 { 2047 | --tw-text-opacity: 1; 2048 | color: rgb(203 213 225 / var(--tw-text-opacity)); 2049 | } 2050 | 2051 | .dark\:text-slate-400 { 2052 | --tw-text-opacity: 1; 2053 | color: rgb(148 163 184 / var(--tw-text-opacity)); 2054 | } 2055 | 2056 | .dark\:placeholder-slate-400::-moz-placeholder { 2057 | --tw-placeholder-opacity: 1; 2058 | color: rgb(148 163 184 / var(--tw-placeholder-opacity)); 2059 | } 2060 | 2061 | .dark\:placeholder-slate-400::placeholder { 2062 | --tw-placeholder-opacity: 1; 2063 | color: rgb(148 163 184 / var(--tw-placeholder-opacity)); 2064 | } 2065 | 2066 | .dark\:scrollbar { 2067 | scrollbar-color: var(--scrollbar-thumb, initial) var(--scrollbar-track, initial); 2068 | } 2069 | 2070 | .dark\:scrollbar::-webkit-scrollbar-track { 2071 | background-color: var(--scrollbar-track); 2072 | border-radius: var(--scrollbar-track-radius); 2073 | } 2074 | 2075 | .dark\:scrollbar::-webkit-scrollbar-track:hover { 2076 | background-color: var(--scrollbar-track-hover, var(--scrollbar-track)); 2077 | } 2078 | 2079 | .dark\:scrollbar::-webkit-scrollbar-track:active { 2080 | background-color: var(--scrollbar-track-active, var(--scrollbar-track-hover, var(--scrollbar-track))); 2081 | } 2082 | 2083 | .dark\:scrollbar::-webkit-scrollbar-thumb { 2084 | background-color: var(--scrollbar-thumb); 2085 | border-radius: var(--scrollbar-thumb-radius); 2086 | } 2087 | 2088 | .dark\:scrollbar::-webkit-scrollbar-thumb:hover { 2089 | background-color: var(--scrollbar-thumb-hover, var(--scrollbar-thumb)); 2090 | } 2091 | 2092 | .dark\:scrollbar::-webkit-scrollbar-thumb:active { 2093 | background-color: var(--scrollbar-thumb-active, var(--scrollbar-thumb-hover, var(--scrollbar-thumb))); 2094 | } 2095 | 2096 | .dark\:scrollbar::-webkit-scrollbar-corner { 2097 | background-color: var(--scrollbar-corner); 2098 | border-radius: var(--scrollbar-corner-radius); 2099 | } 2100 | 2101 | .dark\:scrollbar::-webkit-scrollbar-corner:hover { 2102 | background-color: var(--scrollbar-corner-hover, var(--scrollbar-corner)); 2103 | } 2104 | 2105 | .dark\:scrollbar::-webkit-scrollbar-corner:active { 2106 | background-color: var(--scrollbar-corner-active, var(--scrollbar-corner-hover, var(--scrollbar-corner))); 2107 | } 2108 | 2109 | .dark\:scrollbar { 2110 | scrollbar-width: auto; 2111 | } 2112 | 2113 | .dark\:scrollbar::-webkit-scrollbar { 2114 | display: block; 2115 | width: var(--scrollbar-width, 16px); 2116 | height: var(--scrollbar-height, 16px); 2117 | } 2118 | 2119 | .dark\:scrollbar-track-slate-900 { 2120 | --scrollbar-track: #0f172a !important; 2121 | } 2122 | 2123 | .dark\:scrollbar-thumb-slate-700 { 2124 | --scrollbar-thumb: #334155 !important; 2125 | } 2126 | 2127 | .peer:focus ~ .dark\:peer-focus\:ring-blue-800 { 2128 | --tw-ring-opacity: 1; 2129 | --tw-ring-color: rgb(30 64 175 / var(--tw-ring-opacity)); 2130 | } 2131 | 2132 | .dark\:hover\:bg-blue-600:hover { 2133 | --tw-bg-opacity: 1; 2134 | background-color: rgb(37 99 235 / var(--tw-bg-opacity)); 2135 | } 2136 | 2137 | .dark\:hover\:bg-blue-700:hover { 2138 | --tw-bg-opacity: 1; 2139 | background-color: rgb(29 78 216 / var(--tw-bg-opacity)); 2140 | } 2141 | 2142 | .dark\:hover\:bg-slate-800:hover { 2143 | --tw-bg-opacity: 1; 2144 | background-color: rgb(30 41 59 / var(--tw-bg-opacity)); 2145 | } 2146 | 2147 | .dark\:focus\:ring-blue-600:focus { 2148 | --tw-ring-opacity: 1; 2149 | --tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity)); 2150 | } 2151 | 2152 | .dark\:focus\:ring-blue-800:focus { 2153 | --tw-ring-opacity: 1; 2154 | --tw-ring-color: rgb(30 64 175 / var(--tw-ring-opacity)); 2155 | } 2156 | } 2157 | 2158 | @media (min-width: 640px) { 2159 | .sm\:min-h-0 { 2160 | min-height: 0px; 2161 | } 2162 | 2163 | .sm\:w-16 { 2164 | width: 4rem; 2165 | } 2166 | 2167 | .sm\:w-60 { 2168 | width: 15rem; 2169 | } 2170 | 2171 | .sm\:w-full { 2172 | width: 100%; 2173 | } 2174 | 2175 | .sm\:max-w-md { 2176 | max-width: 28rem; 2177 | } 2178 | 2179 | .sm\:text-base { 2180 | font-size: 1rem; 2181 | line-height: 1.5rem; 2182 | } 2183 | 2184 | .sm\:leading-7 { 2185 | line-height: 1.75rem; 2186 | } 2187 | } 2188 | 2189 | @media (min-width: 768px) { 2190 | .md\:max-w-2xl { 2191 | max-width: 42rem; 2192 | } 2193 | } 2194 | --------------------------------------------------------------------------------