@marimo-team/frontend 0.19.3-dev42 → 0.19.3-dev45

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/index.html CHANGED
@@ -66,7 +66,7 @@
66
66
  <marimo-server-token data-token="{{ server_token }}" hidden></marimo-server-token>
67
67
  <!-- /TODO -->
68
68
  <title>{{ title }}</title>
69
- <script type="module" crossorigin src="./assets/index-DoE3JZXY.js"></script>
69
+ <script type="module" crossorigin src="./assets/index-VUoDw_Qb.js"></script>
70
70
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-BW0IMuFq.js">
71
71
  <link rel="modulepreload" crossorigin href="./assets/hotkeys-uKX61F1_.js">
72
72
  <link rel="modulepreload" crossorigin href="./assets/defaultLocale-BLUna9fQ.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/frontend",
3
- "version": "0.19.3-dev42",
3
+ "version": "0.19.3-dev45",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -15,7 +15,7 @@ export type PluginFunctions = {
15
15
  get_chat_history: (req: {}) => Promise<{ messages: UIMessage[] }>;
16
16
  delete_chat_history: (req: {}) => Promise<null>;
17
17
  delete_chat_message: (req: { index: number }) => Promise<null>;
18
- send_prompt: (req: SendMessageRequest) => Promise<string | null>;
18
+ send_prompt: (req: SendMessageRequest) => Promise<unknown>;
19
19
  };
20
20
 
21
21
  const messageSchema = z.array(
@@ -47,7 +47,6 @@ export const ChatPlugin = createPlugin<{ messages: UIMessage[] }>(
47
47
  maxHeight: z.number().optional(),
48
48
  config: configSchema,
49
49
  allowAttachments: z.union([z.boolean(), z.string().array()]),
50
- frontendManaged: z.boolean(),
51
50
  }),
52
51
  )
53
52
  .withFunctions<PluginFunctions>({
@@ -67,7 +66,7 @@ export const ChatPlugin = createPlugin<{ messages: UIMessage[] }>(
67
66
  config: configSchema,
68
67
  }),
69
68
  )
70
- .output(z.union([z.string(), z.null()])),
69
+ .output(z.unknown()),
71
70
  })
