diff --git a/calcit.cirru b/calcit.cirru index 42deb26..1e1169a 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -1,7 +1,10 @@ -{} (:about "|Machine-generated snapshot. Do not edit directly — changes will be overwritten. Use `calcit query` to inspect and `calcit edit`/`calcit tree` to modify. Run `calcit docs agents --full` first. Manual edits must follow format and schema conventions, then run `calcit edit format`.") (:package |app) - :entries $ {} - :default $ {} (:description |) (:init-fn 'app.main/main!) (:mode :js) (:reload-fn 'app.main/reload!) +{} + :about "|Machine-generated snapshot. Do not edit directly — changes will be overwritten. Use `calcit query` to inspect and `calcit edit`/`calcit tree` to modify. Run `calcit docs agents --contract` before mutations; use `--full` for first orientation or changed contract digest. Manual edits must follow format and schema conventions, then run `calcit edit format`." + :package |app + :entries $ {} $ :default + {} (:description |) (:init-fn 'app.main/main!) (:mode :js) + :reload-fn 'app.main/reload! :feature-policy $ {} :modules $ [] |respo.calcit/ |respo-ui.calcit/ |reel.calcit/ |respo-markdown.calcit/ |alerts.calcit/ :type-slots $ {} @@ -9,528 +12,527 @@ 'app.comp.container $ %{} 'FileEntry :defs $ {} '*abort-control $ %{} 'CodeEntry (:doc |) - :code $ quote (defatom *abort-control false) + :code $ quote $ defatom *abort-control false :examples $ [] :schema $ :: 'Ref '*archived-sessions $ %{} 'CodeEntry (:doc |) - :code $ quote (defatom *archived-sessions false) + :code $ quote $ defatom *archived-sessions false :examples $ [] :schema $ :: 'Ref '*gen-ai-new $ %{} 'CodeEntry (:doc |) - :code $ quote (defatom *gen-ai-new false) + :code $ quote $ defatom *gen-ai-new false :examples $ [] :schema $ :: 'Ref '*image-cache $ %{} 'CodeEntry (:doc |) - :code $ quote (defatom *image-cache false) + :code $ quote $ defatom *image-cache false :examples $ [] :schema $ :: 'Ref - '*openai $ %{} 'CodeEntry (:doc "|called openai sdk, but actually for openrouter") - :code $ quote (defatom *openai false) + '*openai $ %{} 'CodeEntry + :doc "|called openai sdk, but actually for openrouter" + :code $ quote $ defatom *openai false :examples $ [] :schema $ :: 'Ref '*viewing-archive-session $ %{} 'CodeEntry (:doc |) - :code $ quote (defatom *viewing-archive-session false) + :code $ quote $ defatom *viewing-archive-session false :examples $ [] :schema $ :: 'Ref 'DomTokenListHost $ %{} 'CodeEntry (:doc |) - :code $ quote - deftrait DomTokenListHost - .contains? $ :: 'Fn - {} - :args $ [] 'app.comp.container/DomTokenListHost 'String - :return 'Bool - .add! $ :: 'Fn - {} - :args $ [] 'app.comp.container/DomTokenListHost 'String - :return 'Unit - .remove! $ :: 'Fn - {} - :args $ [] 'app.comp.container/DomTokenListHost 'String - :return 'Unit + :code $ quote $ deftrait DomTokenListHost + .contains? $ :: 'Fn $ {} + :args $ [] 'app.comp.container/DomTokenListHost 'String + :return 'Bool + .add! $ :: 'Fn $ {} + :args $ [] 'app.comp.container/DomTokenListHost 'String + :return 'Unit + .remove! $ :: 'Fn $ {} + :args $ [] 'app.comp.container/DomTokenListHost 'String + :return 'Unit :examples $ [] :ffi $ {} (:backend :js) (:kind :external-object) (:target :browser) :names $ {} (:add! |add) (:contains? |contains) (:remove! |remove) :schema $ :: 'Trait 'FocusEventHost $ %{} 'CodeEntry (:doc |) - :code $ quote - deftrait FocusEventHost $ :target (:: 'JsNullish 'app.comp.container/FocusTargetHost) + :code $ quote $ deftrait FocusEventHost + :target $ :: 'JsNullish 'app.comp.container/FocusTargetHost :examples $ [] :ffi $ {} (:backend :js) (:kind :external-object) (:target :browser) :schema $ :: 'Trait 'FocusTargetHost $ %{} 'CodeEntry (:doc |) - :code $ quote - deftrait FocusTargetHost - :parentElement $ :: 'JsNullish 'app.comp.container/FocusTargetHost - :classList 'app.comp.container/DomTokenListHost + :code $ quote $ deftrait FocusTargetHost + :parentElement $ :: 'JsNullish 'app.comp.container/FocusTargetHost + :classList 'app.comp.container/DomTokenListHost :examples $ [] :ffi $ {} (:backend :js) (:kind :external-object) (:target :browser) :schema $ :: 'Trait 'StreamChunk $ %{} 'CodeEntry (:doc |) - :code $ quote - defstruct StreamChunk (:text 'String) (:thinking? 'Bool) + :code $ quote $ defstruct StreamChunk (:text 'String) (:thinking? 'Bool) :examples $ [] :schema $ :: 'StructDef 'append-user-message $ %{} 'CodeEntry (:doc |) - :code $ quote - defn append-user-message (messages content) - conj messages $ %{} ChatMessage (:role :user) (:content content) (:thinking |) - :examples $ [] - :schema $ :: 'Fn - {} - :args $ [] (:: 'List 'app.schema/ChatMessage) 'String - :return $ :: 'List 'app.schema/ChatMessage + :code $ quote $ defn append-user-message (messages content) + conj messages $ %{} ChatMessage (:role :user) (:content content) (:thinking |) + :examples $ [] + :schema $ :: 'Fn $ {} + :args $ [] + :: 'List 'app.schema/ChatMessage + , 'String + :return $ :: 'List 'app.schema/ChatMessage 'call-anthropic-msg! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn call-anthropic-msg! (cursor state prompt-text model thinking? d!) - hint-fn $ {} (:async true) - :args $ [] 'List 'app.schema/ChatState 'String 'String 'Bool 'Dynamic - let - abort $ deref *abort-control - when (js-present-dynamic? abort) - let - abort-controller $ unsafe-coerce abort 'Dynamic - do (js/console.warn |Aborting-prev) (.!abort abort-controller) - d! $ :: :change-model - let - axios $ unsafe-coerce - .-default $ js-await (js/import |axios) - , 'Dynamic - selected $ let - selected0 $ js-await (get-selected) - if (js-present? selected0) (stream-text selected0) "|<未找到内容>" - content $ .replace prompt-text |{{selected}} selected - messages0 $ append-user-message (:messages state) content - messages1 $ upsert-assistant-message messages0 | | - result $ js-await - .!post axios (str |https://sa.chenyong.life/v1/messages) - js-object - :model $ option:unwrap-or (get-env |claude-model) (or model |claude-3-5-sonnet-latest) - :max_tokens 1024 - :stream true - :thinking $ if thinking? - js-object (:type |enabled) (:budget_tokens 2000) - , js/undefined - :messages $ messages->anthropic messages0 - js-object - :params $ js-object - :headers $ js-object (; :Accept |text/event-stream) (; :Content-Type |application/json) - |x-api-key $ get-anthropic-key! - |anthropic-version |2023-06-01 - |anthropic-dangerous-direct-browser-access true - :responseType |stream - :adapter |fetch - :signal $ let - abort $ new js/AbortController - reset! *abort-control abort - .-signal abort - stream $ unsafe-coerce (.-data result) 'Dynamic - reader $ unsafe-coerce - .!getReader $ unsafe-coerce - .!pipeThrough stream $ new js/TextDecoderStream - , 'Dynamic - , 'Dynamic - *text $ atom (str "|Claude AI:" &newline &newline) - js/setTimeout $ fn () - d! $ :: :states-merge cursor state - {} (:answer |) (:thinking |) (:loading? true) (:done? false) (:messages messages1) - apply-args () $ fn () - hint-fn $ {} (:async true) - let - info $ js-await (.!read reader) - value $ wo-js-log (.-value info) - done? $ .-done info - if (wo-log done?) (:: :unit) - do - let - events $ -> (stream-text value) .split-lines - filter $ fn (s) - hint-fn $ {} - :args $ [] 'String - .starts-with? s "|data: " - map $ fn (s) - hint-fn $ {} - :args $ [] 'String - -> (.strip-prefix s "|data: ") js/JSON.parse to-calcit-data - apply-args (events) - fn (xs) - list-match xs - () $ do (println "|no thing to handle in this Loop") &unit - (x0 xss) - let - stop? $ = - option:unwrap-or (get x0 |type) | - , |message_stop - wo-js-log x0 - if stop? - d! $ :: :states-merge cursor state - {} (:answer @*text) (:loading? false) (:done? true) - :messages $ upsert-assistant-message messages1 @*text | - let - content-opt $ get-in x0 ([] |delta |text) - if (option:none? content-opt) - do (println |content-is-nil) (recur xss) - let - content $ stream-text (option:unwrap-or content-opt |) - do (swap! *text str content) - d! (:: :states-merge cursor state) - {} (:answer @*text) (:loading? false) (:done? false) - :messages $ upsert-assistant-message messages1 @*text | - recur xss - recur - :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) + :code $ quote $ defn call-anthropic-msg! (cursor state prompt-text model thinking? d!) + hint-fn $ {} (:async true) :args $ [] 'List 'app.schema/ChatState 'String 'String 'Bool 'Dynamic - :features $ #{} :js-ffi - 'call-flash-imagen-msg! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn call-flash-imagen-msg! (variant cursor state prompt-text d!) - hint-fn $ {} (:async true) - if (= false @*gen-ai-new) + let + abort $ deref *abort-control + when + js-present-dynamic? abort let - mod $ js-await (js/import |@google/genai) - GoogleGenAI $ .-GoogleGenAI mod - reset! *gen-ai-new $ new GoogleGenAI - js-object $ :apiKey (get-gemini-key!) - let - target0 $ js/document.querySelector |.show-image - when (js-present-dynamic? target0) - let - target $ unsafe-coerce target0 'Dynamic - .!setAttribute target |src | - let - abort $ deref *abort-control - when (js-present-dynamic? abort) - let - abort-controller $ unsafe-coerce abort 'Dynamic - do (js/console.warn |Aborting-prev) (.!abort abort-controller) - clear-image-cache! - d! $ :: :states cursor - -> state (assoc :answer |) (assoc :loading? true) + abort-controller $ unsafe-coerce abort 'Dynamic + do (js/console.warn |Aborting-prev) (.!abort abort-controller) + d! $ :: :change-model + let + axios $ unsafe-coerce + .-default $ js-await $ js/import |axios + , 'Dynamic + selected $ let + selected0 $ js-await $ get-selected + if (js-present? selected0) (stream-text selected0) "|<未找到内容>" + content $ .replace prompt-text |{{selected}} selected + messages0 $ append-user-message (:messages state) content + messages1 $ upsert-assistant-message messages0 | | + result $ js-await $ .!post axios + str |https://sa.chenyong.life/v1/messages + js-object + :model $ option:unwrap-or (get-env |claude-model) + or model |claude-3-5-sonnet-latest + :max_tokens 1024 + :stream true + :thinking $ if thinking? + js-object (:type |enabled) (:budget_tokens 2000) + , js/undefined + :messages $ messages->anthropic messages0 + js-object + :params $ js-object + :headers $ js-object + ; :Accept |text/event-stream + ; :Content-Type |application/json + |x-api-key $ get-anthropic-key! + |anthropic-version |2023-06-01 + |anthropic-dangerous-direct-browser-access true + :responseType |stream + :adapter |fetch + :signal $ let + abort $ new js/AbortController + reset! *abort-control abort + .-signal abort + stream $ unsafe-coerce (.-data result) 'Dynamic + reader $ unsafe-coerce + .!getReader $ unsafe-coerce + .!pipeThrough stream $ new js/TextDecoderStream + , 'Dynamic + , 'Dynamic + *text $ atom $ str "|Claude AI:" &newline &newline + js/setTimeout $ fn () $ d! + :: :states-merge cursor state $ {} (:answer |) (:thinking |) (:loading? true) (:done? false) (:messages messages1) + apply-args () $ fn () + hint-fn $ {} $ :async true + let + info $ js-await $ .!read reader + value $ wo-js-log $ .-value info + done? $ .-done info + if (wo-log done?) (:: :unit) + do + let + events $ -> (stream-text value) .split-lines + filter $ fn (s) + hint-fn $ {} $ :args ([] 'String) + .starts-with? s "|data: " + map $ fn (s) + hint-fn $ {} $ :args ([] 'String) + -> (.strip-prefix s "|data: ") js/JSON.parse to-calcit-data + apply-args (events) + fn (xs) + list-match xs + () $ do + println "|no thing to handle in this Loop" + , &unit + (x0 xss) + let + stop? $ = + option:unwrap-or (get x0 |type) | + , |message_stop + wo-js-log x0 + if stop? + d! $ :: :states-merge cursor state $ {} (:answer @*text) (:loading? false) (:done? true) + :messages $ upsert-assistant-message messages1 @*text | + let + content-opt $ get-in x0 $ [] |delta |text + if (option:none? content-opt) + do (println |content-is-nil) (recur xss) + let + content $ stream-text $ option:unwrap-or content-opt | + do (swap! *text str content) + d! (:: :states-merge cursor state) + {} (:answer @*text) (:loading? false) (:done? false) + :messages $ upsert-assistant-message messages1 @*text | + recur xss + recur + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] 'List 'app.schema/ChatState 'String 'String 'Bool 'Dynamic + :features $ #{} :js-ffi + 'call-flash-imagen-msg! $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn call-flash-imagen-msg! (variant cursor state prompt-text d!) + hint-fn $ {} $ :async true + if (= false @*gen-ai-new) let - selected $ let - selected0 $ js-await (get-selected) - if (js-present? selected0) (unsafe-coerce selected0 'String) "|<未找到选中内容>" - gen-ai @*gen-ai-new - content $ .!replace prompt-text |{{selected}} selected - abort-signal $ let - abort $ new js/AbortController - reset! *abort-control abort - .-signal abort - sdk-result $ js-await - .!generateContent - unsafe-coerce - .-models $ unsafe-coerce gen-ai 'Dynamic - , 'Dynamic - js-object (:model |gemini-2.5-flash-image) (:contents content) - :config $ js-object (:abortSignal abort-signal) - :httpOptions $ js-object (:baseUrl |https://ja.chenyong.life) - :responseModalities $ js-array |TEXT |IMAGE - parts $ unsafe-coerce - unsafe-coerce - .-parts $ unsafe-coerce - .-content $ unsafe-coerce - .-0 $ unsafe-coerce - .-candidates $ unsafe-coerce sdk-result 'Dynamic - , 'Dynamic + mod $ js-await $ js/import |@google/genai + GoogleGenAI $ .-GoogleGenAI mod + reset! *gen-ai-new $ new GoogleGenAI $ js-object + :apiKey $ get-gemini-key! + let + target0 $ js/document.querySelector |.show-image + when + js-present-dynamic? target0 + let + target $ unsafe-coerce target0 'Dynamic + .!setAttribute target |src | + let + abort $ deref *abort-control + when + js-present-dynamic? abort + let + abort-controller $ unsafe-coerce abort 'Dynamic + do (js/console.warn |Aborting-prev) (.!abort abort-controller) + clear-image-cache! + d! $ :: :states cursor $ -> state (assoc :answer |) (assoc :loading? true) + let + selected $ let + selected0 $ js-await $ get-selected + if (js-present? selected0) (unsafe-coerce selected0 'String) "|<未找到选中内容>" + gen-ai @*gen-ai-new + content $ .!replace prompt-text |{{selected}} selected + abort-signal $ let + abort $ new js/AbortController + reset! *abort-control abort + .-signal abort + sdk-result $ js-await $ .!generateContent + unsafe-coerce + .-models $ unsafe-coerce gen-ai 'Dynamic + , 'Dynamic + js-object + :model |gemini-2.5-flash-image + :contents content + :config $ js-object (:abortSignal abort-signal) + :httpOptions $ js-object $ :baseUrl |https://ja.chenyong.life + :responseModalities $ js-array |TEXT |IMAGE + parts $ unsafe-coerce + unsafe-coerce + .-parts $ unsafe-coerce + .-content $ unsafe-coerce + .-0 $ unsafe-coerce + .-candidates $ unsafe-coerce sdk-result 'Dynamic , 'Dynamic , 'Dynamic , 'Dynamic , 'Dynamic - *text $ atom | - js-await $ .!forEach parts - fn (chunk _a _b) - if (js-present-dynamic? chunk) + , 'Dynamic + *text $ atom | + js-await $ .!forEach parts $ fn (chunk _a _b) + if + js-present-dynamic? chunk + let + chunk-data $ unsafe-coerce chunk 'Dynamic + text0 $ .-text chunk-data + if + js-present-dynamic? text0 let - chunk-data $ unsafe-coerce chunk 'Dynamic - text0 $ .-text chunk-data - if (js-present-dynamic? text0) + text $ unsafe-coerce text0 'String + do (swap! *text str text) + d! (:: :states cursor) + -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? false) + let + image-data0 $ .-inlineData chunk-data + when + js-present-dynamic? image-data0 let - text $ unsafe-coerce text0 'String - do (swap! *text str text) + image-data $ unsafe-coerce image-data0 'Dynamic + data $ unsafe-coerce (.-data image-data) 'String + image-blob $ base64ToBlob data + url $ js/URL.createObjectURL image-blob + target $ unsafe-coerce + js/document.querySelector |.show-image + , 'Dynamic + .!setAttribute target |src url + reset! *image-cache url + do (swap! *text str | "|(image ready)") d! (:: :states cursor) -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? false) - let - image-data0 $ .-inlineData chunk-data - when (js-present-dynamic? image-data0) - let - image-data $ unsafe-coerce image-data0 'Dynamic - data $ unsafe-coerce (.-data image-data) 'String - image-blob $ base64ToBlob data - url $ js/URL.createObjectURL image-blob - target $ unsafe-coerce (js/document.querySelector |.show-image) 'Dynamic - .!setAttribute target |src url - reset! *image-cache url - do (swap! *text str | "|(image ready)") - d! (:: :states cursor) - -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? false) - d! (:: :states cursor) - -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? false) - d! $ :: :states cursor - -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? false) - d! $ :: :states cursor - -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? true) - :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] 'Tag 'List 'app.schema/ChatState 'String 'Dynamic - :features $ #{} :js-ffi + d! (:: :states cursor) + -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? false) + d! $ :: :states cursor $ -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? false) + d! $ :: :states cursor $ -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? true) + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] 'Tag 'List 'app.schema/ChatState 'String 'Dynamic + :features $ #{} :js-ffi 'call-genai-msg! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn call-genai-msg! (variant cursor state prompt-text search? think? d! *text *thinking-text) - hint-fn $ {} (:async true) - :args $ [] 'Tag 'List 'app.schema/ChatState 'String 'Bool 'Bool 'Dynamic 'Ref 'Ref - if (= false @*gen-ai-new) - let - mod $ js-await (js/import |@google/genai) - GoogleGenAI $ .-GoogleGenAI mod - reset! *gen-ai-new $ new GoogleGenAI - js-object $ :apiKey (get-gemini-key!) - let - abort $ deref *abort-control - when (js-present-dynamic? abort) - let - abort-controller $ unsafe-coerce abort 'Dynamic - do (js/console.warn |Aborting-prev) (.!abort abort-controller) + :code $ quote $ defn call-genai-msg! + variant cursor state prompt-text search? think? d! *text *thinking-text + hint-fn $ {} (:async true) + :args $ [] 'Tag 'List 'app.schema/ChatState 'String 'Bool 'Bool 'Dynamic 'Ref 'Ref + if (= false @*gen-ai-new) let - selected $ let - selected0 $ if (.includes? prompt-text |{{selected}}) - js-await $ get-selected - , js/undefined - if (js-present? selected0) (unsafe-coerce selected0 'String) "|<未找到选中内容>" - gen-ai @*gen-ai-new - model $ pick-model variant - content $ .!replace prompt-text |{{selected}} selected - json? $ or (includes? prompt-text |{{json}}) (includes? prompt-text |{{JSON}}) - pro? $ includes? model |pro - has-url? $ or (includes? prompt-text |http://) (includes? prompt-text |https://) - messages0 $ or (:messages state) ([]) - messages1 $ upsert-assistant-message messages0 | | - abort-signal $ let - abort $ new js/AbortController - reset! *abort-control abort - .-signal abort - tools $ -> - js-array - if search? - js-object $ :googleSearch (js-object) - , js/undefined - if has-url? - js-object $ :urlContext (js-object) - , js/undefined - .!filter $ fn (x _idx _arr) x - sdk-result $ js-await - .!generateContentStream - unsafe-coerce - .-models $ unsafe-coerce gen-ai 'Dynamic - , 'Dynamic - js-object (:model model) - :contents $ messages->gemini messages0 - :config $ js-object - :thinkingConfig $ if think? - js-object (:thinkingLevel |high) (:includeThoughts true) - js-object - :thinkingLevel $ if - contains? (#{} |gemini-3.7-flash |gemini-3.8-flash) model - , |low |minimal - :includeThoughts false - :tools $ if - > - unsafe-coerce - .-length $ unsafe-coerce tools 'Dynamic - , 'Number - , 0 - , tools js/undefined - :abortSignal abort-signal - :httpOptions $ js-object (:baseUrl |https://ja.chenyong.life) - :responseMimeType $ if json? |application/json js/undefined - do - js/setTimeout $ fn () - d! $ :: :states-merge cursor state - {} (:answer |) (:thinking |) (:loading? true) (:done? false) (:messages messages1) - js-await $ js-for-await sdk-result - fn (chunk) - if (js-present-dynamic? chunk) - let - stream-chunk $ unsafe-coerce (decode-genai-chunk chunk) 'app.comp.container/StreamChunk - is-thinking? $ :thinking? stream-chunk - text $ :text stream-chunk - if is-thinking? (swap! *thinking-text str text) (swap! *text str text) - d! $ :: :states-merge cursor state - {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? false) - :messages $ upsert-assistant-message messages1 @*text @*thinking-text - d! $ :: :states-merge cursor state - {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? false) - :messages $ upsert-assistant-message messages1 @*text @*thinking-text - d! $ :: :states-merge cursor state - {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? true) + mod $ js-await $ js/import |@google/genai + GoogleGenAI $ .-GoogleGenAI mod + reset! *gen-ai-new $ new GoogleGenAI $ js-object + :apiKey $ get-gemini-key! + let + abort $ deref *abort-control + when + js-present-dynamic? abort + let + abort-controller $ unsafe-coerce abort 'Dynamic + do (js/console.warn |Aborting-prev) (.!abort abort-controller) + let + selected $ let + selected0 $ if (.includes? prompt-text |{{selected}}) + js-await $ get-selected + , js/undefined + if (js-present? selected0) (unsafe-coerce selected0 'String) "|<未找到选中内容>" + gen-ai @*gen-ai-new + model $ pick-model variant + content $ .!replace prompt-text |{{selected}} selected + json? $ or (includes? prompt-text |{{json}}) (includes? prompt-text |{{JSON}}) + pro? $ includes? model |pro + has-url? $ or (includes? prompt-text |http://) (includes? prompt-text |https://) + messages0 $ or (:messages state) ([]) + messages1 $ upsert-assistant-message messages0 | | + abort-signal $ let + abort $ new js/AbortController + reset! *abort-control abort + .-signal abort + tools $ -> + js-array + if search? + js-object $ :googleSearch $ js-object + , js/undefined + if has-url? + js-object $ :urlContext $ js-object + , js/undefined + .!filter $ fn (x _idx _arr) x + sdk-result $ js-await $ .!generateContentStream + unsafe-coerce + .-models $ unsafe-coerce gen-ai 'Dynamic + , 'Dynamic + js-object (:model model) + :contents $ messages->gemini messages0 + :config $ js-object + :thinkingConfig $ if think? + js-object (:thinkingLevel |high) (:includeThoughts true) + js-object + :thinkingLevel $ if + contains? + #{} |gemini-3.7-flash |gemini-3.8-flash + , model + , |low |minimal + :includeThoughts false + :tools $ if + > + unsafe-coerce + .-length $ unsafe-coerce tools 'Dynamic + , 'Number + , 0 + , tools js/undefined + :abortSignal abort-signal + :httpOptions $ js-object $ :baseUrl |https://ja.chenyong.life + :responseMimeType $ if json? |application/json js/undefined + do + js/setTimeout $ fn () $ d! + :: :states-merge cursor state $ {} (:answer |) (:thinking |) (:loading? true) (:done? false) (:messages messages1) + js-await $ js-for-await sdk-result $ fn (chunk) + if + js-present-dynamic? chunk + let + stream-chunk $ unsafe-coerce + decode-genai-chunk chunk + , 'app.comp.container/StreamChunk + is-thinking? $ :thinking? stream-chunk + text $ :text stream-chunk + if is-thinking? (swap! *thinking-text str text) (swap! *text str text) + d! $ :: :states-merge cursor state $ {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? false) + :messages $ upsert-assistant-message messages1 @*text @*thinking-text + d! $ :: :states-merge cursor state $ {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? false) :messages $ upsert-assistant-message messages1 @*text @*thinking-text + d! $ :: :states-merge cursor state $ {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? true) + :messages $ upsert-assistant-message messages1 @*text @*thinking-text :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] 'Tag 'List 'app.schema/ChatState 'String 'Bool 'Bool 'Dynamic 'Ref 'Ref - :features $ #{} :js-ffi + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] 'Tag 'List 'app.schema/ChatState 'String 'Bool 'Bool 'Dynamic 'Ref 'Ref + :features $ #{} :js-ffi 'call-imagen-4-msg! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn call-imagen-4-msg! (variant cursor state prompt-text d!) - hint-fn $ {} (:async true) - if (= false @*gen-ai-new) - let - mod $ js-await (js/import |@google/genai) - GoogleGenAI $ .-GoogleGenAI mod - reset! *gen-ai-new $ new GoogleGenAI - js-object $ :apiKey (get-gemini-key!) + :code $ quote $ defn call-imagen-4-msg! (variant cursor state prompt-text d!) + hint-fn $ {} $ :async true + if (= false @*gen-ai-new) let - target0 $ js/document.querySelector |.show-image - when (js-present-dynamic? target0) - let - target $ unsafe-coerce target0 'Dynamic - .!removeAttribute target |src + mod $ js-await $ js/import |@google/genai + GoogleGenAI $ .-GoogleGenAI mod + reset! *gen-ai-new $ new GoogleGenAI $ js-object + :apiKey $ get-gemini-key! + let + target0 $ js/document.querySelector |.show-image + when + js-present-dynamic? target0 + let + target $ unsafe-coerce target0 'Dynamic + .!removeAttribute target |src + let + abort $ deref *abort-control + when + js-present-dynamic? abort + let + abort-controller $ unsafe-coerce abort 'Dynamic + do (js/console.warn |Aborting-prev) (.!abort abort-controller) + clear-image-cache! + d! $ :: :states cursor $ -> state (assoc :answer |) (assoc :loading? true) + let + gen-ai @*gen-ai-new + abort-signal $ let + abort $ new js/AbortController + reset! *abort-control abort + .-signal abort + response $ js-await $ .!generateImages + unsafe-coerce + .-models $ unsafe-coerce gen-ai 'Dynamic + , 'Dynamic + js-object + :model |imagen-4.0-generate-001 + :prompt prompt-text + :config $ js-object (:numberOfImages 1) + :includeRaiReason true + :httpOptions $ js-object $ :baseUrl |https://ja.chenyong.life + :signal abort-signal + *text $ atom | let - abort $ deref *abort-control - when (js-present-dynamic? abort) + response-data $ unsafe-coerce response 'Dynamic + generated-images $ unsafe-coerce + .-generatedImages response-data + , 'Dynamic + generated-image $ unsafe-coerce (.-0 generated-images) 'Dynamic + image $ unsafe-coerce (.-image generated-image) 'Dynamic + image-data0 $ .-imageBytes image + when + js-present-dynamic? image-data0 let - abort-controller $ unsafe-coerce abort 'Dynamic - do (js/console.warn |Aborting-prev) (.!abort abort-controller) - clear-image-cache! - d! $ :: :states cursor - -> state (assoc :answer |) (assoc :loading? true) + image-data $ unsafe-coerce image-data0 'String + image-blob $ base64ToBlob image-data + url $ js/URL.createObjectURL image-blob + target $ unsafe-coerce + js/document.querySelector |.show-image + , 'Dynamic + reset! *image-cache url + .!setAttribute target |src url + do (swap! *text str | "|(image ready)") + d! (:: :states cursor) + -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? false) + d! $ :: :states cursor $ -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? true) + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] 'Tag 'List 'app.schema/ChatState 'String 'Dynamic + :features $ #{} :js-ffi + 'call-openrouter! $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn call-openrouter! (cursor state prompt-text variant thinking? d! *text) + hint-fn $ {} (:async true) + :args $ [] 'List 'app.schema/ChatState 'String 'String 'Bool 'Dynamic 'Ref + if (= false @*openai) let - gen-ai @*gen-ai-new - abort-signal $ let + mod $ js-await $ js/import |openai + OpenAI $ .-default mod + deepseek? $ includes? variant |deepseek + base-url $ if deepseek? |https://api.deepseek.com |https://openrouter.ai/api/v1 + key-fn $ if deepseek? get-deepseek-key! get-openrouter-key! + reset! *openai $ new OpenAI $ js-object (:baseURL base-url) + :apiKey $ key-fn + :defaultHeaders $ js-object + :dangerouslyAllowBrowser true + let + abort $ deref *abort-control + when + js-present-dynamic? abort + let + abort-controller $ unsafe-coerce abort 'Dynamic + do (js/console.warn |Aborting-prev) (.!abort abort-controller) + let + selected $ let + selected0 $ js-await $ get-selected + if (js-present? selected0) (unsafe-coerce selected0 'String) "|<未找到选中内容>" + openai $ let + ai @*openai + , ai + content $ .!replace prompt-text |{{selected}} selected + json? $ or (includes? prompt-text |{{json}}) (includes? prompt-text |{{JSON}}) + messages0 $ or (:messages state) ([]) + messages1 $ upsert-assistant-message messages0 | | + sdk-result $ js-await $ .!create + unsafe-coerce + .-completions $ unsafe-coerce + .-chat $ unsafe-coerce openai 'Dynamic + , 'Dynamic + , 'Dynamic + js-object (:model variant) + :messages $ messages->openai messages0 + :stream true + :headers $ js-object $ :HTTP-Referer js/location.host + js-object $ :signal $ let abort $ new js/AbortController reset! *abort-control abort .-signal abort - response $ js-await - .!generateImages - unsafe-coerce - .-models $ unsafe-coerce gen-ai 'Dynamic - , 'Dynamic - js-object (:model |imagen-4.0-generate-001) (:prompt prompt-text) - :config $ js-object (:numberOfImages 1) (:includeRaiReason true) - :httpOptions $ js-object (:baseUrl |https://ja.chenyong.life) - :signal abort-signal - *text $ atom | - let - response-data $ unsafe-coerce response 'Dynamic - generated-images $ unsafe-coerce (.-generatedImages response-data) 'Dynamic - generated-image $ unsafe-coerce (.-0 generated-images) 'Dynamic - image $ unsafe-coerce (.-image generated-image) 'Dynamic - image-data0 $ .-imageBytes image - when (js-present-dynamic? image-data0) - let - image-data $ unsafe-coerce image-data0 'String - image-blob $ base64ToBlob image-data - url $ js/URL.createObjectURL image-blob - target $ unsafe-coerce (js/document.querySelector |.show-image) 'Dynamic - reset! *image-cache url - .!setAttribute target |src url - do (swap! *text str | "|(image ready)") - d! (:: :states cursor) - -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? false) - d! $ :: :states cursor - -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? true) - :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] 'Tag 'List 'app.schema/ChatState 'String 'Dynamic - :features $ #{} :js-ffi - 'call-openrouter! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn call-openrouter! (cursor state prompt-text variant thinking? d! *text) - hint-fn $ {} (:async true) - :args $ [] 'List 'app.schema/ChatState 'String 'String 'Bool 'Dynamic 'Ref - if (= false @*openai) + do + js/setTimeout $ fn () $ d! + :: :states-merge cursor state $ {} (:answer |) (:thinking |) (:loading? true) (:done? false) (:messages messages1) let - mod $ js-await (js/import |openai) - OpenAI $ .-default mod - deepseek? $ includes? variant |deepseek - base-url $ if deepseek? |https://api.deepseek.com |https://openrouter.ai/api/v1 - key-fn $ if deepseek? get-deepseek-key! get-openrouter-key! - reset! *openai $ new OpenAI - js-object (:baseURL base-url) - :apiKey $ key-fn - :defaultHeaders $ js-object - :dangerouslyAllowBrowser true - let - abort $ deref *abort-control - when (js-present-dynamic? abort) - let - abort-controller $ unsafe-coerce abort 'Dynamic - do (js/console.warn |Aborting-prev) (.!abort abort-controller) - let - selected $ let - selected0 $ js-await (get-selected) - if (js-present? selected0) (unsafe-coerce selected0 'String) "|<未找到选中内容>" - openai $ let - ai @*openai - , ai - content $ .!replace prompt-text |{{selected}} selected - json? $ or (includes? prompt-text |{{json}}) (includes? prompt-text |{{JSON}}) - messages0 $ or (:messages state) ([]) - messages1 $ upsert-assistant-message messages0 | | - sdk-result $ js-await - .!create - unsafe-coerce - .-completions $ unsafe-coerce - .-chat $ unsafe-coerce openai 'Dynamic - , 'Dynamic - , 'Dynamic - js-object (:model variant) - :messages $ messages->openai messages0 - :stream true - :headers $ js-object (:HTTP-Referer js/location.host) - js-object $ :signal - let - abort $ new js/AbortController - reset! *abort-control abort - .-signal abort - do - js/setTimeout $ fn () - d! $ :: :states-merge cursor state - {} (:answer |) (:thinking |) (:loading? true) (:done? false) (:messages messages1) - let - *thinking-text $ atom | - *char-buf $ atom 0 - js-await $ js-for-await sdk-result - fn (chunk) - if (js-present-dynamic? chunk) - let - chunk-data $ unsafe-coerce chunk 'Dynamic - choices $ unsafe-coerce (.-choices chunk-data) 'Dynamic - choice0 $ unsafe-coerce (.-0 choices) 'Dynamic - choice $ unsafe-coerce (.-delta choice0) 'Dynamic - reason0 $ .-reasoning_content choice - reason $ if (js-present-dynamic? reason0) (stream-text reason0) | - text0 $ .-content choice - text $ if (js-present-dynamic? text0) (stream-text text0) | - if - not $ blank? reason - swap! *thinking-text str reason - if - not $ blank? text - swap! *text str text - swap! *char-buf + $ count text - when - or (> @*char-buf 80) (includes? text &newline) - do (reset! *char-buf 0) - d! $ :: :states-merge cursor state - {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? false) - :messages $ upsert-assistant-message messages1 @*text @*thinking-text - d! $ :: :states-merge cursor state - {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? false) - :messages $ upsert-assistant-message messages1 @*text @*thinking-text - d! $ :: :states-merge cursor state - {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? true) - :messages $ upsert-assistant-message messages1 @*text @*thinking-text + *thinking-text $ atom | + *char-buf $ atom 0 + js-await $ js-for-await sdk-result $ fn (chunk) + if + js-present-dynamic? chunk + let + chunk-data $ unsafe-coerce chunk 'Dynamic + choices $ unsafe-coerce (.-choices chunk-data) 'Dynamic + choice0 $ unsafe-coerce (.-0 choices) 'Dynamic + choice $ unsafe-coerce (.-delta choice0) 'Dynamic + reason0 $ .-reasoning_content choice + reason $ if + js-present-dynamic? reason0 + stream-text reason0 + , | + text0 $ .-content choice + text $ if + js-present-dynamic? text0 + stream-text text0 + , | + if + not $ blank? reason + swap! *thinking-text str reason + if + not $ blank? text + swap! *text str text + swap! *char-buf + $ count text + when + or (> @*char-buf 80) (includes? text &newline) + do (reset! *char-buf 0) + d! $ :: :states-merge cursor state $ {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? false) + :messages $ upsert-assistant-message messages1 @*text @*thinking-text + d! $ :: :states-merge cursor state $ {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? false) + :messages $ upsert-assistant-message messages1 @*text @*thinking-text + d! $ :: :states-merge cursor state $ {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? true) + :messages $ upsert-assistant-message messages1 @*text @*thinking-text :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] 'List 'app.schema/ChatState 'String 'String 'Bool 'Dynamic 'Ref - :features $ #{} :js-ffi + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] 'List 'app.schema/ChatState 'String 'String 'Bool 'Dynamic 'Ref + :features $ #{} :js-ffi 'clear-image-cache! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn clear-image-cache! () $ let + :code $ quote $ defn clear-image-cache! () + let url @*image-cache when (not= false url) do @@ -539,1096 +541,1172 @@ :examples $ [] :schema $ :: 'Dynamic 'comp-abort $ %{} 'CodeEntry (:doc |) - :code $ quote - defn comp-abort (t) - span - {} (:role |button) - :aria-label $ str |abort- t - :class-name $ str-spaced css/font-fancy css/row-middle style-more - :style $ {} (:cursor :pointer) - :on-click $ fn (e d!) - let - abort $ deref *abort-control - when (js-present-dynamic? abort) - let - abort-controller $ unsafe-coerce abort 'Dynamic - do (js/console.warn |Aborting-prev) (.!abort abort-controller) - , &unit - <> t - =< 8 nil - <> "|✕" style-abort-close + :code $ quote $ defn comp-abort (t) + span + {} (:role |button) + :aria-label $ str |abort- t + :class-name $ str-spaced css/font-fancy css/row-middle style-more + :style $ {} $ :cursor :pointer + :on-click $ fn (e d!) + let + abort $ deref *abort-control + when + js-present-dynamic? abort + let + abort-controller $ unsafe-coerce abort 'Dynamic + do (js/console.warn |Aborting-prev) (.!abort abort-controller) + , &unit + <> t + =< 8 nil + <> "|✕" style-abort-close :examples $ [] :schema $ :: 'Dynamic 'comp-container $ %{} 'CodeEntry (:doc |) - :code $ quote - defcomp comp-container (reel) - let - app-store $ option:fold (get reel :store) - fn () store - fn (raw-store) - if (struct? raw-store) (unsafe-coerce raw-store 'app.schema/Store) (decode-map-as raw-store 'app.schema/Store) - sessions $ :sessions app-store - archived-count $ :archived-count app-store - current-session-id $ :current-session-id app-store - states $ :states app-store - cursor $ option:unwrap-or (get states :cursor) ([]) - state $ let - raw-chat-state $ option:unwrap-or (get states :data) - {} (:answer |) (:loading? false) (:done? true) - :messages $ [] - :model :gemini - :thinking | - unsafe-coerce (normalize-chat-state raw-chat-state) 'app.schema/ChatState - done? $ :done? state - messages $ :messages state - model $ if - tag? $ :model state - :model state - , :gemini - is-viewing-history? $ and (some? current-session-id) - option:fold - first $ filter sessions - fn (s) - hint-fn $ {} - :args $ [] 'app.schema/ChatSession - = (:id s) current-session-id - fn () false - fn (session) - hint-fn $ {} - :args $ [] 'app.schema/ChatSession - :is-history? session - last-assistant $ option:fold (last messages) - fn () $ :answer state - fn (last-msg) - hint-fn $ {} - :args $ [] 'app.schema/ChatMessage - if - = :assistant $ :role last-msg - :content last-msg - :answer state - model-plugin $ use-modal-menu (>> states :model) - {} (; :title "|Select model") - :style $ {} (:width 300) - :backdrop-style $ {} - ; :card-class style-card - ; :backdrop-class style-backdrop - ; :confirm-class style-confirm - :items models-menu - :on-result $ fn (result d!) - d! cursor $ assoc state :model - option:unwrap-or (nth result 1) :gemini - reply-plugin $ use-prompt (>> states :reply-prompt) - {} (:text |Follow-up) (:placeholder "|Enter your follow-up") (:multiline? true) (:button-text |Send) - :validator $ fn (text) - if (blank? text) "|Please enter text" | - api-key-plugin $ use-prompt (>> states :api-key-prompt) - {} (:text "|API key required") (:placeholder "|Enter API key") (:button-text |Save) - :validator $ fn (text) - if (blank? text) "|Please enter API key" | - text-alert-plugin $ use-alert (>> states :empty-message-alert) - {} $ :text "|Please enter text" - submit-with-key! $ fn (submit-state text search? think? d!) - hint-fn $ {} - :args $ [] 'app.schema/ChatState 'String 'Bool 'Bool 'Dynamic - :features $ #{} :js-ffi - if (blank? text) (.show text-alert-plugin d!) - let - storage-key $ model-storage-key model - stored0 $ js/localStorage.getItem storage-key - stored $ if (js-present-dynamic? stored0) (unsafe-coerce stored0 'String) | - if (blank? stored) - .show api-key-plugin d! $ fn (key) - do (js/localStorage.setItem storage-key key) (submit-message! cursor submit-state text search? think? model d!) - submit-message! cursor submit-state text search? think? model d! - message-box-state $ let - raw-message-box-state $ option:unwrap-or - get (>> states :message-box) :data - {} (:content |) (:search? false) (:think? false) (:focus-mode? false) - if (struct? raw-message-box-state) (unsafe-coerce raw-message-box-state 'app.schema/MessageBoxState) (decode-map-as raw-message-box-state 'app.schema/MessageBoxState) - sessions-plugin $ use-drawer (>> states :sessions-modal) - {} (:title "|History Sessions") - :style $ {} (:min-width "||max(320px,30vw)\"") (:max-width |80vw) - :render $ fn (on-close) - comp-sessions-modal sessions archived-count - fn (session-id d!) - when - and - > (count messages) 0 - :done? state - not $ js-present-dynamic? current-session-id - d! $ :: :save-session state - d! $ :: :load-session cursor state - -> sessions - filter $ fn (s) - hint-fn $ {} - :args $ [] 'app.schema/ChatSession - = (:id s) session-id - , first option:unwrap - on-close d! - , on-close $ fn (d!) - hint-fn $ {} (:async true) - let - archive-key $ :archive-key site - raw $ js-await (db-get archive-key) - if (blank? raw) (js/alert "|No archives found!") - let - parsed $ parse-cirru-edn raw - reset! *archived-sessions parsed - div - {} $ :class-name (str-spaced css/preset css/global css/column css/fullscreen css/gap8 style-app-global) - if (not= false @*archived-sessions) - if (not= false @*viewing-archive-session) (; Render specific read-only archived session) + :code $ quote $ defcomp comp-container (reel) + let + app-store $ option:fold (get reel :store) + fn () store + fn (raw-store) + if (struct? raw-store) + unsafe-coerce raw-store 'app.schema/Store + decode-map-as raw-store 'app.schema/Store + sessions $ :sessions app-store + archived-count $ :archived-count app-store + current-session-id $ :current-session-id app-store + states $ :states app-store + cursor $ option:unwrap-or (get states :cursor) ([]) + state $ let + raw-chat-state $ option:unwrap-or (get states :data) + {} (:answer |) (:loading? false) (:done? true) + :messages $ [] + :model :gemini + :thinking | + unsafe-coerce + normalize-chat-state raw-chat-state + , 'app.schema/ChatState + done? $ :done? state + messages $ :messages state + model $ if + tag? $ :model state + :model state + , :gemini + is-viewing-history? $ and + some? current-session-id + option:fold + first $ filter sessions $ fn (s) + hint-fn $ {} $ :args + [] 'app.schema/ChatSession + = (:id s) current-session-id + fn () false + fn (session) + hint-fn $ {} $ :args + [] 'app.schema/ChatSession + :is-history? session + last-assistant $ option:fold (last messages) + fn () $ :answer state + fn (last-msg) + hint-fn $ {} $ :args + [] 'app.schema/ChatMessage + if + = :assistant $ :role last-msg + :content last-msg + :answer state + model-plugin $ use-modal-menu (>> states :model) + {} (; :title "|Select model") + :style $ {} $ :width 300 + :backdrop-style $ {} + ; :card-class style-card + ; :backdrop-class style-backdrop + ; :confirm-class style-confirm + :items models-menu + :on-result $ fn (result d!) + d! cursor $ assoc state :model $ option:unwrap-or (nth result 1) :gemini + reply-plugin $ use-prompt (>> states :reply-prompt) + {} (:text |Follow-up) + :placeholder "|Enter your follow-up" + :multiline? true + :button-text |Send + :validator $ fn (text) + if (blank? text) "|Please enter text" | + api-key-plugin $ use-prompt (>> states :api-key-prompt) + {} + :text "|API key required" + :placeholder "|Enter API key" + :button-text |Save + :validator $ fn (text) + if (blank? text) "|Please enter API key" | + text-alert-plugin $ use-alert + >> states :empty-message-alert + {} $ :text "|Please enter text" + submit-with-key! $ fn (submit-state text search? think? d!) + hint-fn $ {} + :args $ [] 'app.schema/ChatState 'String 'Bool 'Bool 'Dynamic + :features $ #{} :js-ffi + if (blank? text) + .show text-alert-plugin d! "|Please enter text" + let + storage-key $ model-storage-key model + stored0 $ js/localStorage.getItem storage-key + stored $ if + js-present-dynamic? stored0 + unsafe-coerce stored0 'String + , | + if (blank? stored) + .show api-key-plugin d! $ fn (key) + do + js/localStorage.setItem storage-key key + submit-message! cursor submit-state text search? think? model d! + submit-message! cursor submit-state text search? think? model d! + message-box-state $ let + raw-message-box-state $ option:unwrap-or + get (>> states :message-box) :data + {} (:content |) (:search? false) (:think? false) (:focus-mode? false) + if + struct? raw-message-box-state + unsafe-coerce raw-message-box-state 'app.schema/MessageBoxState + decode-map-as raw-message-box-state 'app.schema/MessageBoxState + sessions-plugin $ use-drawer (>> states :sessions-modal) + {} + :title "|History Sessions" + :style $ {} + :min-width "||max(320px,30vw)\"" + :max-width |80vw + :render $ fn (on-close) + comp-sessions-modal sessions archived-count + fn (session-id d!) + when + and + > (count messages) 0 + :done? state + not $ js-present-dynamic? current-session-id + d! $ :: :save-session state + d! $ :: :load-session cursor state $ -> sessions + filter $ fn (s) + hint-fn $ {} $ :args + [] 'app.schema/ChatSession + = (:id s) session-id + , first option:unwrap + on-close d! + , on-close $ fn (d!) + hint-fn $ {} $ :async true + let + archive-key $ :archive-key site + raw $ js-await $ db-get archive-key + if (blank? raw) + js/alert "|No archives found!" + let + parsed $ parse-cirru-edn raw + reset! *archived-sessions parsed + div + {} $ :class-name $ str-spaced css/preset css/global css/column css/fullscreen css/gap8 style-app-global + if + not= false @*archived-sessions + if + not= false @*viewing-archive-session + ; Render specific read-only archived session + div + {} $ :class-name $ str-spaced css/expand style-message-area div - {} $ :class-name (str-spaced css/expand style-message-area) - div - {} $ :class-name style-archive-header - div $ {} - :style $ {} (:font-weight :bold) - :inner-text $ str "|Archived: " - :preview $ assert-type (app.schema/normalize-chat-session @*viewing-archive-session) 'app.schema/ChatSession - div - {} (:class-name style-archive-close) - :on-click $ fn (e d!) (reset! *viewing-archive-session false) &unit - <> "|✕" - ; Messages list $ read only + {} $ :class-name style-archive-header + div $ {} + :style $ {} $ :font-weight :bold + :inner-text $ str "|Archived: " $ :preview + assert-type + app.schema/normalize-chat-session @*viewing-archive-session + , 'app.schema/ChatSession div - {} (:role |region) (:aria-label |message-list) - :class-name $ str-spaced css/column style-message-list - list-> - {} $ :class-name (str-spaced css/column css/gap8) - -> - :messages $ assert-type (app.schema/normalize-chat-session @*viewing-archive-session) 'app.schema/ChatSession - map-indexed $ fn (idx msg) - hint-fn $ {} - :args $ [] 'Number 'app.schema/ChatMessage - [] idx $ let - role $ :role msg - content $ stream-text (:content msg) - thinking $ stream-text (:thinking msg) + {} + :class-name style-archive-close + :on-click $ fn (e d!) + reset! *viewing-archive-session false + , &unit + <> "|✕" + ; Messages list $ read only + div + {} (:role |region) (:aria-label |message-list) + :class-name $ str-spaced css/column style-message-list + list-> + {} $ :class-name $ str-spaced css/column css/gap8 + -> + :messages $ assert-type + app.schema/normalize-chat-session @*viewing-archive-session + , 'app.schema/ChatSession + map-indexed $ fn (idx msg) + hint-fn $ {} $ :args + [] 'Number 'app.schema/ChatMessage + [] idx $ let + role $ :role msg + content $ stream-text $ :content msg + thinking $ stream-text $ :thinking msg + div + {} $ :class-name $ str-spaced style-message-item + if (= role :assistant) style-message-assistant style-message-user div - {} $ :class-name - str-spaced style-message-item $ if (= role :assistant) style-message-assistant style-message-user + {} $ :class-name style-message-role + <> $ if (= role :assistant) |Assistant |You + if + not $ blank? thinking div - {} $ :class-name style-message-role - <> $ if (= role :assistant) |Assistant |You - if - not $ blank? thinking - div - {} $ :class-name style-thinking - memo-comp-by thinking comp-md-block thinking $ {} (:class-name style-md-content) - if (= role :assistant) - if (json-pattern? content) - pre $ {} (:class-name style-code-content) (:inner-text content) - memo-comp-by content comp-md-block content $ {} (:class-name style-md-content) - pre $ {} (:class-name style-message-text) (:inner-text content) - ; Render archived sessions list + {} $ :class-name style-thinking + memo-comp-by thinking comp-md-block thinking $ {} $ :class-name style-md-content + if (= role :assistant) + if (json-pattern? content) + pre $ {} + :class-name style-code-content + :inner-text content + memo-comp-by content comp-md-block content $ {} $ :class-name style-md-content + pre $ {} + :class-name style-message-text + :inner-text content + ; Render archived sessions list + div + {} $ :class-name $ str-spaced css/expand style-message-area div - {} $ :class-name (str-spaced css/expand style-message-area) - div - {} $ :class-name style-archive-header - div $ {} - :style $ {} (:font-weight :bold) - :inner-text "|All Archived Sessions" - div - {} (:class-name style-archive-close) - :on-click $ fn (e d!) (reset! *archived-sessions false) (reset! *viewing-archive-session false) &unit - <> "|✕" - ; List + {} $ :class-name style-archive-header + div $ {} + :style $ {} $ :font-weight :bold + :inner-text "|All Archived Sessions" div - {} $ :class-name (str-spaced css/column css/gap8 style-message-list) - list-> - {} $ :class-name css/column - let - current-archives $ or @*archived-sessions ([]) - if (empty? current-archives) - [] :empty $ div - {} $ :style - {} (:padding |12px) - :color $ hsl 0 0 60 - <> "|No archived sessions left." - -> current-archives reverse $ map - fn (session) - hint-fn $ {} - :args $ [] 'app.schema/ChatSession - let - session-id $ :id session - created-at $ :created-at session - preview $ :preview session - date-str $ unsafe-coerce - .!toLocaleString $ new js/Date created-at - , 'String - [] session-id $ div - {} $ :class-name style-session-item - div - {} (:role |button) - :style $ {} (:flex |1) (:cursor :pointer) (:min-width 0) (:overflow :hidden) - :on-click $ fn (e d!) (reset! *viewing-archive-session session) &unit - div - {} $ :style - {} (:font-size |12px) - :color $ hsl 0 0 60 - <> date-str - div - {} $ :style - {} (:margin-top |4px) (:white-space :nowrap) (:overflow :hidden) (:text-overflow :ellipsis) (:max-height |1.2em) (:line-height |1.2) - <> preview - div - {} (:class-name style-delete-button) (:role |button) - :on-click $ fn (e d!) - hint-fn $ {} (:async true) + {} + :class-name style-archive-close + :on-click $ fn (e d!) + reset! *archived-sessions false + reset! *viewing-archive-session false + , &unit + <> "|✕" + ; List + div + {} $ :class-name $ str-spaced css/column css/gap8 style-message-list + list-> + {} $ :class-name css/column + let + current-archives $ or @*archived-sessions $ [] + if (empty? current-archives) + [] :empty $ div + {} $ :style $ {} (:padding |12px) + :color $ hsl 0 0 60 + <> "|No archived sessions left." + -> current-archives reverse $ map $ fn (session) + hint-fn $ {} $ :args + [] 'app.schema/ChatSession + let + session-id $ :id session + created-at $ :created-at session + preview $ :preview session + date-str $ unsafe-coerce + .!toLocaleString $ new js/Date created-at + , 'String + [] session-id $ div + {} $ :class-name style-session-item + div + {} (:role |button) + :style $ {} (:flex |1) (:cursor :pointer) (:min-width 0) (:overflow :hidden) + :on-click $ fn (e d!) + reset! *viewing-archive-session session + , &unit + div + {} $ :style $ {} (:font-size |12px) + :color $ hsl 0 0 60 + <> date-str + div + {} $ :style $ {} (:margin-top |4px) (:white-space :nowrap) (:overflow :hidden) (:text-overflow :ellipsis) (:max-height |1.2em) (:line-height |1.2) + <> preview + div + {} + :class-name style-delete-button + :role |button + :on-click $ fn (e d!) + hint-fn $ {} $ :async true + let + proceed? $ js/confirm "|Delete this archived session?" + when proceed? $ let + new-archives $ filter @*archived-sessions $ fn (s) + hint-fn $ {} $ :args + [] 'app.schema/ChatSession + not= (:id s) session-id + reset! *archived-sessions new-archives let - proceed? $ js/confirm "|Delete this archived session?" - when proceed? $ let - new-archives $ filter @*archived-sessions - fn (s) - hint-fn $ {} - :args $ [] 'app.schema/ChatSession - not= (:id s) session-id - reset! *archived-sessions new-archives - let - archive-key $ :archive-key site - js-await $ db-set archive-key (format-cirru-edn new-archives) - d! $ :: :update-archived-count (count new-archives) - , &unit - <> "|✕" - ; Else render normal chat view + archive-key $ :archive-key site + js-await $ db-set archive-key $ format-cirru-edn new-archives + d! $ :: :update-archived-count $ count new-archives + , &unit + <> "|✕" + ; Else render normal chat view + div + {} $ :class-name $ str-spaced css/expand css/column div - {} $ :class-name (str-spaced css/expand css/column) + {} $ :class-name $ str-spaced css/expand style-message-area div - {} $ :class-name (str-spaced css/expand style-message-area) + {} + :class-name $ str-spaced css/row-parted + :style $ {} $ :padding |8px + div $ {} div - {} - :class-name $ str-spaced css/row-parted - :style $ {} (:padding |8px) - div $ {} + {} (:class-name css/row-middle) (:title |History) (:role |button) (:aria-label |history-open) + :style $ {} $ :cursor :pointer + :on-click $ fn (e d!) (.show sessions-plugin d!) &unit div - {} (:class-name css/row-middle) (:title |History) (:role |button) (:aria-label |history-open) - :style $ {} (:cursor :pointer) - :on-click $ fn (e d!) (.show sessions-plugin d!) &unit - div - {} $ :class-name style-history-button - comp-local-icon :clock 14 - =< 4 nil - if - > (count sessions) 0 - <> - str $ count sessions - str-spaced css/font-fancy style-history-count - div - {} (:role |region) (:aria-label |message-list) - :class-name $ str-spaced css/column style-message-list + {} $ :class-name style-history-button + comp-local-icon :clock 14 + =< 4 nil if - or (= :imagen-4 model) (= :flash-imagen model) - img $ {} - :class-name $ str-spaced style-image |show-image - list-> - {} $ :class-name (str-spaced css/column css/gap8) - -> messages $ map-indexed - fn (idx msg) - hint-fn $ {} - :args $ [] 'Number 'app.schema/ChatMessage - [] idx $ let - role $ :role msg - content $ stream-text (:content msg) - thinking $ stream-text (:thinking msg) + > (count sessions) 0 + <> + str $ count sessions + str-spaced css/font-fancy style-history-count + div + {} (:role |region) (:aria-label |message-list) + :class-name $ str-spaced css/column style-message-list + if + or (= :imagen-4 model) (= :flash-imagen model) + img $ {} $ :class-name (str-spaced style-image |show-image) + list-> + {} $ :class-name $ str-spaced css/column css/gap8 + -> messages $ map-indexed $ fn (idx msg) + hint-fn $ {} $ :args + [] 'Number 'app.schema/ChatMessage + [] idx $ let + role $ :role msg + content $ stream-text $ :content msg + thinking $ stream-text $ :thinking msg + div + {} $ :class-name $ str-spaced style-message-item + if (= role :assistant) style-message-assistant style-message-user + div + {} $ :class-name style-message-role + <> $ if (= role :assistant) |Assistant |You + if + not $ blank? thinking div - {} $ :class-name - str-spaced style-message-item $ if (= role :assistant) style-message-assistant style-message-user - div - {} $ :class-name style-message-role - <> $ if (= role :assistant) |Assistant |You - if - not $ blank? thinking - div - {} $ :class-name style-thinking - memo-comp-by thinking comp-md-block thinking $ {} (:class-name style-md-content) - if (= role :assistant) - if (json-pattern? content) - pre $ {} (:class-name style-code-content) (:inner-text content) - memo-comp-by content comp-md-block content $ {} (:class-name style-md-content) - pre $ {} (:class-name style-message-text) (:inner-text content) - if - and (= role :assistant) - or done? $ not= idx - dec $ count messages - div - {} $ :class-name (str-spaced css/row-middle css/gap8 style-message-actions) - if chrome-extension? (comp-fill content) nil - comp-copy content - , nil - if - and - > (count messages) 0 - :done? state - not is-viewing-history? - div - {} $ :class-name (str-spaced css/row-middle css/gap8 style-reply-actions) - button - {} (:role |button) (:aria-label |reply-message) - :class-name $ str-spaced css/button style-reply-button - :on-click $ fn (e d!) - .show reply-plugin d! $ fn (text) - submit-with-key! state (stream-text text) (:search? message-box-state) (:think? message-box-state) d! - , &unit - <> |Reply - if (:focus-mode? message-box-state) nil $ a - {} (:class-name style-focus-link) (:inner-text |Focus) (:role |button) (:aria-label |focus-composer) - :on-click $ fn (e d!) - let - focused $ .-activeElement (unsafe-coerce js/document 'Dynamic) - do - when (js-present-dynamic? focused) - .!blur $ unsafe-coerce focused 'Dynamic - d! - option:unwrap-or - get (>> states :message-box) :cursor - [] - assoc message-box-state :focus-mode? true - , &unit - , nil - if (:loading? state) - div ({}) (comp-abort |Loading...) + {} $ :class-name style-thinking + memo-comp-by thinking comp-md-block thinking $ {} $ :class-name style-md-content + if (= role :assistant) + if (json-pattern? content) + pre $ {} + :class-name style-code-content + :inner-text content + memo-comp-by content comp-md-block content $ {} $ :class-name style-md-content + pre $ {} + :class-name style-message-text + :inner-text content + if + and (= role :assistant) + or done? $ not= idx $ dec (count messages) + div + {} $ :class-name $ str-spaced css/row-middle css/gap8 style-message-actions + if chrome-extension? (comp-fill content) nil + comp-copy content + , nil + if + and + > (count messages) 0 + :done? state + not is-viewing-history? div - {} $ :class-name css/row-parted - div - {} $ :class-name (str-spaced css/row-middle css/gap8) - if (:done? state) nil $ div - {} $ :style - {} (:display :flex) (:justify-content :center) (:align-items :center) - comp-abort |Streaming... - if (:done? state) - div $ {} - :class-name $ str-spaced css/row-middle css/gap8 - =< nil 200 - comp-message-box (>> states :message-box) - a $ {} - :inner-text $ or (turn-str model) |- - :role |button - :aria-label $ str |model-picker: (turn-string model) - :class-name $ str-spaced style-a-toggler - :style $ {} - :opacity $ if (= model :anthropic) 1 0.3 - :on-click $ fn (e d!) - ; d! $ :: :change-model - .show model-plugin d! - , &unit - fn (text search? think? d!) - if (blank? text) (.show text-alert-plugin d!) - do - when - and - > (count messages) 0 - :done? state - not $ js-present-dynamic? current-session-id - d! $ :: :save-session state - d! $ :: :session :session-id nil - submit-with-key! - -> state - assoc :messages $ [] - assoc :answer | - assoc :thinking | - assoc :done? false - , text search? think? d! - , model - model-plugin.render - reply-plugin.render - text-alert-plugin.render - api-key-plugin.render - sessions-plugin.render - if dev? $ comp-reel (>> states :reel) reel ({}) - if dev? $ comp-inspect |Store app-store nil - :examples $ [] - :schema $ :: 'Fn - {} (:return 'respo.schema/Component) - :args $ [] (:: 'Map 'Tag 'Dynamic) - :features $ #{} :js-ffi + {} $ :class-name $ str-spaced css/row-middle css/gap8 style-reply-actions + button + {} (:role |button) (:aria-label |reply-message) + :class-name $ str-spaced css/button style-reply-button + :on-click $ fn (e d!) + .show reply-plugin d! $ fn (text) + submit-with-key! state (stream-text text) + :search? message-box-state + :think? message-box-state + , d! + , &unit + <> |Reply + if + :focus-mode? message-box-state + , nil $ a $ {} (:class-name style-focus-link) (:inner-text |Focus) (:role |button) (:aria-label |focus-composer) + :on-click $ fn (e d!) + let + focused $ .-activeElement $ unsafe-coerce js/document 'Dynamic + do + when + js-present-dynamic? focused + .!blur $ unsafe-coerce focused 'Dynamic + d! + option:unwrap-or + get (>> states :message-box) :cursor + [] + assoc message-box-state :focus-mode? true + , &unit + , nil + if (:loading? state) + div ({}) (comp-abort |Loading...) + div + {} $ :class-name css/row-parted + div + {} $ :class-name $ str-spaced css/row-middle css/gap8 + if (:done? state) nil $ div + {} $ :style $ {} (:display :flex) (:justify-content :center) (:align-items :center) + comp-abort |Streaming... + if (:done? state) + div $ {} $ :class-name (str-spaced css/row-middle css/gap8) + =< nil 200 + comp-message-box (>> states :message-box) + a $ {} + :inner-text $ or (turn-str model) |- + :role |button + :aria-label $ str |model-picker: $ turn-string model + :class-name $ str-spaced style-a-toggler + :style $ {} $ :opacity + if (= model :anthropic) 1 0.3 + :on-click $ fn (e d!) + ; d! $ :: :change-model + .show model-plugin d! + , &unit + fn (text search? think? d!) + if (blank? text) + .show text-alert-plugin d! "|Please enter text" + do + when + and + > (count messages) 0 + :done? state + not $ js-present-dynamic? current-session-id + d! $ :: :save-session state + d! $ :: :session :session-id nil + submit-with-key! + -> state + assoc :messages $ [] + assoc :answer | + assoc :thinking | + assoc :done? false + , text search? think? d! + , model + model-plugin.render + reply-plugin.render + text-alert-plugin.render + api-key-plugin.render + sessions-plugin.render + if dev? $ comp-reel (>> states :reel) reel $ {} + if dev? $ comp-inspect |Store app-store nil + :examples $ [] + :schema $ :: 'Fn $ {} + :return 'respo.schema/Component + :args $ [] $ :: 'Map 'Tag 'Dynamic + :features $ #{} :js-ffi 'comp-fill $ %{} 'CodeEntry (:doc |) - :code $ quote - defcomp comp-fill (text) - div - {} (:class-name style-fill) (:role |button) (:aria-label |fill-extension) - :on-click $ fn (e d!) - when chrome-extension? $ js/chrome.runtime.sendMessage - js-object (:action |fill-text) (:text text) - , &unit - comp-local-icon :send 12 + :code $ quote $ defcomp comp-fill (text) + div + {} (:class-name style-fill) (:role |button) (:aria-label |fill-extension) + :on-click $ fn (e d!) + when chrome-extension? $ js/chrome.runtime.sendMessage $ js-object (:action |fill-text) (:text text) + , &unit + comp-local-icon :send 12 :examples $ [] :schema $ :: 'Dynamic 'comp-local-icon $ %{} 'CodeEntry (:doc |) - :code $ quote - defcomp comp-local-icon (icon size) - span $ {} (:aria-hidden |true) - :style $ {} (:display :inline-flex) (:align-items :center) (:justify-content :center) - :width $ str size |px - :height $ str size |px - :line-height |0 - :innerHTML $ case-default icon | (:clock "|") (:send "|") - :examples $ [] - :schema $ :: 'Fn - {} (:return 'respo.schema/Component) - :args $ [] 'Tag 'Number + :code $ quote $ defcomp comp-local-icon (icon size) + span $ {} (:aria-hidden |true) + :style $ {} (:display :inline-flex) (:align-items :center) (:justify-content :center) + :width $ str size |px + :height $ str size |px + :line-height |0 + :innerHTML $ case-default icon | + :clock "|" + :send "|" + :examples $ [] + :schema $ :: 'Fn $ {} + :return 'respo.schema/Component + :args $ [] 'Tag 'Number 'comp-message-box $ %{} 'CodeEntry (:doc |) - :code $ quote - defcomp comp-message-box (states picker-el on-submit model) - let - cursor $ option:unwrap-or (get states :cursor) ([]) - state $ let - raw-message-box-state $ option:unwrap-or (get states :data) - {} (:content |) (:search? false) (:think? false) (:focus-mode? false) - if (struct? raw-message-box-state) (unsafe-coerce raw-message-box-state 'app.schema/MessageBoxState) (decode-map-as raw-message-box-state 'app.schema/MessageBoxState) - [] (effect-focus) (on-fill cursor state on-submit) + :code $ quote $ defcomp comp-message-box (states picker-el on-submit model) + let + cursor $ option:unwrap-or (get states :cursor) ([]) + state $ let + raw-message-box-state $ option:unwrap-or (get states :data) + {} (:content |) (:search? false) (:think? false) (:focus-mode? false) + if + struct? raw-message-box-state + unsafe-coerce raw-message-box-state 'app.schema/MessageBoxState + decode-map-as raw-message-box-state 'app.schema/MessageBoxState + [] (effect-focus) (on-fill cursor state on-submit) + div + {} $ :class-name $ str-spaced css/center style-message-box-panel div - {} $ :class-name (str-spaced css/center style-message-box-panel) - div - {} $ :class-name (str-spaced css/column style-message-box) - if (:focus-mode? state) - div - {} (:role |button) (:aria-label |expand-prompt) - :class-name $ str-spaced css/font-code! style-focus-box style-textbox-compact - :on-click $ fn (e d!) - do - d! cursor $ assoc state :focus-mode? false - js/setTimeout - fn () $ .!focus - unsafe-coerce (js/document.querySelector |#message) 'Dynamic - , 0 - , &unit - <> $ if - blank? $ :content state - , "|Click to expand and type..." (:content state) - textarea $ {} - :value $ :content state - :placeholder "|Prompt to try LLM..." - :id |message - :role |textbox - :aria-label |prompt-input - :class-name $ str-spaced css/textarea css/font-code! style-textbox - :on-input $ fn (e d!) - d! cursor $ assoc state :content - str $ option:unwrap-or (get e :value) | - , &unit - :on-keydown $ fn (e d!) - if - and - = 13 $ option:unwrap-or (get e :keycode) 0 - or - option:unwrap-or (get e :meta?) false - option:unwrap-or (get e :ctrl?) false - on-submit (:content state) (:search? state) (:think? state) d! + {} $ :class-name $ str-spaced css/column style-message-box + if (:focus-mode? state) + div + {} (:role |button) (:aria-label |expand-prompt) + :class-name $ str-spaced css/font-code! style-focus-box style-textbox-compact + :on-click $ fn (e d!) + do + d! cursor $ assoc state :focus-mode? false + js/setTimeout + fn () $ .!focus $ unsafe-coerce + js/document.querySelector |#message + , 'Dynamic + , 0 , &unit - :on-focus $ fn (e d!) (toggle-focus-within! e true) &unit - :on-blur $ fn (e d!) (toggle-focus-within! e false) &unit - if - not $ :focus-mode? state - do (=< nil 4) + <> $ if + blank? $ :content state + , "|Click to expand and type..." $ :content state + textarea $ {} + :value $ :content state + :placeholder "|Prompt to try LLM..." + :id |message + :role |textbox + :aria-label |prompt-input + :class-name $ str-spaced css/textarea css/font-code! style-textbox + :on-input $ fn (e d!) + d! cursor $ assoc state :content $ str + option:unwrap-or (get e :value) | + , &unit + :on-keydown $ fn (e d!) + if + and + = 13 $ option:unwrap-or (get e :keycode) 0 + or + option:unwrap-or (get e :meta?) false + option:unwrap-or (get e :ctrl?) false + on-submit (:content state) (:search? state) (:think? state) d! + , &unit + :on-focus $ fn (e d!) + toggle-focus-within! e true + , &unit + :on-blur $ fn (e d!) + toggle-focus-within! e false + , &unit + if + not $ :focus-mode? state + do (=< nil 4) + div + {} $ :class-name css/row-parted + if + not $ blank? $ :content state + span $ {} (:inner-text "|✕") + :class-name $ str-spaced style-close style-clear + :role |button + :aria-label |clear-prompt + :on-click $ fn (e d!) + d! cursor $ assoc state :content | + .!focus $ unsafe-coerce + js/document.querySelector |#message + , 'Dynamic + , &unit + span $ {} $ :class-name style-clear div - {} $ :class-name css/row-parted - if - not $ blank? (:content state) - span $ {} (:inner-text "|✕") - :class-name $ str-spaced style-close style-clear - :role |button - :aria-label |clear-prompt - :on-click $ fn (e d!) - d! cursor $ assoc state :content | - .!focus $ unsafe-coerce (js/document.querySelector |#message) 'Dynamic - , &unit - span $ {} (:class-name style-clear) - div - {} $ :class-name (str-spaced css/row style-gap12) - , picker-el - if - contains? (#{} :gemini-flash :gemini-3.5-flash :gemini-3.1-flash-lite-preview) model - div - {} (:role |group) (:aria-label |think-toggle) - :class-name $ str-spaced css/row style-checkbox - :on-click $ fn (e d!) - d! cursor $ assoc state :think? - not $ :think? state - , &unit - input $ {} - :checked $ :think? state - :type |checkbox - :role |checkbox - :aria-label |think-toggle - <> |Think css/font-fancy - , nil + {} $ :class-name $ str-spaced css/row style-gap12 + , picker-el + if + contains? + #{} :gemini-flash :gemini-3.5-flash :gemini-3.1-flash-lite-preview + , model div - {} (:role |group) (:aria-label |search-toggle) + {} (:role |group) (:aria-label |think-toggle) :class-name $ str-spaced css/row style-checkbox :on-click $ fn (e d!) - d! cursor $ assoc state :search? - not $ :search? state + d! cursor $ assoc state :think? $ not (:think? state) , &unit input $ {} - :checked $ :search? state + :checked $ :think? state :type |checkbox :role |checkbox - :aria-label |search-toggle - <> |Search css/font-fancy - button $ {} (:role |button) (:aria-label |submit-message) - :class-name $ str-spaced css/button style-submit - :inner-text |Submit + :aria-label |think-toggle + <> |Think css/font-fancy + , nil + div + {} (:role |group) (:aria-label |search-toggle) + :class-name $ str-spaced css/row style-checkbox :on-click $ fn (e d!) - on-submit (:content state) (:search? state) (:think? state) d! + d! cursor $ assoc state :search? $ not (:search? state) , &unit - , nil + input $ {} + :checked $ :search? state + :type |checkbox + :role |checkbox + :aria-label |search-toggle + <> |Search css/font-fancy + button $ {} (:role |button) (:aria-label |submit-message) + :class-name $ str-spaced css/button style-submit + :inner-text |Submit + :on-click $ fn (e d!) + on-submit (:content state) (:search? state) (:think? state) d! + , &unit + , nil :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] 'Map 'Dynamic 'Dynamic 'Dynamic - :features $ #{} :js-ffi + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] 'Map 'Dynamic 'Dynamic 'Dynamic + :features $ #{} :js-ffi 'comp-sessions-modal $ %{} 'CodeEntry (:doc |) - :code $ quote - defcomp comp-sessions-modal (sessions archived-count on-select on-close on-view-archive) - let - history-items $ foldl sessions 0 - fn (acc session) - hint-fn $ {} - :args $ [] 'Number 'app.schema/ChatSession - + acc $ count - or (:messages session) ([]) - div - {} $ :class-name (str-spaced css/column css/gap8 style-sessions-list) - if (> archived-count 0) - div - {} $ :class-name style-archive-row - span $ {} - :style $ {} - :color $ hsl 0 0 50 - :inner-text $ str "|Archived: " archived-count "| sessions" - button - {} (:class-name css/button) - :style $ {} (:cursor :pointer) - :on-click $ fn (e d!) (on-close d!) (on-view-archive d!) &unit - <> "|View Archive" - if (empty? sessions) - div - {} $ :style - {} (:padding |12px) - :color $ hsl 0 0 60 - <> "|No history sessions" - list-> - {} $ :class-name css/column - -> sessions reverse $ map - fn (session) - hint-fn $ {} - :args $ [] 'app.schema/ChatSession - let - session-id $ :id session - created-at $ :created-at session - preview $ :preview session - date-str $ unsafe-coerce - .!toLocaleString $ new js/Date created-at - , 'String - [] session-id $ div - {} $ :class-name style-session-item - div - {} (:role |button) - :aria-label $ str |session-select: preview - :style $ {} (:flex |1) (:cursor :pointer) (:min-width 0) (:overflow :hidden) - :on-click $ fn (e d!) (on-select session-id d!) (on-close d!) &unit - div - {} $ :style - {} (:font-size |12px) - :color $ hsl 0 0 60 - <> date-str - div - {} $ :style - {} (:margin-top |4px) (:white-space :nowrap) (:overflow :hidden) (:text-overflow :ellipsis) (:max-height |1.2em) (:line-height |1.2) - <> preview - div - {} (:class-name style-delete-button) (:role |button) - :aria-label $ str |session-delete: session-id - :on-click $ fn (e d!) - let - event $ unsafe-coerce - option:unwrap-or (get e :event) js/undefined - , 'Dynamic - .!stopPropagation event - d! $ :: :remove-session session-id - , &unit - <> "|✕" - if - > (count sessions) 0 + :code $ quote $ defcomp comp-sessions-modal (sessions archived-count on-select on-close on-view-archive) + let + history-items $ foldl sessions 0 $ fn (acc session) + hint-fn $ {} $ :args + [] 'Number 'app.schema/ChatSession + + acc $ count $ or (:messages session) ([]) + div + {} $ :class-name $ str-spaced css/column css/gap8 style-sessions-list + if (> archived-count 0) + div + {} $ :class-name style-archive-row + span $ {} + :style $ {} $ :color (hsl 0 0 50) + :inner-text $ str "|Archived: " archived-count "| sessions" + button + {} (:class-name css/button) + :style $ {} $ :cursor :pointer + :on-click $ fn (e d!) (on-close d!) (on-view-archive d!) &unit + <> "|View Archive" + if (empty? sessions) + div + {} $ :style $ {} (:padding |12px) + :color $ hsl 0 0 60 + <> "|No history sessions" + list-> + {} $ :class-name css/column + -> sessions reverse $ map $ fn (session) + hint-fn $ {} $ :args + [] 'app.schema/ChatSession + let + session-id $ :id session + created-at $ :created-at session + preview $ :preview session + date-str $ unsafe-coerce + .!toLocaleString $ new js/Date created-at + , 'String + [] session-id $ div + {} $ :class-name style-session-item + div + {} (:role |button) + :aria-label $ str |session-select: preview + :style $ {} (:flex |1) (:cursor :pointer) (:min-width 0) (:overflow :hidden) + :on-click $ fn (e d!) (on-select session-id d!) (on-close d!) &unit + div + {} $ :style $ {} (:font-size |12px) + :color $ hsl 0 0 60 + <> date-str + div + {} $ :style $ {} (:margin-top |4px) (:white-space :nowrap) (:overflow :hidden) (:text-overflow :ellipsis) (:max-height |1.2em) (:line-height |1.2) + <> preview + div + {} + :class-name style-delete-button + :role |button + :aria-label $ str |session-delete: session-id + :on-click $ fn (e d!) + let + event $ unsafe-coerce + option:unwrap-or (get e :event) js/undefined + , 'Dynamic + .!stopPropagation event + d! $ :: :remove-session session-id + , &unit + <> "|✕" + if + > (count sessions) 0 + div + {} + :class-name $ str-spaced css/column css/gap8 + :style $ {} $ :padding "|0 12px 12px 12px" div - {} - :class-name $ str-spaced css/column css/gap8 - :style $ {} (:padding "|0 12px 12px 12px") + {} $ :class-name $ str-spaced css/row-parted div - {} $ :class-name (str-spaced css/row-parted) - div - {} $ :class-name (str-spaced css/row css/gap8) - a $ {} (:class-name style-clear) (:inner-text |Data) (:role |button) (:aria-label |sessions-export-data) - :on-click $ fn (e d!) (tab-echo! sessions :edn) &unit - a $ {} (:class-name style-clear) (:inner-text |Download) (:role |button) (:aria-label |sessions-download) - :on-click $ fn (e d!) (download-sessions! sessions) &unit - if - > (count sessions) 0 - a $ {} (:class-name style-clear) (:inner-text "|Archive all") (:role |button) (:aria-label |sessions-archive-all) - :on-click $ fn (e d!) - hint-fn $ {} (:async true) - let - proceed? $ js/confirm - str "|Archive " (count sessions) "| sessions and clear active view?" - when proceed? $ let - archive-key $ :archive-key site - raw $ js-await (db-get archive-key) - old-archives $ if (blank? raw) ([]) (parse-cirru-edn raw) - new-archives $ concat old-archives sessions - js-await $ db-set archive-key (format-cirru-edn new-archives) - d! $ :: :archive-sessions (count new-archives) - , &unit - span $ {} - div $ {} - :style $ {} (:height 200) - , nil - :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] (:: 'List 'app.schema/ChatSession) 'Number 'Dynamic 'Dynamic 'Dynamic - :features $ #{} :js-ffi + {} $ :class-name $ str-spaced css/row css/gap8 + a $ {} (:class-name style-clear) (:inner-text |Data) (:role |button) + :aria-label |sessions-export-data + :on-click $ fn (e d!) + tab-echo! sessions $ %some :edn + , &unit + a $ {} (:class-name style-clear) (:inner-text |Download) (:role |button) + :aria-label |sessions-download + :on-click $ fn (e d!) + download-sessions! sessions + , &unit + if + > (count sessions) 0 + a $ {} (:class-name style-clear) (:inner-text "|Archive all") (:role |button) + :aria-label |sessions-archive-all + :on-click $ fn (e d!) + hint-fn $ {} $ :async true + let + proceed? $ js/confirm $ str "|Archive " (count sessions) "| sessions and clear active view?" + when proceed? $ let + archive-key $ :archive-key site + raw $ js-await $ db-get archive-key + old-archives $ if (blank? raw) ([]) (parse-cirru-edn raw) + new-archives $ concat old-archives sessions + js-await $ db-set archive-key $ format-cirru-edn new-archives + d! $ :: :archive-sessions $ count new-archives + , &unit + span $ {} + div $ {} $ :style + {} $ :height 200 + , nil + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] + :: 'List 'app.schema/ChatSession + , 'Number 'Dynamic 'Dynamic 'Dynamic + :features $ #{} :js-ffi 'create-session $ %{} 'CodeEntry (:doc |) - :code $ quote - defn create-session (messages model) - let - id $ generate-session-id - first-msg $ option:fold (first messages) - fn () |New-chat - fn (message) - hint-fn $ {} - :args $ [] 'app.schema/ChatMessage - :content message - %{} ChatSession (:id id) - :created-at $ unsafe-coerce (js/Date.now) 'Number - :messages messages - :model model - :preview $ let - len $ count first-msg - end $ if (< len 100) len 100 - slice first-msg 0 end - :is-history? false - :examples $ [] - :schema $ :: 'Fn - {} (:return 'app.schema/ChatSession) - :args $ [] (:: 'List 'app.schema/ChatMessage) 'Tag + :code $ quote $ defn create-session (messages model) + let + id $ generate-session-id + first-msg $ option:fold (first messages) + fn () |New-chat + fn (message) + hint-fn $ {} $ :args + [] 'app.schema/ChatMessage + :content message + %{} ChatSession (:id id) + :created-at $ unsafe-coerce (js/Date.now) 'Number + :messages messages + :model model + :preview $ let + len $ count first-msg + end $ if (< len 100) len 100 + slice first-msg 0 end + :is-history? false + :examples $ [] + :schema $ :: 'Fn $ {} + :return 'app.schema/ChatSession + :args $ [] + :: 'List 'app.schema/ChatMessage + , 'Tag 'decode-genai-chunk $ %{} 'CodeEntry (:doc |) - :code $ quote - defn decode-genai-chunk (chunk) - hint-fn $ {} - :args $ [] 'JsObject - :return 'app.comp.container/StreamChunk - :features $ #{} :js-ffi - let - chunk-data $ unsafe-coerce chunk 'Dynamic - candidates0 $ .?-candidates chunk-data - candidate0 $ if (js-present-dynamic? candidates0) - .?-0 $ unsafe-coerce candidates0 'Dynamic - , js/undefined - content0 $ if (js-present-dynamic? candidate0) - .?-content $ unsafe-coerce candidate0 'Dynamic - , js/undefined - parts0 $ if (js-present-dynamic? content0) - .?-parts $ unsafe-coerce content0 'Dynamic - , js/undefined - part0 $ if (js-present-dynamic? parts0) - .?-0 $ unsafe-coerce parts0 'Dynamic - , js/undefined - part $ unsafe-coerce part0 'Dynamic - thought0 $ if (js-present-dynamic? part0) (.?-thought part) js/undefined - thinking? $ if (bool? thought0) (unsafe-coerce thought0 'Bool) false - part-text $ if (js-present-dynamic? part0) (.?-text part) js/undefined - chunk-text $ .?-text chunk-data - primary-text $ if (js-present-dynamic? part-text) part-text chunk-text - prompt-feedback $ .?-promptFeedback chunk-data - fallback-text $ if (js-present-dynamic? prompt-feedback) - .?-blockReason $ unsafe-coerce prompt-feedback 'Dynamic - , js/undefined - text0 $ if (js-present-dynamic? primary-text) primary-text fallback-text - text $ stream-text text0 - %{} StreamChunk (:text text) (:thinking? thinking?) - :examples $ [] - quote $ let - feedback-only $ js-object - :promptFeedback $ js-object (:blockReason |blocked) - decoded $ unsafe-coerce (decode-genai-chunk feedback-only) 'app.comp.container/StreamChunk - assert= |blocked $ :text decoded - assert= false $ :thinking? decoded - :schema $ :: 'Fn - {} (:return 'app.comp.container/StreamChunk) + :code $ quote $ defn decode-genai-chunk (chunk) + hint-fn $ {} :args $ [] 'JsObject + :return 'app.comp.container/StreamChunk :features $ #{} :js-ffi + let + chunk-data $ unsafe-coerce chunk 'Dynamic + candidates0 $ .?-candidates chunk-data + candidate0 $ if + js-present-dynamic? candidates0 + .?-0 $ unsafe-coerce candidates0 'Dynamic + , js/undefined + content0 $ if + js-present-dynamic? candidate0 + .?-content $ unsafe-coerce candidate0 'Dynamic + , js/undefined + parts0 $ if + js-present-dynamic? content0 + .?-parts $ unsafe-coerce content0 'Dynamic + , js/undefined + part0 $ if + js-present-dynamic? parts0 + .?-0 $ unsafe-coerce parts0 'Dynamic + , js/undefined + part $ unsafe-coerce part0 'Dynamic + thought0 $ if + js-present-dynamic? part0 + .?-thought part + , js/undefined + thinking? $ if (bool? thought0) (unsafe-coerce thought0 'Bool) false + part-text $ if + js-present-dynamic? part0 + .?-text part + , js/undefined + chunk-text $ .?-text chunk-data + primary-text $ if + js-present-dynamic? part-text + , part-text chunk-text + prompt-feedback $ .?-promptFeedback chunk-data + fallback-text $ if + js-present-dynamic? prompt-feedback + .?-blockReason $ unsafe-coerce prompt-feedback 'Dynamic + , js/undefined + text0 $ if + js-present-dynamic? primary-text + , primary-text fallback-text + text $ stream-text text0 + %{} StreamChunk (:text text) (:thinking? thinking?) + :examples $ [] $ quote + let + feedback-only $ js-object $ :promptFeedback + js-object $ :blockReason |blocked + decoded $ unsafe-coerce + decode-genai-chunk feedback-only + , 'app.comp.container/StreamChunk + assert= |blocked $ :text decoded + assert= false $ :thinking? decoded + :schema $ :: 'Fn $ {} + :return 'app.comp.container/StreamChunk + :args $ [] 'JsObject + :features $ #{} :js-ffi 'download-sessions! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn download-sessions! (sessions) - let - content $ format-cirru-edn sessions - blob $ unsafe-coerce - new js/Blob (js-array content) - js-object $ :type |application/edn;charset=utf-8 - , 'Dynamic - url $ unsafe-coerce (js/URL.createObjectURL blob) 'String - link $ unsafe-coerce (js/document.createElement |a) 'Dynamic - filename $ str |sessions- (js/Date.now) |.cirru - do (.!setAttribute link |href url) (.!setAttribute link |download filename) - .!appendChild (unsafe-coerce js/document.body 'Dynamic) link - .!click link - .!remove link - js/setTimeout - fn () $ js/URL.revokeObjectURL url - , 0 - :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] (:: 'List 'app.schema/ChatSession) - :features $ #{} :js-ffi - 'effect-focus $ %{} 'CodeEntry (:doc |) - :code $ quote - defeffect effect-focus () (action el at?) - when (= action :mount) + :code $ quote $ defn download-sessions! (sessions) + let + content $ format-cirru-edn sessions + blob $ unsafe-coerce + new js/Blob (js-array content) + js-object $ :type |application/edn;charset=utf-8 + , 'Dynamic + url $ unsafe-coerce + js/URL.createObjectURL blob + , 'String + link $ unsafe-coerce + js/document.createElement |a + , 'Dynamic + filename $ str |sessions- (js/Date.now) |.cirru + do (.!setAttribute link |href url) (.!setAttribute link |download filename) + .!appendChild (unsafe-coerce js/document.body 'Dynamic) link + .!click link + .!remove link js/setTimeout - fn () $ let - textarea $ unsafe-coerce - .!querySelector (unsafe-coerce el 'Dynamic) |textarea - , 'Dynamic - .!select textarea + fn () $ js/URL.revokeObjectURL url , 0 :examples $ [] - :schema $ :: 'Dynamic - 'first-line $ %{} 'CodeEntry (:doc "|last message from error contains a line starts with \"data: \" and an extra error message. In order that JSON is parsed correctly, only first line is used now.") - :code $ quote - defn first-line (tt) - let - lines $ -> tt (.!split &newline) - .!filter $ fn (line idx _a) - not $ blank? line - if - > (.-length lines) 1 - js/console.warn "|Droping some unexpected lines:" $ .!slice lines 1 - .-0 lines + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] $ :: 'List 'app.schema/ChatSession + :features $ #{} :js-ffi + 'effect-focus $ %{} 'CodeEntry (:doc |) + :code $ quote $ defeffect effect-focus () (action el at?) + when (= action :mount) + js/setTimeout + fn () $ let + textarea $ unsafe-coerce + .!querySelector (unsafe-coerce el 'Dynamic) |textarea + , 'Dynamic + .!select textarea + , 0 :examples $ [] - :schema $ :: 'Fn - {} (:return 'String) - :args $ [] 'String + :schema $ :: 'Dynamic + 'first-line $ %{} 'CodeEntry + :doc "|last message from error contains a line starts with \"data: \" and an extra error message. In order that JSON is parsed correctly, only first line is used now." + :code $ quote $ defn first-line (tt) + let + lines $ -> tt (.!split &newline) + .!filter $ fn (line idx _a) + not $ blank? line + if + > (.-length lines) 1 + js/console.warn "|Droping some unexpected lines:" $ .!slice lines 1 + .-0 lines + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] 'String 'generate-session-id $ %{} 'CodeEntry (:doc |) - :code $ quote - defn generate-session-id () $ str (js/Date.now) + :code $ quote $ defn generate-session-id () + str $ js/Date.now :examples $ [] - :schema $ :: 'Fn - {} (:return 'String) - :args $ [] + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] 'get-anthropic-key! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn get-anthropic-key! () $ required-key! |claude-key "|Required claude-key in localStorage" + :code $ quote $ defn get-anthropic-key! () + required-key! |claude-key "|Required claude-key in localStorage" :examples $ [] - :schema $ :: 'Fn - {} (:return 'String) - :args $ [] - :features $ #{} :js-ffi + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] + :features $ #{} :js-ffi 'get-deepinfra-key! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn get-deepinfra-key! () $ required-key! |deepinfra-key "|Required deepinfra-key in localStorage" + :code $ quote $ defn get-deepinfra-key! () + required-key! |deepinfra-key "|Required deepinfra-key in localStorage" :examples $ [] :schema $ :: 'Dynamic 'get-deepseek-key! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn get-deepseek-key! () $ let + :code $ quote $ defn get-deepseek-key! () + let key $ required-key! |deepseek-key "|Required deepseek-key in localStorage" do - when chrome-extension? $ js/chrome.storage.local.set - js-object $ :deepseekKey key + when chrome-extension? $ js/chrome.storage.local.set $ js-object (:deepseekKey key) , key :examples $ [] - :schema $ :: 'Fn - {} (:return 'String) - :args $ [] - :features $ #{} :js-ffi + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] + :features $ #{} :js-ffi 'get-gemini-key! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn get-gemini-key! () $ let + :code $ quote $ defn get-gemini-key! () + let key $ required-key! |gemini-key "|Required gemini-key in localStorage" do - when chrome-extension? $ js/chrome.storage.local.set - js-object $ :geminiKey key + when chrome-extension? $ js/chrome.storage.local.set $ js-object (:geminiKey key) , key :examples $ [] - :schema $ :: 'Fn - {} (:return 'String) - :args $ [] - :features $ #{} :js-ffi + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] + :features $ #{} :js-ffi 'get-openrouter-key! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn get-openrouter-key! () $ required-key! |openrouter-key "|Required openrouter-key in localStorage" + :code $ quote $ defn get-openrouter-key! () + required-key! |openrouter-key "|Required openrouter-key in localStorage" :examples $ [] - :schema $ :: 'Fn - {} (:return 'String) - :args $ [] - :features $ #{} :js-ffi + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] + :features $ #{} :js-ffi 'js-present-dynamic? $ %{} 'CodeEntry (:doc |) - :code $ quote - defn js-present-dynamic? (value) - unsafe-coerce (js/Boolean value) 'Bool + :code $ quote $ defn js-present-dynamic? (value) + unsafe-coerce (js/Boolean value) 'Bool :examples $ [] - :schema $ :: 'Fn - {} (:return 'Bool) - :args $ [] 'Dynamic + :schema $ :: 'Fn $ {} (:return 'Bool) + :args $ [] 'Dynamic 'json-pattern? $ %{} 'CodeEntry (:doc |) - :code $ quote - defn json-pattern? (text) - or (starts-with? text |{) (starts-with? text |[) + :code $ quote $ defn json-pattern? (text) + or (starts-with? text |{) (starts-with? text |[) :examples $ [] - :schema $ :: 'Fn - {} (:return 'Bool) - :args $ [] 'String + :schema $ :: 'Fn $ {} (:return 'Bool) + :args $ [] 'String 'messages->anthropic $ %{} 'CodeEntry (:doc |) - :code $ quote - defn messages->anthropic (messages) - to-js-data $ map messages - fn (m) - hint-fn $ {} - :args $ [] 'app.schema/ChatMessage - {} - :role $ if - = :assistant $ :role m - , |assistant |user - :content $ :content m - :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] (:: 'List 'app.schema/ChatMessage) - :features $ #{} :js-ffi + :code $ quote $ defn messages->anthropic (messages) + to-js-data $ map messages $ fn (m) + hint-fn $ {} $ :args + [] 'app.schema/ChatMessage + {} + :role $ if + = :assistant $ :role m + , |assistant |user + :content $ :content m + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] $ :: 'List 'app.schema/ChatMessage + :features $ #{} :js-ffi 'messages->gemini $ %{} 'CodeEntry (:doc |) - :code $ quote - defn messages->gemini (messages) - to-js-data $ map messages - fn (m) - hint-fn $ {} - :args $ [] 'app.schema/ChatMessage - {} - :role $ if - = :assistant $ :role m - , |model |user - :parts $ [] - {} $ :text (:content m) - :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] (:: 'List 'app.schema/ChatMessage) - :features $ #{} :js-ffi + :code $ quote $ defn messages->gemini (messages) + to-js-data $ map messages $ fn (m) + hint-fn $ {} $ :args + [] 'app.schema/ChatMessage + {} + :role $ if + = :assistant $ :role m + , |model |user + :parts $ [] $ {} + :text $ :content m + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] $ :: 'List 'app.schema/ChatMessage + :features $ #{} :js-ffi 'messages->openai $ %{} 'CodeEntry (:doc |) - :code $ quote - defn messages->openai (messages) - to-js-data $ map messages - fn (m) - hint-fn $ {} - :args $ [] 'app.schema/ChatMessage - {} - :role $ if - = :assistant $ :role m - , |assistant |user - :content $ :content m - :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] (:: 'List 'app.schema/ChatMessage) - :features $ #{} :js-ffi + :code $ quote $ defn messages->openai (messages) + to-js-data $ map messages $ fn (m) + hint-fn $ {} $ :args + [] 'app.schema/ChatMessage + {} + :role $ if + = :assistant $ :role m + , |assistant |user + :content $ :content m + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] $ :: 'List 'app.schema/ChatMessage + :features $ #{} :js-ffi 'model-storage-key $ %{} 'CodeEntry (:doc |) - :code $ quote - defn model-storage-key (model) - case-default model |gemini-key (:claude-3.7 |claude-key) (:openrouter/anthropic/claude-sonnet-4 |openrouter-key) (:openrouter/anthropic/claude-sonnet-4.5 |openrouter-key) (:openrouter/anthropic/claude-opus-4 |openrouter-key) (:openrouter/anthropic/claude-3.7-sonnet:thinking |openrouter-key) (:openrouter/google/gemini-2.5-pro-preview |openrouter-key) (:openrouter/google/gemini-2.5-flash-preview-05-20 |openrouter-key) (:openrouter/openai/gpt-5 |openrouter-key) (:openrouter/deepseek/deepseek-chat-v3.1 |openrouter-key) (:deepseek-v4-pro |deepseek-key) (:deepseek-v4-flash |deepseek-key) - :examples $ [] - :schema $ :: 'Fn - {} (:return 'String) - :args $ [] 'Tag - :tests $ [] - %{} 'TestEntry (:name |maps-provider-keys) - :code $ quote - do - assert= |gemini-key $ model-storage-key :gemini - assert= |openrouter-key $ model-storage-key :openrouter/openai/gpt-5 - assert= |deepseek-key $ model-storage-key :deepseek-v4-pro - assert= |claude-key $ model-storage-key :claude-3.7 + :code $ quote $ defn model-storage-key (model) + case-default model |gemini-key (:claude-3.7 |claude-key) + :openrouter/anthropic/claude-sonnet-4 |openrouter-key + :openrouter/anthropic/claude-sonnet-4.5 |openrouter-key + :openrouter/anthropic/claude-opus-4 |openrouter-key + :openrouter/anthropic/claude-3.7-sonnet:thinking |openrouter-key + :openrouter/google/gemini-2.5-pro-preview |openrouter-key + :openrouter/google/gemini-2.5-flash-preview-05-20 |openrouter-key + :openrouter/openai/gpt-5 |openrouter-key + :openrouter/deepseek/deepseek-chat-v3.1 |openrouter-key + :deepseek-v4-pro |deepseek-key + :deepseek-v4-flash |deepseek-key + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] 'Tag + :tests $ [] $ %{} 'TestEntry + :name |maps-provider-keys + :code $ quote $ do + assert= |gemini-key $ model-storage-key :gemini + assert= |openrouter-key $ model-storage-key :openrouter/openai/gpt-5 + assert= |deepseek-key $ model-storage-key :deepseek-v4-pro + assert= |claude-key $ model-storage-key :claude-3.7 'models-menu $ %{} 'CodeEntry (:doc |) - :code $ quote - def models-menu $ [] (:: :item :gemini-3.5-flash-lite "|Gemini Flash Lite 3.5") (:: :item :gemini-3.6-flash "|Gemini Flash 3.6") (:: :item :gemini-3.7-flash "|Gemini Flash 3.7") (:: :item :gemini-3.8-flash "|Gemini Flash 3.8") (:: :item :gemini-flash "|Gemini Flash 3") (:: :item :gemini-3.5-flash "|Gemini Flash 3.5") (:: :item :gemini-pro "|Gemini Pro 3.1") (:: :item :gemini-3.1-flash-lite-preview "|Gemini Flash Lite 3.1") (:: :item :flash-imagen "|Flash Imagen") (:: :item :imagen-4 "|Imagen 4") (:: :item :gemma "|Gemma 3 27b") (:: :item :openrouter/anthropic/claude-sonnet-4.5 "|Openrouter Claude Sonnet 4.5") (:: :item :openrouter/anthropic/claude-opus-4 "|Openrouter Claude Opus 4") (:: :item :openrouter/google/gemini-2.5-pro-preview "|Openrouter Google Gemini 2.5 pro preview") (:: :item :openrouter/google/gemini-2.5-flash-preview-05-20 "|Openrouter Google Gemini 2.5 flash preview") (:: :item :openrouter/openai/gpt-5 "|Openrouter GPT 5") (:: :item :openrouter/deepseek/deepseek-chat-v3.1 "|Openrouter deepseek-chat-v3.1") (:: :item :deepseek-v4-pro "|DeepSeek V4 Pro") (:: :item :deepseek-v4-flash "|DeepSeek V4 Flash") (; :: :item :claude-4.5 "|Claude 4.5") + :code $ quote $ def models-menu + [] + :: :item :gemini-3.5-flash-lite "|Gemini Flash Lite 3.5" + :: :item :gemini-3.6-flash "|Gemini Flash 3.6" + :: :item :gemini-3.7-flash "|Gemini Flash 3.7" + :: :item :gemini-3.8-flash "|Gemini Flash 3.8" + :: :item :gemini-flash "|Gemini Flash 3" + :: :item :gemini-3.5-flash "|Gemini Flash 3.5" + :: :item :gemini-pro "|Gemini Pro 3.1" + :: :item :gemini-3.1-flash-lite-preview "|Gemini Flash Lite 3.1" + :: :item :flash-imagen "|Flash Imagen" + :: :item :imagen-4 "|Imagen 4" + :: :item :gemma "|Gemma 3 27b" + :: :item :openrouter/anthropic/claude-sonnet-4.5 "|Openrouter Claude Sonnet 4.5" + :: :item :openrouter/anthropic/claude-opus-4 "|Openrouter Claude Opus 4" + :: :item :openrouter/google/gemini-2.5-pro-preview "|Openrouter Google Gemini 2.5 pro preview" + :: :item :openrouter/google/gemini-2.5-flash-preview-05-20 "|Openrouter Google Gemini 2.5 flash preview" + :: :item :openrouter/openai/gpt-5 "|Openrouter GPT 5" + :: :item :openrouter/deepseek/deepseek-chat-v3.1 "|Openrouter deepseek-chat-v3.1" + :: :item :deepseek-v4-pro "|DeepSeek V4 Pro" + :: :item :deepseek-v4-flash "|DeepSeek V4 Flash" + ; :: :item :claude-4.5 "|Claude 4.5" :examples $ [] :schema $ :: 'List 'normalize-chat-state $ %{} 'CodeEntry (:doc |) - :code $ quote - defn normalize-chat-state (raw) - hint-fn $ {} - :args $ [] 'T - :return 'app.schema/ChatState - :features $ #{} :js-ffi - :generics $ [] 'T - if (struct? raw) - let - state $ unsafe-coerce raw 'app.schema/ChatState - %{} app.schema/ChatState - :answer $ stream-text (:answer state) - :loading? $ :loading? state - :done? $ :done? state - :messages $ :messages state - :model $ if - tag? $ :model state - :model state - , :gemini - :thinking $ stream-text (:thinking state) - let - data $ unsafe-coerce raw (:: 'Map 'Tag 'Dynamic) - loading-value $ option:unwrap-or (get data :loading?) false - done-value $ option:unwrap-or (get data :done?) true - model-value $ option:unwrap-or (get data :model) :gemini - %{} app.schema/ChatState - :answer $ stream-text - option:unwrap-or (get data :answer) | - :loading? $ case-default loading-value false (true true) (false false) - :done? $ case-default done-value true (true true) (false false) - :messages $ unsafe-coerce - option:unwrap-or (get data :messages) ([]) - :: 'List 'app.schema/ChatMessage - :model $ if (tag? model-value) model-value :gemini - :thinking $ stream-text - option:unwrap-or (get data :thinking) | - :examples $ [] - :schema $ :: 'Fn - {} (:return 'app.schema/ChatState) + :code $ quote $ defn normalize-chat-state (raw) + hint-fn $ {} :args $ [] 'T + :return 'app.schema/ChatState :features $ #{} :js-ffi :generics $ [] 'T - :tests $ [] - %{} 'TestEntry (:name |legacy-nil-fields) - :code $ quote - let - legacy $ unsafe-coerce - normalize-chat-state $ {} (:answer nil) (:loading? false) (:done? true) - :messages $ [] - :model :gemini - :thinking nil - , 'app.schema/ChatState - missing-done $ unsafe-coerce - normalize-chat-state $ {} (:answer |) (:loading? false) - :messages $ [] - :model :gemini - :thinking | - , 'app.schema/ChatState - invalid-done $ unsafe-coerce - normalize-chat-state $ {} (:answer |) (:loading? false) (:done? nil) - :messages $ [] - :model :gemini - :thinking | - , 'app.schema/ChatState - is $ = | (:answer legacy) - is $ = | (:thinking legacy) - is $ :done? missing-done - is $ :done? invalid-done + if (struct? raw) + let + state $ unsafe-coerce raw 'app.schema/ChatState + %{} app.schema/ChatState + :answer $ stream-text $ :answer state + :loading? $ :loading? state + :done? $ :done? state + :messages $ :messages state + :model $ if + tag? $ :model state + :model state + , :gemini + :thinking $ stream-text $ :thinking state + let + data $ unsafe-coerce raw $ :: 'Map 'Tag 'Dynamic + loading-value $ option:unwrap-or (get data :loading?) false + done-value $ option:unwrap-or (get data :done?) true + model-value $ option:unwrap-or (get data :model) :gemini + %{} app.schema/ChatState + :answer $ stream-text $ option:unwrap-or (get data :answer) | + :loading? $ case-default loading-value false (true true) (false false) + :done? $ case-default done-value true (true true) (false false) + :messages $ unsafe-coerce + option:unwrap-or (get data :messages) ([]) + :: 'List 'app.schema/ChatMessage + :model $ if (tag? model-value) model-value :gemini + :thinking $ stream-text $ option:unwrap-or (get data :thinking) | + :examples $ [] + :schema $ :: 'Fn $ {} + :return 'app.schema/ChatState + :args $ [] 'T + :features $ #{} :js-ffi + :generics $ [] 'T + :tests $ [] $ %{} 'TestEntry + :name |legacy-nil-fields + :code $ quote $ let + legacy $ unsafe-coerce + normalize-chat-state $ {} (:answer nil) (:loading? false) (:done? true) + :messages $ [] + :model :gemini + :thinking nil + , 'app.schema/ChatState + missing-done $ unsafe-coerce + normalize-chat-state $ {} (:answer |) (:loading? false) + :messages $ [] + :model :gemini + :thinking | + , 'app.schema/ChatState + invalid-done $ unsafe-coerce + normalize-chat-state $ {} (:answer |) (:loading? false) (:done? nil) + :messages $ [] + :model :gemini + :thinking | + , 'app.schema/ChatState + is $ = | $ :answer legacy + is $ = | $ :thinking legacy + is $ :done? missing-done + is $ :done? invalid-done 'on-fill $ %{} 'CodeEntry (:doc |) - :code $ quote - defn on-fill (cursor state on-submit) - %{} respo.schema/RespoListener (:name :on-fill) - :handler $ fn (event dispatch!) - match event $ - :fill-text info - let - submit? $ option:unwrap-or (get info :submit?) true - text $ option:unwrap-or (get info :text) | - do - dispatch! (:: :states cursor) (assoc state :content text) - if submit? - on-submit text (:search? state) (:think? state) dispatch! - , nil + :code $ quote $ defn on-fill (cursor state on-submit) + %{} respo.schema/RespoListener (:name :on-fill) + :handler $ fn (event dispatch!) + match event $ + :fill-text info + let + submit? $ option:unwrap-or (get info :submit?) true + text $ option:unwrap-or (get info :text) | + do + dispatch! (:: :states cursor) (assoc state :content text) + if submit? + on-submit text (:search? state) (:think? state) dispatch! + , nil :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] 'Dynamic 'app.schema/MessageBoxState 'Dynamic + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] 'Dynamic 'app.schema/MessageBoxState 'Dynamic 'pattern-spaced-code $ %{} 'CodeEntry (:doc |) - :code $ quote - def pattern-spaced-code $ noted "|temp fix of nested code block" (&raw-code "|/\\n\\s+```/g") + :code $ quote $ def pattern-spaced-code + noted "|temp fix of nested code block" $ &raw-code "|/\\n\\s+```/g" :examples $ [] :schema $ :: 'Dynamic 'pick-model $ %{} 'CodeEntry (:doc |) - :code $ quote - defn pick-model (variant) - case-default variant |gemini-3.5-flash (:gemini-3.5-flash-lite |gemini-3.1-flash-lite) (:gemini-3.6-flash |gemini-3.5-flash) (:gemini-3.7-flash |gemini-3.7-flash) (:gemini-3.8-flash |gemini-3.8-flash) (:gemini-3.5-flash-lite |gemini-3.1-flash-lite) (:gemini-3.1-flash-lite-preview |gemini-3.1-flash-lite) (:gemini-pro |gemini-3.1-pro-preview) (:gemma |gemma-3-27b-it) - :examples $ [] - :schema $ :: 'Fn - {} (:return 'String) - :args $ [] 'Tag + :code $ quote $ defn pick-model (variant) + case-default variant |gemini-3.5-flash + :gemini-3.5-flash-lite |gemini-3.1-flash-lite + :gemini-3.6-flash |gemini-3.5-flash + :gemini-3.7-flash |gemini-3.7-flash + :gemini-3.8-flash |gemini-3.8-flash + :gemini-3.5-flash-lite |gemini-3.1-flash-lite + :gemini-3.1-flash-lite-preview |gemini-3.1-flash-lite + :gemini-pro |gemini-3.1-pro-preview + :gemma |gemma-3-27b-it + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] 'Tag 'required-key! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn required-key! (storage-key prompt-text) - let - stored $ js/localStorage.getItem storage-key - key $ if (js-present-dynamic? stored) (unsafe-coerce stored 'String) | - if (blank? key) - raise $ new js/Error prompt-text - , key - :examples $ [] - :schema $ :: 'Fn - {} (:return 'String) - :args $ [] 'String 'String - :features $ #{} :js-ffi + :code $ quote $ defn required-key! (storage-key prompt-text) + let + stored $ js/localStorage.getItem storage-key + key $ if + js-present-dynamic? stored + unsafe-coerce stored 'String + , | + if (blank? key) + raise $ new js/Error prompt-text + , key + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] 'String 'String + :features $ #{} :js-ffi 'save-current-session $ %{} 'CodeEntry (:doc |) - :code $ quote - defn save-current-session (store state) - let - messages $ :messages state - model $ either (:model state) :gemini - if - > (count messages) 0 - let - new-session $ create-session messages model - updated-session $ assoc new-session :is-history? true - sessions $ or (:sessions store) ([]) - assoc store :sessions $ append sessions updated-session - , store - :examples $ [] - :schema $ :: 'Fn - {} (:return 'app.schema/Store) - :args $ [] 'app.schema/Store 'app.schema/ChatState + :code $ quote $ defn save-current-session (store state) + let + messages $ :messages state + model $ either (:model state) :gemini + if + > (count messages) 0 + let + new-session $ create-session messages model + updated-session $ assoc new-session :is-history? true + sessions $ or (:sessions store) ([]) + assoc store :sessions $ append sessions updated-session + , store + :examples $ [] + :schema $ :: 'Fn $ {} + :return 'app.schema/Store + :args $ [] 'app.schema/Store 'app.schema/ChatState 'stream-text $ %{} 'CodeEntry (:doc |) - :code $ quote - defn stream-text (value) - hint-fn $ {} - :generics $ [] 'T - :args $ [] 'T - :return 'String - :features $ #{} :js-ffi - if (string? value) value | - :examples $ [] - :schema $ :: 'Fn - {} (:return 'String) + :code $ quote $ defn stream-text (value) + hint-fn $ {} + :generics $ [] 'T :args $ [] 'T + :return 'String :features $ #{} :js-ffi - :generics $ [] 'T + if (string? value) value | + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'String) + :args $ [] 'T + :features $ #{} :js-ffi + :generics $ [] 'T :tests $ [] %{} 'TestEntry (:name |keeps-string) - :code $ quote - is $ = |hello (stream-text |hello) + :code $ quote $ is + = |hello $ stream-text |hello :tags $ #{} :unit - %{} 'TestEntry (:name |ignores-non-string) - :code $ quote - is $ = | (stream-text nil) + %{} 'TestEntry + :name |ignores-non-string + :code $ quote $ is + = | $ stream-text nil :tags $ #{} :regression :unit 'style-a-toggler $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-a-toggler $ {} - |& $ {} (:cursor :pointer) (:background-color :white) (:color :black) - "|.focus-within &" $ {} (:color :black) + :code $ quote $ defstyle style-a-toggler + {} + |& $ {} (:cursor :pointer) + :background-color :white + :color :black + "|.focus-within &" $ {} $ :color :black :examples $ [] :schema $ :: 'Dynamic 'style-abort-close $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-abort-close $ {} - |& $ {} (:vertical-align :middle) (:font-size 10) + :code $ quote $ defstyle style-abort-close + {} $ |& $ {} (:vertical-align :middle) (:font-size 10) :examples $ [] :schema $ :: 'Dynamic 'style-app-global $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-app-global $ {} + :code $ quote $ defstyle style-app-global + {} str "|& ." style-code-block {} $ :max-width |90vw - |& $ {} (:color |#999) (:transition-duration |300ms) + |& $ {} (:color |#999) + :transition-duration |300ms :background-color $ hsl 0 0 98 :touch-action :none |&:hover $ {} (:color |#777) @@ -1636,58 +1714,52 @@ :examples $ [] :schema $ :: 'Dynamic 'style-archive-close $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-archive-close $ {} + :code $ quote $ defstyle style-archive-close + {} |& $ {} (:cursor :pointer) (:font-size 18) :color $ hsl 0 0 50 :transition-duration |200ms - |&:hover $ {} - :color $ hsl 0 0 20 + |&:hover $ {} $ :color (hsl 0 0 20) :examples $ [] :schema $ :: 'Dynamic 'style-archive-header $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-archive-header $ {} - |& $ {} (:padding "|12px 16px") - :border-bottom $ str "|1px solid " (hsl 0 0 90) - :background-color $ hsl 0 0 96 - :display :flex - :flex-direction :row - :align-items :center - :justify-content :space-between + :code $ quote $ defstyle style-archive-header + {} $ |& $ {} (:padding "|12px 16px") + :border-bottom $ str "|1px solid " $ hsl 0 0 90 + :background-color $ hsl 0 0 96 + :display :flex + :flex-direction :row + :align-items :center + :justify-content :space-between :examples $ [] :schema $ :: 'Dynamic 'style-archive-row $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-archive-row $ {} - |& $ {} (:padding |12px) - :border-bottom $ str "|1px solid " (hsl 0 0 90) - :display :flex - :justify-content :space-between - :align-items :center + :code $ quote $ defstyle style-archive-row + {} $ |& $ {} (:padding |12px) + :border-bottom $ str "|1px solid " $ hsl 0 0 90 + :display :flex + :justify-content :space-between + :align-items :center :examples $ [] :schema $ :: 'Dynamic 'style-checkbox $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-checkbox $ {} - |& $ {} (:cursor :pointer) (:user-select :none) (:font-size 12) (:line-height |28px) (:vertical-align :middle) + :code $ quote $ defstyle style-checkbox + {} $ |& $ {} (:cursor :pointer) (:user-select :none) (:font-size 12) (:line-height |28px) (:vertical-align :middle) :examples $ [] :schema $ :: 'Dynamic 'style-clear $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-clear $ {} - |& $ {} (:opacity 0.4) (:padding "|4px 8px") (:display :inline-block) (:height |24px) + :code $ quote $ defstyle style-clear + {} $ |& $ {} (:opacity 0.4) (:padding "|4px 8px") (:display :inline-block) (:height |24px) :examples $ [] :schema $ :: 'Dynamic 'style-code-content $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-code-content $ {} - |& $ {} (:line-height |1.5) (:font-size 13) + :code $ quote $ defstyle style-code-content + {} $ |& $ {} (:line-height |1.5) (:font-size 13) :examples $ [] :schema $ :: 'Dynamic 'style-delete-button $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-delete-button $ {} + :code $ quote $ defstyle style-delete-button + {} |& $ {} (:padding "|4px 8px") (:font-size |18px) (:font-weight |50) :color $ hsl 0 80 50 :opacity 0.5 @@ -1701,9 +1773,10 @@ :examples $ [] :schema $ :: 'Dynamic 'style-fill $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-fill $ {} - |& $ {} (:cursor :pointer) (:user-select :none) (:display :inline-flex) (:align-items :center) (:justify-content :center) (:transition-duration |200ms) + :code $ quote $ defstyle style-fill + {} + |& $ {} (:cursor :pointer) (:user-select :none) (:display :inline-flex) (:align-items :center) (:justify-content :center) + :transition-duration |200ms :color $ hsl 0 0 80 :margin "|0 4px 0 8px" |&:hover $ {} @@ -1712,129 +1785,125 @@ :examples $ [] :schema $ :: 'Dynamic 'style-focus-box $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-focus-box $ {} - |& $ {} (:width |100%) (:border-radius 12) (:min-height 40) (:max-height 40) (:padding "|9px 12px") (:cursor :text) (:overflow :hidden) (:white-space :pre) (:text-overflow :ellipsis) (:background-color :transparent) + :code $ quote $ defstyle style-focus-box + {} $ |& $ {} (:width |100%) (:border-radius 12) (:min-height 40) (:max-height 40) (:padding "|9px 12px") (:cursor :text) (:overflow :hidden) (:white-space :pre) (:text-overflow :ellipsis) + :background-color :transparent :examples $ [] :schema $ :: 'Dynamic 'style-focus-link $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-focus-link $ {} + :code $ quote $ defstyle style-focus-link + {} |& $ {} (:cursor :pointer) (:font-size 13) :color $ hsl 200 80 40 :text-decoration :none :padding "|4px 0" - |&:hover $ {} (:text-decoration :underline) + |&:hover $ {} $ :text-decoration :underline :examples $ [] :schema $ :: 'Dynamic 'style-gap12 $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-gap12 $ {} - |& $ {} (:gap 12) + :code $ quote $ defstyle style-gap12 + {} $ |& $ {} (:gap 12) :examples $ [] :schema $ :: 'Dynamic 'style-history-button $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-history-button $ {} - |& $ {} (:font-size |20px) - :color $ hsl 200 80 60 - :height |14px - :line-height |14px - :display :flex - :align-items :center - :justify-content :center - |&:hover $ {} - :color $ hsl 200 80 50 + :code $ quote $ defstyle style-history-button + {} $ |& $ {} (:font-size |20px) + :color $ hsl 200 80 60 + :height |14px + :line-height |14px + :display :flex + :align-items :center + :justify-content :center + |&:hover $ {} $ :color (hsl 200 80 50) :examples $ [] :schema $ :: 'Dynamic 'style-history-count $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-history-count $ {} - |& $ {} - :color $ hsl 200 80 60 - :font-size |12px - :display :inline-block + :code $ quote $ defstyle style-history-count + {} $ |& $ {} + :color $ hsl 200 80 60 + :font-size |12px + :display :inline-block :examples $ [] :schema $ :: 'Dynamic 'style-image $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-image $ {} - |& $ {} (:max-width |100%) (:align-self :flex-start) (:border-radius |6px) - :border $ str "|1px solid " (hsl 0 0 90) + :code $ quote $ defstyle style-image + {} $ |& $ {} (:max-width |100%) (:align-self :flex-start) (:border-radius |6px) + :border $ str "|1px solid " $ hsl 0 0 90 :examples $ [] :schema $ :: 'Dynamic 'style-md-content $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-md-content $ {} - "|& .md-p" $ {} (:margin "|16px 0") (:line-height |1.6) + :code $ quote $ defstyle style-md-content + {} $ "|& .md-p" $ {} (:margin "|16px 0") (:line-height |1.6) :examples $ [] :schema $ :: 'Dynamic 'style-message-actions $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-message-actions $ {} - |& $ {} (:margin-top 6) (:justify-content :flex-end) (:width |100%) + :code $ quote $ defstyle style-message-actions + {} $ |& $ {} (:margin-top 6) (:justify-content :flex-end) (:width |100%) :examples $ [] :schema $ :: 'Dynamic 'style-message-area $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-message-area $ {} - |& $ {} (:flex 2) (:overflow :scroll) + :code $ quote $ defstyle style-message-area + {} $ |& $ {} (:flex 2) (:overflow :scroll) :examples $ [] :schema $ :: 'Dynamic 'style-message-assistant $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-message-assistant $ {} - |& $ {} (:align-self :flex-start) + :code $ quote $ defstyle style-message-assistant + {} $ |& $ {} (:align-self :flex-start) :examples $ [] :schema $ :: 'Dynamic 'style-message-box $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-message-box $ {} - |& $ {} (:width |100%) (:max-width 1200) (:right |50%) (:padding |8px) (:margin :auto) (:transition-duration |300ms) (; :transform "|translate(50%,0)") (:transition-property |height) - |&:focus-within $ {} (:opacity 1) (; :transform "|translate(50%,0)") + :code $ quote $ defstyle style-message-box + {} + |& $ {} (:width |100%) (:max-width 1200) (:right |50%) (:padding |8px) (:margin :auto) + :transition-duration |300ms + ; :transform "|translate(50%,0)" + :transition-property |height + |&:focus-within $ {} (:opacity 1) + ; :transform "|translate(50%,0)" :examples $ [] :schema $ :: 'Dynamic 'style-message-box-panel $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-message-box-panel $ {} + :code $ quote $ defstyle style-message-box-panel + {} |& $ {} (:position :absolute) (:bottom 0) (:opacity 1) (:width |100%) :background-color $ hsl 0 0 100 0.7 - :border-top $ str "|1px solid " (hsl 0 0 80 0.6) + :border-top $ str "|1px solid " $ hsl 0 0 80 0.6 |&.focus-within $ {} :background-color $ hsl 0 0 100 0.9 - :box-shadow $ str "|0 0px 8px " (hsl 0 0 0 0.3) + :box-shadow $ str "|0 0px 8px " $ hsl 0 0 0 0.3 :examples $ [] :schema $ :: 'Dynamic 'style-message-item $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-message-item $ {} - |& $ {} (:line-height |1.6) + :code $ quote $ defstyle style-message-item + {} $ |& $ {} (:line-height |1.6) :examples $ [] :schema $ :: 'Dynamic 'style-message-list $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-message-list $ {} - |& $ {} (:flex 2) (:padding "|40px 16px 20vh 16px") (:width |100%) (:max-width 1200) (:margin :auto) (:position :relative) + :code $ quote $ defstyle style-message-list + {} $ |& $ {} (:flex 2) + :padding "|40px 16px 20vh 16px" + :width |100% + :max-width 1200 + :margin :auto + :position :relative :examples $ [] :schema $ :: 'Dynamic 'style-message-role $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-message-role $ {} - |& $ {} (:font-size 12) - :color $ hsl 0 0 50 - :margin-bottom 6 - :padding-right |16px + :code $ quote $ defstyle style-message-role + {} $ |& $ {} (:font-size 12) + :color $ hsl 0 0 50 + :margin-bottom 6 + :padding-right |16px :examples $ [] :schema $ :: 'Dynamic 'style-message-text $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-message-text $ {} - |& $ {} (:white-space :pre-wrap) (:line-height |1.6) (:margin 0) (:padding-right |16px) + :code $ quote $ defstyle style-message-text + {} $ |& $ {} (:white-space :pre-wrap) (:line-height |1.6) (:margin 0) (:padding-right |16px) :examples $ [] :schema $ :: 'Dynamic 'style-message-user $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-message-user $ {} + :code $ quote $ defstyle style-message-user + {} |& $ {} (:align-self :flex-end) :background-color $ hsl 0 0 96 :padding "|12px 0 12px 16px" @@ -1842,16 +1911,16 @@ :max-height |240px :max-width |100% :overflow-y :auto - |&::-webkit-scrollbar $ {} (:width |4px) + |&::-webkit-scrollbar $ {} $ :width |4px |&::-webkit-scrollbar-thumb $ {} :background-color $ hsl 0 0 80 :border-radius |2px - |&::-webkit-scrollbar-track $ {} (:background-color :transparent) + |&::-webkit-scrollbar-track $ {} $ :background-color :transparent :examples $ [] :schema $ :: 'Dynamic 'style-more $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-more $ {} + :code $ quote $ defstyle style-more + {} |& $ {} (:text-align :center) (:min-width 80) :background-color $ hsl 0 0 94 :border-radius 16 @@ -1859,19 +1928,18 @@ :margin "|8px 0" :white-space :nowrap :display :inline-block - |&:hover $ {} - :box-shadow $ str "|1px 1px 4px " (hsl 0 0 0 0.2) + |&:hover $ {} $ :box-shadow + str "|1px 1px 4px " $ hsl 0 0 0 0.2 :examples $ [] :schema $ :: 'Dynamic 'style-reply-actions $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-reply-actions $ {} - |& $ {} (:margin-top 6) (:justify-content :flex-start) (:width |100%) + :code $ quote $ defstyle style-reply-actions + {} $ |& $ {} (:margin-top 6) (:justify-content :flex-start) (:width |100%) :examples $ [] :schema $ :: 'Dynamic 'style-reply-button $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-reply-button $ {} + :code $ quote $ defstyle style-reply-button + {} |& $ {} (:text-align :center) (:min-width 80) :background-color $ hsl 0 0 100 :border-radius 16 @@ -1879,52 +1947,51 @@ :margin "|8px 0" :white-space :nowrap :display :inline-block - |&:hover $ {} - :box-shadow $ str "|1px 1px 4px " (hsl 0 0 0 0.2) + |&:hover $ {} $ :box-shadow + str "|1px 1px 4px " $ hsl 0 0 0 0.2 :examples $ [] :schema $ :: 'Dynamic 'style-session-item $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-session-item $ {} - |& $ {} (:padding |12px) - :border-bottom $ str "|1px solid " (hsl 0 0 90) - :display :flex - :flex-direction :row - :align-items :center - :gap |12px - |:hover $ {} - :background-color $ hsl 0 0 96 + :code $ quote $ defstyle style-session-item + {} $ |& $ {} (:padding |12px) + :border-bottom $ str "|1px solid " $ hsl 0 0 90 + :display :flex + :flex-direction :row + :align-items :center + :gap |12px + |:hover $ {} $ :background-color (hsl 0 0 96) :examples $ [] :schema $ :: 'Dynamic 'style-sessions-list $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-sessions-list $ {} - |& $ {} (:flex |1) (:overflow-y :auto) (:min-width |300px) + :code $ quote $ defstyle style-sessions-list + {} $ |& $ {} (:flex |1) (:overflow-y :auto) (:min-width |300px) :examples $ [] :schema $ :: 'Dynamic 'style-submit $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-submit $ {} - |& $ {} + :code $ quote $ defstyle style-submit + {} $ |& $ {} :examples $ [] :schema $ :: 'Dynamic 'style-textbox $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-textbox $ {} - |& $ {} (:border-radius 12) (:height "|max(100px,15vh)") (:width |100%) (:transition-duration |320ms) (:border :none) (:background-color :transparent) + :code $ quote $ defstyle style-textbox + {} + |& $ {} (:border-radius 12) (:height "|max(100px,15vh)") (:width |100%) + :transition-duration |320ms + :border :none + :background-color :transparent |&.focus-within $ {} (:height "|max(240px,32vh)") (:border :none) (:box-shadow :none) :examples $ [] :schema $ :: 'Dynamic 'style-textbox-compact $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-textbox-compact $ {} + :code $ quote $ defstyle style-textbox-compact + {} |& $ {} (:height 40) (:min-height 40) (:max-height 40) (:overflow :hidden) |&.focus-within $ {} (:height "|max(240px,32vh)") (:min-height |unset) (:max-height |unset) :examples $ [] :schema $ :: 'Dynamic 'style-thinking $ %{} 'CodeEntry (:doc |) - :code $ quote - defstyle style-thinking $ {} + :code $ quote $ defstyle style-thinking + {} |& $ {} (:max-height 200) (:overflow :auto) (:padding "|12px 16px") :background-color $ hsl 0 0 96 :font-size 12 @@ -1932,120 +1999,117 @@ :color $ hsl 0 0 50 :border-radius 8 :margin-bottom 12 - :border $ str "|1px solid " (hsl 0 0 90) - "|& .md-p" $ {} (:margin "|4px 0") + :border $ str "|1px solid " $ hsl 0 0 90 + "|& .md-p" $ {} $ :margin "|4px 0" :examples $ [] :schema $ :: 'Dynamic 'submit-message! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn submit-message! (cursor state prompt-text search? think? model d!) - hint-fn $ {} (:async true) - :args $ [] 'List 'app.schema/ChatState 'String 'Bool 'Bool 'Tag 'Dynamic - let - state1 $ unsafe-coerce - assoc state :messages $ append-user-message (:messages state) prompt-text - , 'app.schema/ChatState - *text $ atom | - *thinking-text $ atom | - model $ :model state - d! cursor state1 - try - do $ case-default model - js-await $ call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text - :gemini-3.1-flash-lite-preview $ js-await (call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text) - :gemini-pro $ js-await (call-genai-msg! model cursor state1 prompt-text search? true d! *text *thinking-text) - :flash-imagen $ js-await (call-flash-imagen-msg! model cursor state1 prompt-text d!) - :imagen-4 $ js-await (call-imagen-4-msg! model cursor state1 prompt-text d!) - :gemini-thinking $ js-await (call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text) - :gemini-flash-thinking $ js-await (call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text) - :gemini-flash-lite $ js-await (call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text) - :gemini-flash $ js-await (call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text) - :gemini-learnlm $ js-await (call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text) - :claude-3.7 $ js-await (call-anthropic-msg! cursor state1 prompt-text |claude-3-7-sonnet-20250219 false d!) - :openrouter/anthropic/claude-sonnet-4 $ js-await (call-openrouter! cursor state1 prompt-text |anthropic/claude-sonnet-4 true d! *text) - :openrouter/anthropic/claude-sonnet-4.5 $ js-await (call-openrouter! cursor state1 prompt-text |anthropic/claude-sonnet-4.5 true d! *text) - :openrouter/anthropic/claude-opus-4 $ js-await (call-openrouter! cursor state1 prompt-text |anthropic/claude-opus-4 true d! *text) - :openrouter/anthropic/claude-3.7-sonnet:thinking $ js-await (call-openrouter! cursor state1 prompt-text |anthropic/claude-3.7-sonnet:thinking true d! *text) - :openrouter/google/gemini-2.5-pro-preview $ js-await (call-openrouter! cursor state1 prompt-text |google/gemini-2.5-pro-preview true d! *text) - :openrouter/google/gemini-2.5-flash-preview-05-20 $ js-await (call-openrouter! cursor state1 prompt-text |google/gemini-2.5-flash-preview-05-20 true d! *text) - :openrouter/openai/gpt-5 $ js-await (call-openrouter! cursor state1 prompt-text |openai/gpt-5 true d! *text) - :openrouter/deepseek/deepseek-chat-v3.1 $ js-await (call-openrouter! cursor state1 prompt-text |deepseek/deepseek-chat-v3.1 true d! *text) - :deepseek-v4-pro $ js-await (call-openrouter! cursor state1 prompt-text |deepseek-v4-pro true d! *text) - :deepseek-v4-flash $ js-await (call-openrouter! cursor state1 prompt-text |deepseek-v4-flash true d! *text) - fn (e) - do (js/console.error :msg-buffer-request-failed e) - let - err-text $ str |Failed-to-load: e - d! cursor $ -> state1 (assoc :answer err-text) (assoc :loading? false) (assoc :done? true) - assoc :messages $ upsert-assistant-message (:messages state1) err-text | - :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) + :code $ quote $ defn submit-message! (cursor state prompt-text search? think? model d!) + hint-fn $ {} (:async true) :args $ [] 'List 'app.schema/ChatState 'String 'Bool 'Bool 'Tag 'Dynamic - :features $ #{} :js-ffi - 'toggle-focus-within! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn toggle-focus-within! (respo-event active?) - let - raw-event $ option:unwrap-or (get respo-event :event) js/undefined - when (js-present-dynamic? raw-event) - let - event $ unsafe-coerce raw-event FocusEventHost - target0 $ .-target event - when (js-present? target0) - let - target $ unsafe-coerce target0 FocusTargetHost - parent0 $ .-parentElement target - when (js-present? parent0) - let - parent $ unsafe-coerce parent0 FocusTargetHost - box0 $ .-parentElement parent - when (js-present? box0) - let - box $ unsafe-coerce box0 FocusTargetHost - class-list $ .-classList target - box-class $ .-classList box - if active? - do - when - not $ class-list .contains? |focus-within - class-list .add! |focus-within - when - not $ box-class .contains? |focus-within - box-class .add! |focus-within - do - when (class-list .contains? |focus-within) (class-list .remove! |focus-within) - when (box-class .contains? |focus-within) (box-class .remove! |focus-within) - , &unit + let + state1 $ unsafe-coerce + assoc state :messages $ append-user-message (:messages state) prompt-text + , 'app.schema/ChatState + *text $ atom | + *thinking-text $ atom | + model $ :model state + d! cursor state1 + try + do $ case-default model + js-await $ call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text + :gemini-3.1-flash-lite-preview $ js-await $ call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text + :gemini-pro $ js-await $ call-genai-msg! model cursor state1 prompt-text search? true d! *text *thinking-text + :flash-imagen $ js-await $ call-flash-imagen-msg! model cursor state1 prompt-text d! + :imagen-4 $ js-await $ call-imagen-4-msg! model cursor state1 prompt-text d! + :gemini-thinking $ js-await $ call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text + :gemini-flash-thinking $ js-await $ call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text + :gemini-flash-lite $ js-await $ call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text + :gemini-flash $ js-await $ call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text + :gemini-learnlm $ js-await $ call-genai-msg! model cursor state1 prompt-text search? think? d! *text *thinking-text + :claude-3.7 $ js-await $ call-anthropic-msg! cursor state1 prompt-text |claude-3-7-sonnet-20250219 false d! + :openrouter/anthropic/claude-sonnet-4 $ js-await $ call-openrouter! cursor state1 prompt-text |anthropic/claude-sonnet-4 true d! *text + :openrouter/anthropic/claude-sonnet-4.5 $ js-await $ call-openrouter! cursor state1 prompt-text |anthropic/claude-sonnet-4.5 true d! *text + :openrouter/anthropic/claude-opus-4 $ js-await $ call-openrouter! cursor state1 prompt-text |anthropic/claude-opus-4 true d! *text + :openrouter/anthropic/claude-3.7-sonnet:thinking $ js-await $ call-openrouter! cursor state1 prompt-text |anthropic/claude-3.7-sonnet:thinking true d! *text + :openrouter/google/gemini-2.5-pro-preview $ js-await $ call-openrouter! cursor state1 prompt-text |google/gemini-2.5-pro-preview true d! *text + :openrouter/google/gemini-2.5-flash-preview-05-20 $ js-await $ call-openrouter! cursor state1 prompt-text |google/gemini-2.5-flash-preview-05-20 true d! *text + :openrouter/openai/gpt-5 $ js-await $ call-openrouter! cursor state1 prompt-text |openai/gpt-5 true d! *text + :openrouter/deepseek/deepseek-chat-v3.1 $ js-await $ call-openrouter! cursor state1 prompt-text |deepseek/deepseek-chat-v3.1 true d! *text + :deepseek-v4-pro $ js-await $ call-openrouter! cursor state1 prompt-text |deepseek-v4-pro true d! *text + :deepseek-v4-flash $ js-await $ call-openrouter! cursor state1 prompt-text |deepseek-v4-flash true d! *text + fn (e) + do + js/console.error :msg-buffer-request-failed e + let + err-text $ str |Failed-to-load: e + d! cursor $ -> state1 (assoc :answer err-text) (assoc :loading? false) (assoc :done? true) + assoc :messages $ upsert-assistant-message (:messages state1) err-text | :examples $ [] - :schema $ :: 'Fn - {} (:return 'Unit) - :args $ [] (:: 'Map 'Tag 'Dynamic) 'Bool - :features $ #{} :js-ffi + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] 'List 'app.schema/ChatState 'String 'Bool 'Bool 'Tag 'Dynamic + :features $ #{} :js-ffi + 'toggle-focus-within! $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn toggle-focus-within! (respo-event active?) + let + raw-event $ option:unwrap-or (get respo-event :event) js/undefined + when + js-present-dynamic? raw-event + let + event $ unsafe-coerce raw-event FocusEventHost + target0 $ .-target event + when (js-present? target0) + let + target $ unsafe-coerce target0 FocusTargetHost + parent0 $ .-parentElement target + when (js-present? parent0) + let + parent $ unsafe-coerce parent0 FocusTargetHost + box0 $ .-parentElement parent + when (js-present? box0) + let + box $ unsafe-coerce box0 FocusTargetHost + class-list $ .-classList target + box-class $ .-classList box + if active? + do + when + not $ class-list .contains? |focus-within + class-list .add! |focus-within + when + not $ box-class .contains? |focus-within + box-class .add! |focus-within + do + when (class-list .contains? |focus-within) (class-list .remove! |focus-within) + when (box-class .contains? |focus-within) (box-class .remove! |focus-within) + , &unit + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Unit) + :args $ [] (:: 'Map 'Tag 'Dynamic) 'Bool + :features $ #{} :js-ffi 'upsert-assistant-message $ %{} 'CodeEntry (:doc |) - :code $ quote - defn upsert-assistant-message (messages content thinking) - let - size $ count messages - option:fold (last messages) - fn () $ conj messages - %{} ChatMessage (:role :assistant) (:content content) (:thinking thinking) - fn (last-msg) - hint-fn $ {} - :args $ [] 'app.schema/ChatMessage - if - = :assistant $ :role last-msg - assoc messages (dec size) - -> last-msg (assoc :content content) (assoc :thinking thinking) - conj messages $ %{} ChatMessage (:role :assistant) (:content content) (:thinking thinking) - :examples $ [] - :schema $ :: 'Fn - {} - :args $ [] (:: 'List 'app.schema/ChatMessage) 'String 'String - :return $ :: 'List 'app.schema/ChatMessage + :code $ quote $ defn upsert-assistant-message (messages content thinking) + let + size $ count messages + option:fold (last messages) + fn () $ conj messages $ %{} ChatMessage (:role :assistant) (:content content) (:thinking thinking) + fn (last-msg) + hint-fn $ {} $ :args + [] 'app.schema/ChatMessage + if + = :assistant $ :role last-msg + assoc messages (dec size) + -> last-msg (assoc :content content) (assoc :thinking thinking) + conj messages $ %{} ChatMessage (:role :assistant) (:content content) (:thinking thinking) + :examples $ [] + :schema $ :: 'Fn $ {} + :args $ [] + :: 'List 'app.schema/ChatMessage + , 'String 'String + :return $ :: 'List 'app.schema/ChatMessage :ns $ %{} 'NsEntry (:doc |) - :code $ quote - ns app.comp.container $ :require (respo-ui.css :as css) + :code $ quote $ ns app.comp.container + :require (respo-ui.css :as css) respo.css :refer $ defstyle respo.util.format :refer $ hsl respo.core :refer $ defcomp defeffect <> >> list-> div button textarea span input a pre img memo-comp-by @@ -2065,97 +2129,91 @@ 'app.config $ %{} 'FileEntry :defs $ {} 'Site $ %{} 'CodeEntry (:doc |) - :code $ quote - defstruct Site (:storage-key 'String) (:archive-key 'String) + :code $ quote $ defstruct Site (:storage-key 'String) (:archive-key 'String) :examples $ [] :schema $ :: 'StructDef 'chrome-extension? $ %{} 'CodeEntry (:doc |) - :code $ quote - def chrome-extension? $ let + :code $ quote $ def chrome-extension? + let runtime $ .?-runtime js/window.chrome and (js-present? runtime) js-present? $ .?-id runtime :examples $ [] :schema $ :: 'Bool 'dev? $ %{} 'CodeEntry (:doc |) - :code $ quote - def dev? $ = |dev - option:unwrap-or (get-env |mode) |release + :code $ quote $ def dev? + = |dev $ option:unwrap-or (get-env |mode) |release :examples $ [] :schema $ :: 'Bool 'site $ %{} 'CodeEntry (:doc |) - :code $ quote - %{} Site (:storage-key |msg-buffer) (:archive-key |msg-buffer-archive) + :code $ quote $ %{} Site (:storage-key |msg-buffer) + :archive-key |msg-buffer-archive :examples $ [] :schema $ :: 'app.config/Site :ns $ %{} 'NsEntry (:doc |) - :code $ quote (ns app.config) + :code $ quote $ ns app.config 'app.main $ %{} 'FileEntry :defs $ {} '*reel $ %{} 'CodeEntry (:doc |) - :code $ quote - defatom *reel $ -> reel-schema/reel (assoc :base schema/store) (assoc :store schema/store) + :code $ quote $ defatom *reel + -> reel-schema/reel (assoc :base schema/store) (assoc :store schema/store) :examples $ [] :schema $ :: 'Ref 'DocumentHost $ %{} 'CodeEntry (:doc |) - :code $ quote - deftrait DocumentHost $ :visibilityState (:: 'JsNullish 'String) + :code $ quote $ deftrait DocumentHost + :visibilityState $ :: 'JsNullish 'String :examples $ [] :ffi $ {} (:backend :js) (:kind :external-object) (:target :browser) :schema $ :: 'Trait 'ExtensionMessageHost $ %{} 'CodeEntry (:doc |) - :code $ quote - deftrait ExtensionMessageHost - :action $ :: 'JsNullish 'String - :content $ :: 'JsNullish 'String - :text $ :: 'JsNullish 'String - :submit? $ :: 'JsNullish 'Bool + :code $ quote $ deftrait ExtensionMessageHost + :action $ :: 'JsNullish 'String + :content $ :: 'JsNullish 'String + :text $ :: 'JsNullish 'String + :submit? $ :: 'JsNullish 'Bool :examples $ [] :ffi $ {} (:backend :js) (:kind :external-object) (:target :browser) :schema $ :: 'Trait 'connect-to-worker! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn connect-to-worker! () $ when config/chrome-extension? - let - port $ js/chrome.runtime.connect - js-object $ :name |mySidepanel - when (js-present? port) - let - on-disconnect $ .?-onDisconnect port - when (js-present? on-disconnect) - .!addListener on-disconnect $ fn (event) - do (println |Worker-disconnected-retrying) (js/setTimeout connect-to-worker! 500) + :code $ quote $ defn connect-to-worker! () + when config/chrome-extension? $ let + port $ js/chrome.runtime.connect $ js-object (:name |mySidepanel) + when (js-present? port) + let + on-disconnect $ .?-onDisconnect port + when (js-present? on-disconnect) + .!addListener on-disconnect $ fn (event) + do + println |Worker-disconnected-retrying + js/setTimeout connect-to-worker! 500 :examples $ [] :schema $ :: 'Dynamic 'dispatch! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn dispatch! (op) - when - and config/dev? $ not= op :states - js/console.log |Dispatch: op - reset! *reel $ reel-updater updater @*reel op + :code $ quote $ defn dispatch! (op) + when + and config/dev? $ not= op :states + js/console.log |Dispatch: op + reset! *reel $ reel-updater updater @*reel op :examples $ [] :schema $ :: 'Dynamic 'extension-action $ %{} 'CodeEntry (:doc |) - :code $ quote - defn extension-action (message) - js-nullish->option $ .-action message + :code $ quote $ defn extension-action (message) + js-nullish->option $ .-action message :examples $ [] - :schema $ :: 'Fn - {} - :args $ [] 'app.main/ExtensionMessageHost - :features $ #{} :js-ffi - :return $ :: 'Option 'String + :schema $ :: 'Fn $ {} + :args $ [] 'app.main/ExtensionMessageHost + :features $ #{} :js-ffi + :return $ :: 'Option 'String 'hydrate-storage-later! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn hydrate-storage-later! () $ js/setTimeout + :code $ quote $ defn hydrate-storage-later! () + js/setTimeout fn () $ let - raw $ js/localStorage.getItem (:storage-key config/site) + raw $ js/localStorage.getItem $ :storage-key config/site when (js-present? raw) let t-start $ unsafe-coerce (js/Date.now) 'Number serialized $ unsafe-coerce raw 'String - dispatch! $ :: :hydrate-storage (parse-cirru-edn serialized) + dispatch! $ :: :hydrate-storage $ parse-cirru-edn serialized println |Hydrated-in $ - unsafe-coerce (js/Date.now) 'Number , t-start @@ -2163,60 +2221,54 @@ :examples $ [] :schema $ :: 'Dynamic 'listen-extension! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn listen-extension! () - js/chrome.runtime.onMessage.addListener $ fn (raw-message sender respond!) - let - message $ unsafe-coerce raw-message ExtensionMessageHost - action $ option:unwrap-or (extension-action message) | - when (= |menu-summary action) - let - source $ option:unwrap-or - js-nullish->option $ .-content message - , | - content $ str "|你扮演一个专业的工程师, 对以下内容做一下讲解, 用中文, 注意要简略, 内容注意分块.\n\n" &newline &newline source - event-tuple $ :: :fill-text - {} (:text content) (:submit? true) - send-to-component! event-tuple - when (= |fill-text action) - let - content $ option:unwrap-or - js-nullish->option $ .-text message - , | - submit? $ option:unwrap-or - js-nullish->option $ .-submit? message - , true - event-tuple $ :: :fill-text - {} (:text content) (:submit? submit?) - send-to-component! event-tuple - when (= |menu-translate action) - let - source $ option:unwrap-or - js-nullish->option $ .-content message - , | - content $ str "|请将以下内容翻译成中文, 保持简洁分段:\n\n" &newline &newline source - event-tuple $ :: :fill-text - {} (:text content) (:submit? true) - send-to-component! event-tuple - when (= |menu-custom action) - let - content $ option:unwrap-or - js-nullish->option $ .-content message - , | - event-tuple $ :: :fill-text - {} (:text content) (:submit? false) - send-to-component! event-tuple - , &unit - connect-to-worker! + :code $ quote $ defn listen-extension! () + js/chrome.runtime.onMessage.addListener $ fn (raw-message sender respond!) + let + message $ unsafe-coerce raw-message ExtensionMessageHost + action $ option:unwrap-or (extension-action message) | + when (= |menu-summary action) + let + source $ option:unwrap-or + js-nullish->option $ .-content message + , | + content $ str "|你扮演一个专业的工程师, 对以下内容做一下讲解, 用中文, 注意要简略, 内容注意分块.\n\n" &newline &newline source + event-tuple $ :: :fill-text $ {} (:text content) (:submit? true) + send-to-component! event-tuple + when (= |fill-text action) + let + content $ option:unwrap-or + js-nullish->option $ .-text message + , | + submit? $ option:unwrap-or + js-nullish->option $ .-submit? message + , true + event-tuple $ :: :fill-text $ {} (:text content) (:submit? submit?) + send-to-component! event-tuple + when (= |menu-translate action) + let + source $ option:unwrap-or + js-nullish->option $ .-content message + , | + content $ str "|请将以下内容翻译成中文, 保持简洁分段:\n\n" &newline &newline source + event-tuple $ :: :fill-text $ {} (:text content) (:submit? true) + send-to-component! event-tuple + when (= |menu-custom action) + let + content $ option:unwrap-or + js-nullish->option $ .-content message + , | + event-tuple $ :: :fill-text $ {} (:text content) (:submit? false) + send-to-component! event-tuple , &unit + connect-to-worker! + , &unit :examples $ [] - :schema $ :: 'Fn - {} (:return 'Unit) - :args $ [] - :features $ #{} :js-ffi + :schema $ :: 'Fn $ {} (:return 'Unit) + :args $ [] + :features $ #{} :js-ffi 'main! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn main! () $ let + :code $ quote $ defn main! () + let t0 $ unsafe-coerce (js/Date.now) 'Number println "|Starting main! at" t0 println "|Running mode:" $ if config/dev? |dev |release @@ -2247,31 +2299,31 @@ println "|App started at" t1 |cost (- t1 t0) |ms , &unit :examples $ [] - :schema $ :: 'Fn - {} (:return 'Unit) - :args $ [] - :features $ #{} :js-ffi + :schema $ :: 'Fn $ {} (:return 'Unit) + :args $ [] + :features $ #{} :js-ffi 'mount-target $ %{} 'CodeEntry (:doc |) - :code $ quote - def mount-target $ js/document.querySelector |.app + :code $ quote $ def mount-target + js/document.querySelector |.app :examples $ [] :schema $ :: 'Dynamic 'persist-storage! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn persist-storage! () - println "|Saved at" $ .!toISOString (new js/Date) - js/localStorage.setItem (:storage-key config/site) - format-cirru-edn $ option:unwrap-or (get @*reel :store) schema/store - :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] - :features $ #{} :js-ffi + :code $ quote $ defn persist-storage! () + println "|Saved at" $ .!toISOString $ new js/Date + js/localStorage.setItem (:storage-key config/site) + format-cirru-edn $ option:unwrap-or (get @*reel :store) schema/store + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] + :features $ #{} :js-ffi 'reload! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn reload! () $ if + :code $ quote $ defn reload! () + if = false $ unsafe-coerce (js/Boolean build-errors) 'Bool - do (remove-watch *reel :changes) (remove-watch *archived-sessions :changes) (remove-watch *viewing-archive-session :changes) (clear-cache!) + do (remove-watch *reel :changes) + remove-watch *archived-sessions :changes + remove-watch *viewing-archive-session :changes + clear-cache! add-watch *reel :changes $ fn (reel prev) (render-app!) add-watch *archived-sessions :changes $ fn (s prev) (render-app!) add-watch *viewing-archive-session :changes $ fn (s prev) (render-app!) @@ -2281,8 +2333,8 @@ :examples $ [] :schema $ :: 'Dynamic 'render-app! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn render-app! () $ let + :code $ quote $ defn render-app! () + let t_start $ unsafe-coerce (js/Date.now) 'Number println "|Rendering app..." render-with! mount-target @@ -2294,32 +2346,31 @@ , t_start , |ms :examples $ [] - :schema $ :: 'Fn - {} (:return 'Unit) - :args $ [] - :features $ #{} :js-ffi + :schema $ :: 'Fn $ {} (:return 'Unit) + :args $ [] + :features $ #{} :js-ffi 'sync-gemini-key! $ %{} 'CodeEntry (:doc |) - :code $ quote - defn sync-gemini-key! () $ when config/chrome-extension? - do - let - gemini-key $ js/localStorage.getItem |gemini-key - when (js-present? gemini-key) - js/chrome.runtime.sendMessage $ js-object (:action |sync-gemini-key) (:key gemini-key) - js/chrome.storage.local.set $ js-object (:geminiKey gemini-key) - let - deepseek-key $ js/localStorage.getItem |deepseek-key - when (js-present? deepseek-key) - js/chrome.runtime.sendMessage $ js-object (:action |sync-deepseek-key) (:key deepseek-key) - js/chrome.storage.local.set $ js-object (:deepseekKey deepseek-key) - :examples $ [] - :schema $ :: 'Fn - {} (:return 'Dynamic) - :args $ [] - :features $ #{} :js-ffi + :code $ quote $ defn sync-gemini-key! () + when config/chrome-extension? $ do + let + gemini-key $ js/localStorage.getItem |gemini-key + when (js-present? gemini-key) + js/chrome.runtime.sendMessage $ js-object (:action |sync-gemini-key) (:key gemini-key) + js/chrome.storage.local.set $ js-object $ :geminiKey gemini-key + let + deepseek-key $ js/localStorage.getItem |deepseek-key + when (js-present? deepseek-key) + js/chrome.runtime.sendMessage $ js-object + :action |sync-deepseek-key + :key deepseek-key + js/chrome.storage.local.set $ js-object $ :deepseekKey deepseek-key + :examples $ [] + :schema $ :: 'Fn $ {} (:return 'Dynamic) + :args $ [] + :features $ #{} :js-ffi :ns $ %{} 'NsEntry (:doc |) - :code $ quote - ns app.main $ :require + :code $ quote $ ns app.main + :require respo.core :refer $ render-with! clear-cache! app.comp.container :refer $ comp-container submit-message! *archived-sessions *viewing-archive-session app.updater :refer $ updater @@ -2334,82 +2385,78 @@ 'app.schema $ %{} 'FileEntry :defs $ {} 'ChatMessage $ %{} 'CodeEntry (:doc |) - :code $ quote - defstruct ChatMessage (:role 'Tag) (:content 'String) (:thinking 'String) + :code $ quote $ defstruct ChatMessage (:role 'Tag) (:content 'String) (:thinking 'String) :examples $ [] :schema $ :: 'StructDef 'ChatSession $ %{} 'CodeEntry (:doc |) - :code $ quote - defstruct ChatSession (:id 'String) (:created-at 'Number) - :messages $ :: 'List 'app.schema/ChatMessage - :model 'Dynamic - :preview 'String - :is-history? 'Bool + :code $ quote $ defstruct ChatSession (:id 'String) (:created-at 'Number) + :messages $ :: 'List 'app.schema/ChatMessage + :model 'Dynamic + :preview 'String + :is-history? 'Bool :examples $ [] :schema $ :: 'StructDef 'ChatState $ %{} 'CodeEntry (:doc |) - :code $ quote - defstruct ChatState (:answer 'String) (:loading? 'Bool) (:done? 'Bool) - :messages $ :: 'List 'app.schema/ChatMessage - :model 'Tag - :thinking 'String + :code $ quote $ defstruct ChatState (:answer 'String) (:loading? 'Bool) (:done? 'Bool) + :messages $ :: 'List 'app.schema/ChatMessage + :model 'Tag + :thinking 'String :examples $ [] :schema $ :: 'StructDef 'MessageBoxState $ %{} 'CodeEntry (:doc |) - :code $ quote - defstruct MessageBoxState (:content 'String) (:search? 'Bool) (:think? 'Bool) (:focus-mode? 'Bool) + :code $ quote $ defstruct MessageBoxState (:content 'String) (:search? 'Bool) (:think? 'Bool) (:focus-mode? 'Bool) :examples $ [] :schema $ :: 'StructDef 'Store $ %{} 'CodeEntry (:doc |) - :code $ quote - defstruct Store (:states 'Map) - :sessions $ :: 'List 'app.schema/ChatSession - :current-session-id 'Dynamic - :model 'Dynamic - :archived-count 'Number + :code $ quote $ defstruct Store (:states 'Map) + :sessions $ :: 'List 'app.schema/ChatSession + :current-session-id 'Dynamic + :model 'Dynamic + :archived-count 'Number :examples $ [] :schema $ :: 'StructDef 'normalize-chat-session $ %{} 'CodeEntry (:doc |) - :code $ quote - defn normalize-chat-session (raw) - hint-fn $ {} - :args $ [] 'T - :return 'app.schema/ChatSession - :features $ #{} :js-ffi - :generics $ [] 'T - if (struct? raw) (unsafe-coerce raw 'app.schema/ChatSession) (decode-map-as raw 'app.schema/ChatSession) - :examples $ [] - :schema $ :: 'Fn - {} (:return 'app.schema/ChatSession) + :code $ quote $ defn normalize-chat-session (raw) + hint-fn $ {} :args $ [] 'T + :return 'app.schema/ChatSession :features $ #{} :js-ffi :generics $ [] 'T - :tests $ [] - %{} 'TestEntry (:name |accepts-typed-and-map) - :code $ quote - let - typed $ %{} ChatSession (:id |typed) (:created-at 1) - :messages $ [] - :model :gemini - :preview |Typed - :is-history? true - typed-result $ assert-type (normalize-chat-session typed) 'app.schema/ChatSession - decoded $ assert-type - normalize-chat-session $ {} (:id |decoded) (:created-at 2) - :messages $ [] - :model :anthropic - :preview |Decoded - :is-history? false - , 'app.schema/ChatSession - is $ = |typed (:id typed-result) - is $ = |decoded (:id decoded) - is $ = :anthropic (:model decoded) - :tags $ #{} :regression :unit + if (struct? raw) + unsafe-coerce raw 'app.schema/ChatSession + decode-map-as raw 'app.schema/ChatSession + :examples $ [] + :schema $ :: 'Fn $ {} + :return 'app.schema/ChatSession + :args $ [] 'T + :features $ #{} :js-ffi + :generics $ [] 'T + :tests $ [] $ %{} 'TestEntry + :name |accepts-typed-and-map + :code $ quote $ let + typed $ %{} ChatSession (:id |typed) (:created-at 1) + :messages $ [] + :model :gemini + :preview |Typed + :is-history? true + typed-result $ assert-type + normalize-chat-session typed + , 'app.schema/ChatSession + decoded $ assert-type + normalize-chat-session $ {} (:id |decoded) (:created-at 2) + :messages $ [] + :model :anthropic + :preview |Decoded + :is-history? false + , 'app.schema/ChatSession + is $ = |typed $ :id typed-result + is $ = |decoded $ :id decoded + is $ = :anthropic $ :model decoded + :tags $ #{} :regression :unit 'store $ %{} 'CodeEntry (:doc |) - :code $ quote - def store $ %{} Store - :states $ {} - :cursor $ [] + :code $ quote $ def store + %{} Store + :states $ {} $ :cursor ([]) :sessions $ [] :current-session-id nil :model nil @@ -2417,81 +2464,85 @@ :examples $ [] :schema $ :: 'app.schema/Store :ns $ %{} 'NsEntry (:doc |) - :code $ quote - ns app.schema $ :require - calcit.test :refer $ [] is + :code $ quote $ ns app.schema + :require $ calcit.test :refer $ [] is 'app.updater $ %{} 'FileEntry - :defs $ {} - 'updater $ %{} 'CodeEntry (:doc |) - :code $ quote - defn updater (raw-store op op-id op-time) - let - store raw-store - match op - (:states cursor s) - assoc store :states $ assoc-in (:states store) - concat cursor $ [] :data - , s - (:states-merge cursor s changes) - let - path $ concat cursor ([] :data) - state $ option:unwrap-or - get-in (:states store) path - , s - updated-states $ assoc-in (:states store) path - if - or (map? state) (struct? state) - noted |merge-base-latest-state $ merge state changes - do (js/console.warn |unknown-state-to-merge state) state - assoc store :states updated-states - (:hydrate-storage data) - if (struct? data) (unsafe-coerce data 'app.schema/Store) - decode-map-as (dissoc data :current-chapter-id) 'app.schema/Store - (:change-model) - if - = (:model store) :anthropic - assoc store :model :gemini - assoc store :model :anthropic - (:save-session state) - let - store1 $ save-current-session store state - assoc store1 :current-session-id nil - (:session session-id id) (assoc store :current-session-id id) - (:load-session cursor state session) - let - typed-session $ assert-type (app.schema/normalize-chat-session session) 'app.schema/ChatSession - store1 $ assoc store :states - assoc-in (:states store) - concat cursor $ [] :data - -> state - assoc :messages $ :messages typed-session - assoc :done? true - assoc store1 :current-session-id $ :id typed-session - (:remove-session id) - assoc store :sessions $ filter - or (:sessions store) ([]) - fn (s) - hint-fn $ {} - :args $ [] 'app.schema/ChatSession - not $ = (:id s) id - (:archive-sessions new-count) - -> store - assoc :sessions $ [] - assoc :archived-count new-count - assoc :current-session-id nil - (:update-archived-count new-count) - -> store $ assoc :archived-count new-count - (:clear-sessions) - -> store - assoc :sessions $ [] - assoc :current-session-id nil - _ $ do (eprintln "|unknown op:" op) store - :examples $ [] - :schema $ :: 'Fn - {} (:return 'app.schema/Store) - :args $ [] 'app.schema/Store 'Enum 'String 'Number + :defs $ {} $ 'updater + %{} 'CodeEntry (:doc |) + :code $ quote $ defn updater (raw-store op op-id op-time) + let + store raw-store + match op + (:states cursor s) + assoc store :states $ assoc-in (:states store) + concat cursor $ [] :data + , s + (:states-merge cursor s changes) + let + path $ concat cursor $ [] :data + state $ option:unwrap-or + get-in (:states store) path + , s + updated-states $ assoc-in (:states store) path $ if + or (map? state) (struct? state) + noted |merge-base-latest-state $ merge state changes + do + js/console.warn |unknown-state-to-merge state + , state + assoc store :states updated-states + (:hydrate-storage data) + if (struct? data) + unsafe-coerce data 'app.schema/Store + decode-map-as + dissoc data :current-chapter-id + , 'app.schema/Store + (:change-model) + if + = (:model store) :anthropic + assoc store :model :gemini + assoc store :model :anthropic + (:save-session state) + let + store1 $ save-current-session store state + assoc store1 :current-session-id nil + (:session session-id id) + assoc store :current-session-id id + (:load-session cursor state session) + let + typed-session $ assert-type + app.schema/normalize-chat-session session + , 'app.schema/ChatSession + store1 $ assoc store :states $ assoc-in (:states store) + concat cursor $ [] :data + -> state + assoc :messages $ :messages typed-session + assoc :done? true + assoc store1 :current-session-id $ :id typed-session + (:remove-session id) + assoc store :sessions $ filter + or (:sessions store) ([]) + fn (s) + hint-fn $ {} $ :args + [] 'app.schema/ChatSession + not $ = (:id s) id + (:archive-sessions new-count) + -> store + assoc :sessions $ [] + assoc :archived-count new-count + assoc :current-session-id nil + (:update-archived-count new-count) + -> store $ assoc :archived-count new-count + (:clear-sessions) + -> store + assoc :sessions $ [] + assoc :current-session-id nil + _ $ do (eprintln "|unknown op:" op) store + :examples $ [] + :schema $ :: 'Fn $ {} + :return 'app.schema/Store + :args $ [] 'app.schema/Store 'Enum 'String 'Number :ns $ %{} 'NsEntry (:doc |) - :code $ quote - ns app.updater $ :require + :code $ quote $ ns app.updater + :require app.comp.container :refer $ save-current-session generate-session-id app.schema :refer $ Store ChatState ChatSession diff --git a/deps.cirru b/deps.cirru index a17fb35..24885bb 100644 --- a/deps.cirru +++ b/deps.cirru @@ -1,8 +1,8 @@ -{} (:calcit-version |0.14.4) +{} (:calcit-version |0.14.16) :version |0.0.6 - :dependencies $ {} (|Respo/alerts.calcit |0.10.30) - |Respo/reel.calcit |0.6.19 - |Respo/respo-markdown.calcit |0.4.33 - |Respo/respo-ui.calcit |0.7.19 - |Respo/respo.calcit |0.16.95 + :dependencies $ {} (|Respo/alerts.calcit |0.10.33) + |Respo/reel.calcit |0.6.22 + |Respo/respo-markdown.calcit |0.4.35 + |Respo/respo-ui.calcit |0.7.21 + |Respo/respo.calcit |0.16.101 diff --git a/package.json b/package.json index a17e03b..5181e27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "@calcit/procs": "0.14.4", + "@calcit/procs": "0.14.16", "@google/genai": "^2.20.0", "@tiye/main-fonts": "0.0.1", "axios": "^1.15.0", diff --git a/yarn.lock b/yarn.lock index bd7edbf..05cd295 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,14 +5,14 @@ __metadata: version: 8 cacheKey: 10c0 -"@calcit/procs@npm:0.14.4": - version: 0.14.4 - resolution: "@calcit/procs@npm:0.14.4" +"@calcit/procs@npm:0.14.16": + version: 0.14.16 + resolution: "@calcit/procs@npm:0.14.16" dependencies: "@calcit/ternary-tree": "npm:0.0.26" "@cirru/parser.ts": "npm:^0.0.9" "@cirru/writer.ts": "npm:^0.1.9" - checksum: 10c0/50b8e0cb0f65eab75dffdbf2624ed5b02610a75546f04d53d2ee7adaa81dceb1676b268a76112a715972a0fe52faacf2ffd953042fb86c673e749f214b73e8f2 + checksum: 10c0/627197e5b42d6eb550eab24c2fd83b9cddfdc0cd00b9bd8a462e53f211267d36514fecbe77f722b5d591ca7e2d0c49bbd28f7e0d9cf0652eee202ed8f4f6794e languageName: node linkType: hard @@ -1503,7 +1503,7 @@ __metadata: version: 0.0.0-use.local resolution: "root-workspace-0b6124@workspace:." dependencies: - "@calcit/procs": "npm:0.14.4" + "@calcit/procs": "npm:0.14.16" "@google/genai": "npm:^2.20.0" "@tiye/main-fonts": "npm:0.0.1" axios: "npm:^1.15.0"