diff --git a/.gitattributes b/.gitattributes index ef65336..f4d5078 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ -calcit.cirru -diff linguist-generated yarn.lock -diff linguist-generated Agents.md -diff linguist-generated -llms/*.md -diff linguist-generated \ No newline at end of file +llms/*.md -diff linguist-generated + +history/*.md -diff linguist-generated \ No newline at end of file diff --git a/.github/workflows/upload.yaml b/.github/workflows/upload.yaml index bca16bb..b7da071 100644 --- a/.github/workflows/upload.yaml +++ b/.github/workflows/upload.yaml @@ -22,9 +22,7 @@ jobs: corepack prepare yarn@4.12.0 --activate yarn --version - - uses: calcit-lang/setup-cr@0.0.8 - with: - version: 0.12.14 + - uses: calcit-lang/setup-cr@0.0.9 - name: "compiles to js" run: > diff --git a/compact.cirru b/calcit.cirru similarity index 55% rename from compact.cirru rename to calcit.cirru index b5d39a1..262ac72 100644 --- a/compact.cirru +++ b/calcit.cirru @@ -1,61 +1,69 @@ -{} (:about "|file is generated - never edit directly; learn cr edit/tree workflows before changing") (:package |app) +{} (:about "|Machine-generated snapshot. Do not edit directly — changes will be overwritten. Use `cr query` to inspect and `cr edit`/`cr tree` to modify. Run `cr docs agents --full` first. Manual edits must follow format and schema conventions, then run `cr edit format`.") (:package |app) :configs $ {} (:init-fn |app.main/main!) (:reload-fn |app.main/reload!) (:version |0.0.1) :modules $ [] |respo.calcit/ |memof/ |respo-ui.calcit/ |reel.calcit/ |respo-markdown.calcit/ |alerts.calcit/ |respo-feather.calcit/ |genai.calcit/ :entries $ {} :files $ {} |app.comp.container $ %{} :FileEntry :defs $ {} - |*abort-control $ %{} :CodeEntry (:doc |) (:schema nil) + |*abort-control $ %{} :CodeEntry (:doc |) (:schema :ref) :code $ quote (defatom *abort-control nil) :examples $ [] - |*gen-ai-new $ %{} :CodeEntry (:doc |) (:schema nil) + |*archived-sessions $ %{} :CodeEntry (:doc |) (:schema :ref) + :code $ quote (defatom *archived-sessions nil) + :examples $ [] + |*gen-ai-new $ %{} :CodeEntry (:doc |) (:schema :ref) :code $ quote (defatom *gen-ai-new nil) :examples $ [] - |*image-cache $ %{} :CodeEntry (:doc |) (:schema nil) + |*image-cache $ %{} :CodeEntry (:doc |) (:schema :ref) :code $ quote (defatom *image-cache nil) :examples $ [] - |*openai $ %{} :CodeEntry (:doc "|called openai sdk, but actually for openrouter") (:schema nil) + |*openai $ %{} :CodeEntry (:doc "|called openai sdk, but actually for openrouter") (:schema :ref) :code $ quote (defatom *openai nil) :examples $ [] - |append-user-message $ %{} :CodeEntry (:doc |) (:schema nil) + |*viewing-archive-session $ %{} :CodeEntry (:doc |) (:schema :ref) + :code $ quote (defatom *viewing-archive-session nil) + :examples $ [] + |append-user-message $ %{} :CodeEntry (:doc |) :code $ quote defn append-user-message (messages content) let messages0 $ if (some? messages) messages ([]) conj messages0 $ {} (:role :user) (:content content) :examples $ [] - |call-anthropic-msg! $ %{} :CodeEntry (:doc |) (:schema nil) + :schema $ :: :fn $ {} (:return (:: :list (:: :map :tag :dynamic))) + :args $ [] (:: :optional (:: :list (:: :map :tag :dynamic))) :string + |call-anthropic-msg! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn call-anthropic-msg! (cursor state prompt-text model thinking? d!) hint-fn $ {} (:async true) if-let abort $ deref *abort-control - do (js/console.warn "\"Aborting prev") (.!abort abort) + do (js/console.warn "|Aborting prev") (.!abort abort) d! $ :: :change-model let selected $ js-await (get-selected) - content $ .replace prompt-text "\"{{selected}}" (or selected "\"<未找到内容>") + content $ .replace prompt-text |{{selected}} (or selected "|<未找到内容>") messages0 $ append-user-message (:messages state) content - messages1 $ upsert-assistant-message messages0 "\"" nil + messages1 $ upsert-assistant-message messages0 | nil result $ js-await - .!post axios (str "\"https://sa.chenyong.life/v1/messages") + .!post axios (str |https://sa.chenyong.life/v1/messages) js-object - :model $ get-env "\"claude-model" (or model "\"claude-3-5-sonnet-latest") + :model $ 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-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" + :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 @@ -64,7 +72,7 @@ reader $ -> .!pipeThrough stream $ new js/TextDecoderStream .!getReader - *text $ atom (str "\"Claude AI:" &newline &newline) + *text $ atom (str "|Claude AI:" &newline &newline) js/setTimeout $ fn () d! $ :: :states-merge cursor state {} (:answer nil) (:thinking nil) (:loading? true) (:done? false) (:messages messages1) @@ -78,31 +86,31 @@ do let events $ -> value .split-lines - filter $ fn (s) (.starts-with? s "\"data: ") + filter $ fn (s) (.starts-with? s "|data: ") map $ fn (s) - -> (.strip-prefix s "\"data: ") js/JSON.parse to-calcit-data + -> (.strip-prefix s "|data: ") js/JSON.parse to-calcit-data apply-args (events) fn (xs) list-match xs - () $ ;nil println "\"no thing to handle in this Loop" + () $ ;nil println "|no thing to handle in this Loop" (x0 xss) let - stop? $ = (get x0 "\"type") "\"message_stop" + stop? $ = (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 nil let - content $ get-in x0 ([] "\"delta" "\"text") + content $ get-in x0 ([] |delta |text) if (nil? content) do ;nil d! $ :: :states cursor -> state - assoc :answer $ str @*text &newline "\"[STOPPED: " (.-finishReason candidate0) "\"]" + assoc :answer $ str @*text &newline "|[STOPPED: " (.-finishReason candidate0) |] assoc :loading? false assoc :done? true - println "\"content is nil" + println "|content is nil" recur xss let () (swap! *text str content) d! $ :: :states-merge cursor state @@ -111,7 +119,7 @@ recur xss recur :examples $ [] - |call-flash-imagen-msg! $ %{} :CodeEntry (:doc |) (:schema nil) + |call-flash-imagen-msg! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn call-flash-imagen-msg! (variant cursor state prompt-text d!) hint-fn $ {} (:async true) @@ -122,30 +130,30 @@ reset! *gen-ai-new $ new GoogleGenAI js-object $ :apiKey (get-gemini-key!) if-let - target $ js/document.querySelector "\".show-image" - .!setAttribute target "\"src" "\"" + target $ js/document.querySelector |.show-image + .!setAttribute target |src | if-let abort $ deref *abort-control - do (js/console.warn "\"Aborting prev") (.!abort abort) + do (js/console.warn "|Aborting prev") (.!abort abort) clear-image-cache! d! $ :: :states cursor -> state (assoc :answer nil) (assoc :loading? true) let selected $ js-await (get-selected) gen-ai @*gen-ai-new - content $ .!replace prompt-text "\"{{selected}}" (or selected "\"<未找到选中内容>") + content $ .!replace prompt-text |{{selected}} (or selected "|<未找到选中内容>") abort-signal $ let abort $ new js/AbortController reset! *abort-control abort .-signal abort sdk-result $ js-await .!generateContent (.-models gen-ai) - js-object (:model "\"gemini-2.5-flash-image") (:contents content) + 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" + :responseModalities $ js-array |TEXT |IMAGE parts $ -> sdk-result .-candidates .-0 .-content .-parts - *text $ atom "\"" + *text $ atom | js-await $ .!forEach parts fn (? chunk _a _b) if (some? chunk) @@ -159,10 +167,10 @@ let image-blob $ base64ToBlob (.-data image-data) url $ js/URL.createObjectURL image-blob - target $ js/document.querySelector "\".show-image" - -> target $ .!setAttribute "\"src" url + target $ js/document.querySelector |.show-image + -> target $ .!setAttribute |src url reset! *image-cache url - do (swap! *text str "\"(image ready)") + do (swap! *text str "|(image ready)") d! $ :: :states cursor -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? false) d! $ :: :states cursor @@ -170,7 +178,7 @@ d! $ :: :states cursor -> state (assoc :answer @*text) (assoc :loading? false) (assoc :done? true) :examples $ [] - |call-genai-msg! $ %{} :CodeEntry (:doc |) (:schema nil) + |call-genai-msg! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn call-genai-msg! (variant cursor state prompt-text search? think? d! *text *thinking-text) hint-fn $ {} (:async true) @@ -182,18 +190,18 @@ js-object $ :apiKey (get-gemini-key!) if-let abort $ deref *abort-control - do (js/console.warn "\"Aborting prev") (.!abort abort) + do (js/console.warn "|Aborting prev") (.!abort abort) let - selected $ if (.includes? prompt-text "\"{{selected}}") + selected $ if (.includes? prompt-text |{{selected}}) js-await $ get-selected gen-ai @*gen-ai-new model $ pick-model variant - content $ .!replace prompt-text "\"{{selected}}" (or 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://") + content $ .!replace prompt-text |{{selected}} (or 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 "\"" nil + messages1 $ upsert-assistant-message messages0 | nil abort-signal $ let abort $ new js/AbortController reset! *abort-control abort @@ -214,7 +222,7 @@ :config $ js-object :thinkingConfig $ if think? js-object - :thinkingBudget $ get-env "\"think-budget" (if pro? 3200 800) + :thinkingBudget $ get-env |think-budget (if pro? 3200 800) :includeThoughts true js-object (:thinkingBudget 0) (:includeThoughts false) :tools $ if @@ -222,7 +230,7 @@ , tools js/undefined :abortSignal abort-signal :httpOptions $ js-object (:baseUrl |https://ja.chenyong.life) - :responseMimeType $ if json? "\"application/json" js/undefined + :responseMimeType $ if json? |application/json js/undefined do js/setTimeout $ fn () d! $ :: :states-merge cursor state @@ -247,7 +255,7 @@ {} (:answer @*text) (:thinking @*thinking-text) (:loading? false) (:done? true) :messages $ upsert-assistant-message messages1 @*text @*thinking-text :examples $ [] - |call-imagen-4-msg! $ %{} :CodeEntry (:doc |) (:schema nil) + |call-imagen-4-msg! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn call-imagen-4-msg! (variant cursor state prompt-text d!) hint-fn $ {} (:async true) @@ -258,11 +266,11 @@ reset! *gen-ai-new $ new GoogleGenAI js-object $ :apiKey (get-gemini-key!) if-let - target $ js/document.querySelector "\".show-image" - .!removeAttribute target "\"src" + target $ js/document.querySelector |.show-image + .!removeAttribute target |src if-let abort $ deref *abort-control - do (js/console.warn "\"Aborting prev") (.!abort abort) + do (js/console.warn "|Aborting prev") (.!abort abort) clear-image-cache! d! $ :: :states cursor -> state (assoc :answer nil) (assoc :loading? true) @@ -275,26 +283,26 @@ .-signal abort response $ js-await .!generateImages (.-models gen-ai) - js-object (:model "\"imagen-4.0-generate-001") (:prompt prompt-text) + 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 "\"" + *text $ atom | if-let image-data $ -> response .-generatedImages .-0 .-image .-imageBytes let image-blob $ base64ToBlob image-data url $ js/URL.createObjectURL image-blob - target $ js/document.querySelector "\".show-image" + target $ js/document.querySelector |.show-image reset! *image-cache url - -> target $ .!setAttribute "\"src" url - do (swap! *text str "\"(image ready)") + -> target $ .!setAttribute |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 $ [] - |call-openrouter! $ %{} :CodeEntry (:doc |) (:schema nil) + |call-openrouter! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn call-openrouter! (cursor state prompt-text variant thinking? d! *text) hint-fn $ {} (:async true) @@ -303,28 +311,28 @@ mod $ js-await (js/import |openai) OpenAI $ .-default mod reset! *openai $ new OpenAI - js-object (:baseURL "\"https://openrouter.ai/api/v1") + js-object (:baseURL |https://openrouter.ai/api/v1) :apiKey $ get-openrouter-key! :defaultHeaders $ js-object :dangerouslyAllowBrowser true if-let abort $ deref *abort-control - do (js/console.warn "\"Aborting prev") (.!abort abort) + do (js/console.warn "|Aborting prev") (.!abort abort) let selected $ js-await (get-selected) openai $ let ai @*openai , ai - content $ .!replace prompt-text "\"{{selected}}" (or selected "\"<未找到选中内容>") - json? $ or (.!includes prompt-text "\"{{json}}") (.!includes prompt-text "\"{{JSON}}") + content $ .!replace prompt-text |{{selected}} (or selected "|<未找到选中内容>") + json? $ or (.!includes prompt-text |{{json}}) (.!includes prompt-text |{{JSON}}) messages0 $ append-user-message (:messages state) content - messages1 $ upsert-assistant-message messages0 "\"" nil + messages1 $ upsert-assistant-message messages0 | nil sdk-result $ js-await -> openai .-chat .-completions $ .!create js-object (:model variant) :messages $ messages->openai messages0 ; :generationConfig $ if json? - js-object $ "\"responseMimeType" "\"application/json" + js-object $ |responseMimeType |application/json , js/undefined :stream true :headers $ js-object (:HTTP-Referer js/location.host) @@ -341,7 +349,7 @@ fn (? chunk) if (some? chunk) do - swap! *text str $ -> chunk .-choices .-0 .-delta .-content (or "\"") + swap! *text str $ -> chunk .-choices .-0 .-delta .-content (or |) d! $ :: :states-merge cursor state {} (:answer @*text) (:loading? false) (:done? false) :messages $ upsert-assistant-message messages1 @*text nil @@ -352,32 +360,34 @@ {} (:answer @*text) (:loading? false) (:done? true) :messages $ upsert-assistant-message messages1 @*text nil :examples $ [] - |clear-image-cache! $ %{} :CodeEntry (:doc |) (:schema nil) + |clear-image-cache! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn clear-image-cache! () $ if-let (url @*image-cache) do (js/URL.revokeObjectURL url) (reset! *image-cache nil) :examples $ [] - |comp-abort $ %{} :CodeEntry (:doc |) (:schema nil) + |comp-abort $ %{} :CodeEntry (:doc |) (:schema :dynamic) :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!) if-let abort $ deref *abort-control - do (js/console.warn "\"Aborting prev") (.!abort abort) + do (js/console.warn "|Aborting prev") (.!abort abort) <> t =< 8 nil - <> "\"✕" style-abort-close + <> "|✕" style-abort-close :examples $ [] - |comp-container $ %{} :CodeEntry (:doc |) (:schema nil) + |comp-container $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defcomp comp-container (reel) let store $ :store reel sessions $ or (:sessions store) ([]) + archived-count $ or (:archived-count store) 0 current-session-id $ :current-session-id store states $ :states store cursor $ or (:cursor states) ([]) @@ -421,9 +431,9 @@ {} (:search? false) (:think? false) sessions-plugin $ use-drawer (>> states :sessions-modal) {} (:title "|History Sessions") - :style $ {} (:min-width "\"|max(320px,30vw)\"") (:max-width |80vw) + :style $ {} (:min-width "||max(320px,30vw)\"") (:max-width |80vw) :render $ fn (on-close) - comp-sessions-modal sessions + comp-sessions-modal sessions archived-count fn (session-id d!) d! cursor $ -> state assoc :messages $ :messages @@ -434,167 +444,289 @@ assoc :done? true d! $ :: :session :session-id session-id on-close d! - , on-close + , 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) - div - {} $ :class-name (str-spaced css/expand style-message-area) - div - {} - :class-name $ str-spaced css/row-parted - :style $ {} (:padding |8px) - div $ {} + if (some? @*archived-sessions) + if (some? @*viewing-archive-session) (; Render specific read-only archived session) div - {} (:class-name css/row-middle) (:title |History) - :style $ {} (:cursor :pointer) - :on-click $ fn (e d!) (.show sessions-plugin d!) + {} $ :class-name (str-spaced css/expand style-message-area) div - {} $ :class-name style-history-button - comp-i |clock - =< 4 nil - if - > (count sessions) 0 - <> - str $ count sessions - str-spaced css/font-fancy style-history-count - div - {} $ :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) - [] idx $ let - role $ :role msg - content $ :content msg - thinking $ :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 style-thinking - memof1-call comp-md-block - -> thinking $ either "\"" - {} $ :class-name style-md-content - if (= role :assistant) - if (json-pattern? content) - pre $ {} (:class-name style-code-content) (:inner-text content) - memof1-call comp-md-block - -> content $ either "\"" - {} $ :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 + {} $ :class-name style-archive-header + div $ {} + :style $ {} (:font-weight :bold) + :inner-text $ str "|Archived: " (:preview @*viewing-archive-session) + div + {} (:class-name style-archive-close) + :on-click $ fn (e d!) (reset! *viewing-archive-session nil) + <> "|✕" + ; 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 @*viewing-archive-session) + map-indexed $ fn (idx msg) + [] idx $ let + role $ :role msg + content $ :content msg + thinking $ :thinking msg div - {} $ :class-name (str-spaced css/row-middle css/gap8 style-message-actions) - if chrome-extension? - comp-fill $ either content "\"" - , nil - comp-copy $ either content "\"" - , nil - if - and - > (count messages) 0 - :done? state - not is-viewing-history? + {} $ :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 + memof1-call comp-md-block + -> thinking $ either | + {} $ :class-name style-md-content + if (= role :assistant) + if (json-pattern? content) + pre $ {} (:class-name style-code-content) (:inner-text content) + memof1-call comp-md-block + -> content $ either | + {} $ :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/row-middle css/gap8 style-reply-actions) - button - {} - :class-name $ str-spaced css/button style-reply-button - :on-click $ fn (e d!) - .show reply-plugin d! $ fn (text) - submit-message! cursor state text (:search? message-box-state) (:think? message-box-state) model d! - <> |Reply - if (:focus-mode? message-box-state) nil $ a - {} (:class-name style-focus-link) (:inner-text |Focus) - :on-click $ fn (e d!) - let - focused $ .-activeElement js/document - do - if (some? focused) (.!blur focused) - d! - :cursor $ >> states :message-box - assoc message-box-state :focus-mode? true - , nil - if (:loading? state) - div ({}) (memof1-call-by :abort-loading comp-abort "\"Loading...") + {} $ :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 nil) (reset! *viewing-archive-session nil) + <> "|✕" + ; 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) + let + session-id $ :id session + created-at $ :created-at session + preview $ :preview session + date-str $ .!toLocaleString (new js/Date created-at) + [] 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) + 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) + 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) + <> "|✕" + ; Else render normal chat view + div + {} $ :class-name (str-spaced css/expand css/column) div - {} $ :class-name css/row-parted + {} $ :class-name (str-spaced css/expand style-message-area) div - {} $ :class-name (str-spaced css/row-middle css/gap8) - if (:done? state) nil $ div - {} $ :style - {} (:display :flex) (:justify-content :center) (:align-items :center) - memof1-call-by :abort-streaming comp-abort "\"Streaming..." - if (:done? state) + {} + :class-name $ str-spaced css/row-parted + :style $ {} (:padding |8px) 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) "\"-" - :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! - fn (text search? think? d!) - do - when - and - > (count messages) 0 - :done? state - nil? current-session-id - d! $ :: :save-session state - d! cursor $ -> state - assoc :messages $ [] - assoc :answer nil - assoc :thinking nil - assoc :done? false - d! $ :: :session :session-id nil - submit-message! cursor - -> state - assoc :messages $ [] - assoc :answer nil - assoc :thinking nil - assoc :done? false - , text search? think? model d! - , model + 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!) + div + {} $ :class-name style-history-button + comp-i |clock + =< 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 + 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) + [] idx $ let + role $ :role msg + content $ :content msg + thinking $ :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 style-thinking + memof1-call comp-md-block + -> thinking $ either | + {} $ :class-name style-md-content + if (= role :assistant) + if (json-pattern? content) + pre $ {} (:class-name style-code-content) (:inner-text content) + memof1-call comp-md-block + -> content $ either | + {} $ :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 $ either content | + , nil + comp-copy $ either 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-message! cursor state text (:search? message-box-state) (:think? message-box-state) model d! + <> |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 js/document + do + if (some? focused) (.!blur focused) + d! + :cursor $ >> states :message-box + assoc message-box-state :focus-mode? true + , nil + if (:loading? state) + div ({}) (memof1-call-by :abort-loading 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) + memof1-call-by :abort-streaming 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! + fn (text search? think? d!) + do + when + and + > (count messages) 0 + :done? state + nil? current-session-id + d! $ :: :save-session state + d! cursor $ -> state + assoc :messages $ [] + assoc :answer nil + assoc :thinking nil + assoc :done? false + d! $ :: :session :session-id nil + submit-message! cursor + -> state + assoc :messages $ [] + assoc :answer nil + assoc :thinking nil + assoc :done? false + , text search? think? model d! + , model model-plugin.render reply-plugin.render sessions-plugin.render if dev? $ comp-reel (>> states :reel) reel ({}) - if dev? $ comp-inspect "\"Store" store nil + if dev? $ comp-inspect |Store store nil :examples $ [] - |comp-fill $ %{} :CodeEntry (:doc |) (:schema nil) + |comp-fill $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defcomp comp-fill (text) div - {} (:class-name style-fill) + {} (: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) comp-i :send 12 :currentColor :examples $ [] - |comp-message-box $ %{} :CodeEntry (:doc |) (:schema nil) + |comp-message-box $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defcomp comp-message-box (states picker-el on-submit model) let cursor $ :cursor states state $ either (:data states) - {} (:content "\"") (:search? false) (:think? false) (:focus-mode? false) + {} (:content |) (:search? false) (:think? false) (:focus-mode? false) [] (effect-focus) (on-fill cursor state on-submit) div {} $ :class-name (str-spaced css/center style-message-box-panel) @@ -602,24 +734,27 @@ {} $ :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 () $ -> (js/document.querySelector "\"#message") (.!focus) + fn () $ -> (js/document.querySelector |#message) (.!focus) , 0 <> $ if blank? $ :content state - , "\"Click to expand and type..." (:content state) + , "|Click to expand and type..." (:content state) textarea $ {} :value $ :content state - :placeholder "\"Prompt to try LLM..." - :id "\"message" + :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 (:value e) + d! cursor $ assoc state :content + str $ :value e :on-keydown $ fn (e d!) if and @@ -633,19 +768,19 @@ class-list $ .-classList target box-class $ .-classList box if - not $ .!contains class-list "\"focus-within" - .!add class-list "\"focus-within" + not $ .!contains class-list |focus-within + .!add class-list |focus-within if - not $ .!contains box-class "\"focus-within" - .!add box-class "\"focus-within" + not $ .!contains box-class |focus-within + .!add box-class |focus-within :on-blur $ fn (e d!) let target $ .-target (:event e) box $ .-parentElement (.-parentElement target) class-list $ .-classList target box-class $ .-classList box - if (.!contains class-list "\"focus-within") (.!remove class-list "\"focus-within") - if (.!contains box-class "\"focus-within") (.!remove box-class "\"focus-within") + if (.!contains class-list |focus-within) (.!remove class-list |focus-within) + if (.!contains box-class |focus-within) (.!remove box-class |focus-within) if not $ :focus-mode? state do (=< nil 4) @@ -653,47 +788,54 @@ {} $ :class-name css/row-parted if not $ blank? (:content state) - comp-close $ {} (:class-name style-clear) + 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 "\"" - -> (js/document.querySelector "\"#message") (.!focus) + d! cursor $ assoc state :content | + -> (js/document.querySelector |#message) (.!focus) span $ {} (:class-name style-clear) div {} $ :class-name (str-spaced css/row style-gap12) , picker-el if - contains? (#{} :gemini-flash :gemini-3.1-flash-lite-preview) model + 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 input $ {} :checked $ :think? state - :type "\"checkbox" - <> "\"Think" css/font-fancy + :type |checkbox + :role |checkbox + :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!) d! cursor $ assoc state :search? not $ :search? state input $ {} :checked $ :search? state - :type "\"checkbox" - <> "\"Search" css/font-fancy - button $ {} + :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" + :inner-text |Submit :on-click $ fn (e d!) on-submit (:content state) (:search? state) (:think? state) d! , nil :examples $ [] - |comp-sessions-modal $ %{} :CodeEntry (:doc |) (:schema nil) + |comp-sessions-modal $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote - defcomp comp-sessions-modal (sessions on-select on-close) + defcomp comp-sessions-modal (sessions archived-count on-select on-close on-view-archive) let history-items $ foldl sessions 0 fn (acc session) @@ -701,6 +843,18 @@ 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!) + <> "|View Archive" if (empty? sessions) div {} $ :style @@ -719,7 +873,8 @@ [] 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!) div @@ -732,7 +887,8 @@ {} (: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) + {} (:class-name style-delete-button) (:role |button) + :aria-label $ str |session-delete: session-id :on-click $ fn (e d!) (-> e :event .!stopPropagation) d! $ :: :remove-session session-id <> "|✕" @@ -746,25 +902,31 @@ {} $ :class-name (str-spaced css/row-parted) div {} $ :class-name (str-spaced css/row css/gap8) - a $ {} (:class-name style-clear) (:inner-text |Data) + a $ {} (:class-name style-clear) (:inner-text |Data) (:role |button) (:aria-label |sessions-export-data) :on-click $ fn (e d!) (tab-echo! sessions :edn) - a $ {} (:class-name style-clear) (:inner-text |Download) + a $ {} (:class-name style-clear) (:inner-text |Download) (:role |button) (:aria-label |sessions-download) :on-click $ fn (e d!) (download-sessions! sessions) if > (count sessions) 0 - a $ {} (:class-name style-clear) (:inner-text "|Clear all") + 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? $ if (> history-items 10) - js/confirm $ str-spaced |Clear history-items "|messages from history?" - , true - when proceed? $ d! (:: :clear-sessions) + 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) span $ {} div $ {} :style $ {} (:height 200) , nil :examples $ [] - |create-session $ %{} :CodeEntry (:doc |) (:schema nil) + |create-session $ %{} :CodeEntry (:doc |) :code $ quote defn create-session (messages model) let @@ -783,7 +945,8 @@ .!slice first-msg 0 end :is-history? false :examples $ [] - |download-sessions! $ %{} :CodeEntry (:doc |) (:schema nil) + :schema $ :: :fn $ {} (:return (:: :map :tag :dynamic)) (:args $ [] (:: :list (:: :map :tag :dynamic)) :tag) + |download-sessions! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn download-sessions! (sessions) let @@ -798,14 +961,14 @@ fn () $ js/URL.revokeObjectURL url , 0 :examples $ [] - |effect-focus $ %{} :CodeEntry (:doc |) (:schema nil) + |effect-focus $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defeffect effect-focus () (action el at?) when (= action :mount) js/setTimeout $ fn () - .!select $ .!querySelector el "\"textarea" + .!select $ .!querySelector el |textarea :examples $ [] - |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.") (:schema nil) + |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 @@ -814,71 +977,78 @@ not $ blank? line if > (.-length lines) 1 - js/console.warn "\"Droping some unexpected lines:" $ .!slice lines 1 + js/console.warn "|Droping some unexpected lines:" $ .!slice lines 1 .-0 lines :examples $ [] - |generate-session-id $ %{} :CodeEntry (:doc |) (:schema nil) + :schema $ :: :fn $ {} (:return :string) (:args $ [] :string) + > (.-length lines) 1 + js/console.warn "|Droping some unexpected lines:" $ .!slice lines 1 + .-0 lines + :examples $ [] + |generate-session-id $ %{} :CodeEntry (:doc |) :code $ quote defn generate-session-id () $ str (js/Date.now) :examples $ [] - |get-anthropic-key! $ %{} :CodeEntry (:doc |) (:schema nil) + :schema $ :: :fn $ {} (:return :string) (:args $ []) + |get-anthropic-key! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn get-anthropic-key! () $ let - key $ js/localStorage.getItem "\"claude-key" + key $ js/localStorage.getItem |claude-key if (blank? key) let - v $ js/prompt "\"Required claude-key in localStorage" + v $ js/prompt "|Required claude-key in localStorage" if (blank? v) - raise $ new js/Error "\"key is empty" - js/localStorage.setItem "\"claude-key" v + raise $ new js/Error "|key is empty" + js/localStorage.setItem |claude-key v , v , key :examples $ [] - |get-deepinfra-key! $ %{} :CodeEntry (:doc |) (:schema nil) + |get-deepinfra-key! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn get-deepinfra-key! () $ let - key $ js/localStorage.getItem "\"deepinfra-key" + key $ js/localStorage.getItem |deepinfra-key if (blank? key) let - v $ js/prompt "\"Required deepinfra-key in localStorage" + v $ js/prompt "|Required deepinfra-key in localStorage" if (blank? v) - raise $ new js/Error "\"key is empty" - js/localStorage.setItem "\"deepinfra-key" v + raise $ new js/Error "|key is empty" + js/localStorage.setItem |deepinfra-key v , v , key :examples $ [] - |get-gemini-key! $ %{} :CodeEntry (:doc |) (:schema nil) + |get-gemini-key! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn get-gemini-key! () $ let - key $ js/localStorage.getItem "\"gemini-key" + key $ js/localStorage.getItem |gemini-key if (blank? key) let - v $ js/prompt "\"Required gemini-key in localStorage" + v $ js/prompt "|Required gemini-key in localStorage" if (blank? v) - raise $ new js/Error "\"key is empty" - js/localStorage.setItem "\"gemini-key" v + raise $ new js/Error "|key is empty" + js/localStorage.setItem |gemini-key v , v , key :examples $ [] - |get-openrouter-key! $ %{} :CodeEntry (:doc |) (:schema nil) + |get-openrouter-key! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn get-openrouter-key! () $ let - key $ js/localStorage.getItem "\"openrouter-key" + key $ js/localStorage.getItem |openrouter-key if (blank? key) let - v $ js/prompt "\"Required openrouter-key in localStorage" + v $ js/prompt "|Required openrouter-key in localStorage" if (blank? v) - raise $ new js/Error "\"key is empty" - js/localStorage.setItem "\"openrouter-key" v + raise $ new js/Error "|key is empty" + js/localStorage.setItem |openrouter-key v , v , key :examples $ [] - |json-pattern? $ %{} :CodeEntry (:doc |) (:schema nil) + |json-pattern? $ %{} :CodeEntry (:doc |) :code $ quote defn json-pattern? (text) - or (.!startsWith text "\"{") (.!startsWith text "\"[") + or (.!startsWith text |{) (.!startsWith text |[) :examples $ [] - |messages->anthropic $ %{} :CodeEntry (:doc |) (:schema nil) + :schema $ :: :fn $ {} (:return :bool) (:args $ [] :string) + |messages->anthropic $ %{} :CodeEntry (:doc |) :code $ quote defn messages->anthropic (messages) to-js-data $ map (or messages []) @@ -889,7 +1059,9 @@ , |assistant |user :content $ :content m :examples $ [] - |messages->gemini $ %{} :CodeEntry (:doc |) (:schema nil) + :schema $ :: :fn $ {} (:return :dynamic) + :args $ [] (:: :optional (:: :list (:: :map :tag :dynamic))) + |messages->gemini $ %{} :CodeEntry (:doc |) :code $ quote defn messages->gemini (messages) let @@ -903,7 +1075,9 @@ :parts $ [] {} $ :text (:content m) :examples $ [] - |messages->openai $ %{} :CodeEntry (:doc |) (:schema nil) + :schema $ :: :fn $ {} (:return :dynamic) + :args $ [] (:: :optional (:: :list (:: :map :tag :dynamic))) + |messages->openai $ %{} :CodeEntry (:doc |) :code $ quote defn messages->openai (messages) let @@ -916,11 +1090,13 @@ , |assistant |user :content $ :content m :examples $ [] - |models-menu $ %{} :CodeEntry (:doc |) (:schema nil) + :schema $ :: :fn $ {} (:return :dynamic) + :args $ [] (:: :optional (:: :list (:: :map :tag :dynamic))) + |models-menu $ %{} :CodeEntry (:doc |) (:schema :list) :code $ quote - def models-menu $ [] (:: :item :gemini-flash "|Gemini Flash 3") (:: :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 :claude-4.5 "\"Claude 4.5") + def models-menu $ [] (:: :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 :claude-4.5 "|Claude 4.5") :examples $ [] - |on-fill $ %{} :CodeEntry (:doc |) (:schema nil) + |on-fill $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn on-fill (cursor state on-submit) %{} respo.schema/RespoListener (:name :on-fill) @@ -936,16 +1112,17 @@ on-submit (:text info) (:search? state) (:think? state) dispatch! , nil :examples $ [] - |pattern-spaced-code $ %{} :CodeEntry (:doc |) (:schema nil) + |pattern-spaced-code $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote - def pattern-spaced-code $ noted "\"temp fix of nested code block" (&raw-code "\"/\\n\\s+```/g") + def pattern-spaced-code $ noted "|temp fix of nested code block" (&raw-code "|/\\n\\s+```/g") :examples $ [] - |pick-model $ %{} :CodeEntry (:doc |) (:schema nil) + |pick-model $ %{} :CodeEntry (:doc |) :code $ quote defn pick-model (variant) - case-default variant "\"gemini-3-flash-preview" (:gemini-3.1-flash-lite-preview "\"gemini-3.1-flash-lite-preview") (:gemini-pro "\"gemini-3.1-pro-preview") (:gemma "\"gemma-3-27b-it") + case-default variant |gemini-3-flash-preview (:gemini-3.5-flash |gemini-3.5-flash) (:gemini-3.1-flash-lite-preview |gemini-3.1-flash-lite-preview) (:gemini-pro |gemini-3.1-pro-preview) (:gemma |gemma-3-27b-it) :examples $ [] - |save-current-session $ %{} :CodeEntry (:doc |) (:schema nil) + :schema $ :: :fn $ {} (:return :string) (:args $ [] :tag) + |save-current-session $ %{} :CodeEntry (:doc |) :code $ quote defn save-current-session (store state) let @@ -960,44 +1137,74 @@ assoc store :sessions $ append sessions updated-session , store :examples $ [] - |style-a-toggler $ %{} :CodeEntry (:doc |) (:schema nil) + :schema $ :: :fn $ {} (:return (:: :map :tag :dynamic)) (:args $ [] (:: :map :tag :dynamic) (:: :map :tag :dynamic)) + |style-a-toggler $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-a-toggler $ {} - "\"&" $ {} (:cursor :pointer) (:background-color :white) (:color :black) - "\".focus-within &" $ {} (:color :black) + |& $ {} (:cursor :pointer) (:background-color :white) (:color :black) + "|.focus-within &" $ {} (:color :black) :examples $ [] - |style-abort-close $ %{} :CodeEntry (:doc |) (:schema nil) + |style-abort-close $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-abort-close $ {} - "\"&" $ {} (:vertical-align :middle) (:font-size 10) + |& $ {} (:vertical-align :middle) (:font-size 10) :examples $ [] - |style-app-global $ %{} :CodeEntry (:doc |) (:schema nil) + |style-app-global $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-app-global $ {} - str "\"& ." style-code-block - {} $ :max-width "\"90vw" - "\"&" $ {} (:color "\"#999") (:transition-duration "\"300ms") + str "|& ." style-code-block + {} $ :max-width |90vw + |& $ {} (:color |#999) (:transition-duration |300ms) :background-color $ hsl 0 0 98 :touch-action :none - "\"&:hover" $ {} (:color "\"#777") + |&:hover $ {} (:color |#777) :background-color $ hsl 0 0 100 :examples $ [] - |style-checkbox $ %{} :CodeEntry (:doc |) (:schema nil) + |style-archive-close $ %{} :CodeEntry (:doc |) (:schema :dynamic) + :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 + :examples $ [] + |style-archive-header $ %{} :CodeEntry (:doc |) (:schema :dynamic) + :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 $ [] + |style-archive-row $ %{} :CodeEntry (:doc |) (:schema :dynamic) + :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 $ [] + |style-checkbox $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-checkbox $ {} - "\"&" $ {} (:cursor :pointer) (:user-select :none) (:font-size 12) (:line-height "\"28px") (:vertical-align :middle) + |& $ {} (:cursor :pointer) (:user-select :none) (:font-size 12) (:line-height |28px) (:vertical-align :middle) :examples $ [] - |style-clear $ %{} :CodeEntry (:doc |) (:schema nil) + |style-clear $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-clear $ {} - "\"&" $ {} (:opacity 0.4) (:padding "\"4px 8px") (:display :inline-block) (:height "\"24px") + |& $ {} (:opacity 0.4) (:padding "|4px 8px") (:display :inline-block) (:height |24px) :examples $ [] - |style-code-content $ %{} :CodeEntry (:doc |) (:schema nil) + |style-code-content $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-code-content $ {} - "\"&" $ {} (:line-height "\"1.5") (:font-size 13) + |& $ {} (:line-height |1.5) (:font-size 13) :examples $ [] - |style-delete-button $ %{} :CodeEntry (:doc |) (:schema nil) + |style-delete-button $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-delete-button $ {} |& $ {} (:padding "|4px 8px") (:font-size |18px) (:font-weight |50) @@ -1011,36 +1218,36 @@ |&:active $ {} (:opacity 1) :color $ hsl 0 90 40 :examples $ [] - |style-fill $ %{} :CodeEntry (:doc |) (:schema nil) + |style-fill $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-fill $ {} - "\"&" $ {} (:cursor :pointer) (:user-select :none) (:display :inline-flex) (:align-items :center) (:justify-content :center) (:transition-duration "\"200ms") + |& $ {} (: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" $ {} + :margin "|0 4px 0 8px" + |&:hover $ {} :color $ hsl 0 0 40 - :transform "\"scale(1.06)" + :transform "|scale(1.06)" :examples $ [] - |style-focus-box $ %{} :CodeEntry (:doc |) (:schema nil) + |style-focus-box $ %{} :CodeEntry (:doc |) (:schema :dynamic) :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) + |& $ {} (: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 $ [] - |style-focus-link $ %{} :CodeEntry (:doc |) (:schema nil) + |style-focus-link $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-focus-link $ {} - "\"&" $ {} (:cursor :pointer) (:font-size 13) + |& $ {} (:cursor :pointer) (:font-size 13) :color $ hsl 200 80 40 :text-decoration :none - :padding "\"4px 0" - "\"&:hover" $ {} (:text-decoration :underline) + :padding "|4px 0" + |&:hover $ {} (:text-decoration :underline) :examples $ [] - |style-gap12 $ %{} :CodeEntry (:doc |) (:schema nil) + |style-gap12 $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-gap12 $ {} - "\"&" $ {} (:gap 12) + |& $ {} (:gap 12) :examples $ [] - |style-history-button $ %{} :CodeEntry (:doc |) (:schema nil) + |style-history-button $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-history-button $ {} |& $ {} (:font-size |20px) @@ -1053,7 +1260,7 @@ |&:hover $ {} :color $ hsl 200 80 50 :examples $ [] - |style-history-count $ %{} :CodeEntry (:doc |) (:schema nil) + |style-history-count $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-history-count $ {} |& $ {} @@ -1061,119 +1268,119 @@ :font-size |12px :display :inline-block :examples $ [] - |style-image $ %{} :CodeEntry (:doc |) (:schema nil) + |style-image $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-image $ {} - "\"&" $ {} (:max-width "\"100%") (:align-self :flex-start) (:border-radius "\"6px") - :border $ str "\"1px solid " (hsl 0 0 90) + |& $ {} (:max-width |100%) (:align-self :flex-start) (:border-radius |6px) + :border $ str "|1px solid " (hsl 0 0 90) :examples $ [] - |style-md-content $ %{} :CodeEntry (:doc |) (:schema nil) + |style-md-content $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-md-content $ {} - "\"& .md-p" $ {} (:margin "\"16px 0") (:line-height "\"1.6") + "|& .md-p" $ {} (:margin "|16px 0") (:line-height |1.6) :examples $ [] - |style-message-actions $ %{} :CodeEntry (:doc |) (:schema nil) + |style-message-actions $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-message-actions $ {} - "\"&" $ {} (:margin-top 6) (:justify-content :flex-end) (:width "\"100%") + |& $ {} (:margin-top 6) (:justify-content :flex-end) (:width |100%) :examples $ [] - |style-message-area $ %{} :CodeEntry (:doc |) (:schema nil) + |style-message-area $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-message-area $ {} - "\"&" $ {} (:flex 2) (:overflow :scroll) + |& $ {} (:flex 2) (:overflow :scroll) :examples $ [] - |style-message-assistant $ %{} :CodeEntry (:doc |) (:schema nil) + |style-message-assistant $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-message-assistant $ {} - "\"&" $ {} (:align-self :flex-start) + |& $ {} (:align-self :flex-start) :examples $ [] - |style-message-box $ %{} :CodeEntry (:doc |) (:schema nil) + |style-message-box $ %{} :CodeEntry (:doc |) (:schema :dynamic) :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)") + |& $ {} (: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 $ [] - |style-message-box-panel $ %{} :CodeEntry (:doc |) (:schema nil) + |style-message-box-panel $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-message-box-panel $ {} - "\"&" $ {} (:position :absolute) (:bottom 0) (:opacity 1) (:width "\"100%") + |& $ {} (: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) - "\"&.focus-within" $ {} + :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 $ [] - |style-message-item $ %{} :CodeEntry (:doc |) (:schema nil) + |style-message-item $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-message-item $ {} - "\"&" $ {} (:line-height "\"1.6") + |& $ {} (:line-height |1.6) :examples $ [] - |style-message-list $ %{} :CodeEntry (:doc |) (:schema nil) + |style-message-list $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-message-list $ {} - "\"&" $ {} (:flex 2) (:padding "\"40px 16px 20vh 16px") (:width "\"100%") (:max-width 1200) (:margin :auto) (:position :relative) + |& $ {} (:flex 2) (:padding "|40px 16px 20vh 16px") (:width |100%) (:max-width 1200) (:margin :auto) (:position :relative) :examples $ [] - |style-message-role $ %{} :CodeEntry (:doc |) (:schema nil) + |style-message-role $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-message-role $ {} - "\"&" $ {} (:font-size 12) + |& $ {} (:font-size 12) :color $ hsl 0 0 50 :margin-bottom 6 - :padding-right "\"16px" + :padding-right |16px :examples $ [] - |style-message-text $ %{} :CodeEntry (:doc |) (:schema nil) + |style-message-text $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-message-text $ {} - "\"&" $ {} (:white-space :pre-wrap) (:line-height "\"1.6") (:margin 0) (:padding-right "\"16px") + |& $ {} (:white-space :pre-wrap) (:line-height |1.6) (:margin 0) (:padding-right |16px) :examples $ [] - |style-message-user $ %{} :CodeEntry (:doc |) (:schema nil) + |style-message-user $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-message-user $ {} - "\"&" $ {} (:align-self :flex-end) + |& $ {} (:align-self :flex-end) :background-color $ hsl 0 0 96 - :padding "\"12px 0 12px 16px" + :padding "|12px 0 12px 16px" :border-radius 10 - :max-height "\"240px" + :max-height |240px :max-width |100% :overflow-y :auto - "\"&::-webkit-scrollbar" $ {} (:width "\"4px") - "\"&::-webkit-scrollbar-thumb" $ {} + |&::-webkit-scrollbar $ {} (:width |4px) + |&::-webkit-scrollbar-thumb $ {} :background-color $ hsl 0 0 80 - :border-radius "\"2px" - "\"&::-webkit-scrollbar-track" $ {} (:background-color :transparent) + :border-radius |2px + |&::-webkit-scrollbar-track $ {} (:background-color :transparent) :examples $ [] - |style-more $ %{} :CodeEntry (:doc |) (:schema nil) + |style-more $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-more $ {} - "\"&" $ {} (:text-align :center) (:min-width 80) + |& $ {} (:text-align :center) (:min-width 80) :background-color $ hsl 0 0 94 :border-radius 16 - :padding "\"4px 12px" - :margin "\"8px 0" + :padding "|4px 12px" + :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 $ [] - |style-reply-actions $ %{} :CodeEntry (:doc |) (:schema nil) + |style-reply-actions $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-reply-actions $ {} - "\"&" $ {} (:margin-top 6) (:justify-content :flex-start) (:width "\"100%") + |& $ {} (:margin-top 6) (:justify-content :flex-start) (:width |100%) :examples $ [] - |style-reply-button $ %{} :CodeEntry (:doc |) (:schema nil) + |style-reply-button $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-reply-button $ {} - "\"&" $ {} (:text-align :center) (:min-width 80) + |& $ {} (:text-align :center) (:min-width 80) :background-color $ hsl 0 0 100 :border-radius 16 - :padding "\"4px 12px" - :margin "\"8px 0" + :padding "|4px 12px" + :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 $ [] - |style-session-item $ %{} :CodeEntry (:doc |) (:schema nil) + |style-session-item $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-session-item $ {} |& $ {} (:padding |12px) @@ -1185,50 +1392,50 @@ |:hover $ {} :background-color $ hsl 0 0 96 :examples $ [] - |style-sessions-list $ %{} :CodeEntry (:doc |) (:schema nil) + |style-sessions-list $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-sessions-list $ {} |& $ {} (:flex |1) (:overflow-y :auto) (:min-width |300px) :examples $ [] - |style-submit $ %{} :CodeEntry (:doc |) (:schema nil) + |style-submit $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-submit $ {} - "\"&" $ {} + |& $ {} :examples $ [] - |style-textbox $ %{} :CodeEntry (:doc |) (:schema nil) + |style-textbox $ %{} :CodeEntry (:doc |) (:schema :dynamic) :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) + |& $ {} (: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 $ [] - |style-textbox-compact $ %{} :CodeEntry (:doc |) (:schema nil) + |style-textbox-compact $ %{} :CodeEntry (:doc |) (:schema :dynamic) :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") + |& $ {} (:height 40) (:min-height 40) (:max-height 40) (:overflow :hidden) + |&.focus-within $ {} (:height "|max(240px,32vh)") (:min-height |unset) (:max-height |unset) :examples $ [] - |style-thinking $ %{} :CodeEntry (:doc |) (:schema nil) + |style-thinking $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defstyle style-thinking $ {} - "\"&" $ {} (:max-height 200) (:overflow :auto) (:padding "\"12px 16px") + |& $ {} (:max-height 200) (:overflow :auto) (:padding "|12px 16px") :background-color $ hsl 0 0 96 :font-size 12 - :line-height "\"1.8" + :line-height |1.8 :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 $ [] - |submit-message! $ %{} :CodeEntry (:doc |) (:schema nil) + |submit-message! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn submit-message! (cursor state prompt-text search? think? model d!) hint-fn $ {} (:async true) let state1 $ assoc state :messages append-user-message (:messages state) prompt-text - *text $ atom "\"" - *thinking-text $ atom "\"" + *text $ atom | + *thinking-text $ atom | model $ :model state d! cursor state1 try @@ -1243,21 +1450,21 @@ :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-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) + :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-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) fn (e) let - err-text $ str "\"Failed to load: " e + err-text $ str "|Failed to load: " e d! cursor $ -> state (assoc :answer err-text) (assoc :loading? false) (assoc :done? true) assoc :messages $ upsert-assistant-message (:messages state) err-text nil :examples $ [] - |upsert-assistant-message $ %{} :CodeEntry (:doc |) (:schema nil) + |upsert-assistant-message $ %{} :CodeEntry (:doc |) :code $ quote defn upsert-assistant-message (messages content thinking) let @@ -1271,6 +1478,8 @@ -> last-msg (assoc :content content) (assoc :thinking thinking) conj messages0 $ {} (:role :assistant) (:content content) (:thinking thinking) :examples $ [] + :schema $ :: :fn $ {} (:return (:: :list (:: :map :tag :dynamic))) + :args $ [] (:: :optional (:: :list (:: :map :tag :dynamic))) (:: :optional :string) (:: :optional :string) :ns $ %{} :NsEntry (:doc |) :code $ quote ns app.comp.container $ :require (respo-ui.css :as css) @@ -1280,39 +1489,40 @@ respo.comp.space :refer $ =< respo.comp.inspect :refer $ comp-inspect reel.comp.reel :refer $ comp-reel - app.config :refer $ dev? chrome-extension? - "\"axios" :default axios + app.config :refer $ dev? chrome-extension? site + |axios :default axios respo-md.comp.md :refer $ comp-md-block style-code-block - respo-ui.comp :refer $ comp-copy comp-close - "\"../extension/get-selected" :refer $ get-selected + respo-ui.comp :refer $ comp-copy style-close + |../extension/get-selected :refer $ get-selected + |../lib/db :refer $ db-get db-set memof.once :refer $ memof1-call memof1-call-by - "\"../lib/image" :refer $ base64ToBlob + |../lib/image :refer $ base64ToBlob feather.core :refer $ comp-i respo-alerts.core :refer $ [] use-modal-menu use-prompt use-drawer respo-ui.util :refer $ tab-echo! |app.config $ %{} :FileEntry :defs $ {} - |chrome-extension? $ %{} :CodeEntry (:doc |) (:schema nil) + |chrome-extension? $ %{} :CodeEntry (:doc |) (:schema :bool) :code $ quote def chrome-extension? $ and (some? js/window.chrome) (some? js/window.chrome.runtime) (some? js/window.chrome.runtime.id) :examples $ [] - |dev? $ %{} :CodeEntry (:doc |) (:schema nil) + |dev? $ %{} :CodeEntry (:doc |) (:schema :bool) :code $ quote - def dev? $ = "\"dev" (get-env "\"mode" "\"release") + def dev? $ = |dev (get-env |mode |release) :examples $ [] - |site $ %{} :CodeEntry (:doc |) (:schema nil) + |site $ %{} :CodeEntry (:doc |) (:schema :map) :code $ quote - def site $ {} (:storage-key "\"msg-buffer") + def site $ {} (:storage-key |msg-buffer) (:archive-key |msg-buffer-archive) :examples $ [] :ns $ %{} :NsEntry (:doc |) :code $ quote (ns app.config) |app.main $ %{} :FileEntry :defs $ {} - |*reel $ %{} :CodeEntry (:doc |) (:schema nil) + |*reel $ %{} :CodeEntry (:doc |) (:schema :ref) :code $ quote defatom *reel $ -> reel-schema/reel (assoc :base schema/store) (assoc :store schema/store) :examples $ [] - |connect-to-worker! $ %{} :CodeEntry (:doc |) (:schema nil) + |connect-to-worker! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn connect-to-worker! () $ if and (some? js/window.chrome) (some? js/window.chrome.runtime) (some? js/window.chrome.runtime.connect) @@ -1325,15 +1535,15 @@ do (println "|Worker disconnected, retrying in 500ms...") (js/setTimeout connect-to-worker! 500) , nil :examples $ [] - |dispatch! $ %{} :CodeEntry (:doc |) (:schema nil) + |dispatch! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn dispatch! (op) when and config/dev? $ not= op :states - js/console.log "\"Dispatch:" op + js/console.log |Dispatch: op reset! *reel $ reel-updater updater @*reel op :examples $ [] - |hydrate-storage-later! $ %{} :CodeEntry (:doc |) (:schema nil) + |hydrate-storage-later! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn hydrate-storage-later! () $ js/setTimeout fn () $ let @@ -1342,23 +1552,23 @@ let t_start $ .!now js/Date dispatch! $ :: :hydrate-storage (parse-cirru-edn raw) - println "\"Hydrated in" + println "|Hydrated in" - (.!now js/Date) t_start - , "\"ms" + , |ms :examples $ [] - |listen-extension! $ %{} :CodeEntry (:doc |) (:schema nil) + |listen-extension! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn listen-extension! () js/chrome.runtime.onMessage.addListener $ fn (message sender respond!) when - = "\"menu-summary" $ .-action message + = |menu-summary $ .-action message let - content $ str "\"你扮演一个专业的工程师, 对以下内容做一下讲解, 用中文, 注意要简略, 内容注意分块.\n\n" &newline &newline (.-content message) + content $ str "|你扮演一个专业的工程师, 对以下内容做一下讲解, 用中文, 注意要简略, 内容注意分块.\n\n" &newline &newline (.-content message) event-tuple $ :: :fill-text {} (:text content) (:submit? true) send-to-component! event-tuple when - = "\"fill-text" $ .-action message + = |fill-text $ .-action message let content $ .-text message submit? $ either (.-submit? message) true @@ -1366,14 +1576,14 @@ {} (:text content) (:submit? submit?) send-to-component! event-tuple when - = "\"menu-translate" $ .-action message + = |menu-translate $ .-action message let - content $ str "\"请将以下内容翻译成中文, 保持简洁分段:\n\n" &newline &newline (.-content message) + content $ str "|请将以下内容翻译成中文, 保持简洁分段:\n\n" &newline &newline (.-content message) event-tuple $ :: :fill-text {} (:text content) (:submit? true) send-to-component! event-tuple when - = "\"menu-custom" $ .-action message + = |menu-custom $ .-action message let content $ .-content message event-tuple $ :: :fill-text @@ -1381,19 +1591,21 @@ send-to-component! event-tuple connect-to-worker! :examples $ [] - |main! $ %{} :CodeEntry (:doc |) (:schema nil) + |main! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn main! () $ let t0 $ .!now js/Date - println "\"Starting main! at" t0 - println "\"Running mode:" $ if config/dev? "\"dev" "\"release" + println "|Starting main! at" t0 + println "|Running mode:" $ if config/dev? |dev |release if config/dev? $ load-console-formatter! render-app! 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!) listen-devtools! |k dispatch! js/window.addEventListener |beforeunload $ fn (event) (persist-storage!) js/window.addEventListener |visibilitychange $ fn (event) - if (= "\"hidden" js/document.visibilityState) (persist-storage!) + if (= |hidden js/document.visibilityState) (persist-storage!) js/window.addEventListener |dblclick $ fn (event) (.!preventDefault event) js/window.addEventListener |wheel fn (event) @@ -1403,57 +1615,59 @@ if config/chrome-extension? $ listen-extension! let t1 $ .!now js/Date - println "|App started at" t1 |cost (- t1 t0) "\"ms" + println "|App started at" t1 |cost (- t1 t0) |ms :examples $ [] - |mount-target $ %{} :CodeEntry (:doc |) (:schema nil) + |mount-target $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote def mount-target $ js/document.querySelector |.app :examples $ [] - |persist-storage! $ %{} :CodeEntry (:doc |) (:schema nil) + |persist-storage! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn persist-storage! () - println "\"Saved at" $ .!toISOString (new js/Date) + println "|Saved at" $ .!toISOString (new js/Date) js/localStorage.setItem (:storage-key config/site) format-cirru-edn $ :store @*reel :examples $ [] - |reload! $ %{} :CodeEntry (:doc |) (:schema nil) + |reload! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn reload! () $ if (nil? build-errors) - do (remove-watch *reel :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!) reset! *reel $ refresh-reel @*reel schema/store updater - hud! "\"ok~" "\"Ok" - hud! "\"error" build-errors + hud! |ok~ |Ok + hud! |error build-errors :examples $ [] - |render-app! $ %{} :CodeEntry (:doc |) (:schema nil) + |render-app! $ %{} :CodeEntry (:doc |) (:schema :dynamic) :code $ quote defn render-app! () let t_start $ .!now js/Date - println "\"Rendering app..." + println "|Rendering app..." render! mount-target (comp-container @*reel) dispatch! - println "\"Rendered in" + println "|Rendered in" - (.!now js/Date) t_start - , "\"ms" + , |ms render! mount-target (comp-container @*reel) dispatch! :examples $ [] :ns $ %{} :NsEntry (:doc |) :code $ quote ns app.main $ :require respo.core :refer $ render! clear-cache! - app.comp.container :refer $ comp-container submit-message! + app.comp.container :refer $ comp-container submit-message! *archived-sessions *viewing-archive-session app.updater :refer $ updater app.schema :as schema reel.util :refer $ listen-devtools! reel.core :refer $ reel-updater refresh-reel reel.schema :as reel-schema app.config :as config - "\"./calcit.build-errors" :default build-errors - "\"bottom-tip" :default hud! + |./calcit.build-errors :default build-errors + |bottom-tip :default hud! respo.controller.client :refer $ send-to-component! |app.schema $ %{} :FileEntry :defs $ {} - |store $ %{} :CodeEntry (:doc |) (:schema nil) + |store $ %{} :CodeEntry (:doc |) (:schema :map) :code $ quote def store $ {} :states $ {} @@ -1461,17 +1675,17 @@ :sessions $ [] :current-session-id nil :model nil + :archived-count 0 :examples $ [] :ns $ %{} :NsEntry (:doc |) :code $ quote (ns app.schema) |app.updater $ %{} :FileEntry :defs $ {} - |updater $ %{} :CodeEntry (:doc |) (:schema nil) + |updater $ %{} :CodeEntry (:doc |) :code $ quote defn updater (store op op-id op-time) tag-match op - :states cursor s - update-states store cursor s + (:states cursor s) (update-states store cursor s) (:states-merge cursor s changes) let store1 $ update-states-merge store cursor s changes @@ -1492,12 +1706,20 @@ or (:sessions store) ([]) fn (s) 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 + _ $ do (eprintln "|unknown op:" op) store :examples $ [] + :schema $ :: :fn $ {} (:return :map) (:args $ [] :map :list :string :number) :ns $ %{} :NsEntry (:doc |) :code $ quote ns app.updater $ :require diff --git a/deps.cirru b/deps.cirru index 2d3c7aa..8282c56 100644 --- a/deps.cirru +++ b/deps.cirru @@ -1,10 +1,10 @@ -{} (:calcit-version |0.12.35) +{} (:calcit-version |0.12.45) :dependencies $ {} (|Memkits/genai.calcit |0.0.3) - |Respo/alerts.calcit |0.10.10 - |Respo/reel.calcit |main + |Respo/alerts.calcit |0.10.12 + |Respo/reel.calcit |0.6.4 |Respo/respo-feather.calcit |main |Respo/respo-markdown.calcit |0.4.13 |Respo/respo-ui.calcit |0.6.4 - |Respo/respo.calcit |0.16.45 - |calcit-lang/memof |0.0.23 + |Respo/respo.calcit |0.16.47 + |calcit-lang/memof |0.0.24 diff --git a/history/202502121500-schema-improvements.md b/history/202502121500-schema-improvements.md new file mode 100644 index 0000000..733e1da --- /dev/null +++ b/history/202502121500-schema-improvements.md @@ -0,0 +1,29 @@ +# 修改记录: Schema 标注与样式节点 Bug 修复 (2025-02-12) + +## 知识点与变动树 + +1. **Respo `<>` (文本节点) 语法约束**: + - `respo.core/<>` 后面仅支持接受字符串/叶子节点,不能把有子节点的标签(如 `div`、`span`)写在 `<>` 的子级中。 + - 修复了 `app.comp.container/comp-container` else-block 中错误地把 `div` 包在 `<>` 里的问题,改用正常的 `div` 布局包围并进行 flex 调节。 + +2. **Calcit / CodeEntry schema 类型标注**: + - 对以下全局变量、常量、辅助函数补充了正确的 `schema` 类型约束: + - `*archived-sessions` => `:schema :ref` + - `*viewing-archive-session` => `:schema :ref` + - `*abort-control` => `:schema :ref` + - `chrome-extension?` => `:schema :bool` + - `dev?` => `:schema :bool` + - `site` => `:schema :map` + - `first-line` => `:schema $ :: :fn $ {} (:return :string) (:args $ [] :string)` + - `pick-model` => `:schema $ :: :fn $ {} (:return :string) (:args $ [] :dynamic)` + - `save-current-session` => `:schema $ :: :fn $ {} (:return :map) (:args $ [] :map :map)` + - `messages->openai` => `:schema $ :: :fn $ {} (:return :list) (:args $ [] :list)` + - `app.schema/store` => `:schema :map` + - `app.updater/updater` => `:schema $ :: :fn $ {} (:return :map) (:args $ [] :map :list :string :number)` + +3. **Calcit 基础类型标签限定**: + - 在静态类型检测中,Calcit 支持的原生 bool 标签为 `:bool`,使用 `:boolean` 触发 `unknown primitive schema tag` 报错,现已全部修正为 `:bool`。 + +4. **编译与构建验证**: + - 执行 `cr --check-only` 全量通过。 + - 执行 `yarn build` Vite 正确生成 production 混淆打包产物且未引入任何运行时警告项目。 diff --git a/history/20260612-schema-type-improvements.md b/history/20260612-schema-type-improvements.md new file mode 100644 index 0000000..a34462d --- /dev/null +++ b/history/20260612-schema-type-improvements.md @@ -0,0 +1,26 @@ +# 修改记录: Schema 弱类型深度优化与修复 (2026-06-12) + +## 知识点与变动树 + +1. **顶级/常量 Schema 约束**: + - 在 Calcit 编译器的 `:schema` 中,非函数、非宏的 CodeEntry(如 `def`、`defatom` 等常量和引用)不支持复杂的参数化 nested schema(如 `(:: :map ...)` 或 `(:: :list ...)`)。如果使用了复杂 nested schema,会导致类似 `failed to normalize CodeEntry.schema` 的编译错误。 + - 这类 CodeEntry 必须定义为简单/原始的顶级类型标签(如 `:ref`、`:map`、`:list`)。 + +2. **嵌套集合/列表的精确类型声明**: + - 在声明函数签名时,参数和返回值支持完整的 nested parameters。 + - 对于聊天应用中最常出现的 “消息列表” (List of Message Maps),我们可以将其类型从模糊的 `(:: :list :map)` 深度细化为 `(:: :list (:: :map :tag :dynamic))`,其中 map 的 keys 精确定义为关键字 `:tag`(如 `:role`、`:content` 等),从而有效消除了 `cr analyze weak-types` 检测中大量的 `map-key` 弱类型警告。 + - 相应优化的函数包括: + - `append-user-message` + - `create-session` + - `upsert-assistant-message` + - `messages->anthropic` + - `messages->gemini` + - `messages->openai` + +3. **类型纠正与完善**: + - 修正了 `json-pattern?` 的返回类型,将错误的 `:boolean` 统一更正为 Calcit 原生支持的 `:bool` 类型。 + - 精确指定了 `pick-model` 的参数模型 `variant` 类型为 `:tag`。 + +4. **编译与质量保障**: + - 运行 `cr --check-only` 全量完美通过。 + - 运行 `yarn build` Vite production build 编译打包一切正常。 diff --git a/history/202606131230-migrate-archive-to-indexeddb.md b/history/202606131230-migrate-archive-to-indexeddb.md new file mode 100644 index 0000000..9de37d4 --- /dev/null +++ b/history/202606131230-migrate-archive-to-indexeddb.md @@ -0,0 +1,15 @@ +# Migrate Archive Storage from localStorage to IndexedDB + +## Timestamp: 2026-06-13 12:30 + +## Context & Issue: +The application was crashing with `Uncaught QuotaExceededError: Failed to execute 'setItem' on 'Storage'` because the dialogue archive list of `msg-buffer-archive` exceeded the 5MB limit imposed on `localStorage` by browsers, specifically in Chrome side panels/extensions. + +## Solution & Architectural Trade-off: +We migrated the large, ever-growing history archives to **IndexedDB**, which provides hundreds of MBs/GBs of quota storage, while keeping the main active buffer (`msg-buffer`) and settings inside `localStorage` to ensure synchronous persistence mechanics (e.g. `beforeunload`, `visibilitychange`) remain 100% reliable. + +## Changes: +1. Created `lib/db.mjs` containing async helper functions `db_get` and `db_set` using standard raw browser IndexedDB APIs. +2. Imported `db-get` and `db-set` into `app.comp.container`. +3. Updated the 3 major archive CRUD handlers (`on-view-archive`, delete-archived, and `Archive all`) to use `js-await` alongside `db-get`/`db-set`. +4. Successfully verified types, compilation, and integration workflows with no regressions. diff --git a/lib/db.mjs b/lib/db.mjs new file mode 100644 index 0000000..bd92ace --- /dev/null +++ b/lib/db.mjs @@ -0,0 +1,50 @@ +const dbName = 'msg_buffer_db'; +const storeName = 'kv'; + +function getDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(dbName, 1); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + request.onupgradeneeded = () => { + request.result.createObjectStore(storeName); + }; + }); +} + +export async function db_get(key) { + const db = await getDB(); + const dbValue = await new Promise((resolve, reject) => { + const transaction = db.transaction(storeName, 'readonly'); + const store = transaction.objectStore(storeName); + const request = store.get(key); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + }); + if (dbValue !== undefined && dbValue !== null) { + return dbValue; + } + try { + const localValue = localStorage.getItem(key); + if (localValue !== null) { + await db_set(key, localValue); + localStorage.removeItem(key); + console.log(`Successfully migrated key "${key}" from localStorage to IndexedDB.`); + return localValue; + } + } catch (e) { + console.error('Migration from localStorage failed:', e); + } + return null; +} + +export async function db_set(key, value) { + const db = await getDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(storeName, 'readwrite'); + const store = transaction.objectStore(storeName); + const request = store.put(value, key); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(); + }); +} diff --git a/package.json b/package.json index 4e55610..d354067 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "@calcit/procs": "^0.12.35", + "@calcit/procs": "^0.12.46", "@google/genai": "^1.49.0", "@tiye/main-fonts": "0.0.1", "axios": "^1.15.0", diff --git a/yarn.lock b/yarn.lock index 4ef1620..391f504 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,14 +5,14 @@ __metadata: version: 8 cacheKey: 10c0 -"@calcit/procs@npm:^0.12.35": - version: 0.12.35 - resolution: "@calcit/procs@npm:0.12.35" +"@calcit/procs@npm:^0.12.46": + version: 0.12.46 + resolution: "@calcit/procs@npm:0.12.46" dependencies: "@calcit/ternary-tree": "npm:0.0.26" "@cirru/parser.ts": "npm:^0.0.9" "@cirru/writer.ts": "npm:^0.1.9" - checksum: 10c0/b9d8e2926911e81e56635653c596c2b217fe260ae768a132df49dfd2e7f4ee7707882802e06be0f191a4b5e7df6c415f78abb8d8fb8fb06176611139badc2fde + checksum: 10c0/c45bc67d42e25a3b3d8681b348209f48e1d38154679d66804ed03b234685e6645906f6589c6efb3cfc3d1535858a8881660955a6eb41051e6c7fd8ed573724f6 languageName: node linkType: hard @@ -1573,7 +1573,7 @@ __metadata: version: 0.0.0-use.local resolution: "root-workspace-0b6124@workspace:." dependencies: - "@calcit/procs": "npm:^0.12.35" + "@calcit/procs": "npm:^0.12.46" "@google/genai": "npm:^1.49.0" "@tiye/main-fonts": "npm:0.0.1" axios: "npm:^1.15.0"