@marimo-team/islands 0.15.2 → 0.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ConnectedDataExplorerComponent-C39nQwtD.js → ConnectedDataExplorerComponent-B68gXlbY.js} +323 -328
- package/dist/{ImageComparisonComponent-BhkiyswP.js → ImageComparisonComponent-Cw1oA8Tn.js} +13 -13
- package/dist/{_baseUniq-DdHL34FO.js → _baseUniq-CQrhBg_9.js} +67 -67
- package/dist/any-language-editor-pzUl6lxp.js +27 -0
- package/dist/{arc-BXrety1g.js → arc-BOhn-m2C.js} +1 -1
- package/dist/{architectureDiagram-KFL7JDKH-BMy6ywCF.js → architectureDiagram-W76B3OCA-DdYf2VnU.js} +144 -144
- package/dist/assets/{worker-COGufAQn.js → worker-BcG8m3h5.js} +33 -29
- package/dist/asterisk-DS281yxp.js +271 -0
- package/dist/{blockDiagram-ZYB65J3Q-DYT2-nlI.js → blockDiagram-QIGZ2CNN-DmmYotkP.js} +10 -10
- package/dist/{c4Diagram-AAMF2YG6-ZiQzioe6.js → c4Diagram-FPNF74CW-Dyz4zMHJ.js} +8 -8
- package/dist/{channel-CeuXqUAU.js → channel-CCL8jXAe.js} +1 -1
- package/dist/{chunk-ANTBXLJU-BvYnIrdq.js → chunk-4BX2VUAB-BfKwWLfJ.js} +1 -1
- package/dist/{chunk-WVR4S24B-DXj8yaUk.js → chunk-55IACEB6-CFQU7zSp.js} +1 -1
- package/dist/{chunk-GLLZNHP4-CyFsosAe.js → chunk-FMBD7UC4-DDjZzUcl.js} +1 -1
- package/dist/{chunk-JBRWN2VN-DA_EEhy2.js → chunk-K7UQS3LO-nBRjBU1H.js} +117 -117
- package/dist/{chunk-NRVI72HA-BYx2jMlI.js → chunk-QN33PNHL-B-g8sJzl.js} +1 -1
- package/dist/{chunk-FHKO5MBM-DfCztBk8.js → chunk-QZHKN3VN-B7QSJS3J.js} +1 -1
- package/dist/{chunk-LXBSTHXV-Se7vdY6J.js → chunk-TVAH2DTR-pGXll4d1.js} +7 -7
- package/dist/{chunk-OMD6QJNC-CqgcPMgL.js → chunk-TZMSLE5B-Dx9h-1mv.js} +1 -1
- package/dist/{classDiagram-v2-QTMF73CY-B19A3G1l.js → classDiagram-KNZD7YFC-BXryI7DY.js} +2 -2
- package/dist/{classDiagram-3BZAVTQC-B19A3G1l.js → classDiagram-v2-RKCZMP56-BXryI7DY.js} +2 -2
- package/dist/{clone-78au0tn1.js → clone-DqwV7ges.js} +1 -1
- package/dist/cose-bilkent-S5V4N54A-Di6FNMXz.js +2609 -0
- package/dist/{cytoscape.esm-BYnVVhJX.js → cytoscape.esm-DfdJODL8.js} +34 -34
- package/dist/{dagre-2BBEFEWP-BfEn3ZUV.js → dagre-5GWH7T2D-BTZPMTey.js} +6 -6
- package/dist/{data-grid-overlay-editor-CH_qLkV2.js → data-grid-overlay-editor-ryatXXby.js} +11 -11
- package/dist/{diagram-4IRLE6MV-CL8xidnG.js → diagram-N5W7TBWH-D79_zdOu.js} +59 -60
- package/dist/{diagram-RP2FKANI-B1BPcUew.js → diagram-QEK2KX5R-DX2A_SD0.js} +15 -15
- package/dist/{diagram-GUPCWM2R-CZ5cfqlq.js → diagram-S2PKOQOG-DM6VMTrJ.js} +10 -10
- package/dist/dockerfile-BoowzQlp.js +194 -0
- package/dist/ebnf-DUPDuY4r.js +78 -0
- package/dist/{erDiagram-HZWUO2LU-BEAIww50.js → erDiagram-AWTI2OKA-BBirxtlI.js} +8 -8
- package/dist/fcl-CPC2WYrI.js +103 -0
- package/dist/{flowDiagram-THRYKUMA-Czs2UAI2.js → flowDiagram-PVAE7QVJ-DyVweEMs.js} +9 -9
- package/dist/{ganttDiagram-WV7ZQ7D5-ByYIAVFO.js → ganttDiagram-OWAHRB6G-DTB7FX7r.js} +34 -34
- package/dist/{gitGraphDiagram-OJR772UL-BcpDsiyB.js → gitGraphDiagram-NY62KEGX-BrbIb5pD.js} +4 -4
- package/dist/{glide-data-editor-CmN6FVyi.js → glide-data-editor-DhMX4nmM.js} +33 -33
- package/dist/{graph-77W6heli.js → graph-CuLSrclI.js} +3 -3
- package/dist/http-D9LttvKF.js +44 -0
- package/dist/{index-BOojn38D.js → index-BNgdUQ2e.js} +7711 -7711
- package/dist/index-DIy6LHLJ.js +98 -0
- package/dist/{index-CmozKMxx.js → index-Df2dsx1t.js} +6 -6
- package/dist/{index-pBmAzQJl.js → index-MCx5v1x0.js} +2 -2
- package/dist/{index-Bfk9dnyS.js → index-cz_xaKvT.js} +33090 -32892
- package/dist/{infoDiagram-6WOFNB3A-CfzLHHVP.js → infoDiagram-STP46IZ2-CCBHc7-K.js} +2 -2
- package/dist/{journeyDiagram-FFXJYRFH-ndAcpkGn.js → journeyDiagram-BIP6EPQ6-LhGSj54j.js} +24 -26
- package/dist/{kanban-definition-KOZQBZVT-DcQYzNvc.js → kanban-definition-6OIFK2YF-aegTMFS6.js} +14 -14
- package/dist/{layout-XySVHJgD.js → layout-BEARWMhl.js} +81 -81
- package/dist/{linear-PbooOqg7.js → linear-fbJq6cdO.js} +35 -35
- package/dist/{main-B5yML0bw.js → main-HerZgEhd.js} +76533 -69945
- package/dist/main.js +1 -1
- package/dist/{mermaid-Cg5IX6Nv.js → mermaid-DxPYK0KX.js} +6160 -7493
- package/dist/min-DBJkhObB.js +80 -0
- package/dist/mindmap-definition-Q6HEUPPD-A3Fh5XDZ.js +785 -0
- package/dist/nginx-zDPm3Z74.js +89 -0
- package/dist/{number-overlay-editor-DUhfZqtP.js → number-overlay-editor-USMrY6k3.js} +19 -19
- package/dist/{pieDiagram-DBDJKBY4-DTOlNsja.js → pieDiagram-ADFJNKIX-Q9uFlCV0.js} +17 -17
- package/dist/{quadrantDiagram-YPSRARAO-BX2d8VS-.js → quadrantDiagram-LMRXKWRM-BuPh-qpK.js} +6 -6
- package/dist/{react-plotly-Dcyw-3Sa.js → react-plotly-HSqJPRfa.js} +3577 -3577
- package/dist/{requirementDiagram-EGVEC5DT-D1T5u-wG.js → requirementDiagram-4UW4RH46-CHROYNU_.js} +7 -7
- package/dist/{sankeyDiagram-HRAUVNP4-G6xDfnp-.js → sankeyDiagram-GR3RE2ED-DkUqHP2d.js} +5 -5
- package/dist/sequenceDiagram-C3RYC4MD-YoPTMplP.js +2519 -0
- package/dist/{slides-component-BJLlPJSr.js → slides-component-D7CHSR00.js} +66 -66
- package/dist/solr-BNlsLglM.js +41 -0
- package/dist/spreadsheet-C-cy4P5N.js +49 -0
- package/dist/{stateDiagram-UUKSUZ4H-CYXbjaom.js → stateDiagram-KXAO66HF-DEN00mVU.js} +5 -5
- package/dist/{stateDiagram-v2-EYPG3UTE-Br1HYKT6.js → stateDiagram-v2-UMBNRL4Z-DlQqSUAa.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/tiddlywiki-5wqsXtSk.js +155 -0
- package/dist/tiki-__Kn3CeS.js +181 -0
- package/dist/{time-B9SZnSen.js → time-BtVcKqeD.js} +58 -58
- package/dist/{timeline-definition-3HZDQTIS-DeK_ZRD0.js → timeline-definition-XQNQX7LJ-DEteLt8D.js} +10 -12
- package/dist/{timer-BYwnU4DF.js → timer-B0-z63CM.js} +16 -16
- package/dist/{treemap-75Q7IDZK-CKP4vV_0.js → treemap-75Q7IDZK-8S6podme.js} +14 -14
- package/dist/{vega-component-CpgdqX2d.js → vega-component-D35L45kI.js} +30 -30
- package/dist/{xychartDiagram-FDP5SA34-AMEPsx_R.js → xychartDiagram-6GGTOJPD-DKwGThyy.js} +7 -7
- package/package.json +44 -41
- package/src/__mocks__/notebook.ts +3 -0
- package/src/__mocks__/requests.ts +3 -0
- package/src/__tests__/__snapshots__/CellStatus.test.tsx.snap +12 -12
- package/src/__tests__/chat-utils.test.ts +26 -211
- package/src/components/ai/ai-model-dropdown.tsx +25 -9
- package/src/components/ai/ai-provider-icon.tsx +5 -1
- package/src/components/app-config/ai-config.tsx +7 -0
- package/src/components/chat/acp/__tests__/__snapshots__/prompt.test.ts.snap +304 -0
- package/src/components/chat/acp/__tests__/atoms.test.ts +56 -0
- package/src/components/chat/acp/__tests__/prompt.test.ts +12 -0
- package/src/components/chat/acp/__tests__/state.test.ts +621 -0
- package/src/components/chat/acp/agent-docs.tsx +78 -0
- package/src/components/chat/acp/agent-panel.css +23 -0
- package/src/components/chat/acp/agent-panel.tsx +715 -0
- package/src/components/chat/acp/agent-selector.tsx +138 -0
- package/src/components/chat/acp/blocks.tsx +664 -0
- package/src/components/chat/acp/common.tsx +198 -0
- package/src/components/chat/acp/prompt.ts +284 -0
- package/src/components/chat/acp/scroll-to-bottom-button.tsx +50 -0
- package/src/components/chat/acp/session-tabs.tsx +138 -0
- package/src/components/chat/acp/state.ts +263 -0
- package/src/components/chat/acp/thread.tsx +121 -0
- package/src/components/chat/acp/types.ts +63 -0
- package/src/components/chat/acp/utils.ts +45 -0
- package/src/components/chat/chat-components.tsx +71 -0
- package/src/components/chat/chat-panel.tsx +481 -291
- package/src/components/chat/chat-utils.ts +50 -0
- package/src/components/chat/markdown-renderer.tsx +3 -7
- package/src/components/chat/tool-call-accordion.tsx +6 -6
- package/src/components/datasources/__tests__/utils.test.ts +6 -0
- package/src/components/datasources/column-preview.tsx +1 -3
- package/src/components/editor/actions/useNotebookActions.tsx +1 -1
- package/src/components/editor/ai/add-cell-with-ai.tsx +20 -15
- package/src/components/editor/ai/ai-completion-editor.tsx +22 -3
- package/src/components/editor/ai/completion-handlers.tsx +2 -4
- package/src/components/editor/ai/completion-utils.ts +85 -11
- package/src/components/editor/alerts/startup-logs-alert.tsx +72 -0
- package/src/components/editor/chrome/panels/datasources-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/dependency-graph-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/documentation-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/error-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/file-explorer-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/logs-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/outline-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/packages-panel.tsx +4 -2
- package/src/components/editor/chrome/panels/scratchpad-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/secrets-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/tracing-panel.tsx +3 -1
- package/src/components/editor/chrome/panels/variable-panel.tsx +3 -1
- package/src/components/editor/chrome/types.ts +10 -0
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +55 -31
- package/src/components/editor/controls/command-palette-button.tsx +1 -1
- package/src/components/editor/controls/command-palette.tsx +5 -4
- package/src/components/editor/controls/state.ts +4 -0
- package/src/components/editor/package-alert.tsx +108 -58
- package/src/components/editor/renderers/CellArray.tsx +2 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +0 -1
- package/src/components/pages/edit-page.tsx +7 -3
- package/src/core/ai/chat-utils.ts +26 -43
- package/src/core/ai/config.ts +1 -1
- package/src/core/ai/context/__tests__/registry.test.ts +277 -3
- package/src/core/ai/context/context.ts +11 -1
- package/src/core/ai/context/providers/__tests__/cell-output.test.ts +378 -0
- package/src/core/ai/context/providers/__tests__/error.test.ts +3 -2
- package/src/core/ai/context/providers/__tests__/file.test.ts +119 -0
- package/src/core/ai/context/providers/cell-output.ts +349 -0
- package/src/core/ai/context/providers/common.ts +5 -1
- package/src/core/ai/context/providers/file.ts +287 -0
- package/src/core/ai/context/registry.ts +79 -0
- package/src/core/ai/state.ts +22 -5
- package/src/core/alerts/state.ts +71 -3
- package/src/core/cells/cell.ts +2 -2
- package/src/core/cells/cells.ts +1 -1
- package/src/core/cells/logs.ts +1 -1
- package/src/core/cells/runs.ts +6 -5
- package/src/core/codemirror/ai/resources.ts +47 -5
- package/src/core/codemirror/ai/state.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +45 -0
- package/src/core/codemirror/markdown/__tests__/commands.test.ts +1 -0
- package/src/core/codemirror/theme/dark.ts +1 -1
- package/src/core/config/capabilities.ts +1 -1
- package/src/core/config/feature-flag.tsx +2 -0
- package/src/core/datasets/__tests__/data-source.test.ts +24 -0
- package/src/core/errors/__tests__/errors.test.ts +2 -0
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/islands/main.ts +1 -0
- package/src/core/kernel/messages.ts +12 -6
- package/src/core/layout/layout.ts +3 -3
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.ts +1 -0
- package/src/core/network/types.ts +4 -1
- package/src/core/wasm/bridge.ts +18 -2
- package/src/core/wasm/worker/bootstrap.ts +3 -1
- package/src/core/wasm/worker/getMarimoWheel.ts +3 -8
- package/src/core/wasm/worker/types.ts +3 -0
- package/src/core/websocket/useMarimoWebSocket.tsx +7 -1
- package/src/css/app/Cell.css +42 -21
- package/src/css/app/codemirror.css +5 -1
- package/src/css/globals.css +3 -0
- package/src/css/md.css +1 -1
- package/src/plugins/impl/MicrophonePlugin.tsx +2 -2
- package/src/plugins/impl/chat/ChatPlugin.tsx +2 -9
- package/src/plugins/impl/chat/chat-ui.tsx +129 -110
- package/src/plugins/impl/chat/types.ts +5 -8
- package/src/plugins/impl/code/__tests__/language.test.ts +15 -0
- package/src/plugins/impl/code/any-language-editor.tsx +11 -8
- package/src/plugins/impl/vega/vega.css +121 -0
- package/src/plugins/layout/MimeRenderPlugin.tsx +3 -6
- package/src/stories/cell.stories.tsx +6 -0
- package/src/stories/layout/vertical/one-column.stories.tsx +215 -0
- package/src/theme/useTheme.ts +11 -6
- package/src/utils/Logger.ts +5 -6
- package/src/utils/__tests__/blob.test.ts +37 -0
- package/src/utils/arrays.ts +13 -0
- package/src/utils/fileToBase64.ts +21 -6
- package/src/utils/json/base64.ts +5 -2
- package/src/utils/numbers.ts +9 -7
- package/dist/any-language-editor-DC5170DQ.js +0 -45
- package/dist/asn1-jKiBa2Ya.js +0 -95
- package/dist/clojure-CCKyeQKf.js +0 -800
- package/dist/css-BkF-NPzE.js +0 -1553
- package/dist/index-5ZH_qS8j.js +0 -288
- package/dist/index-U4yn89qO.js +0 -341
- package/dist/javascript-C2yteZeJ.js +0 -691
- package/dist/min-DS5Jz-hg.js +0 -80
- package/dist/mindmap-definition-LNHGMQRG-0aOVaMR8.js +0 -3234
- package/dist/mllike-BSnXJBGA.js +0 -272
- package/dist/pug-CwAQJzGR.js +0 -248
- package/dist/python-BkR3uSy8.js +0 -313
- package/dist/rpm-IznJm2Xc.js +0 -57
- package/dist/sequenceDiagram-WFGC7UMF-DMhHzllb.js +0 -2284
- package/dist/ttcn-cfg-Bac_acMi.js +0 -88
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
import type { UIMessage } from "@ai-sdk/react";
|
|
3
4
|
import { useChat } from "@ai-sdk/react";
|
|
4
5
|
import { storePrompt } from "@marimo-team/codemirror-ai";
|
|
5
6
|
import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
|
6
|
-
import type
|
|
7
|
-
import { useAtom, useAtomValue } from "jotai";
|
|
7
|
+
import { DefaultChatTransport, type ToolUIPart } from "ai";
|
|
8
|
+
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
8
9
|
import {
|
|
10
|
+
AtSignIcon,
|
|
9
11
|
BotMessageSquareIcon,
|
|
10
12
|
ClockIcon,
|
|
11
13
|
Loader2,
|
|
14
|
+
PaperclipIcon,
|
|
12
15
|
PlusIcon,
|
|
13
16
|
SendIcon,
|
|
14
17
|
SettingsIcon,
|
|
15
18
|
SquareIcon,
|
|
16
19
|
} from "lucide-react";
|
|
17
|
-
import {
|
|
18
|
-
type Dispatch,
|
|
19
|
-
memo,
|
|
20
|
-
type SetStateAction,
|
|
21
|
-
useEffect,
|
|
22
|
-
useMemo,
|
|
23
|
-
useRef,
|
|
24
|
-
useState,
|
|
25
|
-
} from "react";
|
|
20
|
+
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
26
21
|
import useEvent from "react-use-event-hook";
|
|
27
22
|
import { Button } from "@/components/ui/button";
|
|
28
23
|
import {
|
|
@@ -39,51 +34,79 @@ import {
|
|
|
39
34
|
SelectLabel,
|
|
40
35
|
SelectTrigger,
|
|
41
36
|
} from "@/components/ui/select";
|
|
42
|
-
import {
|
|
37
|
+
import { replaceMessagesInChat } from "@/core/ai/chat-utils";
|
|
43
38
|
import { useModelChange } from "@/core/ai/config";
|
|
39
|
+
import { AiModelId, type ProviderId } from "@/core/ai/ids/ids";
|
|
44
40
|
import {
|
|
45
41
|
activeChatAtom,
|
|
46
42
|
type Chat,
|
|
47
43
|
type ChatId,
|
|
48
|
-
type ChatState,
|
|
49
44
|
chatStateAtom,
|
|
50
45
|
} from "@/core/ai/state";
|
|
51
|
-
import { getCodes } from "@/core/codemirror/copilot/getCodes";
|
|
52
46
|
import { aiAtom, aiEnabledAtom } from "@/core/config/config";
|
|
53
47
|
import { DEFAULT_AI_MODEL } from "@/core/config/config-schema";
|
|
54
48
|
import { FeatureFlagged } from "@/core/config/feature-flag";
|
|
55
49
|
import { useRequestClient } from "@/core/network/requests";
|
|
56
50
|
import { useRuntimeManager } from "@/core/runtime/config";
|
|
51
|
+
import type { ChatMessage } from "@/plugins/impl/chat/types";
|
|
57
52
|
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
58
53
|
import { cn } from "@/utils/cn";
|
|
59
54
|
import { timeAgo } from "@/utils/dates";
|
|
60
55
|
import { Logger } from "@/utils/Logger";
|
|
61
|
-
import { generateUUID } from "@/utils/uuid";
|
|
62
56
|
import { AIModelDropdown } from "../ai/ai-model-dropdown";
|
|
63
57
|
import { useOpenSettingsToTab } from "../app-config/state";
|
|
64
58
|
import { PromptInput } from "../editor/ai/add-cell-with-ai";
|
|
65
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
addContextCompletion,
|
|
61
|
+
CONTEXT_TRIGGER,
|
|
62
|
+
getAICompletionBodyWithAttachments,
|
|
63
|
+
} from "../editor/ai/completion-utils";
|
|
66
64
|
import { PanelEmptyState } from "../editor/chrome/panels/empty-state";
|
|
67
65
|
import { CopyClipboardIcon } from "../icons/copy-icon";
|
|
66
|
+
import { Input } from "../ui/input";
|
|
68
67
|
import { Tooltip, TooltipProvider } from "../ui/tooltip";
|
|
68
|
+
import { toast } from "../ui/use-toast";
|
|
69
|
+
import { AttachmentRenderer, FileAttachmentPill } from "./chat-components";
|
|
70
|
+
import {
|
|
71
|
+
convertToFileUIPart,
|
|
72
|
+
generateChatTitle,
|
|
73
|
+
isLastMessageReasoning,
|
|
74
|
+
} from "./chat-utils";
|
|
69
75
|
import { MarkdownRenderer } from "./markdown-renderer";
|
|
70
76
|
import { ReasoningAccordion } from "./reasoning-accordion";
|
|
71
77
|
import { ToolCallAccordion } from "./tool-call-accordion";
|
|
72
78
|
|
|
79
|
+
// Default mode for the AI
|
|
80
|
+
const DEFAULT_MODE = "manual";
|
|
81
|
+
|
|
82
|
+
// We need to modify the backend to support attachments for other providers
|
|
83
|
+
// And other types
|
|
84
|
+
const PROVIDERS_THAT_SUPPORT_ATTACHMENTS = new Set<ProviderId>([
|
|
85
|
+
"openai",
|
|
86
|
+
"google",
|
|
87
|
+
"anthropic",
|
|
88
|
+
]);
|
|
89
|
+
const SUPPORTED_ATTACHMENT_TYPES = ["image/*", "text/*"];
|
|
90
|
+
const MAX_ATTACHMENT_SIZE = 1024 * 1024 * 50; // 50MB
|
|
91
|
+
|
|
73
92
|
interface ChatHeaderProps {
|
|
74
93
|
onNewChat: () => void;
|
|
75
94
|
activeChatId: ChatId | undefined;
|
|
76
95
|
setActiveChat: (id: ChatId | null) => void;
|
|
77
|
-
chats: Chat[];
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|
81
99
|
onNewChat,
|
|
82
100
|
activeChatId,
|
|
83
101
|
setActiveChat,
|
|
84
|
-
chats,
|
|
85
102
|
}) => {
|
|
86
103
|
const { handleClick } = useOpenSettingsToTab();
|
|
104
|
+
const chatState = useAtomValue(chatStateAtom);
|
|
105
|
+
const chats = useMemo(() => {
|
|
106
|
+
return [...chatState.chats.values()].sort(
|
|
107
|
+
(a, b) => b.updatedAt - a.updatedAt,
|
|
108
|
+
);
|
|
109
|
+
}, [chatState.chats]);
|
|
87
110
|
|
|
88
111
|
return (
|
|
89
112
|
<div className="flex border-b px-2 py-1 justify-between shrink-0 items-center">
|
|
@@ -149,28 +172,32 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|
|
149
172
|
};
|
|
150
173
|
|
|
151
174
|
interface ChatMessageProps {
|
|
152
|
-
message:
|
|
175
|
+
message: UIMessage;
|
|
153
176
|
index: number;
|
|
154
177
|
onEdit: (index: number, newValue: string) => void;
|
|
155
|
-
setChatState: Dispatch<SetStateAction<ChatState>>;
|
|
156
|
-
chatState: ChatState;
|
|
157
178
|
isStreamingReasoning: boolean;
|
|
158
179
|
isLast: boolean;
|
|
159
180
|
}
|
|
160
181
|
|
|
161
|
-
|
|
162
|
-
(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
182
|
+
function isToolPart(part: UIMessage["parts"][number]): part is ToolUIPart {
|
|
183
|
+
return part.type.startsWith("tool-");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const ChatMessageDisplay: React.FC<ChatMessageProps> = memo(
|
|
187
|
+
({ message, index, onEdit, isStreamingReasoning, isLast }) => {
|
|
188
|
+
const renderUserMessage = (message: UIMessage) => {
|
|
189
|
+
const textParts = message.parts?.filter((p) => p.type === "text");
|
|
190
|
+
const content = textParts?.map((p) => p.text).join("\n");
|
|
191
|
+
const fileParts = message.parts?.filter((p) => p.type === "file");
|
|
192
|
+
|
|
193
|
+
return (
|
|
170
194
|
<div className="w-[95%] bg-background border p-1 rounded-sm">
|
|
195
|
+
{fileParts?.map((filePart, idx) => (
|
|
196
|
+
<AttachmentRenderer attachment={filePart} key={idx} />
|
|
197
|
+
))}
|
|
171
198
|
<PromptInput
|
|
172
199
|
key={message.id}
|
|
173
|
-
value={
|
|
200
|
+
value={content}
|
|
174
201
|
placeholder="Type your message..."
|
|
175
202
|
onChange={() => {
|
|
176
203
|
// noop
|
|
@@ -186,12 +213,31 @@ const ChatMessage: React.FC<ChatMessageProps> = memo(
|
|
|
186
213
|
}}
|
|
187
214
|
/>
|
|
188
215
|
</div>
|
|
189
|
-
)
|
|
216
|
+
);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const renderOtherMessage = (message: UIMessage) => {
|
|
220
|
+
const textParts = message.parts.filter((p) => p.type === "text");
|
|
221
|
+
const content = textParts.map((p) => p.text).join("\n");
|
|
222
|
+
|
|
223
|
+
return (
|
|
190
224
|
<div className="w-[95%] break-words">
|
|
191
225
|
<div className="absolute right-1 top-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
192
|
-
<CopyClipboardIcon className="h-3 w-3" value={
|
|
226
|
+
<CopyClipboardIcon className="h-3 w-3" value={content || ""} />
|
|
193
227
|
</div>
|
|
194
|
-
{message.parts
|
|
228
|
+
{message.parts.map((part, i) => {
|
|
229
|
+
if (isToolPart(part)) {
|
|
230
|
+
return (
|
|
231
|
+
<ToolCallAccordion
|
|
232
|
+
key={i}
|
|
233
|
+
index={i}
|
|
234
|
+
toolName={part.type}
|
|
235
|
+
result={part.output}
|
|
236
|
+
state={part.state}
|
|
237
|
+
/>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
195
241
|
switch (part.type) {
|
|
196
242
|
case "text":
|
|
197
243
|
return <MarkdownRenderer key={i} content={part.text} />;
|
|
@@ -199,60 +245,100 @@ const ChatMessage: React.FC<ChatMessageProps> = memo(
|
|
|
199
245
|
case "reasoning":
|
|
200
246
|
return (
|
|
201
247
|
<ReasoningAccordion
|
|
202
|
-
reasoning={part.
|
|
248
|
+
reasoning={part.text}
|
|
203
249
|
key={i}
|
|
204
250
|
index={i}
|
|
205
251
|
isStreaming={
|
|
206
252
|
isLast &&
|
|
207
253
|
isStreamingReasoning &&
|
|
208
254
|
// If there are multiple reasoning parts, only show the last one
|
|
209
|
-
i === (message.parts
|
|
255
|
+
i === (message.parts.length || 0) - 1
|
|
210
256
|
}
|
|
211
257
|
/>
|
|
212
258
|
);
|
|
213
259
|
|
|
214
|
-
case "tool
|
|
260
|
+
case "dynamic-tool":
|
|
215
261
|
return (
|
|
216
262
|
<ToolCallAccordion
|
|
217
263
|
key={i}
|
|
218
264
|
index={i}
|
|
219
|
-
toolName={part.
|
|
220
|
-
result={
|
|
221
|
-
|
|
222
|
-
? part.toolInvocation.result
|
|
223
|
-
: null
|
|
224
|
-
}
|
|
225
|
-
state={part.toolInvocation.state}
|
|
265
|
+
toolName={part.type}
|
|
266
|
+
result={part.output}
|
|
267
|
+
state={part.state}
|
|
226
268
|
/>
|
|
227
269
|
);
|
|
228
270
|
|
|
271
|
+
// These are cryptographic signatures, so we don't need to render them
|
|
272
|
+
case "data-reasoning-signature":
|
|
273
|
+
return null;
|
|
274
|
+
|
|
229
275
|
/* handle other part types … */
|
|
230
276
|
default:
|
|
231
|
-
|
|
277
|
+
if (part.type.startsWith("data-")) {
|
|
278
|
+
Logger.log("Found data part", part);
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
Logger.error("Unhandled part type:", part.type);
|
|
283
|
+
try {
|
|
284
|
+
return (
|
|
285
|
+
<MarkdownRenderer
|
|
286
|
+
key={i}
|
|
287
|
+
content={JSON.stringify(part, null, 2)}
|
|
288
|
+
/>
|
|
289
|
+
);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
Logger.error("Error rendering part:", part.type, error);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
232
294
|
}
|
|
233
295
|
})}
|
|
234
296
|
</div>
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div
|
|
302
|
+
className={cn(
|
|
303
|
+
"flex group relative",
|
|
304
|
+
message.role === "user" ? "justify-end" : "justify-start",
|
|
305
|
+
)}
|
|
306
|
+
>
|
|
307
|
+
{message.role === "user"
|
|
308
|
+
? renderUserMessage(message)
|
|
309
|
+
: renderOtherMessage(message)}
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
},
|
|
238
313
|
);
|
|
239
|
-
|
|
314
|
+
ChatMessageDisplay.displayName = "ChatMessage";
|
|
240
315
|
|
|
241
316
|
interface ChatInputFooterProps {
|
|
242
317
|
isEmpty: boolean;
|
|
243
318
|
onSendClick: () => void;
|
|
244
319
|
isLoading: boolean;
|
|
245
320
|
onStop: () => void;
|
|
321
|
+
onAddFiles: (files: File[]) => void;
|
|
322
|
+
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
|
323
|
+
onAddContext: () => void;
|
|
246
324
|
}
|
|
247
325
|
|
|
248
|
-
const DEFAULT_MODE = "manual";
|
|
249
|
-
|
|
250
326
|
const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
|
|
251
|
-
({
|
|
327
|
+
({
|
|
328
|
+
isEmpty,
|
|
329
|
+
onSendClick,
|
|
330
|
+
isLoading,
|
|
331
|
+
onStop,
|
|
332
|
+
fileInputRef,
|
|
333
|
+
onAddFiles,
|
|
334
|
+
onAddContext,
|
|
335
|
+
}) => {
|
|
252
336
|
const ai = useAtomValue(aiAtom);
|
|
253
337
|
const currentMode = ai?.mode || DEFAULT_MODE;
|
|
254
338
|
const currentModel = ai?.models?.chat_model || DEFAULT_AI_MODEL;
|
|
255
|
-
const
|
|
339
|
+
const currentProvider = AiModelId.parse(currentModel).providerId;
|
|
340
|
+
|
|
341
|
+
const { saveModeChange } = useModelChange();
|
|
256
342
|
|
|
257
343
|
const modeOptions = [
|
|
258
344
|
{
|
|
@@ -268,59 +354,99 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
|
|
|
268
354
|
},
|
|
269
355
|
];
|
|
270
356
|
|
|
357
|
+
const isAttachmentSupported =
|
|
358
|
+
PROVIDERS_THAT_SUPPORT_ATTACHMENTS.has(currentProvider);
|
|
359
|
+
|
|
271
360
|
return (
|
|
272
|
-
<
|
|
273
|
-
<div className="flex items-center
|
|
274
|
-
<
|
|
275
|
-
<
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
<
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
361
|
+
<TooltipProvider>
|
|
362
|
+
<div className="px-3 py-2 border-t border-border/20 flex flex-row items-center justify-between">
|
|
363
|
+
<div className="flex items-center gap-2">
|
|
364
|
+
<FeatureFlagged feature="mcp_docs">
|
|
365
|
+
<Select value={currentMode} onValueChange={saveModeChange}>
|
|
366
|
+
<SelectTrigger className="h-6 text-xs border-border shadow-none! ring-0! bg-muted hover:bg-muted/30 py-0 px-2 gap-1 capitalize">
|
|
367
|
+
{currentMode}
|
|
368
|
+
</SelectTrigger>
|
|
369
|
+
<SelectContent>
|
|
370
|
+
<SelectGroup>
|
|
371
|
+
<SelectLabel>AI Mode</SelectLabel>
|
|
372
|
+
{modeOptions.map((option) => (
|
|
373
|
+
<SelectItem
|
|
374
|
+
key={option.value}
|
|
375
|
+
value={option.value}
|
|
376
|
+
className="text-xs"
|
|
377
|
+
>
|
|
378
|
+
<div className="flex flex-col">
|
|
379
|
+
{option.label}
|
|
380
|
+
<div className="text-muted-foreground text-xs pt-1 block">
|
|
381
|
+
{option.subtitle}
|
|
382
|
+
</div>
|
|
292
383
|
</div>
|
|
293
|
-
</
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
</
|
|
297
|
-
</
|
|
298
|
-
</
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
384
|
+
</SelectItem>
|
|
385
|
+
))}
|
|
386
|
+
</SelectGroup>
|
|
387
|
+
</SelectContent>
|
|
388
|
+
</Select>
|
|
389
|
+
</FeatureFlagged>
|
|
390
|
+
<AIModelDropdown
|
|
391
|
+
placeholder="Model"
|
|
392
|
+
triggerClassName="h-6 text-xs shadow-none! ring-0! bg-muted hover:bg-muted/30 rounded-sm"
|
|
393
|
+
iconSize="small"
|
|
394
|
+
showAddCustomModelDocs={true}
|
|
395
|
+
forRole="chat"
|
|
396
|
+
/>
|
|
397
|
+
</div>
|
|
398
|
+
<div className="flex flex-row">
|
|
399
|
+
<Tooltip content="Add context">
|
|
400
|
+
<Button variant="text" size="icon" onClick={onAddContext}>
|
|
401
|
+
<AtSignIcon className="h-3.5 w-3.5" />
|
|
402
|
+
</Button>
|
|
403
|
+
</Tooltip>
|
|
404
|
+
{isAttachmentSupported && (
|
|
405
|
+
<>
|
|
406
|
+
<Tooltip content="Attach a file">
|
|
407
|
+
<Button
|
|
408
|
+
variant="text"
|
|
409
|
+
size="icon"
|
|
410
|
+
className="cursor-pointer"
|
|
411
|
+
onClick={() => fileInputRef.current?.click()}
|
|
412
|
+
title="Attach a file"
|
|
413
|
+
>
|
|
414
|
+
<PaperclipIcon className="h-3.5 w-3.5" />
|
|
415
|
+
</Button>
|
|
416
|
+
</Tooltip>
|
|
417
|
+
<Input
|
|
418
|
+
ref={fileInputRef}
|
|
419
|
+
type="file"
|
|
420
|
+
multiple={true}
|
|
421
|
+
hidden={true}
|
|
422
|
+
onChange={(event) => {
|
|
423
|
+
if (event.target.files) {
|
|
424
|
+
onAddFiles([...event.target.files]);
|
|
425
|
+
}
|
|
426
|
+
}}
|
|
427
|
+
accept={SUPPORTED_ATTACHMENT_TYPES.join(",")}
|
|
428
|
+
/>
|
|
429
|
+
</>
|
|
430
|
+
)}
|
|
431
|
+
|
|
432
|
+
<Tooltip content="Submit">
|
|
433
|
+
<Button
|
|
434
|
+
variant="text"
|
|
435
|
+
size="sm"
|
|
436
|
+
className="h-6 w-6 p-0 hover:bg-muted/30 cursor-pointer"
|
|
437
|
+
onClick={isLoading ? onStop : onSendClick}
|
|
438
|
+
disabled={isLoading ? false : isEmpty}
|
|
439
|
+
>
|
|
440
|
+
{isLoading ? (
|
|
441
|
+
<SquareIcon className="h-3 w-3 fill-current" />
|
|
442
|
+
) : (
|
|
443
|
+
<SendIcon className="h-3 w-3" />
|
|
444
|
+
)}
|
|
445
|
+
</Button>
|
|
446
|
+
</Tooltip>
|
|
447
|
+
</div>
|
|
309
448
|
</div>
|
|
310
|
-
|
|
311
|
-
variant="ghost"
|
|
312
|
-
size="sm"
|
|
313
|
-
className="h-6 w-6 p-0 hover:bg-muted/30"
|
|
314
|
-
onClick={isLoading ? onStop : onSendClick}
|
|
315
|
-
disabled={isLoading ? false : isEmpty}
|
|
316
|
-
>
|
|
317
|
-
{isLoading ? (
|
|
318
|
-
<SquareIcon className="h-3 w-3 fill-current" />
|
|
319
|
-
) : (
|
|
320
|
-
<SendIcon className="h-3 w-3" />
|
|
321
|
-
)}
|
|
322
|
-
</Button>
|
|
323
|
-
</div>
|
|
449
|
+
</TooltipProvider>
|
|
324
450
|
);
|
|
325
451
|
},
|
|
326
452
|
);
|
|
@@ -328,16 +454,33 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
|
|
|
328
454
|
ChatInputFooter.displayName = "ChatInputFooter";
|
|
329
455
|
|
|
330
456
|
interface ChatInputProps {
|
|
457
|
+
placeholder?: string;
|
|
331
458
|
input: string;
|
|
459
|
+
inputClassName?: string;
|
|
332
460
|
setInput: (value: string) => void;
|
|
333
461
|
onSubmit: (e: KeyboardEvent | undefined, value: string) => void;
|
|
334
462
|
inputRef: React.RefObject<ReactCodeMirrorRef | null>;
|
|
335
463
|
isLoading: boolean;
|
|
336
464
|
onStop: () => void;
|
|
465
|
+
onClose: () => void;
|
|
466
|
+
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
|
467
|
+
onAddFiles: (files: File[]) => void;
|
|
337
468
|
}
|
|
338
469
|
|
|
339
470
|
const ChatInput: React.FC<ChatInputProps> = memo(
|
|
340
|
-
({
|
|
471
|
+
({
|
|
472
|
+
placeholder,
|
|
473
|
+
input,
|
|
474
|
+
inputClassName,
|
|
475
|
+
setInput,
|
|
476
|
+
onSubmit,
|
|
477
|
+
inputRef,
|
|
478
|
+
isLoading,
|
|
479
|
+
onStop,
|
|
480
|
+
fileInputRef,
|
|
481
|
+
onAddFiles,
|
|
482
|
+
onClose,
|
|
483
|
+
}) => {
|
|
341
484
|
const handleSendClick = useEvent(() => {
|
|
342
485
|
if (input.trim()) {
|
|
343
486
|
onSubmit(undefined, input);
|
|
@@ -345,22 +488,26 @@ const ChatInput: React.FC<ChatInputProps> = memo(
|
|
|
345
488
|
});
|
|
346
489
|
|
|
347
490
|
return (
|
|
348
|
-
<div className="
|
|
349
|
-
<div className="px-2 py-3 flex-1">
|
|
491
|
+
<div className="relative shrink-0 min-h-[80px] flex flex-col border-t">
|
|
492
|
+
<div className={cn("px-2 py-3 flex-1", inputClassName)}>
|
|
350
493
|
<PromptInput
|
|
351
494
|
inputRef={inputRef}
|
|
352
495
|
value={input}
|
|
353
496
|
onChange={setInput}
|
|
354
497
|
onSubmit={onSubmit}
|
|
355
|
-
onClose={
|
|
356
|
-
|
|
498
|
+
onClose={onClose}
|
|
499
|
+
onAddFiles={onAddFiles}
|
|
500
|
+
placeholder={placeholder || "Type your message..."}
|
|
357
501
|
/>
|
|
358
502
|
</div>
|
|
359
503
|
<ChatInputFooter
|
|
360
504
|
isEmpty={!input.trim()}
|
|
505
|
+
onAddContext={() => addContextCompletion(inputRef)}
|
|
361
506
|
onSendClick={handleSendClick}
|
|
362
507
|
isLoading={isLoading}
|
|
363
508
|
onStop={onStop}
|
|
509
|
+
fileInputRef={fileInputRef}
|
|
510
|
+
onAddFiles={onAddFiles}
|
|
364
511
|
/>
|
|
365
512
|
</div>
|
|
366
513
|
);
|
|
@@ -369,7 +516,7 @@ const ChatInput: React.FC<ChatInputProps> = memo(
|
|
|
369
516
|
|
|
370
517
|
ChatInput.displayName = "ChatInput";
|
|
371
518
|
|
|
372
|
-
|
|
519
|
+
const ChatPanel = () => {
|
|
373
520
|
const aiEnabled = useAtomValue(aiEnabledAtom);
|
|
374
521
|
const { handleClick } = useOpenSettingsToTab();
|
|
375
522
|
|
|
@@ -392,130 +539,172 @@ export const ChatPanel = () => {
|
|
|
392
539
|
};
|
|
393
540
|
|
|
394
541
|
const ChatPanelBody = () => {
|
|
395
|
-
const
|
|
542
|
+
const setChatState = useSetAtom(chatStateAtom);
|
|
396
543
|
const [activeChat, setActiveChat] = useAtom(activeChatAtom);
|
|
544
|
+
const [input, setInput] = useState("");
|
|
397
545
|
const [newThreadInput, setNewThreadInput] = useState("");
|
|
546
|
+
const [files, setFiles] = useState<File[]>();
|
|
398
547
|
const newThreadInputRef = useRef<ReactCodeMirrorRef>(null);
|
|
399
548
|
const newMessageInputRef = useRef<ReactCodeMirrorRef>(null);
|
|
400
549
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
550
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
401
551
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
402
552
|
const runtimeManager = useRuntimeManager();
|
|
403
553
|
const { invokeAiTool } = useRequestClient();
|
|
404
554
|
|
|
555
|
+
const activeChatId = activeChat?.id;
|
|
556
|
+
|
|
405
557
|
const {
|
|
406
558
|
messages,
|
|
407
|
-
|
|
408
|
-
setInput,
|
|
409
|
-
setMessages,
|
|
410
|
-
append,
|
|
411
|
-
handleSubmit,
|
|
559
|
+
sendMessage,
|
|
412
560
|
error,
|
|
413
561
|
status,
|
|
414
|
-
|
|
562
|
+
regenerate,
|
|
415
563
|
stop,
|
|
564
|
+
addToolResult,
|
|
565
|
+
id: chatId,
|
|
416
566
|
} = useChat({
|
|
417
|
-
id:
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
567
|
+
id: activeChatId,
|
|
568
|
+
// Only automatically submit if we have tool calls but no text response yet
|
|
569
|
+
sendAutomaticallyWhen: ({ messages }) => {
|
|
570
|
+
if (messages.length === 0) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const lastMessage = messages[messages.length - 1];
|
|
575
|
+
const parts = lastMessage.parts;
|
|
576
|
+
|
|
577
|
+
if (parts.length === 0) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Only auto-send if the last message is an assistant message
|
|
582
|
+
// Because assistant messages are the ones that can have tool calls
|
|
583
|
+
if (lastMessage.role !== "assistant") {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const toolParts = parts.filter((part) =>
|
|
588
|
+
part.type.startsWith("tool-"),
|
|
589
|
+
) as ToolUIPart[];
|
|
590
|
+
|
|
591
|
+
const hasCompletedToolCalls = toolParts.some(
|
|
592
|
+
(part) => part.state === "output-available",
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// Check if the last part has any text content
|
|
596
|
+
const lastPart = parts[parts.length - 1];
|
|
597
|
+
const hasTextContent =
|
|
598
|
+
lastPart.type === "text" && lastPart.text?.trim().length > 0;
|
|
599
|
+
|
|
600
|
+
// Only auto-send if we have completed tool calls and there is no reply yet
|
|
601
|
+
return hasCompletedToolCalls && !hasTextContent;
|
|
433
602
|
},
|
|
434
|
-
|
|
603
|
+
messages: activeChat?.messages || [], // initial messages
|
|
604
|
+
transport: new DefaultChatTransport({
|
|
605
|
+
api: runtimeManager.getAiURL("chat").toString(),
|
|
606
|
+
headers: runtimeManager.headers(),
|
|
607
|
+
prepareSendMessagesRequest: async (options) => {
|
|
608
|
+
// Map from parts to a single string
|
|
609
|
+
function toContent(parts: UIMessage["parts"]): string {
|
|
610
|
+
return parts
|
|
611
|
+
.map((part) => (part.type === "text" ? part.text : ""))
|
|
612
|
+
.join("\n");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const input = toContent(options.messages.flatMap((m) => m.parts));
|
|
616
|
+
const completionBody = await getAICompletionBodyWithAttachments({
|
|
617
|
+
input,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Map from UIMessage to our ChatMessage type
|
|
621
|
+
// If it's the last message, add the attachments from the completion body
|
|
622
|
+
function mapMessage(m: UIMessage, isLastMessage: boolean): ChatMessage {
|
|
623
|
+
const parts = m.parts;
|
|
624
|
+
if (isLastMessage) {
|
|
625
|
+
parts.push(...completionBody.attachments);
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
role: m.role,
|
|
629
|
+
content: toContent(m.parts),
|
|
630
|
+
parts: parts,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
body: {
|
|
636
|
+
...options,
|
|
637
|
+
...completionBody.body,
|
|
638
|
+
messages: options.messages.map((m, idx) => ({
|
|
639
|
+
...m,
|
|
640
|
+
...mapMessage(m, idx === options.messages.length - 1),
|
|
641
|
+
})),
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
},
|
|
645
|
+
}),
|
|
646
|
+
onFinish: ({ messages }) => {
|
|
435
647
|
setChatState((prev) => {
|
|
436
|
-
return
|
|
437
|
-
prev,
|
|
438
|
-
prev.activeChatId,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
message.content,
|
|
442
|
-
message.parts,
|
|
443
|
-
);
|
|
648
|
+
return replaceMessagesInChat({
|
|
649
|
+
chatState: prev,
|
|
650
|
+
chatId: prev.activeChatId,
|
|
651
|
+
messages: messages,
|
|
652
|
+
});
|
|
444
653
|
});
|
|
445
654
|
},
|
|
446
655
|
onToolCall: async ({ toolCall }) => {
|
|
447
656
|
try {
|
|
448
657
|
const response = await invokeAiTool({
|
|
449
658
|
toolName: toolCall.toolName,
|
|
450
|
-
arguments: toolCall.
|
|
659
|
+
arguments: toolCall.input as Record<string, never>,
|
|
660
|
+
});
|
|
661
|
+
addToolResult({
|
|
662
|
+
tool: toolCall.toolName,
|
|
663
|
+
toolCallId: toolCall.toolCallId,
|
|
664
|
+
output: response.result || response.error,
|
|
451
665
|
});
|
|
452
|
-
|
|
453
|
-
// This response triggers the onFinish callback
|
|
454
|
-
return response.result || response.error;
|
|
455
666
|
} catch (error) {
|
|
456
667
|
Logger.error("Tool call failed:", error);
|
|
457
|
-
|
|
668
|
+
addToolResult({
|
|
669
|
+
tool: toolCall.toolName,
|
|
670
|
+
toolCallId: toolCall.toolCallId,
|
|
671
|
+
output: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
672
|
+
});
|
|
458
673
|
}
|
|
459
674
|
},
|
|
460
675
|
onError: (error) => {
|
|
461
676
|
Logger.error("An error occurred:", error);
|
|
462
677
|
},
|
|
463
|
-
onResponse: (response) => {
|
|
464
|
-
Logger.debug("Received HTTP response from server:", response);
|
|
465
|
-
},
|
|
466
678
|
});
|
|
467
679
|
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
// Sync user messages from useChat to storage when they become available
|
|
471
|
-
// Only when we are done loading, for performance.
|
|
472
|
-
useEffect(() => {
|
|
473
|
-
if (!chatState.activeChatId || messages.length === 0 || isLoading) {
|
|
680
|
+
const onAddFiles = useEvent((files: File[]) => {
|
|
681
|
+
if (files.length === 0) {
|
|
474
682
|
return;
|
|
475
683
|
}
|
|
476
684
|
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
return;
|
|
685
|
+
let fileSize = 0;
|
|
686
|
+
for (const file of files) {
|
|
687
|
+
fileSize += file.size;
|
|
481
688
|
}
|
|
482
689
|
|
|
483
|
-
|
|
484
|
-
|
|
690
|
+
if (fileSize > MAX_ATTACHMENT_SIZE) {
|
|
691
|
+
toast({
|
|
692
|
+
title: "File size exceeds 50MB limit",
|
|
693
|
+
description: "Please remove some files and try again.",
|
|
694
|
+
});
|
|
485
695
|
return;
|
|
486
696
|
}
|
|
487
697
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
// Find user messages from useChat that aren't in storage yet
|
|
491
|
-
const missingUserMessages = messages.filter(
|
|
492
|
-
(m) => m.role === "user" && !storedMessageIds.has(m.id),
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
if (missingUserMessages.length > 0) {
|
|
496
|
-
setChatState((prev) => {
|
|
497
|
-
let result = prev;
|
|
498
|
-
|
|
499
|
-
for (const userMessage of missingUserMessages) {
|
|
500
|
-
result = addMessageToChat(
|
|
501
|
-
result,
|
|
502
|
-
prev.activeChatId,
|
|
503
|
-
userMessage.id,
|
|
504
|
-
"user",
|
|
505
|
-
userMessage.content,
|
|
506
|
-
);
|
|
507
|
-
}
|
|
698
|
+
setFiles((prev) => [...(prev ?? []), ...files]);
|
|
699
|
+
});
|
|
508
700
|
|
|
509
|
-
|
|
510
|
-
|
|
701
|
+
const removeFile = useEvent((file: File) => {
|
|
702
|
+
if (files) {
|
|
703
|
+
setFiles(files.filter((f) => f !== file));
|
|
511
704
|
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
chatState.chats,
|
|
516
|
-
setChatState,
|
|
517
|
-
isLoading,
|
|
518
|
-
]);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const isLoading = status === "submitted" || status === "streaming";
|
|
519
708
|
|
|
520
709
|
// Check if we're currently streaming reasoning in the latest message
|
|
521
710
|
const isStreamingReasoning =
|
|
@@ -531,19 +720,19 @@ const ChatPanelBody = () => {
|
|
|
531
720
|
};
|
|
532
721
|
|
|
533
722
|
requestAnimationFrame(scrollToBottom);
|
|
534
|
-
}, [
|
|
723
|
+
}, [activeChatId]);
|
|
535
724
|
|
|
536
|
-
const createNewThread = (
|
|
537
|
-
|
|
725
|
+
const createNewThread = async (
|
|
726
|
+
initialMessage: string,
|
|
727
|
+
initialAttachments?: File[],
|
|
728
|
+
) => {
|
|
729
|
+
const now = Date.now();
|
|
538
730
|
const newChat: Chat = {
|
|
539
|
-
id:
|
|
540
|
-
title:
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
messages: [], // Don't pre-populate - let useChat handle it and sync back
|
|
545
|
-
createdAt: CURRENT_TIME,
|
|
546
|
-
updatedAt: CURRENT_TIME,
|
|
731
|
+
id: chatId as ChatId,
|
|
732
|
+
title: generateChatTitle(initialMessage),
|
|
733
|
+
messages: [],
|
|
734
|
+
createdAt: now,
|
|
735
|
+
updatedAt: now,
|
|
547
736
|
};
|
|
548
737
|
|
|
549
738
|
// Create new chat and set as active
|
|
@@ -558,12 +747,23 @@ const ChatPanelBody = () => {
|
|
|
558
747
|
return newState;
|
|
559
748
|
});
|
|
560
749
|
|
|
750
|
+
const fileParts =
|
|
751
|
+
initialAttachments && initialAttachments.length > 0
|
|
752
|
+
? await convertToFileUIPart(initialAttachments)
|
|
753
|
+
: undefined;
|
|
754
|
+
|
|
561
755
|
// Trigger AI conversation with append
|
|
562
|
-
|
|
563
|
-
id: generateUUID(),
|
|
756
|
+
sendMessage({
|
|
564
757
|
role: "user",
|
|
565
|
-
|
|
758
|
+
parts: [
|
|
759
|
+
{
|
|
760
|
+
type: "text" as const,
|
|
761
|
+
text: initialMessage,
|
|
762
|
+
},
|
|
763
|
+
...(fileParts ?? []),
|
|
764
|
+
],
|
|
566
765
|
});
|
|
766
|
+
setFiles(undefined);
|
|
567
767
|
setInput("");
|
|
568
768
|
};
|
|
569
769
|
|
|
@@ -571,51 +771,43 @@ const ChatPanelBody = () => {
|
|
|
571
771
|
setActiveChat(null);
|
|
572
772
|
setInput("");
|
|
573
773
|
setNewThreadInput("");
|
|
774
|
+
setFiles(undefined);
|
|
574
775
|
});
|
|
575
776
|
|
|
576
777
|
const handleMessageEdit = useEvent((index: number, newValue: string) => {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const activeChatId = chatState.activeChatId;
|
|
580
|
-
if (activeChatId) {
|
|
581
|
-
setChatState((prev) => {
|
|
582
|
-
const nextChats = new Map(prev.chats);
|
|
583
|
-
const activeChat = chatState.chats.get(activeChatId);
|
|
584
|
-
if (activeChat) {
|
|
585
|
-
nextChats.set(activeChat.id, {
|
|
586
|
-
...activeChat,
|
|
587
|
-
messages: activeChat.messages.slice(0, index),
|
|
588
|
-
updatedAt: Date.now(),
|
|
589
|
-
});
|
|
590
|
-
}
|
|
778
|
+
const editedMessage = messages[index];
|
|
779
|
+
const fileParts = editedMessage.parts?.filter((p) => p.type === "file");
|
|
591
780
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
};
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
append({
|
|
781
|
+
const messageId = editedMessage.id;
|
|
782
|
+
sendMessage({
|
|
783
|
+
messageId: messageId, // replace the message
|
|
600
784
|
role: "user",
|
|
601
|
-
|
|
785
|
+
parts: [{ type: "text", text: newValue }, ...fileParts],
|
|
602
786
|
});
|
|
603
787
|
});
|
|
604
788
|
|
|
605
789
|
const handleChatInputSubmit = useEvent(
|
|
606
|
-
(e: KeyboardEvent | undefined, newValue: string): void => {
|
|
790
|
+
async (e: KeyboardEvent | undefined, newValue: string): Promise<void> => {
|
|
607
791
|
if (!newValue.trim()) {
|
|
608
792
|
return;
|
|
609
793
|
}
|
|
610
794
|
if (newMessageInputRef.current?.view) {
|
|
611
795
|
storePrompt(newMessageInputRef.current.view);
|
|
612
796
|
}
|
|
613
|
-
|
|
797
|
+
const fileParts = files ? await convertToFileUIPart(files) : undefined;
|
|
798
|
+
|
|
799
|
+
e?.preventDefault();
|
|
800
|
+
sendMessage({
|
|
801
|
+
text: newValue,
|
|
802
|
+
files: fileParts,
|
|
803
|
+
});
|
|
804
|
+
setInput("");
|
|
805
|
+
setFiles(undefined);
|
|
614
806
|
},
|
|
615
807
|
);
|
|
616
808
|
|
|
617
809
|
const handleReload = () => {
|
|
618
|
-
|
|
810
|
+
regenerate();
|
|
619
811
|
};
|
|
620
812
|
|
|
621
813
|
const handleNewThreadSubmit = useEvent(() => {
|
|
@@ -625,16 +817,57 @@ const ChatPanelBody = () => {
|
|
|
625
817
|
if (newThreadInputRef.current?.view) {
|
|
626
818
|
storePrompt(newThreadInputRef.current.view);
|
|
627
819
|
}
|
|
628
|
-
createNewThread(newThreadInput.trim());
|
|
820
|
+
createNewThread(newThreadInput.trim(), files);
|
|
629
821
|
});
|
|
630
822
|
|
|
631
823
|
const handleOnCloseThread = () => newThreadInputRef.current?.editor?.blur();
|
|
632
824
|
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
825
|
+
const isNewThread = messages.length === 0;
|
|
826
|
+
const chatInput = isNewThread ? (
|
|
827
|
+
<ChatInput
|
|
828
|
+
key="new-thread-input"
|
|
829
|
+
placeholder={`Ask anything, ${CONTEXT_TRIGGER} to include context about tables or dataframes`}
|
|
830
|
+
input={newThreadInput}
|
|
831
|
+
inputRef={newThreadInputRef}
|
|
832
|
+
inputClassName="px-1 py-0"
|
|
833
|
+
setInput={setNewThreadInput}
|
|
834
|
+
onSubmit={handleNewThreadSubmit}
|
|
835
|
+
isLoading={isLoading}
|
|
836
|
+
onStop={stop}
|
|
837
|
+
fileInputRef={fileInputRef}
|
|
838
|
+
onAddFiles={onAddFiles}
|
|
839
|
+
onClose={handleOnCloseThread}
|
|
840
|
+
/>
|
|
841
|
+
) : (
|
|
842
|
+
<ChatInput
|
|
843
|
+
input={input}
|
|
844
|
+
setInput={setInput}
|
|
845
|
+
onSubmit={handleChatInputSubmit}
|
|
846
|
+
inputRef={newMessageInputRef}
|
|
847
|
+
isLoading={isLoading}
|
|
848
|
+
onStop={stop}
|
|
849
|
+
onClose={() => newMessageInputRef.current?.editor?.blur()}
|
|
850
|
+
fileInputRef={fileInputRef}
|
|
851
|
+
onAddFiles={onAddFiles}
|
|
852
|
+
/>
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
const filesPills = files && files.length > 0 && (
|
|
856
|
+
<div
|
|
857
|
+
className={cn(
|
|
858
|
+
"flex flex-row gap-1 flex-wrap p-1",
|
|
859
|
+
isNewThread && "py-2 px-1",
|
|
860
|
+
)}
|
|
861
|
+
>
|
|
862
|
+
{files?.map((file) => (
|
|
863
|
+
<FileAttachmentPill
|
|
864
|
+
file={file}
|
|
865
|
+
key={file.name}
|
|
866
|
+
onRemove={() => removeFile(file)}
|
|
867
|
+
/>
|
|
868
|
+
))}
|
|
869
|
+
</div>
|
|
870
|
+
);
|
|
638
871
|
|
|
639
872
|
return (
|
|
640
873
|
<div className="flex flex-col h-[calc(100%-53px)]">
|
|
@@ -643,7 +876,6 @@ const ChatPanelBody = () => {
|
|
|
643
876
|
onNewChat={handleNewChat}
|
|
644
877
|
activeChatId={activeChat?.id}
|
|
645
878
|
setActiveChat={setActiveChat}
|
|
646
|
-
chats={sortedChats}
|
|
647
879
|
/>
|
|
648
880
|
</TooltipProvider>
|
|
649
881
|
|
|
@@ -651,36 +883,19 @@ const ChatPanelBody = () => {
|
|
|
651
883
|
className="flex-1 px-3 bg-(--slate-1) gap-4 py-3 flex flex-col overflow-y-auto"
|
|
652
884
|
ref={scrollContainerRef}
|
|
653
885
|
>
|
|
654
|
-
{
|
|
655
|
-
<div className="
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
inputRef={newThreadInputRef}
|
|
659
|
-
key="new-thread-input"
|
|
660
|
-
value={newThreadInput}
|
|
661
|
-
placeholder="Ask anything, @ to include context about tables or dataframes"
|
|
662
|
-
onClose={handleOnCloseThread}
|
|
663
|
-
onChange={setNewThreadInput}
|
|
664
|
-
onSubmit={handleNewThreadSubmit}
|
|
665
|
-
/>
|
|
666
|
-
</div>
|
|
667
|
-
<ChatInputFooter
|
|
668
|
-
isEmpty={!newThreadInput.trim()}
|
|
669
|
-
onSendClick={handleNewThreadSubmit}
|
|
670
|
-
isLoading={isLoading}
|
|
671
|
-
onStop={stop}
|
|
672
|
-
/>
|
|
886
|
+
{isNewThread && (
|
|
887
|
+
<div className="rounded-md border bg-background">
|
|
888
|
+
{filesPills}
|
|
889
|
+
{chatInput}
|
|
673
890
|
</div>
|
|
674
891
|
)}
|
|
675
892
|
|
|
676
893
|
{messages.map((message, idx) => (
|
|
677
|
-
<
|
|
894
|
+
<ChatMessageDisplay
|
|
678
895
|
key={message.id}
|
|
679
896
|
message={message}
|
|
680
897
|
index={idx}
|
|
681
898
|
onEdit={handleMessageEdit}
|
|
682
|
-
setChatState={setChatState}
|
|
683
|
-
chatState={chatState}
|
|
684
899
|
isStreamingReasoning={isStreamingReasoning}
|
|
685
900
|
isLast={idx === messages.length - 1}
|
|
686
901
|
/>
|
|
@@ -712,40 +927,15 @@ const ChatPanelBody = () => {
|
|
|
712
927
|
</div>
|
|
713
928
|
)}
|
|
714
929
|
|
|
715
|
-
{
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
isLoading={isLoading}
|
|
722
|
-
onStop={stop}
|
|
723
|
-
/>
|
|
930
|
+
{/* For existing threads, we place the chat input at the bottom */}
|
|
931
|
+
{!isNewThread && (
|
|
932
|
+
<>
|
|
933
|
+
{filesPills}
|
|
934
|
+
{chatInput}
|
|
935
|
+
</>
|
|
724
936
|
)}
|
|
725
937
|
</div>
|
|
726
938
|
);
|
|
727
939
|
};
|
|
728
940
|
|
|
729
|
-
|
|
730
|
-
if (messages.length === 0) {
|
|
731
|
-
return false;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const lastMessage = messages.at(-1);
|
|
735
|
-
if (!lastMessage) {
|
|
736
|
-
return false;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (lastMessage.role !== "assistant" || !lastMessage.parts) {
|
|
740
|
-
return false;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
const parts = lastMessage.parts;
|
|
744
|
-
if (parts.length === 0) {
|
|
745
|
-
return false;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// Check if the last part is reasoning
|
|
749
|
-
const lastPart = parts[parts.length - 1];
|
|
750
|
-
return lastPart.type === "reasoning";
|
|
751
|
-
}
|
|
941
|
+
export default ChatPanel;
|