@gendive/chatllm 0.8.1 → 0.10.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.
@@ -568,7 +568,15 @@ var useChatUI = (options) => {
568
568
  // Memory options
569
569
  useGlobalMemoryEnabled = true,
570
570
  globalMemoryConfig,
571
- enableAutoExtraction = true
571
+ enableAutoExtraction = true,
572
+ // External storage options
573
+ useExternalStorage = false,
574
+ onLoadSessions,
575
+ onCreateSession,
576
+ onLoadSession,
577
+ onDeleteSession: onDeleteSessionCallback,
578
+ onUpdateSessionTitle,
579
+ onSaveMessages
572
580
  } = options;
573
581
  const [sessions, setSessions] = (0, import_react3.useState)([]);
574
582
  const [currentSessionId, setCurrentSessionId] = (0, import_react3.useState)(null);
@@ -586,6 +594,8 @@ var useChatUI = (options) => {
586
594
  ...initialPersonalization
587
595
  });
588
596
  const [activeAlternatives, setActiveAlternatives] = (0, import_react3.useState)({});
597
+ const [isSessionsLoading, setIsSessionsLoading] = (0, import_react3.useState)(false);
598
+ const [isSessionLoading, setIsSessionLoading] = (0, import_react3.useState)(false);
589
599
  const abortControllerRef = (0, import_react3.useRef)(null);
