@marimo-team/frontend 0.15.1-dev14 → 0.15.1-dev16
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/assets/{ConnectedDataExplorerComponent-BBUI6vmA.js → ConnectedDataExplorerComponent-BPfZgm5R.js} +1 -1
- package/dist/assets/{ImageComparisonComponent-CX4CDvS_.js → ImageComparisonComponent-DN7EJDoX.js} +1 -1
- package/dist/assets/{VegaLite-BtjbyLAA.js → VegaLite-CUBSrB9h.js} +1 -1
- package/dist/assets/{_baseEach-Ca2O9Wne.js → _baseEach-COj8UYrB.js} +1 -1
- package/dist/assets/_baseMap-CzIRdvop.js +1 -0
- package/dist/assets/{_baseUniq-D4iU71YJ.js → _baseUniq-smyj1abf.js} +1 -1
- package/dist/assets/{_createAggregator-QKQ5gj2H.js → _createAggregator-BAk0hBur.js} +1 -1
- package/dist/assets/{any-language-editor-Pcus9OFi.js → any-language-editor-D4fMF9FA.js} +1 -1
- package/dist/assets/{architectureDiagram-KFL7JDKH-DwsPlBM_.js → architectureDiagram-KFL7JDKH-DBOzElPL.js} +1 -1
- package/dist/assets/{blockDiagram-ZYB65J3Q-B65IkzKX.js → blockDiagram-ZYB65J3Q-u8cQEOZT.js} +1 -1
- package/dist/assets/{c4Diagram-AAMF2YG6-DFmoIQ6u.js → c4Diagram-AAMF2YG6-DwbICH2A.js} +1 -1
- package/dist/assets/channel-KTUeCTOf.js +1 -0
- package/dist/assets/{chunk-ANTBXLJU-BPOzSwai.js → chunk-ANTBXLJU-Dh4bjrG_.js} +1 -1
- package/dist/assets/{chunk-FHKO5MBM-BbM8E9B5.js → chunk-FHKO5MBM-CcxXcZ33.js} +1 -1
- package/dist/assets/{chunk-GLLZNHP4-BqFTg_51.js → chunk-GLLZNHP4-Dmy2T6iT.js} +1 -1
- package/dist/assets/{chunk-JBRWN2VN-DMbINMTT.js → chunk-JBRWN2VN-DF6eV4wI.js} +1 -1
- package/dist/assets/{chunk-LXBSTHXV-C6VjboaW.js → chunk-LXBSTHXV-BZK5AZPH.js} +1 -1
- package/dist/assets/{chunk-NRVI72HA-CQ-LV0Dy.js → chunk-NRVI72HA-BSUK3njt.js} +1 -1
- package/dist/assets/{chunk-OMD6QJNC-Cwl2YrS3.js → chunk-OMD6QJNC-DQeyHllN.js} +1 -1
- package/dist/assets/{chunk-WVR4S24B-B9Jid-bk.js → chunk-WVR4S24B-BrcfqJzw.js} +1 -1
- package/dist/assets/{circle-play-BZUxpncG.js → circle-play-yBw8lfzY.js} +1 -1
- package/dist/assets/classDiagram-3BZAVTQC-DPHwWa4N.js +1 -0
- package/dist/assets/classDiagram-v2-QTMF73CY-DPHwWa4N.js +1 -0
- package/dist/assets/clone-BmLPfKbO.js +1 -0
- package/dist/assets/{compile-yJZ0Xdaw.js → compile-Bec0k3mh.js} +1 -1
- package/dist/assets/{dagre-2BBEFEWP-D8ZiA7Qh.js → dagre-2BBEFEWP-P-g9NZ2K.js} +1 -1
- package/dist/assets/{data-grid-overlay-editor-DU4SCMnn.js → data-grid-overlay-editor-COTyuTA0.js} +1 -1
- package/dist/assets/{diagram-4IRLE6MV-n4YVHjql.js → diagram-4IRLE6MV-BSJp0gka.js} +1 -1
- package/dist/assets/{diagram-GUPCWM2R-Bo5QTdQd.js → diagram-GUPCWM2R-DRjfoI14.js} +1 -1
- package/dist/assets/{diagram-RP2FKANI-Dfj2_fkX.js → diagram-RP2FKANI-CdIkZYD2.js} +1 -1
- package/dist/assets/{edit-page-DKcm_3qI.js → edit-page-DZr7-GvB.js} +56 -56
- package/dist/assets/{erDiagram-HZWUO2LU-DZzYtx3L.js → erDiagram-HZWUO2LU-DTHN9I8P.js} +1 -1
- package/dist/assets/{flowDiagram-THRYKUMA-BGysw2Yl.js → flowDiagram-THRYKUMA-BTCjKp92.js} +1 -1
- package/dist/assets/{ganttDiagram-WV7ZQ7D5-bevExrPn.js → ganttDiagram-WV7ZQ7D5-DRi68HWD.js} +1 -1
- package/dist/assets/{gitGraphDiagram-OJR772UL-Cz-LKk6z.js → gitGraphDiagram-OJR772UL-DRj-3FPQ.js} +1 -1
- package/dist/assets/{glide-data-editor-_ErWjEGN.js → glide-data-editor-GC2Tm7eO.js} +4 -4
- package/dist/assets/{graph-BotwWgC_.js → graph-R7CWosbg.js} +1 -1
- package/dist/assets/{home-page-Gm-HyQW-.js → home-page-CyNI8llk.js} +1 -1
- package/dist/assets/{index-bFhhN-yc.js → index-Bpq6foom.js} +1 -1
- package/dist/assets/{index-BJQhAWxG.js → index-C2E3cOJc.js} +1 -1
- package/dist/assets/{index-u0nnWJen.js → index-CD2ivQTR.js} +1 -1
- package/dist/assets/{index-Bdm26Lv3.js → index-CG3NSOEk.js} +1 -1
- package/dist/assets/{index-LsvpkwND.js → index-COYAjgyj.js} +1 -1
- package/dist/assets/{index-C2JHPfAE.js → index-Cv0VD6q8.js} +1 -1
- package/dist/assets/{index-B8DEeCJ-.js → index-D-HWwsQB.js} +1 -1
- package/dist/assets/{index-B8Xj7x_9.js → index-DF0A4qiq.js} +1 -1
- package/dist/assets/{index-C4NF73JD.js → index-DKNnr1M-.js} +1 -1
- package/dist/assets/{index-D7PpFqo7.js → index-DTug6sja.js} +1 -1
- package/dist/assets/{index-BcvK_4kl.js → index-DUFKeg2p.js} +1 -1
- package/dist/assets/{index-boRDRVH5.js → index-DcmLW1fG.js} +1 -1
- package/dist/assets/{index-D31E2jjI.js → index-DePLe-OM.js} +1 -1
- package/dist/assets/{index-DQYkp82V.js → index-EZIz7w08.js} +1 -1
- package/dist/assets/{index-BvXaqQ0m.js → index-El7wdsft.js} +1 -1
- package/dist/assets/{index-KDj__fqX.js → index-P9NNDpC1.js} +1 -1
- package/dist/assets/{index-BhSNFJlD.js → index-Tc_0hlVK.js} +131 -131
- package/dist/assets/{index-BAML-avM.js → index-ZK30DKfs.js} +1 -1
- package/dist/assets/{index-D_fBaBkI.js → index-kC7oVlSz.js} +1 -1
- package/dist/assets/{index-D22p-NeS.js → index-sPITtia-.js} +1 -1
- package/dist/assets/{infoDiagram-6WOFNB3A-EUOwUSFP.js → infoDiagram-6WOFNB3A-7y7TIB48.js} +1 -1
- package/dist/assets/{journeyDiagram-FFXJYRFH-CB9jZPwI.js → journeyDiagram-FFXJYRFH-C6FV9piB.js} +1 -1
- package/dist/assets/{kanban-definition-KOZQBZVT-BHbgzVC3.js → kanban-definition-KOZQBZVT-BGil-OkO.js} +1 -1
- package/dist/assets/{layout-BZvWSid2.js → layout-D7woCajw.js} +1 -1
- package/dist/assets/{linear-EdVDlbvK.js → linear-CqdRUecd.js} +1 -1
- package/dist/assets/{links-CO7cA2Fg.js → links-a9aaa35o.js} +1 -1
- package/dist/assets/{mermaid-DsKZ0k6o.js → mermaid-Axj__7kG.js} +4 -4
- package/dist/assets/{min-CnCkyEM4.js → min-B893_0jy.js} +1 -1
- package/dist/assets/{mindmap-definition-LNHGMQRG-Js0ddZCg.js → mindmap-definition-LNHGMQRG-CZUWGNhT.js} +1 -1
- package/dist/assets/{number-overlay-editor-sEOE1Lxn.js → number-overlay-editor-DroE0_Ko.js} +1 -1
- package/dist/assets/{pieDiagram-DBDJKBY4-6mOM4izU.js → pieDiagram-DBDJKBY4-CbR4R3Rc.js} +1 -1
- package/dist/assets/{quadrantDiagram-YPSRARAO-BGEtnc0_.js → quadrantDiagram-YPSRARAO-CeooR17T.js} +1 -1
- package/dist/assets/{react-plotly-J0l9XhwH.js → react-plotly-CuC-he6i.js} +1 -1
- package/dist/assets/{requirementDiagram-EGVEC5DT-CmouXJCf.js → requirementDiagram-EGVEC5DT-BkimxR14.js} +1 -1
- package/dist/assets/{run-page-Kbd0Syqg.js → run-page-BVwqPRN6.js} +1 -1
- package/dist/assets/{sankeyDiagram-HRAUVNP4-B8B8bCqh.js → sankeyDiagram-HRAUVNP4-CTZKv6rj.js} +1 -1
- package/dist/assets/{sequenceDiagram-WFGC7UMF-BCnoyrnn.js → sequenceDiagram-WFGC7UMF-CItujBr8.js} +1 -1
- package/dist/assets/{slides-component-C7vRb0EU.js → slides-component-CFYgEqEh.js} +1 -1
- package/dist/assets/{sortBy-MlLjgbjX.js → sortBy-DxJYl_MG.js} +1 -1
- package/dist/assets/{stateDiagram-UUKSUZ4H-BqpQEZpf.js → stateDiagram-UUKSUZ4H-CVflrtzd.js} +1 -1
- package/dist/assets/stateDiagram-v2-EYPG3UTE-CaL-mSFo.js +1 -0
- package/dist/assets/{storage-K_hnzEm3.js → storage-Cnne9zKF.js} +3 -3
- package/dist/assets/{terminal-YyumG7Qd.js → terminal-cp6Ptqd0.js} +1 -1
- package/dist/assets/{time-D6Mn02Nz.js → time-DGreD2Zc.js} +1 -1
- package/dist/assets/{timeline-definition-3HZDQTIS-CoishIBE.js → timeline-definition-3HZDQTIS-V85f9_hM.js} +1 -1
- package/dist/assets/{tracing-NWwbuXzA.js → tracing-Cznlg8Fk.js} +2 -2
- package/dist/assets/{trash-CljYMg5D.js → trash-B3RKvBUN.js} +1 -1
- package/dist/assets/{treemap-75Q7IDZK-BrZyfDQ2.js → treemap-75Q7IDZK-cKRL_h4o.js} +1 -1
- package/dist/assets/{vega-component-DfKv95vf.js → vega-component-CzgePQFb.js} +1 -1
- package/dist/assets/{xychartDiagram-FDP5SA34-Hi8NAr2N.js → xychartDiagram-FDP5SA34-D71Ck33W.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/__tests__/chat-utils.test.ts +50 -36
- package/src/components/chat/chat-panel.tsx +91 -68
- package/src/components/chat/markdown-renderer.tsx +29 -16
- package/src/core/ai/chat-utils.ts +13 -16
- package/src/core/ai/state.ts +30 -10
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -9
- package/src/core/codemirror/language/languages/sql/utils.ts +13 -1
- package/src/utils/__tests__/storage.test.ts +144 -0
- package/src/utils/storage.ts +38 -0
- package/dist/assets/_baseMap-CeNM_WhA.js +0 -1
- package/dist/assets/channel-fQDJ54fB.js +0 -1
- package/dist/assets/classDiagram-3BZAVTQC-CFX5hd8N.js +0 -1
- package/dist/assets/classDiagram-v2-QTMF73CY-CFX5hd8N.js +0 -1
- package/dist/assets/clone-sKX7VVYQ.js +0 -1
- package/dist/assets/stateDiagram-v2-EYPG3UTE-CQ0TxHAR.js +0 -1
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { Maps } from "@/utils/maps";
|
|
4
5
|
import { addMessageToChat } from "../core/ai/chat-utils";
|
|
5
|
-
import type { ChatState } from "../core/ai/state";
|
|
6
|
+
import type { Chat, ChatId, ChatState } from "../core/ai/state";
|
|
7
|
+
|
|
8
|
+
const CHAT_1 = "chat-1" as ChatId;
|
|
9
|
+
const CHAT_2 = "chat-2" as ChatId;
|
|
10
|
+
|
|
11
|
+
function first(map: Map<ChatId, Chat>) {
|
|
12
|
+
return [...map.values()][0];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function asMap(list: Iterable<Chat>) {
|
|
16
|
+
return Maps.keyBy(list, (c) => c.id);
|
|
17
|
+
}
|
|
6
18
|
|
|
7
19
|
describe("addMessageToChat", () => {
|
|
8
20
|
const mockChatState: ChatState = {
|
|
9
|
-
chats: [
|
|
21
|
+
chats: asMap([
|
|
10
22
|
{
|
|
11
|
-
id:
|
|
23
|
+
id: CHAT_1,
|
|
12
24
|
title: "Test Chat 1",
|
|
13
25
|
messages: [
|
|
14
26
|
{
|
|
@@ -28,7 +40,7 @@ describe("addMessageToChat", () => {
|
|
|
28
40
|
updatedAt: 2000,
|
|
29
41
|
},
|
|
30
42
|
{
|
|
31
|
-
id:
|
|
43
|
+
id: CHAT_2,
|
|
32
44
|
title: "Test Chat 2",
|
|
33
45
|
messages: [
|
|
34
46
|
{
|
|
@@ -41,21 +53,21 @@ describe("addMessageToChat", () => {
|
|
|
41
53
|
createdAt: 3000,
|
|
42
54
|
updatedAt: 3000,
|
|
43
55
|
},
|
|
44
|
-
],
|
|
45
|
-
activeChatId:
|
|
56
|
+
]),
|
|
57
|
+
activeChatId: CHAT_1,
|
|
46
58
|
};
|
|
47
59
|
|
|
48
60
|
it("should add a new message to an existing chat", () => {
|
|
49
61
|
const result = addMessageToChat(
|
|
50
62
|
mockChatState,
|
|
51
|
-
|
|
63
|
+
CHAT_1,
|
|
52
64
|
"msg-4",
|
|
53
65
|
"user",
|
|
54
66
|
"New message",
|
|
55
67
|
);
|
|
56
68
|
|
|
57
69
|
expect(result.chats).toHaveLength(2);
|
|
58
|
-
const updatedChat = result.chats.
|
|
70
|
+
const updatedChat = result.chats.get(CHAT_1);
|
|
59
71
|
expect(updatedChat?.messages).toHaveLength(3);
|
|
60
72
|
expect(updatedChat?.messages[2]).toEqual({
|
|
61
73
|
id: "msg-4",
|
|
@@ -64,21 +76,21 @@ describe("addMessageToChat", () => {
|
|
|
64
76
|
timestamp: expect.any(Number),
|
|
65
77
|
});
|
|
66
78
|
expect(updatedChat?.updatedAt).toBeGreaterThan(
|
|
67
|
-
mockChatState.chats
|
|
79
|
+
first(mockChatState.chats).updatedAt,
|
|
68
80
|
);
|
|
69
81
|
});
|
|
70
82
|
|
|
71
83
|
it("should update an existing message", () => {
|
|
72
84
|
const result = addMessageToChat(
|
|
73
85
|
mockChatState,
|
|
74
|
-
|
|
86
|
+
CHAT_1,
|
|
75
87
|
"msg-1",
|
|
76
88
|
"user",
|
|
77
89
|
"Updated content",
|
|
78
90
|
);
|
|
79
91
|
|
|
80
92
|
expect(result.chats).toHaveLength(2);
|
|
81
|
-
const updatedChat = result.chats.
|
|
93
|
+
const updatedChat = result.chats.get(CHAT_1);
|
|
82
94
|
expect(updatedChat?.messages).toHaveLength(2);
|
|
83
95
|
expect(updatedChat?.messages[0]).toEqual({
|
|
84
96
|
id: "msg-1",
|
|
@@ -87,7 +99,7 @@ describe("addMessageToChat", () => {
|
|
|
87
99
|
timestamp: 1000,
|
|
88
100
|
});
|
|
89
101
|
expect(updatedChat?.updatedAt).toBeGreaterThan(
|
|
90
|
-
mockChatState.chats
|
|
102
|
+
first(mockChatState.chats).updatedAt,
|
|
91
103
|
);
|
|
92
104
|
});
|
|
93
105
|
|
|
@@ -95,48 +107,49 @@ describe("addMessageToChat", () => {
|
|
|
95
107
|
const parts = [{ type: "text" as const, text: "Part content" }];
|
|
96
108
|
const result = addMessageToChat(
|
|
97
109
|
mockChatState,
|
|
98
|
-
|
|
110
|
+
CHAT_1,
|
|
99
111
|
"msg-5",
|
|
100
112
|
"assistant",
|
|
101
113
|
"Message with parts",
|
|
102
114
|
parts,
|
|
103
115
|
);
|
|
104
116
|
|
|
105
|
-
const updatedChat = result.chats.
|
|
117
|
+
const updatedChat = result.chats.get(CHAT_1);
|
|
106
118
|
expect(updatedChat?.messages[2].parts).toEqual(parts);
|
|
107
119
|
});
|
|
108
120
|
|
|
109
121
|
it("should update message parts", () => {
|
|
110
122
|
const originalParts = [{ type: "text" as const, text: "Original" }];
|
|
111
123
|
const updatedParts = [{ type: "text" as const, text: "Updated" }];
|
|
124
|
+
const chats = [...mockChatState.chats.values()];
|
|
112
125
|
|
|
113
126
|
const stateWithParts: ChatState = {
|
|
114
127
|
...mockChatState,
|
|
115
|
-
chats: [
|
|
128
|
+
chats: asMap([
|
|
116
129
|
{
|
|
117
|
-
...
|
|
130
|
+
...chats[0],
|
|
118
131
|
messages: [
|
|
119
132
|
{
|
|
120
|
-
...
|
|
133
|
+
...chats[0].messages[0],
|
|
121
134
|
parts: originalParts,
|
|
122
135
|
},
|
|
123
|
-
|
|
136
|
+
chats[0].messages[1],
|
|
124
137
|
],
|
|
125
138
|
},
|
|
126
|
-
|
|
127
|
-
],
|
|
139
|
+
chats[1],
|
|
140
|
+
]),
|
|
128
141
|
};
|
|
129
142
|
|
|
130
143
|
const result = addMessageToChat(
|
|
131
144
|
stateWithParts,
|
|
132
|
-
|
|
145
|
+
CHAT_1,
|
|
133
146
|
"msg-1",
|
|
134
147
|
"user",
|
|
135
148
|
"Updated content",
|
|
136
149
|
updatedParts,
|
|
137
150
|
);
|
|
138
151
|
|
|
139
|
-
const updatedChat = result.chats.
|
|
152
|
+
const updatedChat = result.chats.get(CHAT_1);
|
|
140
153
|
expect(updatedChat?.messages[0].parts).toEqual(updatedParts);
|
|
141
154
|
});
|
|
142
155
|
|
|
@@ -155,7 +168,7 @@ describe("addMessageToChat", () => {
|
|
|
155
168
|
it("should return unchanged state when chatId does not exist", () => {
|
|
156
169
|
const result = addMessageToChat(
|
|
157
170
|
mockChatState,
|
|
158
|
-
"non-existent-chat",
|
|
171
|
+
"non-existent-chat" as ChatId,
|
|
159
172
|
"msg-4",
|
|
160
173
|
"user",
|
|
161
174
|
"New message",
|
|
@@ -167,54 +180,55 @@ describe("addMessageToChat", () => {
|
|
|
167
180
|
it("should not modify other chats when updating a specific chat", () => {
|
|
168
181
|
const result = addMessageToChat(
|
|
169
182
|
mockChatState,
|
|
170
|
-
|
|
183
|
+
CHAT_1,
|
|
171
184
|
"msg-4",
|
|
172
185
|
"user",
|
|
173
186
|
"New message",
|
|
174
187
|
);
|
|
175
188
|
|
|
176
|
-
const unchangedChat = result.chats.
|
|
177
|
-
expect(unchangedChat).toEqual(mockChatState.chats[1]);
|
|
189
|
+
const unchangedChat = result.chats.get(CHAT_2);
|
|
190
|
+
expect(unchangedChat).toEqual([...mockChatState.chats.values()][1]);
|
|
178
191
|
});
|
|
179
192
|
|
|
180
193
|
it("should preserve message order when adding new messages", () => {
|
|
181
194
|
const result = addMessageToChat(
|
|
182
195
|
mockChatState,
|
|
183
|
-
|
|
196
|
+
CHAT_1,
|
|
184
197
|
"msg-4",
|
|
185
198
|
"user",
|
|
186
199
|
"New message",
|
|
187
200
|
);
|
|
188
201
|
|
|
189
|
-
const updatedChat = result.chats.
|
|
202
|
+
const updatedChat = result.chats.get(CHAT_1);
|
|
190
203
|
expect(updatedChat?.messages[0].id).toBe("msg-1");
|
|
191
204
|
expect(updatedChat?.messages[1].id).toBe("msg-2");
|
|
192
205
|
expect(updatedChat?.messages[2].id).toBe("msg-4");
|
|
193
206
|
});
|
|
194
207
|
|
|
195
208
|
it("should handle empty chat messages array", () => {
|
|
209
|
+
const chatId = "empty-chat" as ChatId;
|
|
196
210
|
const emptyChatState: ChatState = {
|
|
197
|
-
chats: [
|
|
211
|
+
chats: asMap([
|
|
198
212
|
{
|
|
199
|
-
id:
|
|
213
|
+
id: chatId,
|
|
200
214
|
title: "Empty Chat",
|
|
201
215
|
messages: [],
|
|
202
216
|
createdAt: 1000,
|
|
203
217
|
updatedAt: 1000,
|
|
204
218
|
},
|
|
205
|
-
],
|
|
206
|
-
activeChatId:
|
|
219
|
+
]),
|
|
220
|
+
activeChatId: chatId,
|
|
207
221
|
};
|
|
208
222
|
|
|
209
223
|
const result = addMessageToChat(
|
|
210
224
|
emptyChatState,
|
|
211
|
-
|
|
225
|
+
chatId,
|
|
212
226
|
"msg-1",
|
|
213
227
|
"user",
|
|
214
228
|
"First message",
|
|
215
229
|
);
|
|
216
230
|
|
|
217
|
-
const updatedChat = result.chats.
|
|
231
|
+
const updatedChat = result.chats.get(chatId);
|
|
218
232
|
expect(updatedChat?.messages).toHaveLength(1);
|
|
219
233
|
expect(updatedChat?.messages[0].content).toBe("First message");
|
|
220
234
|
});
|
|
@@ -222,13 +236,13 @@ describe("addMessageToChat", () => {
|
|
|
222
236
|
it("should handle different message roles", () => {
|
|
223
237
|
const result = addMessageToChat(
|
|
224
238
|
mockChatState,
|
|
225
|
-
|
|
239
|
+
CHAT_1,
|
|
226
240
|
"msg-4",
|
|
227
241
|
"assistant",
|
|
228
242
|
"Assistant response",
|
|
229
243
|
);
|
|
230
244
|
|
|
231
|
-
const updatedChat = result.chats.
|
|
245
|
+
const updatedChat = result.chats.get(CHAT_1);
|
|
232
246
|
expect(updatedChat?.messages[2].role).toBe("assistant");
|
|
233
247
|
});
|
|
234
248
|
});
|
|
@@ -18,9 +18,11 @@ import {
|
|
|
18
18
|
memo,
|
|
19
19
|
type SetStateAction,
|
|
20
20
|
useEffect,
|
|
21
|
+
useMemo,
|
|
21
22
|
useRef,
|
|
22
23
|
useState,
|
|
23
24
|
} from "react";
|
|
25
|
+
import useEvent from "react-use-event-hook";
|
|
24
26
|
import { Button } from "@/components/ui/button";
|
|
25
27
|
import {
|
|
26
28
|
Popover,
|
|
@@ -41,6 +43,7 @@ import { useModelChange } from "@/core/ai/config";
|
|
|
41
43
|
import {
|
|
42
44
|
activeChatAtom,
|
|
43
45
|
type Chat,
|
|
46
|
+
type ChatId,
|
|
44
47
|
type ChatState,
|
|
45
48
|
chatStateAtom,
|
|
46
49
|
} from "@/core/ai/state";
|
|
@@ -68,8 +71,8 @@ import { ToolCallAccordion } from "./tool-call-accordion";
|
|
|
68
71
|
|
|
69
72
|
interface ChatHeaderProps {
|
|
70
73
|
onNewChat: () => void;
|
|
71
|
-
activeChatId:
|
|
72
|
-
setActiveChat: (id:
|
|
74
|
+
activeChatId: ChatId | undefined;
|
|
75
|
+
setActiveChat: (id: ChatId | null) => void;
|
|
73
76
|
chats: Chat[];
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -151,11 +154,11 @@ interface ChatMessageProps {
|
|
|
151
154
|
setChatState: Dispatch<SetStateAction<ChatState>>;
|
|
152
155
|
chatState: ChatState;
|
|
153
156
|
isStreamingReasoning: boolean;
|
|
154
|
-
|
|
157
|
+
isLast: boolean;
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
const ChatMessage: React.FC<ChatMessageProps> = memo(
|
|
158
|
-
({ message, index, onEdit, isStreamingReasoning,
|
|
161
|
+
({ message, index, onEdit, isStreamingReasoning, isLast }) => (
|
|
159
162
|
<div
|
|
160
163
|
className={cn(
|
|
161
164
|
"flex group relative",
|
|
@@ -199,7 +202,7 @@ const ChatMessage: React.FC<ChatMessageProps> = memo(
|
|
|
199
202
|
key={i}
|
|
200
203
|
index={i}
|
|
201
204
|
isStreaming={
|
|
202
|
-
|
|
205
|
+
isLast &&
|
|
203
206
|
isStreamingReasoning &&
|
|
204
207
|
// If there are multiple reasoning parts, only show the last one
|
|
205
208
|
i === (message.parts?.length || 0) - 1
|
|
@@ -235,7 +238,7 @@ const ChatMessage: React.FC<ChatMessageProps> = memo(
|
|
|
235
238
|
ChatMessage.displayName = "ChatMessage";
|
|
236
239
|
|
|
237
240
|
interface ChatInputFooterProps {
|
|
238
|
-
|
|
241
|
+
isEmpty: boolean;
|
|
239
242
|
onSendClick: () => void;
|
|
240
243
|
isLoading: boolean;
|
|
241
244
|
onStop: () => void;
|
|
@@ -244,7 +247,7 @@ interface ChatInputFooterProps {
|
|
|
244
247
|
const DEFAULT_MODE = "manual";
|
|
245
248
|
|
|
246
249
|
const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
|
|
247
|
-
({
|
|
250
|
+
({ isEmpty, onSendClick, isLoading, onStop }) => {
|
|
248
251
|
const ai = useAtomValue(aiAtom);
|
|
249
252
|
const currentMode = ai?.mode || DEFAULT_MODE;
|
|
250
253
|
const currentModel = ai?.models?.chat_model || DEFAULT_AI_MODEL;
|
|
@@ -308,7 +311,7 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
|
|
|
308
311
|
size="sm"
|
|
309
312
|
className="h-6 w-6 p-0 hover:bg-muted/30"
|
|
310
313
|
onClick={isLoading ? onStop : onSendClick}
|
|
311
|
-
disabled={isLoading ? false :
|
|
314
|
+
disabled={isLoading ? false : isEmpty}
|
|
312
315
|
>
|
|
313
316
|
{isLoading ? (
|
|
314
317
|
<SquareIcon className="h-3 w-3 fill-current" />
|
|
@@ -334,11 +337,11 @@ interface ChatInputProps {
|
|
|
334
337
|
|
|
335
338
|
const ChatInput: React.FC<ChatInputProps> = memo(
|
|
336
339
|
({ input, setInput, onSubmit, inputRef, isLoading, onStop }) => {
|
|
337
|
-
const handleSendClick = () => {
|
|
340
|
+
const handleSendClick = useEvent(() => {
|
|
338
341
|
if (input.trim()) {
|
|
339
342
|
onSubmit(undefined, input);
|
|
340
343
|
}
|
|
341
|
-
};
|
|
344
|
+
});
|
|
342
345
|
|
|
343
346
|
return (
|
|
344
347
|
<div className="border-t relative shrink-0 min-h-[80px] flex flex-col">
|
|
@@ -352,7 +355,7 @@ const ChatInput: React.FC<ChatInputProps> = memo(
|
|
|
352
355
|
/>
|
|
353
356
|
</div>
|
|
354
357
|
<ChatInputFooter
|
|
355
|
-
|
|
358
|
+
isEmpty={!input.trim()}
|
|
356
359
|
onSendClick={handleSendClick}
|
|
357
360
|
isLoading={isLoading}
|
|
358
361
|
onStop={onStop}
|
|
@@ -463,8 +466,9 @@ const ChatPanelBody = () => {
|
|
|
463
466
|
const isLoading = status === "submitted" || status === "streaming";
|
|
464
467
|
|
|
465
468
|
// Sync user messages from useChat to storage when they become available
|
|
469
|
+
// Only when we are done loading, for performance.
|
|
466
470
|
useEffect(() => {
|
|
467
|
-
if (!chatState.activeChatId || messages.length === 0) {
|
|
471
|
+
if (!chatState.activeChatId || messages.length === 0 || isLoading) {
|
|
468
472
|
return;
|
|
469
473
|
}
|
|
470
474
|
|
|
@@ -474,9 +478,7 @@ const ChatPanelBody = () => {
|
|
|
474
478
|
return;
|
|
475
479
|
}
|
|
476
480
|
|
|
477
|
-
const currentChat = chatState.chats.
|
|
478
|
-
(c) => c.id === chatState.activeChatId,
|
|
479
|
-
);
|
|
481
|
+
const currentChat = chatState.chats.get(chatState.activeChatId);
|
|
480
482
|
if (!currentChat) {
|
|
481
483
|
return;
|
|
482
484
|
}
|
|
@@ -505,31 +507,13 @@ const ChatPanelBody = () => {
|
|
|
505
507
|
return result;
|
|
506
508
|
});
|
|
507
509
|
}
|
|
508
|
-
}, [
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const lastMessage = messages.at(-1);
|
|
516
|
-
if (!lastMessage) {
|
|
517
|
-
return false;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (lastMessage.role !== "assistant" || !lastMessage.parts) {
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const parts = lastMessage.parts;
|
|
525
|
-
if (parts.length === 0) {
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Check if the last part is reasoning
|
|
530
|
-
const lastPart = parts[parts.length - 1];
|
|
531
|
-
return lastPart.type === "reasoning";
|
|
532
|
-
};
|
|
510
|
+
}, [
|
|
511
|
+
messages,
|
|
512
|
+
chatState.activeChatId,
|
|
513
|
+
chatState.chats,
|
|
514
|
+
setChatState,
|
|
515
|
+
isLoading,
|
|
516
|
+
]);
|
|
533
517
|
|
|
534
518
|
// Check if we're currently streaming reasoning in the latest message
|
|
535
519
|
const isStreamingReasoning =
|
|
@@ -550,7 +534,7 @@ const ChatPanelBody = () => {
|
|
|
550
534
|
const createNewThread = (initialMessage: string) => {
|
|
551
535
|
const CURRENT_TIME = Date.now();
|
|
552
536
|
const newChat: Chat = {
|
|
553
|
-
id: generateUUID(),
|
|
537
|
+
id: generateUUID() as ChatId,
|
|
554
538
|
title:
|
|
555
539
|
initialMessage.length > 50
|
|
556
540
|
? `${initialMessage.slice(0, 50)}...`
|
|
@@ -562,53 +546,59 @@ const ChatPanelBody = () => {
|
|
|
562
546
|
|
|
563
547
|
// Create new chat and set as active
|
|
564
548
|
setChatState((prev) => {
|
|
549
|
+
const newChats = new Map(prev.chats);
|
|
550
|
+
newChats.set(newChat.id, newChat);
|
|
565
551
|
const newState = {
|
|
566
552
|
...prev,
|
|
567
|
-
chats:
|
|
553
|
+
chats: newChats,
|
|
568
554
|
activeChatId: newChat.id,
|
|
569
555
|
};
|
|
570
556
|
return newState;
|
|
571
557
|
});
|
|
572
558
|
|
|
573
559
|
// Trigger AI conversation with append
|
|
574
|
-
const MESSAGE_ID = generateUUID();
|
|
575
560
|
append({
|
|
576
|
-
id:
|
|
561
|
+
id: generateUUID(),
|
|
577
562
|
role: "user",
|
|
578
563
|
content: initialMessage,
|
|
579
564
|
});
|
|
580
565
|
setInput("");
|
|
581
566
|
};
|
|
582
567
|
|
|
583
|
-
const handleNewChat = () => {
|
|
568
|
+
const handleNewChat = useEvent(() => {
|
|
584
569
|
setActiveChat(null);
|
|
585
570
|
setInput("");
|
|
586
571
|
setNewThreadInput("");
|
|
587
|
-
};
|
|
572
|
+
});
|
|
588
573
|
|
|
589
|
-
const handleMessageEdit = (index: number, newValue: string) => {
|
|
574
|
+
const handleMessageEdit = useEvent((index: number, newValue: string) => {
|
|
590
575
|
// Truncate both useChat and storage
|
|
591
576
|
setMessages((messages) => messages.slice(0, index));
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
577
|
+
const activeChatId = chatState.activeChatId;
|
|
578
|
+
if (activeChatId) {
|
|
579
|
+
setChatState((prev) => {
|
|
580
|
+
const nextChats = new Map(prev.chats);
|
|
581
|
+
const activeChat = chatState.chats.get(activeChatId);
|
|
582
|
+
if (activeChat) {
|
|
583
|
+
nextChats.set(activeChat.id, {
|
|
584
|
+
...activeChat,
|
|
585
|
+
messages: activeChat.messages.slice(0, index),
|
|
586
|
+
updatedAt: Date.now(),
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
...prev,
|
|
592
|
+
chats: nextChats,
|
|
593
|
+
};
|
|
594
|
+
});
|
|
605
595
|
}
|
|
606
596
|
|
|
607
597
|
append({
|
|
608
598
|
role: "user",
|
|
609
599
|
content: newValue,
|
|
610
600
|
});
|
|
611
|
-
};
|
|
601
|
+
});
|
|
612
602
|
|
|
613
603
|
const handleChatInputSubmit = (
|
|
614
604
|
e: KeyboardEvent | undefined,
|
|
@@ -624,12 +614,21 @@ const ChatPanelBody = () => {
|
|
|
624
614
|
reload();
|
|
625
615
|
};
|
|
626
616
|
|
|
627
|
-
const handleNewThreadSubmit = () => {
|
|
628
|
-
|
|
629
|
-
|
|
617
|
+
const handleNewThreadSubmit = useEvent(() => {
|
|
618
|
+
if (!newThreadInput.trim()) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
createNewThread(newThreadInput.trim());
|
|
622
|
+
});
|
|
630
623
|
|
|
631
624
|
const handleOnCloseThread = () => newThreadInputRef.current?.editor?.blur();
|
|
632
625
|
|
|
626
|
+
const sortedChats = useMemo(() => {
|
|
627
|
+
return [...chatState.chats.values()].sort(
|
|
628
|
+
(a, b) => b.updatedAt - a.updatedAt,
|
|
629
|
+
);
|
|
630
|
+
}, [chatState.chats]);
|
|
631
|
+
|
|
633
632
|
return (
|
|
634
633
|
<div className="flex flex-col h-[calc(100%-53px)]">
|
|
635
634
|
<TooltipProvider>
|
|
@@ -637,7 +636,7 @@ const ChatPanelBody = () => {
|
|
|
637
636
|
onNewChat={handleNewChat}
|
|
638
637
|
activeChatId={activeChat?.id}
|
|
639
638
|
setActiveChat={setActiveChat}
|
|
640
|
-
chats={
|
|
639
|
+
chats={sortedChats}
|
|
641
640
|
/>
|
|
642
641
|
</TooltipProvider>
|
|
643
642
|
|
|
@@ -658,7 +657,7 @@ const ChatPanelBody = () => {
|
|
|
658
657
|
/>
|
|
659
658
|
</div>
|
|
660
659
|
<ChatInputFooter
|
|
661
|
-
|
|
660
|
+
isEmpty={!newThreadInput.trim()}
|
|
662
661
|
onSendClick={handleNewThreadSubmit}
|
|
663
662
|
isLoading={isLoading}
|
|
664
663
|
onStop={stop}
|
|
@@ -668,14 +667,14 @@ const ChatPanelBody = () => {
|
|
|
668
667
|
|
|
669
668
|
{messages.map((message, idx) => (
|
|
670
669
|
<ChatMessage
|
|
671
|
-
key={
|
|
670
|
+
key={message.id}
|
|
672
671
|
message={message}
|
|
673
672
|
index={idx}
|
|
674
673
|
onEdit={handleMessageEdit}
|
|
675
674
|
setChatState={setChatState}
|
|
676
675
|
chatState={chatState}
|
|
677
676
|
isStreamingReasoning={isStreamingReasoning}
|
|
678
|
-
|
|
677
|
+
isLast={idx === messages.length - 1}
|
|
679
678
|
/>
|
|
680
679
|
))}
|
|
681
680
|
|
|
@@ -718,3 +717,27 @@ const ChatPanelBody = () => {
|
|
|
718
717
|
</div>
|
|
719
718
|
);
|
|
720
719
|
};
|
|
720
|
+
|
|
721
|
+
function isLastMessageReasoning(messages: Message[]): boolean {
|
|
722
|
+
if (messages.length === 0) {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const lastMessage = messages.at(-1);
|
|
727
|
+
if (!lastMessage) {
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (lastMessage.role !== "assistant" || !lastMessage.parts) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const parts = lastMessage.parts;
|
|
736
|
+
if (parts.length === 0) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Check if the last part is reasoning
|
|
741
|
+
const lastPart = parts[parts.length - 1];
|
|
742
|
+
return lastPart.type === "reasoning";
|
|
743
|
+
}
|
|
@@ -81,19 +81,13 @@ function maybeTransform(
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const
|
|
85
|
-
const { theme } = useTheme();
|
|
84
|
+
const InsertCodeBlockButton = ({ code, language }: CodeBlockProps) => {
|
|
86
85
|
const { createNewCell } = useCellActions();
|
|
87
86
|
const lastFocusedCellId = useLastFocusedCellId();
|
|
88
87
|
const autoInstantiate = useAtomValue(autoInstantiateAtom);
|
|
89
|
-
const [value, setValue] = useState(code);
|
|
90
|
-
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
setValue(code);
|
|
93
|
-
}, [code]);
|
|
94
88
|
|
|
95
89
|
const handleInsertCode = () => {
|
|
96
|
-
const result = maybeTransform(language,
|
|
90
|
+
const result = maybeTransform(language, code);
|
|
97
91
|
|
|
98
92
|
if (language === "sql") {
|
|
99
93
|
maybeAddMarimoImport({
|
|
@@ -109,6 +103,22 @@ const CodeBlock = ({ code, language }: CodeBlockProps) => {
|
|
|
109
103
|
});
|
|
110
104
|
};
|
|
111
105
|
|
|
106
|
+
return (
|
|
107
|
+
<Button size="xs" variant="outline" onClick={handleInsertCode}>
|
|
108
|
+
Add to Notebook
|
|
109
|
+
<BetweenHorizontalStartIcon className="ml-2 h-4 w-4" />
|
|
110
|
+
</Button>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const CodeBlock = ({ code, language }: CodeBlockProps) => {
|
|
115
|
+
const { theme } = useTheme();
|
|
116
|
+
const [value, setValue] = useState(code);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
setValue(code);
|
|
120
|
+
}, [code]);
|
|
121
|
+
|
|
112
122
|
const handleCopyCode = async () => {
|
|
113
123
|
await copyToClipboard(value);
|
|
114
124
|
};
|
|
@@ -132,10 +142,7 @@ const CodeBlock = ({ code, language }: CodeBlockProps) => {
|
|
|
132
142
|
<CopyButton size="xs" variant="outline" onClick={handleCopyCode}>
|
|
133
143
|
Copy
|
|
134
144
|
</CopyButton>
|
|
135
|
-
<
|
|
136
|
-
Add to Notebook
|
|
137
|
-
<BetweenHorizontalStartIcon className="ml-2 h-4 w-4" />
|
|
138
|
-
</Button>
|
|
145
|
+
<InsertCodeBlockButton code={value} language={language} />
|
|
139
146
|
</div>
|
|
140
147
|
</div>
|
|
141
148
|
);
|
|
@@ -164,17 +171,21 @@ const CopyButton: React.FC<ButtonProps> = ({ onClick, ...props }) => {
|
|
|
164
171
|
};
|
|
165
172
|
|
|
166
173
|
const COMPONENTS: Components = {
|
|
167
|
-
code: ({ children, className }) => {
|
|
174
|
+
code: ({ children, className, key }) => {
|
|
168
175
|
const language = className?.replace("language-", "");
|
|
169
176
|
if (language && typeof children === "string") {
|
|
170
177
|
return (
|
|
171
|
-
<div>
|
|
178
|
+
<div key={key}>
|
|
172
179
|
<div className="text-xs text-muted-foreground pl-1">{language}</div>
|
|
173
180
|
<CodeBlock code={children.trim()} language={language} />
|
|
174
181
|
</div>
|
|
175
182
|
);
|
|
176
183
|
}
|
|
177
|
-
return
|
|
184
|
+
return (
|
|
185
|
+
<code key={key} className={className}>
|
|
186
|
+
{children}
|
|
187
|
+
</code>
|
|
188
|
+
);
|
|
178
189
|
},
|
|
179
190
|
};
|
|
180
191
|
|
|
@@ -183,12 +194,14 @@ function parseMarkdownIntoBlocks(markdown: string): string[] {
|
|
|
183
194
|
return tokens.map((token) => token.raw);
|
|
184
195
|
}
|
|
185
196
|
|
|
197
|
+
const PLUGINS = [remarkGfm];
|
|
198
|
+
|
|
186
199
|
const MemoizedMarkdownBlock = memo(
|
|
187
200
|
({ content }: { content: string }) => {
|
|
188
201
|
return (
|
|
189
202
|
<Markdown
|
|
190
203
|
components={COMPONENTS}
|
|
191
|
-
remarkPlugins={
|
|
204
|
+
remarkPlugins={PLUGINS}
|
|
192
205
|
className="prose dark:prose-invert max-w-none prose-pre:pl-0"
|
|
193
206
|
>
|
|
194
207
|
{content}
|