@gendive/chatllm 0.3.1 → 0.4.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.
@@ -191,6 +191,8 @@ interface MessageListProps {
191
191
  onEdit: (message: ChatMessage) => void;
192
192
  onRegenerate: (id: string) => void;
193
193
  onQuote: (text: string) => void;
194
+ onAskOtherModel?: (messageId: string, targetModel: string) => void;
195
+ models?: ModelConfig[];
194
196
  copiedId: string | null;
195
197
  editingId: string | null;
196
198
  }
@@ -203,6 +205,8 @@ interface MessageBubbleProps {
203
205
  onEdit: () => void;
204
206
  onRegenerate?: () => void;
205
207
  onQuote?: (text: string) => void;
208
+ onAskOtherModel?: (targetModel: string) => void;
209
+ models?: ModelConfig[];
206
210
  alternatives?: AlternativeResponse[];
207
211
  activeAlternativeIndex?: number;
208
212
  onAlternativeChange?: (index: number) => void;
@@ -281,7 +285,9 @@ interface UseChatUIReturn {
281
285
  cancelEdit: () => void;
282
286
  saveEdit: (content: string) => Promise<void>;
283
287
  regenerate: (messageId: string) => Promise<void>;
288
+ askOtherModel: (messageId: string, targetModel: string) => Promise<void>;
284
289
  updatePersonalization: (config: Partial<PersonalizationConfig>) => void;
290
+ models: ModelConfig[];
285
291
  }
286
292
 
287
293
  /**
@@ -191,6 +191,8 @@ interface MessageListProps {
191
191
  onEdit: (message: ChatMessage) => void;
192
192
  onRegenerate: (id: string) => void;
193
193
  onQuote: (text: string) => void;
194
+ onAskOtherModel?: (messageId: string, targetModel: string) => void;
195
+ models?: ModelConfig[];
194
196
  copiedId: string | null;
195
197
  editingId: string | null;
196
198
  }
@@ -203,6 +205,8 @@ interface MessageBubbleProps {
203
205
  onEdit: () => void;
204
206
  onRegenerate?: () => void;
205
207
  onQuote?: (text: string) => void;
208
+ onAskOtherModel?: (targetModel: string) => void;
209
+ models?: ModelConfig[];
206
210
  alternatives?: AlternativeResponse[];
207
211
  activeAlternativeIndex?: number;
208
212
  onAlternativeChange?: (index: number) => void;
@@ -281,7 +285,9 @@ interface UseChatUIReturn {
281
285
  cancelEdit: () => void;
282
286
  saveEdit: (content: string) => Promise<void>;
283
287
  regenerate: (messageId: string) => Promise<void>;
288
+ askOtherModel: (messageId: string, targetModel: string) => Promise<void>;
284
289
  updatePersonalization: (config: Partial<PersonalizationConfig>) => void;
290
+ models: ModelConfig[];
285
291
  }
286
292
 
287
293
  /**
@@ -539,6 +539,153 @@ ${contextSummary}` },
539
539
  setInput(userMessage.content);
540
540
  await sendMessage(userMessage.content);
541
541
  }, [currentSession, currentSessionId, isLoading, sendMessage]);
542
+ const askOtherModel = (0, import_react.useCallback)(async (messageId, targetModel) => {
543
+ if (!currentSession || !currentSessionId || isLoading) return;
544
+ const assistantIndex = currentSession.messages.findIndex((m) => m.id === messageId);
545
+ if (assistantIndex === -1) return;
546
+ const assistantMessage = currentSession.messages[assistantIndex];
547
+ if (assistantMessage.role !== "assistant") return;
548
+ const userMessage = currentSession.messages[assistantIndex - 1];
549
+ if (!userMessage || userMessage.role !== "user") return;
550
+ setIsLoading(true);
551
+ abortControllerRef.current = new AbortController();
552
+ try {
553
+ const messagesToSend = currentSession.messages.slice(0, assistantIndex);
554
+ let chatMessages;
555
+ if (currentSession.contextSummary) {
556
+ const recentMessages = messagesToSend.slice(-keepRecentMessages);
557
+ chatMessages = [
558
+ { role: "system", content: `[\uC774\uC804 \uB300\uD654 \uC694\uC57D]
559
+ ${currentSession.contextSummary}` },
560
+ ...recentMessages.map((m) => ({ role: m.role, content: m.content }))
561
+ ];
562
+ } else {
563
+ chatMessages = messagesToSend.map((m) => ({ role: m.role, content: m.content }));
564
+ }
565
+ const baseSystemPrompt = buildSystemPrompt();
566
+ const messagesForApi = baseSystemPrompt ? [{ role: "system", content: baseSystemPrompt }, ...chatMessages] : chatMessages;
567
+ const modelConfig = models.find((m) => m.id === targetModel);
568
+ const provider = modelConfig?.provider || "ollama";
569
+ let responseContent = "";
570
+ if (onSendMessage) {
571
+ const result = await onSendMessage({
572
+ messages: messagesForApi,
573
+ model: targetModel,
574
+ provider,
575
+ apiKey,
576
+ systemPrompt: baseSystemPrompt
577
+ });
578
+ if (typeof result === "string") {
579
+ responseContent = result;
580
+ } else {
581
+ const reader = result.getReader();
582
+ const decoder = new TextDecoder();
583
+ let buffer = "";
584
+ while (true) {
585
+ const { done, value } = await reader.read();
586
+ if (done) break;
587
+ buffer += decoder.decode(value, { stream: true });
588
+ const lines = buffer.split("\n");
589
+ buffer = lines.pop() || "";
590
+ for (const line of lines) {
591
+ if (line.startsWith("data: ")) {
592
+ const data = line.slice(6);
593
+ if (data === "[DONE]") continue;
594
+ try {
595
+ const parsed = JSON.parse(data);
596
+ if (parsed.content) responseContent += parsed.content;
597
+ } catch {
598
+ }
599
+ }
600
+ }
601
+ }
602
+ }
603
+ } else {
604
+ const response = await fetch(apiEndpoint, {
605
+ method: "POST",
606
+ headers: { "Content-Type": "application/json" },
607
+ body: JSON.stringify({
608
+ messages: messagesForApi,
609
+ model: targetModel,
610
+ provider,
611
+ apiKey: provider === "devdive" ? apiKey : void 0
612
+ }),
613
+ signal: abortControllerRef.current.signal
614
+ });
615
+ if (!response.ok) throw new Error("API error");
616
+ const reader = response.body?.getReader();
617
+ if (!reader) throw new Error("No reader");
618
+ const decoder = new TextDecoder();
619
+ let buffer = "";
620
+ while (true) {
621
+ const { done, value } = await reader.read();
622
+ if (done) break;
623
+ buffer += decoder.decode(value, { stream: true });
624
+ const lines = buffer.split("\n");
625
+ buffer = lines.pop() || "";
626
+ for (const line of lines) {
627
+ if (line.startsWith("data: ")) {
628
+ const data = line.slice(6);
629
+ if (data === "[DONE]") continue;
630
+ try {
631
+ const parsed = JSON.parse(data);
632
+ if (parsed.content) responseContent += parsed.content;
633
+ } catch {
634
+ }
635
+ }
636
+ }
637
+ }
638
+ }
639
+ const alternative = {
640
+ id: generateId("alt"),
641
+ model: targetModel,
642
+ content: responseContent,
643
+ timestamp: Date.now()
644
+ };
645
+ const capturedSessionId = currentSessionId;
646
+ setSessions(
647
+ (prev) => prev.map((s) => {
648
+ if (s.id === capturedSessionId) {
649
+ return {
650
+ ...s,
651
+ messages: s.messages.map((m) => {
652
+ if (m.id === messageId) {
653
+ const existingAlts = m.alternatives || [];
654
+ return {
655
+ ...m,
656
+ alternatives: [...existingAlts, alternative]
657
+ };
658
+ }
659
+ return m;
660
+ }),
661
+ updatedAt: Date.now()
662
+ };
663
+ }
664
+ return s;
665
+ })
666
+ );
667
+ } catch (error) {
668
+ if (error instanceof Error && error.name === "AbortError") {
669
+ return;
670
+ }
671
+ const err = error instanceof Error ? error : new Error("Unknown error");
672
+ onError?.(err);
673
+ } finally {
674
+ setIsLoading(false);
675
+ abortControllerRef.current = null;
676
+ }
677
+ }, [
678
+ currentSession,
679
+ currentSessionId,
680
+ isLoading,
681
+ keepRecentMessages,
682
+ buildSystemPrompt,
683
+ models,
684
+ apiEndpoint,
685
+ apiKey,
686
+ onSendMessage,
687
+ onError
688
+ ]);
542
689
  return {
543
690
  // State
544
691
  sessions,
@@ -573,7 +720,9 @@ ${contextSummary}` },
573
720
  cancelEdit,
574
721
  saveEdit,
575
722
  regenerate,
576
- updatePersonalization
723
+ askOtherModel,
724
+ updatePersonalization,
725
+ models
577
726
  };
578
727
  };
579
728
 
@@ -2033,13 +2182,17 @@ var MessageBubble = ({
2033
2182
  onEdit,
2034
2183
  onRegenerate,
2035
2184
  onQuote,
2185
+ onAskOtherModel,
2186
+ models,
2036
2187
  alternatives,
2037
2188
  activeAlternativeIndex = 0,
2038
2189
  onAlternativeChange
2039
2190
  }) => {
2040
2191
  const [showActions, setShowActions] = (0, import_react6.useState)(false);
2192
+ const [showModelMenu, setShowModelMenu] = (0, import_react6.useState)(false);
2041
2193
  const isUser = message.role === "user";
2042
2194
  const isAssistant = message.role === "assistant";
2195
+ const otherModels = models?.filter((m) => m.id !== message.model) || [];
2043
2196
  const displayContent = alternatives && alternatives.length > 0 && activeAlternativeIndex > 0 ? alternatives[activeAlternativeIndex - 1]?.content || message.content : message.content;
2044
2197
  const displayModel = alternatives && alternatives.length > 0 && activeAlternativeIndex > 0 ? alternatives[activeAlternativeIndex - 1]?.model : message.model;
2045
2198
  const handleMouseUp = () => {
@@ -2230,7 +2383,110 @@ var MessageBubble = ({
2230
2383
  }
2231
2384
  ) }),
2232
2385
  isUser && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { onClick: onEdit, style: actionButtonStyle, title: "\uC218\uC815", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconSvg, { name: "edit-line", size: 16, color: "var(--chatllm-text-muted, #9ca3af)" }) }),
2233
- isAssistant && onRegenerate && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { onClick: onRegenerate, style: actionButtonStyle, title: "\uB2E4\uC2DC \uC0DD\uC131", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconSvg, { name: "refresh-line", size: 16, color: "var(--chatllm-text-muted, #9ca3af)" }) })
2386
+ isAssistant && onRegenerate && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { onClick: onRegenerate, style: actionButtonStyle, title: "\uB2E4\uC2DC \uC0DD\uC131", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconSvg, { name: "refresh-line", size: 16, color: "var(--chatllm-text-muted, #9ca3af)" }) }),
2387
+ isAssistant && onAskOtherModel && otherModels.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { position: "relative" }, children: [
2388
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2389
+ "button",
2390
+ {
2391
+ onClick: () => setShowModelMenu(!showModelMenu),
2392
+ style: actionButtonStyle,
2393
+ title: "\uB2E4\uB978 \uBAA8\uB378\uC5D0\uAC8C \uC9C8\uBB38",
2394
+ children: [
2395
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconSvg, { name: "robot-line", size: 16, color: "var(--chatllm-text-muted, #9ca3af)" }),
2396
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2397
+ IconSvg,
2398
+ {
2399
+ name: "arrow-down-s-line",
2400
+ size: 12,
2401
+ color: "var(--chatllm-text-muted, #9ca3af)",
2402
+ style: { marginLeft: "2px" }
2403
+ }
2404
+ )
2405
+ ]
2406
+ }
2407
+ ),
2408
+ showModelMenu && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2409
+ "div",
2410
+ {
2411
+ style: {
2412
+ position: "absolute",
2413
+ bottom: "100%",
2414
+ left: 0,
2415
+ marginBottom: "4px",
2416
+ backgroundColor: "var(--chatllm-bg, #ffffff)",
2417
+ border: "1px solid var(--chatllm-border, #e5e7eb)",
2418
+ borderRadius: "8px",
2419
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
2420
+ minWidth: "160px",
2421
+ zIndex: 100,
2422
+ overflow: "hidden"
2423
+ },
2424
+ onMouseLeave: () => setShowModelMenu(false),
2425
+ children: [
2426
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2427
+ "div",
2428
+ {
2429
+ style: {
2430
+ padding: "8px 12px",
2431
+ fontSize: "11px",
2432
+ fontWeight: 600,
2433
+ color: "var(--chatllm-text-muted, #9ca3af)",
2434
+ textTransform: "uppercase",
2435
+ borderBottom: "1px solid var(--chatllm-border-light, #f3f4f6)"
2436
+ },
2437
+ children: "\uB2E4\uB978 \uBAA8\uB378\uC5D0\uAC8C \uC9C8\uBB38"
2438
+ }
2439
+ ),
2440
+ otherModels.map((model) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2441
+ "button",
2442
+ {
2443
+ onClick: () => {
2444
+ onAskOtherModel(model.id);
2445
+ setShowModelMenu(false);
2446
+ },
2447
+ style: {
2448
+ width: "100%",
2449
+ padding: "10px 12px",
2450
+ display: "flex",
2451
+ alignItems: "center",
2452
+ gap: "8px",
2453
+ backgroundColor: "transparent",
2454
+ border: "none",
2455
+ cursor: "pointer",
2456
+ fontSize: "13px",
2457
+ color: "var(--chatllm-text, #1f2937)",
2458
+ textAlign: "left"
2459
+ },
2460
+ onMouseEnter: (e) => {
2461
+ e.currentTarget.style.backgroundColor = "var(--chatllm-bg-hover, #f3f4f6)";
2462
+ },
2463
+ onMouseLeave: (e) => {
2464
+ e.currentTarget.style.backgroundColor = "transparent";
2465
+ },
2466
+ children: [
2467
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconSvg, { name: "robot-line", size: 14, color: "var(--chatllm-primary, #3b82f6)" }),
2468
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { flex: 1 }, children: model.name }),
2469
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2470
+ "span",
2471
+ {
2472
+ style: {
2473
+ fontSize: "10px",
2474
+ padding: "2px 6px",
2475
+ backgroundColor: "var(--chatllm-bg-tertiary, #f3f4f6)",
2476
+ borderRadius: "4px",
2477
+ color: "var(--chatllm-text-muted, #9ca3af)"
2478
+ },
2479
+ children: model.provider
2480
+ }
2481
+ )
2482
+ ]
2483
+ },
2484
+ model.id
2485
+ ))
2486
+ ]
2487
+ }
2488
+ )
2489
+ ] })
2234
2490
  ]
2235
2491
  }
2236
2492
  )
@@ -2277,6 +2533,8 @@ var MessageList = ({
2277
2533
  onEdit,
2278
2534
  onRegenerate,
2279
2535
  onQuote,
2536
+ onAskOtherModel,
2537
+ models,
2280
2538
  copiedId,
2281
2539
  editingId
2282
2540
  }) => {
@@ -2341,6 +2599,8 @@ var MessageList = ({
2341
2599
  onEdit: () => onEdit(message),
2342
2600
  onRegenerate: message.role === "assistant" ? () => onRegenerate(message.id) : void 0,
2343
2601
  onQuote,
2602
+ onAskOtherModel: message.role === "assistant" && onAskOtherModel ? (targetModel) => onAskOtherModel(message.id, targetModel) : void 0,
2603
+ models,
2344
2604
  alternatives: message.alternatives
2345
2605
  },
2346
2606
  message.id
@@ -3644,7 +3904,9 @@ var ChatUI = ({
3644
3904
  cancelEdit,
3645
3905
  saveEdit,
3646
3906
  regenerate,
3647
- updatePersonalization
3907
+ askOtherModel,
3908
+ updatePersonalization,
3909
+ models: hookModels
3648
3910
  } = useChatUI(hookOptions);
3649
3911
  const greeting = currentPersonalization.userProfile.nickname ? `\uC548\uB155\uD558\uC138\uC694, ${currentPersonalization.userProfile.nickname}\uB2D8` : "\uC548\uB155\uD558\uC138\uC694";
3650
3912
  const handleTemplateClick = (template) => {
@@ -3762,6 +4024,8 @@ var ChatUI = ({
3762
4024
  onEdit: startEdit,
3763
4025
  onRegenerate: regenerate,
3764
4026
  onQuote: setQuotedText,
4027
+ onAskOtherModel: askOtherModel,
4028
+ models: hookModels,
3765
4029
  copiedId: copiedMessageId,
3766
4030
  editingId: editingMessageId
3767
4031
  }