590
600
  const memoryOptions = (0, import_react3.useMemo)(
591
601
  () => ({
@@ -609,6 +619,36 @@ var useChatUI = (options) => {
609
619
  const compressionState = currentSession?.compressionState || null;
610
620
  (0, import_react3.useEffect)(() => {
611
621
  if (typeof window === "undefined") return;
622
+ if (useExternalStorage && onLoadSessions) {
623
+ setIsSessionsLoading(true);
624
+ onLoadSessions().then((sessionList) => {
625
+ const sessionsWithoutMessages = sessionList.map((s) => ({
626
+ id: s.id,
627
+ title: s.title,
628
+ messages: [],
629
+ // 메시지는 세션 선택 시 로드
630
+ model: initialModel || models[0]?.id || "",
631
+ createdAt: Date.now(),
632
+ updatedAt: Date.now()
633
+ }));
634
+ setSessions(sessionsWithoutMessages);
635
+ if (sessionsWithoutMessages.length > 0) {
636
+ setCurrentSessionId(sessionsWithoutMessages[0].id);
637
+ }
638
+ }).catch((error) => {
639
+ onError?.(error instanceof Error ? error : new Error("Failed to load sessions"));
640
+ }).finally(() => {
641
+ setIsSessionsLoading(false);
642
+ });
643
+ const savedPersonalization2 = localStorage.getItem(`${storageKey}_personalization`);
644
+ if (savedPersonalization2) {
645
+ try {
646
+ setPersonalization(JSON.parse(savedPersonalization2));
647
+ } catch {
648
+ }
649
+ }
650
+ return;
651
+ }
612
652
  const saved = localStorage.getItem(storageKey);
613
653
  if (saved) {
614
654
  try {
@@ -628,13 +668,14 @@ var useChatUI = (options) => {
628
668
  } catch {
629
669
  }
630
670
  }
631
- }, [storageKey]);
671
+ }, [storageKey, useExternalStorage, onLoadSessions, initialModel, models, onError]);
632
672
  (0, import_react3.useEffect)(() => {
633
673
  if (typeof window === "undefined") return;
674
+ if (useExternalStorage) return;
634
675
  if (sessions.length > 0) {
635
676
  localStorage.setItem(storageKey, JSON.stringify(sessions));
636
677
  }
637
- }, [sessions, storageKey]);
678
+ }, [sessions, storageKey, useExternalStorage]);
638
679
  (0, import_react3.useEffect)(() => {
639
680
  if (typeof window === "undefined") return;
640
681
  localStorage.setItem(`${storageKey}_personalization`, JSON.stringify(personalization));
@@ -787,7 +828,29 @@ ${newConversation}
787
828
  const estimateTokens = (0, import_react3.useCallback)((messages2) => {
788
829
  return messages2.reduce((sum, m) => sum + Math.ceil(m.content.length / 4), 0);
789
830
  }, []);
790
- const newSession = (0, import_react3.useCallback)(() => {
831
+ const newSession = (0, import_react3.useCallback)(async () => {
832
+ if (useExternalStorage && onCreateSession) {
833
+ setIsSessionLoading(true);
834
+ try {
835
+ const created = await onCreateSession();
836
+ const now2 = Date.now();
837
+ const newSess2 = {
838
+ id: created.id,
839
+ title: created.title,
840
+ messages: [],
841
+ model: selectedModel,
842
+ createdAt: now2,
843
+ updatedAt: now2
844
+ };
845
+ setSessions((prev) => [newSess2, ...prev]);
846
+ setCurrentSessionId(newSess2.id);
847
+ } catch (error) {
848
+ onError?.(error instanceof Error ? error : new Error("Failed to create session"));
849
+ } finally {
850
+ setIsSessionLoading(false);
851
+ }
852
+ return;
853
+ }
791
854
  const now = Date.now();
792
855
  const newSess = {
793
856
  id: generateId("session"),
@@ -799,15 +862,60 @@ ${newConversation}
799
862
  };
800
863
  setSessions((prev) => [newSess, ...prev]);
801
864
  setCurrentSessionId(newSess.id);
802
- }, [selectedModel]);
803
- const selectSession = (0, import_react3.useCallback)((id) => {
865
+ }, [selectedModel, useExternalStorage, onCreateSession, onError]);
866
+ const selectSession = (0, import_react3.useCallback)(async (id) => {
867
+ if (useExternalStorage && onLoadSession) {
868
+ setIsSessionLoading(true);
869
+ try {
870
+ const sessionDetail = await onLoadSession(id);
871
+ const loadedMessages = sessionDetail.messages.map((m, idx) => ({
872
+ id: m.id || generateId("msg"),
873
+ role: typeof m.role === "string" ? m.role.toLowerCase() : m.role,
874
+ content: m.content,
875
+ timestamp: m.timestamp || Date.now() - (sessionDetail.messages.length - idx) * 1e3,
876
+ model: m.model,
877
+ alternatives: m.alternatives,
878
+ sources: m.sources
879
+ }));
880
+ setSessions(
881
+ (prev) => prev.map(
882
+ (s) => s.id === id ? { ...s, title: sessionDetail.title, messages: loadedMessages, updatedAt: Date.now() } : s
883
+ )
884
+ );
885
+ setCurrentSessionId(id);
886
+ const existingSession = sessions.find((s) => s.id === id);
887
+ if (existingSession) {
888
+ setSelectedModel(existingSession.model);
889
+ }
890
+ } catch (error) {
891
+ onError?.(error instanceof Error ? error : new Error("Failed to load session"));
892
+ } finally {
893
+ setIsSessionLoading(false);
894
+ }
895
+ return;
896
+ }
804
897
  const session = sessions.find((s) => s.id === id);
805
898
  if (session) {
806
899
  setCurrentSessionId(id);
807
900
  setSelectedModel(session.model);
808
901
  }
809
- }, [sessions]);
810
- const deleteSession = (0, import_react3.useCallback)((id) => {
902
+ }, [sessions, useExternalStorage, onLoadSession, onError]);
903
+ const deleteSession = (0, import_react3.useCallback)(async (id) => {
904
+ if (useExternalStorage && onDeleteSessionCallback) {
905
+ try {
906
+ await onDeleteSessionCallback(id);
907
+ setSessions((prev) => {
908
+ const filtered = prev.filter((s) => s.id !== id);
909
+ if (currentSessionId === id) {
910
+ setCurrentSessionId(filtered.length > 0 ? filtered[0].id : null);
911
+ }
912
+ return filtered;
913
+ });
914
+ } catch (error) {
915
+ onError?.(error instanceof Error ? error : new Error("Failed to delete session"));
916
+ }
917
+ return;
918
+ }
811
919
  setSessions((prev) => {
812
920
  const filtered = prev.filter((s) => s.id !== id);
813
921
  if (currentSessionId === id) {
@@ -818,16 +926,30 @@ ${newConversation}
818
926
  }
819
927
  return filtered;
820
928
  });
821
- }, [currentSessionId, storageKey]);
822
- const renameSession = (0, import_react3.useCallback)((id, newTitle) => {
929
+ }, [currentSessionId, storageKey, useExternalStorage, onDeleteSessionCallback, onError]);
930
+ const renameSession = (0, import_react3.useCallback)(async (id, newTitle) => {
823
931
  if (!newTitle.trim()) return;
932
+ if (useExternalStorage && onUpdateSessionTitle) {
933
+ try {
934
+ await onUpdateSessionTitle(id, newTitle.trim());
935
+ setSessions(
936
+ (prev) => prev.map(
937
+ (s) => s.id === id ? { ...s, title: newTitle.trim(), updatedAt: Date.now() } : s
938
+ )
939
+ );
940
+ onTitleChange?.(id, newTitle.trim());
941
+ } catch (error) {
942
+ onError?.(error instanceof Error ? error : new Error("Failed to update session title"));
943
+ }
944
+ return;
945
+ }
824
946
  setSessions(
825
947
  (prev) => prev.map(
826
948
  (s) => s.id === id ? { ...s, title: newTitle.trim(), updatedAt: Date.now() } : s
827
949
  )
828
950
  );
829
951
  onTitleChange?.(id, newTitle.trim());
830
- }, [onTitleChange]);
952
+ }, [onTitleChange, useExternalStorage, onUpdateSessionTitle, onError]);
831
953
  const setModel = (0, import_react3.useCallback)((model) => {
832
954
  setSelectedModel(model);
833
955
  if (currentSessionId) {
@@ -1095,6 +1217,21 @@ ${contextSummary}` },
1095
1217
  }
1096
1218
  }
1097
1219
  }
1220
+ if (useExternalStorage && onSaveMessages && capturedSessionId) {
1221
+ const updatedSession = sessions.find((s) => s.id === capturedSessionId);
1222
+ const assistantContent = updatedSession?.messages.find(
1223
+ (m) => m.id === assistantMessageId
1224
+ )?.content || "";
1225
+ if (assistantContent) {
1226
+ const messagesToSave = [
1227
+ { role: "USER", message: finalContent },
1228
+ { role: "ASSISTANT", message: assistantContent }
1229
+ ];
1230
+ onSaveMessages(capturedSessionId, messagesToSave).catch((saveError) => {
1231
+ console.error("[useChatUI] Failed to save messages:", saveError);
1232
+ });
1233
+ }
1234
+ }
1098
1235
  } catch (error) {
1099
1236
  if (error instanceof Error && error.name === "AbortError") {
1100
1237
  return;
@@ -1136,7 +1273,9 @@ ${contextSummary}` },
1136
1273
  onSendMessage,
1137
1274
  onError,
1138
1275
  generateTitleCallback,
1139
- onTitleChange
1276
+ onTitleChange,
1277
+ useExternalStorage,
1278
+ onSaveMessages
1140
1279
  ]);
1141
1280
  const saveEdit = (0, import_react3.useCallback)(async (content) => {
1142
1281
  if (!editingMessageId || !currentSession || !currentSessionId) return;
@@ -1389,7 +1528,18 @@ ${currentSession.contextSummary}` },
1389
1528
  content: m.content
1390
1529
  }));
1391
1530
  await infoExtraction.extractInfo(recentMessages);
1392
- }
1531
+ },
1532
+ // External Storage Loading States
1533
+ /**
1534
+ * @description 세션 목록 로딩 상태
1535
+ * @Todo vibecode - 외부 스토리지 사용 시
1536
+ */
1537
+ isSessionsLoading,
1538
+ /**
1539
+ * @description 단일 세션 로딩 상태
1540
+ * @Todo vibecode - 외부 스토리지 사용 시
1541
+ */
1542
+ isSessionLoading
1393
1543
  };
1394
1544
  };
1395
1545
 
@@ -2547,6 +2697,9 @@ var SOURCE_LINKS_REGEX = /(\*{0,2}출처:?\*{0,2}\s*)?((?:(?<!!)\[`?[^\]]+`?\]\(
2547
2697
  var INLINE_CHOICE_PATTERN = /\[(\d+)\]\s*([^\[\n]+?)(?=\s*\[\d+\]|$)/g;
2548
2698
  var INLINE_CHOICE_LINE_PATTERN = /^(.+?)?\s*(\[\d+\][^\[]+(?:\[\d+\][^\[]+)+)$/;
2549
2699
  var CODE_BLOCK_REGEX = /```(\w*)\n?([\s\S]*?)```/g;
2700
+ var THINKING_TAG_REGEX = /<thinking>([\s\S]*?)<\/thinking>/gi;
2701
+ var THINKING_CODEBLOCK_REGEX = /```thinking\n?([\s\S]*?)```/gi;
2702
+ var THINKING_TEXT_REGEX = /^Thinking:\s*\n([\s\S]*?)(?=\n\n|$)/gim;
2550
2703
  var INLINE_CODE_REGEX = /`([^`]+)`/g;
2551
2704
  var BOLD_REGEX = /\*\*([^*]+)\*\*/g;
2552
2705
  var ITALIC_REGEX = /(?<!\*)\*([^*]+)\*(?!\*)/g;
@@ -2724,6 +2877,80 @@ var MarkdownTable = ({ data }) => {
2724
2877
  }
2725
2878
  );
2726
2879
  };
2880
+ var ThinkingBlock = ({ content, defaultOpen = false }) => {
2881
+ const [isOpen, setIsOpen] = import_react8.default.useState(defaultOpen);
2882
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2883
+ "details",
2884
+ {
2885
+ open: isOpen,
2886
+ className: "chatllm-thinking",
2887
+ style: {
2888
+ margin: "12px 0",
2889
+ borderRadius: "8px",
2890
+ border: "1px solid var(--chatllm-border, #e5e7eb)",
2891
+ background: "var(--chatllm-bg-secondary, #f9fafb)",
2892
+ overflow: "hidden"
2893
+ },
2894
+ children: [
2895
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2896
+ "summary",
2897
+ {
2898
+ onClick: (e) => {
2899
+ e.preventDefault();
2900
+ setIsOpen(!isOpen);
2901
+ },
2902
+ style: {
2903
+ padding: "10px 14px",
2904
+ cursor: "pointer",
2905
+ fontSize: "13px",
2906
+ color: "var(--chatllm-text-secondary, #6b7280)",
2907
+ userSelect: "none",
2908
+ listStyle: "none",
2909
+ display: "flex",
2910
+ alignItems: "center",
2911
+ gap: "6px"
2912
+ },
2913
+ children: [
2914
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { fontSize: "14px" }, children: "\u{1F4AD}" }),
2915
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
2916
+ "Thinking",
2917
+ isOpen ? "" : "..."
2918
+ ] }),
2919
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2920
+ "span",
2921
+ {
2922
+ style: {
2923
+ marginLeft: "auto",
2924
+ fontSize: "10px",
2925
+ transition: "transform 0.2s",
2926
+ transform: isOpen ? "rotate(180deg)" : "rotate(0deg)"
2927
+ },
2928
+ children: "\u25BC"
2929
+ }
2930
+ )
2931
+ ]
2932
+ }
2933
+ ),
2934
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2935
+ "div",
2936
+ {
2937
+ className: "chatllm-thinking-content",
2938
+ style: {
2939
+ padding: "12px 14px",
2940
+ borderTop: "1px solid var(--chatllm-border, #e5e7eb)",
2941
+ fontSize: "13px",
2942
+ color: "var(--chatllm-text-secondary, #6b7280)",
2943
+ whiteSpace: "pre-wrap",
2944
+ lineHeight: "1.6",
2945
+ background: "var(--chatllm-bg, #ffffff)"
2946
+ },
2947
+ children: content.trim()
2948
+ }
2949
+ )
2950
+ ]
2951
+ }
2952
+ );
2953
+ };
2727
2954
  var CodeBlock = ({ language, code }) => {
2728
2955
  const [copied, setCopied] = import_react8.default.useState(false);
2729
2956
  const handleCopy = async () => {
@@ -3148,10 +3375,35 @@ var SourceLinksSection = ({ links, label }) => {
3148
3375
  }
3149
3376
  );
3150
3377
  };
