@gendive/chatllm 0.3.1 → 0.5.0

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.
@@ -359,6 +359,22 @@ ${contextSummary}` },
359
359
  );
360
360
  return;
361
361
  }
362
+ if (typeof result === "object" && "content" in result) {
363
+ setSessions(
364
+ (prev) => prev.map((s) => {
365
+ if (s.id === capturedSessionId) {
366
+ return {
367
+ ...s,
368
+ messages: s.messages.map(
369
+ (m) => m.id === assistantMessageId ? { ...m, content: result.content, sources: result.sources } : m
370
+ )
371
+ };
372
+ }
373
+ return s;
374
+ })
375
+ );
376
+ return;
377
+ }
362
378
  response = new Response(result);
363
379
  } else {
364
380
  response = await fetch(apiEndpoint, {
@@ -490,6 +506,158 @@ ${contextSummary}` },
490
506
  setInput(userMessage.content);
491
507
  await sendMessage(userMessage.content);
492
508
  }, [currentSession, currentSessionId, isLoading, sendMessage]);
509
+ const askOtherModel = useCallback(async (messageId, targetModel) => {
510
+ if (!currentSession || !currentSessionId || isLoading) return;
511
+ const assistantIndex = currentSession.messages.findIndex((m) => m.id === messageId);
512
+ if (assistantIndex === -1) return;
513
+ const assistantMessage = currentSession.messages[assistantIndex];
514
+ if (assistantMessage.role !== "assistant") return;
515
+ const userMessage = currentSession.messages[assistantIndex - 1];
516
+ if (!userMessage || userMessage.role !== "user") return;
517
+ setIsLoading(true);
518
+ abortControllerRef.current = new AbortController();
519
+ try {
520
+ const messagesToSend = currentSession.messages.slice(0, assistantIndex);
521
+ let chatMessages;
522
+ if (currentSession.contextSummary) {
523
+ const recentMessages = messagesToSend.slice(-keepRecentMessages);
524
+ chatMessages = [
525
+ { role: "system", content: `[\uC774\uC804 \uB300\uD654 \uC694\uC57D]
526
+ ${currentSession.contextSummary}` },
527
+ ...recentMessages.map((m) => ({ role: m.role, content: m.content }))
528
+ ];
529
+ } else {
530
+ chatMessages = messagesToSend.map((m) => ({ role: m.role, content: m.content }));
531
+ }
532
+ const baseSystemPrompt = buildSystemPrompt();
533
+ const messagesForApi = baseSystemPrompt ? [{ role: "system", content: baseSystemPrompt }, ...chatMessages] : chatMessages;
534
+ const modelConfig = models.find((m) => m.id === targetModel);
535
+ const provider = modelConfig?.provider || "ollama";
536
+ let responseContent = "";
537
+ let responseSources;
538
+ if (onSendMessage) {
539
+ const result = await onSendMessage({
540
+ messages: messagesForApi,
541
+ model: targetModel,
542
+ provider,
543
+ apiKey,
544
+ systemPrompt: baseSystemPrompt
545
+ });
546
+ if (typeof result === "string") {
547
+ responseContent = result;
548
+ } else if (typeof result === "object" && "content" in result) {
549
+ responseContent = result.content;
550
+ responseSources = result.sources;
551
+ } else {
552
+ const reader = result.getReader();
553
+ const decoder = new TextDecoder();
554
+ let buffer = "";
555
+ while (true) {
556
+ const { done, value } = await reader.read();
557
+ if (done) break;
558
+ buffer += decoder.decode(value, { stream: true });
559
+ const lines = buffer.split("\n");
560
+ buffer = lines.pop() || "";
561
+ for (const line of lines) {
562
+ if (line.startsWith("data: ")) {
563
+ const data = line.slice(6);
564
+ if (data === "[DONE]") continue;
565
+ try {
566
+ const parsed = JSON.parse(data);
567
+ if (parsed.content) responseContent += parsed.content;
568
+ } catch {
569
+ }
570
+ }
571
+ }
572
+ }
573
+ }
574
+ } else {
575
+ const response = await fetch(apiEndpoint, {
576
+ method: "POST",
577
+ headers: { "Content-Type": "application/json" },
578
+ body: JSON.stringify({
579
+ messages: messagesForApi,
580
+ model: targetModel,
581
+ provider,
582
+ apiKey: provider === "devdive" ? apiKey : void 0
583
+ }),
584
+ signal: abortControllerRef.current.signal
585
+ });
586
+ if (!response.ok) throw new Error("API error");
587
+ const reader = response.body?.getReader();
588
+ if (!reader) throw new Error("No reader");
589
+ const decoder = new TextDecoder();
590
+ let buffer = "";
591
+ while (true) {
592
+ const { done, value } = await reader.read();
593
+ if (done) break;
594
+ buffer += decoder.decode(value, { stream: true });
595
+ const lines = buffer.split("\n");
596
+ buffer = lines.pop() || "";
597
+ for (const line of lines) {
598
+ if (line.startsWith("data: ")) {
599
+ const data = line.slice(6);
600
+ if (data === "[DONE]") continue;
601
+ try {
602
+ const parsed = JSON.parse(data);
603
+ if (parsed.content) responseContent += parsed.content;
604
+ } catch {
605
+ }
606
+ }
607
+ }
608
+ }
609
+ }
610
+ const alternative = {
611
+ id: generateId("alt"),
612
+ model: targetModel,
613
+ content: responseContent,
614
+ timestamp: Date.now(),
615
+ sources: responseSources
616
+ };
617
+ const capturedSessionId = currentSessionId;
618
+ setSessions(
619
+ (prev) => prev.map((s) => {
620
+ if (s.id === capturedSessionId) {
621
+ return {
622
+ ...s,
623
+ messages: s.messages.map((m) => {
624
+ if (m.id === messageId) {
625
+ const existingAlts = m.alternatives || [];
626
+ return {
627
+ ...m,
628
+ alternatives: [...existingAlts, alternative]
629
+ };
630
+ }
631
+ return m;
632
+ }),
633
+ updatedAt: Date.now()
634
+ };
635
+ }
636
+ return s;
637
+ })
638
+ );
639
+ } catch (error) {
640
+ if (error instanceof Error && error.name === "AbortError") {
641
+ return;
642
+ }
643
+ const err = error instanceof Error ? error : new Error("Unknown error");
644
+ onError?.(err);
645
+ } finally {
646
+ setIsLoading(false);
647
+ abortControllerRef.current = null;
648
+ }
649
+ }, [
650
+ currentSession,
651
+ currentSessionId,
652
+ isLoading,
653
+ keepRecentMessages,
654
+ buildSystemPrompt,
655
+ models,
656
+ apiEndpoint,
657
+ apiKey,
658
+ onSendMessage,
659
+ onError
660
+ ]);
493
661
  return {
494
662
  // State
495
663
  sessions,
@@ -524,7 +692,9 @@ ${contextSummary}` },
524
692
  cancelEdit,
525
693
  saveEdit,
526
694
  regenerate,
527
- updatePersonalization
695
+ askOtherModel,
696
+ updatePersonalization,
697
+ models
528
698
  };
529
699
  };
530
700
 
@@ -1984,15 +2154,20 @@ var MessageBubble = ({
1984
2154
  onEdit,
1985
2155
  onRegenerate,
1986
2156
  onQuote,
2157
+ onAskOtherModel,
2158
+ models,
1987
2159
  alternatives,
1988
2160
  activeAlternativeIndex = 0,
1989
2161
  onAlternativeChange
1990
2162
  }) => {
1991
2163
  const [showActions, setShowActions] = useState5(false);
2164
+ const [showModelMenu, setShowModelMenu] = useState5(false);
1992
2165
  const isUser = message.role === "user";
1993
2166
  const isAssistant = message.role === "assistant";
2167
+ const otherModels = models?.filter((m) => m.id !== message.model) || [];
1994
2168
  const displayContent = alternatives && alternatives.length > 0 && activeAlternativeIndex > 0 ? alternatives[activeAlternativeIndex - 1]?.content || message.content : message.content;
1995
2169
  const displayModel = alternatives && alternatives.length > 0 && activeAlternativeIndex > 0 ? alternatives[activeAlternativeIndex - 1]?.model : message.model;
2170
+ const displaySources = alternatives && alternatives.length > 0 && activeAlternativeIndex > 0 ? alternatives[activeAlternativeIndex - 1]?.sources : message.sources;
1996
2171
  const handleMouseUp = () => {
1997
2172
  if (!onQuote) return;
1998
2173
  const selection = window.getSelection();
@@ -2117,6 +2292,43 @@ var MessageBubble = ({
2117
2292
  ]
2118
2293
  }
2119
2294
  ),
2295
+ displaySources && displaySources.length > 0 && /* @__PURE__ */ jsxs6(
2296
+ "div",
2297
+ {
2298
+ style: {
2299
+ display: "flex",
2300
+ flexWrap: "wrap",
2301
+ gap: "8px",
2302
+ marginTop: "12px",
2303
+ paddingTop: "12px",
2304
+ borderTop: "1px solid var(--chatllm-border-light, #f3f4f6)"
2305
+ },
2306
+ children: [
2307
+ /* @__PURE__ */ jsx7(
2308
+ "span",
2309
+ {
2310
+ style: {
2311
+ fontSize: "12px",
2312
+ fontWeight: 500,
2313
+ color: "var(--chatllm-text-muted, #9ca3af)",
2314
+ marginRight: "4px"
2315
+ },
2316
+ children: "\uCD9C\uCC98:"
2317
+ }
2318
+ ),
2319
+ displaySources.map((source, index) => /* @__PURE__ */ jsx7(
2320
+ LinkChip,
2321
+ {
2322
+ text: source.title,
2323
+ url: source.url,
2324
+ index: index + 1,
2325
+ showFavicon: true
2326
+ },
2327
+ source.id
2328
+ ))
2329
+ ]
2330
+ }
2331
+ ),
2120
2332
  alternatives && alternatives.length > 0 && /* @__PURE__ */ jsxs6(
2121
2333
  "div",
2122
2334
  {
@@ -2181,7 +2393,110 @@ var MessageBubble = ({
2181
2393
  }
2182
2394
  ) }),
2183
2395
  isUser && /* @__PURE__ */ jsx7("button", { onClick: onEdit, style: actionButtonStyle, title: "\uC218\uC815", children: /* @__PURE__ */ jsx7(IconSvg, { name: "edit-line", size: 16, color: "var(--chatllm-text-muted, #9ca3af)" }) }),
2184
- isAssistant && onRegenerate && /* @__PURE__ */ jsx7("button", { onClick: onRegenerate, style: actionButtonStyle, title: "\uB2E4\uC2DC \uC0DD\uC131", children: /* @__PURE__ */ jsx7(IconSvg, { name: "refresh-line", size: 16, color: "var(--chatllm-text-muted, #9ca3af)" }) })
2396
+ isAssistant && onRegenerate && /* @__PURE__ */ jsx7("button", { onClick: onRegenerate, style: actionButtonStyle, title: "\uB2E4\uC2DC \uC0DD\uC131", children: /* @__PURE__ */ jsx7(IconSvg, { name: "refresh-line", size: 16, color: "var(--chatllm-text-muted, #9ca3af)" }) }),
2397
+ isAssistant && onAskOtherModel && otherModels.length > 0 && /* @__PURE__ */ jsxs6("div", { style: { position: "relative" }, children: [
2398
+ /* @__PURE__ */ jsxs6(
2399
+ "button",
2400
+ {
2401
+ onClick: () => setShowModelMenu(!showModelMenu),
2402
+ style: actionButtonStyle,
2403
+ title: "\uB2E4\uB978 \uBAA8\uB378\uC5D0\uAC8C \uC9C8\uBB38",
2404
+ children: [
2405
+ /* @__PURE__ */ jsx7(IconSvg, { name: "robot-line", size: 16, color: "var(--chatllm-text-muted, #9ca3af)" }),
2406
+ /* @__PURE__ */ jsx7(
2407
+ IconSvg,
2408
+ {
2409
+ name: "arrow-down-s-line",
2410
+ size: 12,
2411
+ color: "var(--chatllm-text-muted, #9ca3af)",
2412
+ style: { marginLeft: "2px" }
2413
+ }
2414
+ )
2415
+ ]
2416
+ }
2417
+ ),
2418
+ showModelMenu && /* @__PURE__ */ jsxs6(
2419
+ "div",
2420
+ {
2421
+ style: {
2422
+ position: "absolute",
2423
+ bottom: "100%",
2424
+ left: 0,
2425
+ marginBottom: "4px",
2426
+ backgroundColor: "var(--chatllm-bg, #ffffff)",
2427
+ border: "1px solid var(--chatllm-border, #e5e7eb)",
2428
+ borderRadius: "8px",
2429
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
2430
+ minWidth: "160px",
2431
+ zIndex: 100,
2432
+ overflow: "hidden"
2433
+ },
2434
+ onMouseLeave: () => setShowModelMenu(false),
2435
+ children: [
2436
+ /* @__PURE__ */ jsx7(
2437
+ "div",
2438
+ {
2439
+ style: {
2440
+ padding: "8px 12px",
2441
+ fontSize: "11px",
2442
+ fontWeight: 600,
2443
+ color: "var(--chatllm-text-muted, #9ca3af)",
2444
+ textTransform: "uppercase",
2445
+ borderBottom: "1px solid var(--chatllm-border-light, #f3f4f6)"
2446
+ },
2447
+ children: "\uB2E4\uB978 \uBAA8\uB378\uC5D0\uAC8C \uC9C8\uBB38"
2448
+ }
2449
+ ),
2450
+ otherModels.map((model) => /* @__PURE__ */ jsxs6(
2451
+ "button",
2452
+ {
2453
+ onClick: () => {
2454
+ onAskOtherModel(model.id);
2455
+ setShowModelMenu(false);
2456
+ },
2457
+ style: {
2458
+ width: "100%",
2459
+ padding: "10px 12px",
2460
+ display: "flex",
2461
+ alignItems: "center",
2462
+ gap: "8px",
2463
+ backgroundColor: "transparent",
2464
+ border: "none",
2465
+ cursor: "pointer",
2466
+ fontSize: "13px",
2467
+ color: "var(--chatllm-text, #1f2937)",
2468
+ textAlign: "left"
2469
+ },
2470
+ onMouseEnter: (e) => {
2471
+ e.currentTarget.style.backgroundColor = "var(--chatllm-bg-hover, #f3f4f6)";
2472
+ },
2473
+ onMouseLeave: (e) => {
2474
+ e.currentTarget.style.backgroundColor = "transparent";
2475
+ },
2476
+ children: [
2477
+ /* @__PURE__ */ jsx7(IconSvg, { name: "robot-line", size: 14, color: "var(--chatllm-primary, #3b82f6)" }),
2478
+ /* @__PURE__ */ jsx7("span", { style: { flex: 1 }, children: model.name }),
2479
+ /* @__PURE__ */ jsx7(
2480
+ "span",
2481
+ {
2482
+ style: {
2483
+ fontSize: "10px",
2484
+ padding: "2px 6px",
2485
+ backgroundColor: "var(--chatllm-bg-tertiary, #f3f4f6)",
2486
+ borderRadius: "4px",
2487
+ color: "var(--chatllm-text-muted, #9ca3af)"
2488
+ },
2489
+ children: model.provider
2490
+ }
2491
+ )
2492
+ ]
2493
+ },
2494
+ model.id
2495
+ ))
2496
+ ]
2497
+ }
2498
+ )
2499
+ ] })
2185
2500
  ]
2186
2501
  }
2187
2502
  )
@@ -2228,6 +2543,8 @@ var MessageList = ({
2228
2543
  onEdit,
2229
2544
  onRegenerate,
2230
2545
  onQuote,
2546
+ onAskOtherModel,
2547
+ models,
2231
2548
  copiedId,
2232
2549
  editingId
2233
2550
  }) => {
@@ -2292,6 +2609,8 @@ var MessageList = ({
2292
2609
  onEdit: () => onEdit(message),
2293
2610
  onRegenerate: message.role === "assistant" ? () => onRegenerate(message.id) : void 0,
2294
2611
  onQuote,
2612
+ onAskOtherModel: message.role === "assistant" && onAskOtherModel ? (targetModel) => onAskOtherModel(message.id, targetModel) : void 0,
2613
+ models,
2295
2614
  alternatives: message.alternatives
2296
2615
  },
2297
2616
  message.id
@@ -3595,7 +3914,9 @@ var ChatUI = ({
3595
3914
  cancelEdit,
3596
3915
  saveEdit,
3597
3916
  regenerate,
3598
- updatePersonalization
3917
+ askOtherModel,
3918
+ updatePersonalization,
3919
+ models: hookModels
3599
3920
  } = useChatUI(hookOptions);
3600
3921
  const greeting = currentPersonalization.userProfile.nickname ? `\uC548\uB155\uD558\uC138\uC694, ${currentPersonalization.userProfile.nickname}\uB2D8` : "\uC548\uB155\uD558\uC138\uC694";
3601
3922
  const handleTemplateClick = (template) => {
@@ -3713,6 +4034,8 @@ var ChatUI = ({
3713
4034
  onEdit: startEdit,
3714
4035
  onRegenerate: regenerate,
3715
4036
  onQuote: setQuotedText,
4037
+ onAskOtherModel: askOtherModel,
4038
+ models: hookModels,
3716
4039
  copiedId: copiedMessageId,
3717
4040
  editingId: editingMessageId
3718
4041
  }