72
71
  .renderer((props) => (
73
72
  <TooltipProvider>
@@ -77,7 +76,6 @@ export const ChatPlugin = createPlugin<{ messages: UIMessage[] }>(
77
76
  showConfigurationControls={props.data.showConfigurationControls}
78
77
  maxHeight={props.data.maxHeight}
79
78
  allowAttachments={props.data.allowAttachments}
80
- frontendManaged={props.data.frontendManaged}
81
79
  config={props.data.config}
82
80
  get_chat_history={props.functions.get_chat_history}
83
81
  delete_chat_history={props.functions.delete_chat_history}
@@ -66,7 +66,6 @@ interface Props extends PluginFunctions {
66
66
  showConfigurationControls: boolean;
67
67
  maxHeight: number | undefined;
68
68
  allowAttachments: boolean | string[];
69
- frontendManaged: boolean;
70
69
  value: UIMessage[];
71
70
  setValue: (messages: UIMessage[]) => void;
72
71
  host: HTMLElement;
@@ -166,113 +165,42 @@ export const Chatbot: React.FC<Props> = (props) => {
166
165
  };
167
166
  });
168
167
 
169
- if (props.frontendManaged) {
170
- const stream = new ReadableStream<UIMessageChunk>({
171
- start(controller) {
172
- frontendStreamControllerRef.current = controller;
168
+ const stream = new ReadableStream<UIMessageChunk>({
169
+ start(controller) {
170
+ frontendStreamControllerRef.current = controller;
173
171
 
174
- const abortHandler = () => {
175
- try {
176
- controller.close();
177
- } catch (error) {
178
- Logger.debug("Controller may already be closed", { error });
179
- }
180
- frontendStreamControllerRef.current = null;
181
- };
182
- signal?.addEventListener("abort", abortHandler);
183
-
184
- return () => {
185
- signal?.removeEventListener("abort", abortHandler);
186
- };
187
- },
188
- cancel() {
189
- frontendStreamControllerRef.current = null;
190
- },
191
- });
192
-
193
- // Start the prompt, chunks will be sent via events
194
- props
195
- .send_prompt({
196
- messages: messages,
197
- config: chatConfig,
198
- })
199
- .catch((error: Error) => {
200
- frontendStreamControllerRef.current?.error(error);
201
- frontendStreamControllerRef.current = null;
202
- });
203
-
204
- return createUIMessageStreamResponse({ stream });
205
- }
206
-
207
- if (signal?.aborted) {
208
- return new Response("Aborted", { status: 499 });
209
- }
210
-
211
- // Create a placeholder message for streaming (backend-managed)
212
- const messageId = Date.now().toString();
213
-
214
- setMessages((prev) => [
215
- ...prev,
216
- {
217
- id: messageId,
218
- role: "assistant",
219
- parts: [{ type: "text", text: "" }],
220
- },
221
- ]);
222
-
223
- // Create an abort-aware promise for the send_prompt call
224
- const sendPromptPromise = props.send_prompt({
225
- messages: messages,
226
- config: chatConfig,
227
- });
228
-
229
- // Race the send_prompt with an abort signal
230
- const response = await new Promise<string | null>(
231
- (resolve, reject) => {
232
- // Listen for abort
233
172
  const abortHandler = () => {
234
- reject(new DOMException("Aborted", "AbortError"));
173
+ try {
174
+ controller.close();
175
+ } catch (error) {
176
+ Logger.debug("Controller may already be closed", { error });
177
+ }
178
+ frontendStreamControllerRef.current = null;
235
179
  };
236
180
  signal?.addEventListener("abort", abortHandler);
237
181
 
238
- sendPromptPromise
239
- .then(resolve)
240
- .catch(reject)
241
- .finally(() => {
242
- signal?.removeEventListener("abort", abortHandler);
243
- });
182
+ return () => {
183
+ signal?.removeEventListener("abort", abortHandler);
184
+ };
244
185
  },
245
- );
186
+ cancel() {
187
+ frontendStreamControllerRef.current = null;
188
+ },
189
+ });
246
190
 
247
- if (response === null) {
248
- Logger.error("Non-frontend-managed response is null", {
249
- response,
191
+ // Start the prompt, chunks will be sent via events
192
+ void props
193
+ .send_prompt({
194
+ messages: messages,
195
+ config: chatConfig,
196
+ })
197
+ .catch((error: Error) => {
198
+ frontendStreamControllerRef.current?.error(error);
199
+ frontendStreamControllerRef.current = null;
250
200
  });
251
- return new Response("Internal server error", { status: 500 });
252
- }
253
201
 
254
- // If streaming didn't happen (non-generator response), update the message
255
- // Check if streaming state is still set (meaning no chunks were received)
256
- if (
257
- streamingStateRef.current.backendMessageId === null &&
258
- streamingStateRef.current.frontendMessageIndex === null
259
- ) {
260
- setMessages((prev) => {
261
- const updated = [...prev];
262
- const index = updated.findIndex((m) => m.id === messageId);
263
- if (index !== -1) {
264
- updated[index] = {
265
- ...updated[index],
266
- parts: [{ type: "text", text: response }],
267
- };
268
- }
269
- return updated;
270
- });
271
- }
272
-
273
- return new Response(response);
274
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
275
- } catch (error: any) {
202
+ return createUIMessageStreamResponse({ stream });
203
+ } catch (error: unknown) {
276
204
  // Clear streaming state on error
277
205
  streamingStateRef.current = {
278
206
  backendMessageId: null,
@@ -280,13 +208,13 @@ export const Chatbot: React.FC<Props> = (props) => {
280
208
  };
281
209
 
282
210
  // Handle abort gracefully without showing an error
283
- if (error.name === "AbortError") {
211
+ if (error instanceof Error && error.name === "AbortError") {
284
212
  return new Response("Aborted", { status: 499 });
285
213
  }
286
214
 
287
215
  // HACK: strip the error message to clean up the response
288
- const strippedError = error.message
289
- .split("failed with exception ")
216
+ const strippedError = (error as Error).message
217
+ ?.split("failed with exception ")
290
218
  .pop();
291
219
  return new Response(strippedError, { status: 400 });
292
220
  }
@@ -307,11 +235,7 @@ export const Chatbot: React.FC<Props> = (props) => {
307
235
  frontendMessageIndex: null,
308
236
  };
309
237
 
310
- // For frontend-managed streaming, we set the value directly from the frontend.
311
- // Because useChat creates the proper message structure for us.
312
- if (props.frontendManaged) {
313
- props.setValue(message.messages);
314
- }
238
+ props.setValue(message.messages);
315
239
  },
316
240
  onError: (error) => {
317
241
  Logger.error("An error occurred:", error);
@@ -338,90 +262,28 @@ export const Chatbot: React.FC<Props> = (props) => {
338
262
  return;
339
263
  }
340
264
 
341
- if (props.frontendManaged) {
342
- // Push to the stream for useChat to process
343
- const controller = frontendStreamControllerRef.current;
344
- if (!controller) {
345
- return;
346
- }
347
-
348
- const frontendMessage = message as {
349
- type: string;
350
- message_id: string;
351
- content?: UIMessageChunk;
352
- is_final?: boolean;
353
- };
354
-
355
- if (frontendMessage.content) {
356
- controller.enqueue(frontendMessage.content);
357
- }
358
- if (frontendMessage.is_final) {
359
- controller.close();
360
- frontendStreamControllerRef.current = null;
361
- }
265
+ // Push to the stream for useChat to process
266
+ const controller = frontendStreamControllerRef.current;
267
+ if (!controller) {
362
268
  return;
363
269
  }
364
270
 
365
- // Handle regular text streaming chunks
366
- const chunkMessage = message as {
271
+ const frontendMessage = message as {
367
272
  type: string;
368
273
  message_id: string;
369
- content: string;
370
- is_final: boolean;
274
+ content?: UIMessageChunk;
275
+ is_final?: boolean;
371
276
  };
372
277
 
373
- // Initialize streaming state on first chunk if not already set
374
- if (streamingStateRef.current.backendMessageId === null) {
375
- // Find the last assistant message (which should be the placeholder we created)
376
- setMessages((prev) => {
377
- const updated = [...prev];
378
- // Find the last assistant message
379
- for (let i = updated.length - 1; i >= 0; i--) {
380
- if (updated[i].role === "assistant") {
381
- streamingStateRef.current = {
382
- backendMessageId: chunkMessage.message_id,
383
- frontendMessageIndex: i,
384
- };
385
- break;
386
- }
387
- }
388
- return updated;
389
- });
278
+ if (frontendMessage.content) {
279
+ controller.enqueue(frontendMessage.content);
390
280
  }
391
-
392
- // Only process chunks for the current streaming message
393
- const frontendIndex = streamingStateRef.current.frontendMessageIndex;
394
- if (
395
- streamingStateRef.current.backendMessageId ===
396
- chunkMessage.message_id &&
397
- frontendIndex !== null
398
- ) {
399
- setMessages((prev) => {
400
- const updated = [...prev];
401
- const index = frontendIndex;
402
-
403
- // Update the message at the tracked index
404
- if (index < updated.length) {
405
- const messageToUpdate = updated[index];
406
- if (messageToUpdate.role === "assistant") {
407
- updated[index] = {
408
- ...messageToUpdate,
409
- parts: [{ type: "text", text: chunkMessage.content }],
410
- };
411
- }
412
- }
413
-
414
- return updated;
415
- });
416
-
417
- // Clear streaming state when final chunk arrives
418
- if (chunkMessage.is_final) {
419
- streamingStateRef.current = {
420
- backendMessageId: null,
421
- frontendMessageIndex: null,
422
- };
423
- }
281
+ if (frontendMessage.is_final) {
282
+ controller.close();
283
+ frontendStreamControllerRef.current = null;
424
284
  }
285
+
286
+ return;
425
287
  },
426
288
  );
427
289
 
@@ -434,10 +296,7 @@ export const Chatbot: React.FC<Props> = (props) => {
434
296
  props.delete_chat_message({ index });
435
297
  setMessages(newMessages);
436
298
 
437
- // Since we manage the state in the frontend, we need to update the value.
438
- if (props.frontendManaged) {
439
- props.setValue(newMessages);
440
- }
299
+ props.setValue(newMessages);
441
300
  }
442
301
  };
443
302
 
@@ -526,7 +385,7 @@ export const Chatbot: React.FC<Props> = (props) => {
526
385
 
527
386
  return (
528
387
  <div
529
- key={message.id}
388
+ key={`${message.id}-${index}`}
530
389
  className={cn(
531
390
  "flex flex-col group gap-2",
532
391
  message.role === "user" ? "items-end" : "items-start",