3151
- var MarkdownRenderer = ({ content, className, onChoiceClick }) => {
3378
+ var MarkdownRenderer = ({
3379
+ content,
3380
+ className,
3381
+ onChoiceClick,
3382
+ showThinking = true,
3383
+ thinkingDefaultOpen = false
3384
+ }) => {
3152
3385
  const rendered = (0, import_react8.useMemo)(() => {
3153
3386
  const elements = [];
3154
3387
  let processedContent = content;
3388
+ const thinkingBlocks = [];
3389
+ if (showThinking) {
3390
+ processedContent = processedContent.replace(THINKING_TAG_REGEX, (_, thinkContent) => {
3391
+ thinkingBlocks.push(thinkContent);
3392
+ return `\xA7THINKING\xA7${thinkingBlocks.length - 1}\xA7/THINKING\xA7`;
3393
+ });
3394
+ processedContent = processedContent.replace(THINKING_CODEBLOCK_REGEX, (_, thinkContent) => {
3395
+ thinkingBlocks.push(thinkContent);
3396
+ return `\xA7THINKING\xA7${thinkingBlocks.length - 1}\xA7/THINKING\xA7`;
3397
+ });
3398
+ processedContent = processedContent.replace(THINKING_TEXT_REGEX, (_, thinkContent) => {
3399
+ thinkingBlocks.push(thinkContent);
3400
+ return `\xA7THINKING\xA7${thinkingBlocks.length - 1}\xA7/THINKING\xA7`;
3401
+ });
3402
+ } else {
3403
+ processedContent = processedContent.replace(THINKING_TAG_REGEX, "");
3404
+ processedContent = processedContent.replace(THINKING_CODEBLOCK_REGEX, "");
3405
+ processedContent = processedContent.replace(THINKING_TEXT_REGEX, "");
3406
+ }
3155
3407
  const codeBlocks = [];
3156
3408
  processedContent = processedContent.replace(CODE_BLOCK_REGEX, (_, lang, code) => {
3157
3409
  codeBlocks.push({ language: lang || "", code });
@@ -3259,6 +3511,23 @@ var MarkdownRenderer = ({ content, className, onChoiceClick }) => {
3259
3511
  }
3260
3512
  };
3261
3513
  lines.forEach((line, lineIndex) => {
3514
+ const thinkingMatch = line.match(/§THINKING§(\d+)§\/THINKING§/);
3515
+ if (thinkingMatch) {
3516
+ flushList();
3517
+ flushBlockquote();
3518
+ const index = parseInt(thinkingMatch[1]);
3519
+ elements.push(
3520
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3521
+ ThinkingBlock,
3522
+ {
3523
+ content: thinkingBlocks[index],
3524
+ defaultOpen: thinkingDefaultOpen
3525
+ },
3526
+ `thinking-${lineIndex}`
3527
+ )
3528
+ );
3529
+ return;
3530
+ }
3262
3531
  const codeBlockMatch = line.match(/§CODEBLOCK§(\d+)§\/CODEBLOCK§/);
3263
3532
  if (codeBlockMatch) {
3264
3533
  flushList();
@@ -3447,7 +3716,9 @@ var MessageBubble = ({
3447
3716
  activeAlternativeIndex = 0,
3448
3717
  onAlternativeChange,
3449
3718
  nextAssistantMessage,
3450
- onChoiceClick
3719
+ onChoiceClick,
3720
+ showThinking = true,
3721
+ thinkingDefaultOpen = false
3451
3722
  }) => {
3452
3723
  const [showActions, setShowActions] = (0, import_react9.useState)(false);
3453
3724
  const [showModelMenu, setShowModelMenu] = (0, import_react9.useState)(false);
@@ -3551,7 +3822,15 @@ var MessageBubble = ({
3551
3822
  children: [
3552
3823
  isAssistant ? (
3553
3824
  // AI 메시지는 마크다운 렌더링
3554
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MarkdownRenderer, { content: displayContent, onChoiceClick })
3825
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3826
+ MarkdownRenderer,
3827
+ {
3828
+ content: displayContent,
3829
+ onChoiceClick,
3830
+ showThinking,
3831
+ thinkingDefaultOpen
3832
+ }
3833
+ )
3555
3834
  ) : (
3556
3835
  // 사용자 메시지는 일반 텍스트
3557
3836
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
@@ -3841,7 +4120,9 @@ var MessageList = ({
3841
4120
  models,
3842
4121
  copiedId,
3843
4122
  editingId,
3844
- onChoiceClick
4123
+ onChoiceClick,
4124
+ showThinking = true,
4125
+ thinkingDefaultOpen = false
3845
4126
  }) => {
3846
4127
  const messagesEndRef = (0, import_react10.useRef)(null);
3847
4128
  const containerRef = (0, import_react10.useRef)(null);
@@ -3914,7 +4195,9 @@ var MessageList = ({
3914
4195
  onAlternativeChange: message.role === "user" && assistantForAlts && onSetActiveAlternative ? (idx) => onSetActiveAlternative(assistantForAlts.id, idx) : message.role === "assistant" && onSetActiveAlternative ? (idx) => onSetActiveAlternative(message.id, idx) : void 0,
3915
4196
  models,
3916
4197
  alternatives: message.role === "assistant" ? message.alternatives : assistantForAlts?.alternatives,
3917
- onChoiceClick: message.role === "assistant" ? onChoiceClick : void 0
4198
+ onChoiceClick: message.role === "assistant" ? onChoiceClick : void 0,
4199
+ showThinking,
4200
+ thinkingDefaultOpen
3918
4201
  },
3919
4202
  message.id
3920
4203
  );
@@ -5202,7 +5485,18 @@ var ChatUI = ({
5202
5485
  onSessionChange,
5203
5486
  onError,
5204
5487
  onTitleChange,
5205
- generateTitle: generateTitle2
5488
+ generateTitle: generateTitle2,
5489
+ // External Storage Props
5490
+ useExternalStorage = false,
5491
+ onLoadSessions,
5492
+ onCreateSession,
5493
+ onLoadSession,
5494
+ onDeleteSession,
5495
+ onUpdateSessionTitle,
5496
+ onSaveMessages,
5497
+ // Thinking Block Props
5498
+ showThinking = true,
5499
+ thinkingDefaultOpen = false
5206
5500
  }) => {
5207
5501
  import_react13.default.useEffect(() => {
5208
5502
  injectStyles();
@@ -5221,7 +5515,15 @@ var ChatUI = ({
5221
5515
  onSessionChange,
5222
5516
  onError,
5223
5517
  onTitleChange,
5224
- generateTitle: generateTitle2
5518
+ generateTitle: generateTitle2,
5519
+ // External Storage Options
5520
+ useExternalStorage,
5521
+ onLoadSessions,
5522
+ onCreateSession,
5523
+ onLoadSession,
5524
+ onDeleteSession,
5525
+ onUpdateSessionTitle,
5526
+ onSaveMessages
5225
5527
  };
5226
5528
  const {
5227
5529
  sessions,
@@ -5390,7 +5692,9 @@ var ChatUI = ({
5390
5692
  models: hookModels,
5391
5693
  copiedId: copiedMessageId,
5392
5694
  editingId: editingMessageId,
5393
- onChoiceClick: handleChoiceClick
5695
+ onChoiceClick: handleChoiceClick,
5696
+ showThinking,
5697
+ thinkingDefaultOpen
5394
5698
  }
5395
5699
  ),
5396
5700
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(