@gendive/chatllm 0.16.0 → 0.17.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.
@@ -1469,9 +1469,11 @@ var convertToolsToSkills = (tools, onToolCall) => {
1469
1469
  for (const tool of tools) {
1470
1470
  skillMap[tool.name] = {
1471
1471
  description: tool.description,
1472
- trigger: "both",
1472
+ /** @Todo vibecode - tool.trigger 지원 (기본 'both', 'attachment' 가능) */
1473
+ trigger: tool.trigger || "both",
1473
1474
  label: tool.label || tool.name,
1474
1475
  icon: tool.icon,
1476
+ acceptedTypes: tool.acceptedTypes,
1475
1477
  parameters: {
1476
1478
  type: "object",
1477
1479
  properties: Object.fromEntries(
@@ -1596,7 +1598,12 @@ var useChatUI = (options) => {
1596
1598
  onUpdateProject,
1597
1599
  onDeleteProject,
1598
1600
  onAddProjectFile,
1599
- onDeleteProjectFile
1601
+ onDeleteProjectFile,
1602
+ // Stream control
1603
+ continueAfterToolResult = true,
1604
+ onSkillComplete,
1605
+ // Dynamic model loading
1606
+ onLoadModels
1600
1607
  } = options;
1601
1608
  const enableAutoExtraction = enableAutoExtractionProp ?? !useExternalStorage;
1602
1609
  const [sessions, setSessions] = useState5([]);
@@ -1620,6 +1627,8 @@ var useChatUI = (options) => {
1620
1627
  const [isSessionLoading, setIsSessionLoading] = useState5(false);
1621
1628
  const [isDeepResearchMode, setIsDeepResearchMode] = useState5(false);
1622
1629
  const [attachments, setAttachments] = useState5([]);
1630
+ const [isModelsLoading, setIsModelsLoading] = useState5(false);
1631
+ const [loadedModels, setLoadedModels] = useState5(null);
1623
1632
  const [deepResearchProgress, setDeepResearchProgress] = useState5(
1624
1633
  null
1625
1634
  );
@@ -1627,6 +1636,40 @@ var useChatUI = (options) => {
1627
1636
  useEffect3(() => {
1628
1637
  sessionsRef.current = sessions;
1629
1638
  }, [sessions]);
1639
+ const onSendMessageRef = useRef4(onSendMessage);
1640
+ const onSessionChangeRef = useRef4(onSessionChange);
1641
+ const onErrorRef = useRef4(onError);
1642
+ const onTitleChangeRef = useRef4(onTitleChange);
1643
+ const generateTitleRef = useRef4(generateTitleCallback);
1644
+ const onPersonalizationChangeRef = useRef4(options.onPersonalizationChange);
1645
+ const onPersonalizationSaveRef = useRef4(options.onPersonalizationSave);
1646
+ const onLoadSessionsRef = useRef4(onLoadSessions);
1647
+ const onCreateSessionRef = useRef4(onCreateSession);
1648
+ const onLoadSessionRef = useRef4(onLoadSession);
1649
+ const onDeleteSessionCallbackRef = useRef4(onDeleteSessionCallback);
1650
+ const onUpdateSessionTitleRef = useRef4(onUpdateSessionTitle);
1651
+ const onSaveMessagesRef = useRef4(onSaveMessages);
1652
+ const onToolCallRef = useRef4(onToolCall);
1653
+ const onSkillCompleteRef = useRef4(onSkillComplete);
1654
+ const onLoadModelsRef = useRef4(onLoadModels);
1655
+ useEffect3(() => {
1656
+ onSendMessageRef.current = onSendMessage;
1657
+ onSessionChangeRef.current = onSessionChange;
1658
+ onErrorRef.current = onError;
1659
+ onTitleChangeRef.current = onTitleChange;
1660
+ generateTitleRef.current = generateTitleCallback;
1661
+ onPersonalizationChangeRef.current = options.onPersonalizationChange;
1662
+ onPersonalizationSaveRef.current = options.onPersonalizationSave;
1663
+ onLoadSessionsRef.current = onLoadSessions;
1664
+ onCreateSessionRef.current = onCreateSession;
1665
+ onLoadSessionRef.current = onLoadSession;
1666
+ onDeleteSessionCallbackRef.current = onDeleteSessionCallback;
1667
+ onUpdateSessionTitleRef.current = onUpdateSessionTitle;
1668
+ onSaveMessagesRef.current = onSaveMessages;
1669
+ onToolCallRef.current = onToolCall;
1670
+ onSkillCompleteRef.current = onSkillComplete;
1671
+ onLoadModelsRef.current = onLoadModels;
1672
+ });
1630
1673
  const abortControllerRef = useRef4(null);
1631
1674
  const skipNextPollParsingRef = useRef4(false);
1632
1675
  const skipNextSkillParsingRef = useRef4(false);
@@ -1642,11 +1685,15 @@ var useChatUI = (options) => {
1642
1685
  [globalMemoryConfig, storageKey]
1643
1686
  );
1644
1687
  const globalMemory = useGlobalMemoryEnabled ? useGlobalMemory(memoryOptions) : null;
1688
+ const stableToolCall = useCallback5(
1689
+ (name, params) => onToolCallRef.current(name, params),
1690
+ []
1691
+ );
1645
1692
  const mergedSkills = useMemo2(() => {
1646
1693
  if (!tools || !onToolCall) return skills || {};
1647
- const toolSkills = convertToolsToSkills(tools, onToolCall);
1694
+ const toolSkills = convertToolsToSkills(tools, stableToolCall);
1648
1695
  return { ...skills || {}, ...toolSkills };
1649
- }, [skills, tools, onToolCall]);
1696
+ }, [skills, tools, !!onToolCall, stableToolCall]);
1650
1697
  const {
1651
1698
  buildSkillsPrompt,
1652
1699
  handleSkillCall,
@@ -1700,9 +1747,9 @@ var useChatUI = (options) => {
1700
1747
  }, [sessions, enableProjects, projectHook.currentProjectId]);
1701
1748
  useEffect3(() => {
1702
1749
  if (typeof window === "undefined") return;
1703
- if (useExternalStorage && onLoadSessions) {
1750
+ if (useExternalStorage && onLoadSessionsRef.current) {
1704
1751
  setIsSessionsLoading(true);
1705
- onLoadSessions().then((sessionList) => {
1752
+ onLoadSessionsRef.current().then((sessionList) => {
1706
1753
  const sessionsWithoutMessages = sessionList.map((s) => ({
1707
1754
  id: s.id,
1708
1755
  title: s.title,
@@ -1718,7 +1765,7 @@ var useChatUI = (options) => {
1718
1765
  setCurrentSessionId(targetId);
1719
1766
  }
1720
1767
  }).catch((error) => {
1721
- onError?.(error instanceof Error ? error : new Error("Failed to load sessions"));
1768
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to load sessions"));
1722
1769
  }).finally(() => {
1723
1770
  setIsSessionsLoading(false);
1724
1771
  });
@@ -1755,7 +1802,22 @@ var useChatUI = (options) => {
1755
1802
  } catch {
1756
1803
  }
1757
1804
  }
1758
- }, [storageKey, useExternalStorage, onLoadSessions, initialModel, models, onError]);
1805
+ }, [storageKey, useExternalStorage, initialModel, models]);
1806
+ useEffect3(() => {
1807
+ if (!onLoadModelsRef.current) return;
1808
+ setIsModelsLoading(true);
1809
+ onLoadModelsRef.current().then((modelList) => {
1810
+ setLoadedModels(modelList);
1811
+ if (modelList.length > 0 && !modelList.find((m) => m.id === selectedModel)) {
1812
+ setSelectedModel(modelList[0].id);
1813
+ }
1814
+ }).catch((error) => {
1815
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to load models"));
1816
+ }).finally(() => {
1817
+ setIsModelsLoading(false);
1818
+ });
1819
+ }, []);
1820
+ const effectiveModels = loadedModels || models;
1759
1821
  useEffect3(() => {
1760
1822
  if (typeof window === "undefined") return;
1761
1823
  if (useExternalStorage) return;
@@ -1768,8 +1830,8 @@ var useChatUI = (options) => {
1768
1830
  localStorage.setItem(`${storageKey}_personalization`, JSON.stringify(personalization));
1769
1831
  }, [personalization, storageKey]);
1770
1832
  useEffect3(() => {
1771
- onSessionChange?.(currentSession);
1772
- }, [currentSession, onSessionChange]);
1833
+ onSessionChangeRef.current?.(currentSession);
1834
+ }, [currentSession]);
1773
1835
  const buildSystemPrompt = useCallback5(() => {
1774
1836
  const parts = [];
1775
1837
  const { userProfile, responseStyle, language } = personalization;
@@ -1975,10 +2037,10 @@ ${newConversation}
1975
2037
  }, []);
1976
2038
  const newSession = useCallback5(async () => {
1977
2039
  const projectId = enableProjects ? projectHook.currentProjectId || DEFAULT_PROJECT_ID : void 0;
1978
- if (useExternalStorage && onCreateSession) {
2040
+ if (useExternalStorage && onCreateSessionRef.current) {
1979
2041
  setIsSessionLoading(true);
1980
2042
  try {
1981
- const created = await onCreateSession();
2043
+ const created = await onCreateSessionRef.current();
1982
2044
  const now2 = Date.now();
1983
2045
  const newSess2 = {
1984
2046
  id: created.id,
@@ -1992,7 +2054,7 @@ ${newConversation}
1992
2054
  setSessions((prev) => [newSess2, ...prev]);
1993
2055
  setCurrentSessionId(newSess2.id);
1994
2056
  } catch (error) {
1995
- onError?.(error instanceof Error ? error : new Error("Failed to create session"));
2057
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to create session"));
1996
2058
  } finally {
1997
2059
  setIsSessionLoading(false);
1998
2060
  }
@@ -2010,12 +2072,12 @@ ${newConversation}
2010
2072
  };
2011
2073
  setSessions((prev) => [newSess, ...prev]);
2012
2074
  setCurrentSessionId(newSess.id);
2013
- }, [selectedModel, useExternalStorage, onCreateSession, onError, enableProjects, projectHook.currentProjectId]);
2075
+ }, [selectedModel, useExternalStorage, enableProjects, projectHook.currentProjectId]);
2014
2076
  const selectSession = useCallback5(async (id) => {
2015
- if (useExternalStorage && onLoadSession) {
2077
+ if (useExternalStorage && onLoadSessionRef.current) {
2016
2078
  setIsSessionLoading(true);
2017
2079
  try {
2018
- const sessionDetail = await onLoadSession(id);
2080
+ const sessionDetail = await onLoadSessionRef.current(id);
2019
2081
  let loadedMessages = sessionDetail.messages.map((m, idx) => ({
2020
2082
  id: m.id || generateId3("msg"),
2021
2083
  role: typeof m.role === "string" ? m.role.toLowerCase() : m.role,
@@ -2061,7 +2123,7 @@ ${newConversation}
2061
2123
  setSelectedModel(existingSession.model);
2062
2124
  }
2063
2125
  } catch (error) {
2064
- onError?.(error instanceof Error ? error : new Error("Failed to load session"));
2126
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to load session"));
2065
2127
  const cached = readSessionCache(storageKey, id);
2066
2128
  if (cached && cached.messages.length > 0) {
2067
2129
  console.warn("[useChatUI] onLoadSession failed, using localStorage cache");
@@ -2086,11 +2148,11 @@ ${newConversation}
2086
2148
  setCurrentSessionId(id);
2087
2149
  setSelectedModel(session.model);
2088
2150
  }
2089
- }, [sessions, useExternalStorage, onLoadSession, onError, storageKey, initialModel, models]);
2151
+ }, [sessions, useExternalStorage, storageKey, initialModel, models]);
2090
2152
  const deleteSession = useCallback5(async (id) => {
2091
- if (useExternalStorage && onDeleteSessionCallback) {
2153
+ if (useExternalStorage && onDeleteSessionCallbackRef.current) {
2092
2154
  try {
2093
- await onDeleteSessionCallback(id);
2155
+ await onDeleteSessionCallbackRef.current(id);
2094
2156
  removeSessionCache(storageKey, id);
2095
2157
  setSessions((prev) => {
2096
2158
  const filtered = prev.filter((s) => s.id !== id);
@@ -2100,7 +2162,7 @@ ${newConversation}
2100
2162
  return filtered;
2101
2163
  });
2102
2164
  } catch (error) {
2103
- onError?.(error instanceof Error ? error : new Error("Failed to delete session"));
2165
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to delete session"));
2104
2166
  }
2105
2167
  return;
2106
2168
  }
@@ -2114,20 +2176,20 @@ ${newConversation}
2114
2176
  }
2115
2177
  return filtered;
2116
2178
  });
2117
- }, [currentSessionId, storageKey, useExternalStorage, onDeleteSessionCallback, onError]);
2179
+ }, [currentSessionId, storageKey, useExternalStorage]);
2118
2180
  const renameSession = useCallback5(async (id, newTitle) => {
2119
2181
  if (!newTitle.trim()) return;
2120
- if (useExternalStorage && onUpdateSessionTitle) {
2182
+ if (useExternalStorage && onUpdateSessionTitleRef.current) {
2121
2183
  try {
2122
- await onUpdateSessionTitle(id, newTitle.trim());
2184
+ await onUpdateSessionTitleRef.current(id, newTitle.trim());
2123
2185
  setSessions(
2124
2186
  (prev) => prev.map(
2125
2187
  (s) => s.id === id ? { ...s, title: newTitle.trim(), updatedAt: Date.now() } : s
2126
2188
  )
2127
2189
  );
2128
- onTitleChange?.(id, newTitle.trim());
2190
+ onTitleChangeRef.current?.(id, newTitle.trim());
2129
2191
  } catch (error) {
2130
- onError?.(error instanceof Error ? error : new Error("Failed to update session title"));
2192
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to update session title"));
2131
2193
  }
2132
2194
  return;
2133
2195
  }
@@ -2136,8 +2198,8 @@ ${newConversation}
2136
2198
  (s) => s.id === id ? { ...s, title: newTitle.trim(), updatedAt: Date.now() } : s
2137
2199
  )
2138
2200
  );
2139
- onTitleChange?.(id, newTitle.trim());
2140
- }, [onTitleChange, useExternalStorage, onUpdateSessionTitle, onError]);
2201
+ onTitleChangeRef.current?.(id, newTitle.trim());
2202
+ }, [useExternalStorage]);
2141
2203
  const setModel = useCallback5((model) => {
2142
2204
  setSelectedModel(model);
2143
2205
  if (currentSessionId) {
@@ -2171,13 +2233,13 @@ ${newConversation}
2171
2233
  const updatePersonalization = useCallback5((config) => {
2172
2234
  setPersonalization((prev) => {
2173
2235
  const next = { ...prev, ...config };
2174
- options.onPersonalizationChange?.(next);
2236
+ onPersonalizationChangeRef.current?.(next);
2175
2237
  return next;
2176
2238
  });
2177
- }, [options.onPersonalizationChange]);
2239
+ }, []);
2178
2240
  const savePersonalization = useCallback5(() => {
2179
- options.onPersonalizationSave?.(personalization);
2180
- }, [options.onPersonalizationSave, personalization]);
2241
+ onPersonalizationSaveRef.current?.(personalization);
2242
+ }, [personalization]);
2181
2243
  const addAttachments = useCallback5((files) => {
2182
2244
  const newAttachments = files.map((file) => {
2183
2245
  const isImage = file.type.startsWith("image/");
@@ -2210,9 +2272,9 @@ ${newConversation}
2210
2272
  if (!messageContent.trim() || isLoading) return;
2211
2273
  let sessionId = currentSessionId;
2212
2274
  if (!sessionId) {
2213
- if (useExternalStorage && onCreateSession) {
2275
+ if (useExternalStorage && onCreateSessionRef.current) {
2214
2276
  try {
2215
- const created = await onCreateSession();
2277
+ const created = await onCreateSessionRef.current();
2216
2278
  const now = Date.now();
2217
2279
  const newSess = {
2218
2280
  id: created.id,
@@ -2226,7 +2288,7 @@ ${newConversation}
2226
2288
  sessionId = newSess.id;
2227
2289
  setCurrentSessionId(sessionId);
2228
2290
  } catch (error) {
2229
- onError?.(error instanceof Error ? error : new Error("Failed to create session"));
2291
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to create session"));
2230
2292
  setIsLoading(false);
2231
2293
  return;
2232
2294
  }
@@ -2279,13 +2341,51 @@ ${finalContent}`;
2279
2341
  ...isHidden && { hidden: true },
2280
2342
  ...userContentParts && { contentParts: userContentParts }
2281
2343
  };
2344
+ let attachmentResults = [];
2345
+ if (currentAttachments.length > 0) {
2346
+ const attachmentSkills = Object.entries(resolvedSkills).filter(
2347
+ ([, config]) => config.trigger === "attachment"
2348
+ );
2349
+ for (const [skillName, skillConfig] of attachmentSkills) {
2350
+ const matchedFiles = currentAttachments.filter((att) => {
2351
+ if (!skillConfig.acceptedTypes || skillConfig.acceptedTypes.length === 0) return true;
2352
+ return skillConfig.acceptedTypes.some((type) => {
2353
+ if (type.startsWith(".")) return att.name.toLowerCase().endsWith(type.toLowerCase());
2354
+ if (type.includes("*")) {
2355
+ const regex = new RegExp("^" + type.replace("*", ".*") + "$");
2356
+ return regex.test(att.mimeType);
2357
+ }
2358
+ return att.mimeType === type;
2359
+ });
2360
+ });
2361
+ if (matchedFiles.length === 0) continue;
2362
+ try {
2363
+ const result = await skillConfig.execute({ files: matchedFiles, userMessage: finalContent });
2364
+ attachmentResults.push({
2365
+ type: "tool_result",
2366
+ toolName: skillName,
2367
+ label: skillConfig.label,
2368
+ icon: skillConfig.icon,
2369
+ result: {
2370
+ type: "text",
2371
+ content: result.content,
2372
+ metadata: result.metadata,
2373
+ sources: result.sources
2374
+ }
2375
+ });
2376
+ } catch (error) {
2377
+ console.error(`[useChatUI] attachment skill ${skillName} failed:`, error);
2378
+ }
2379
+ }
2380
+ }
2282
2381
  const assistantMessageId = generateId3("msg");
2283
2382
  const assistantMessage = {
2284
2383
  id: assistantMessageId,
2285
2384
  role: "assistant",
2286
2385
  content: "",
2287
2386
  model: selectedModel,
2288
- timestamp: Date.now()
2387
+ timestamp: Date.now(),
2388
+ ...attachmentResults.length > 0 && { contentParts: attachmentResults }
2289
2389
  };
2290
2390
  setInput("");
2291
2391
  setQuotedText(null);
@@ -2312,15 +2412,15 @@ ${finalContent}`;
2312
2412
  return s;
2313
2413
  })
2314
2414
  );
2315
- if (isFirstMessage && generateTitleCallback) {
2316
- Promise.resolve(generateTitleCallback(finalContent)).then((generatedTitle) => {
2415
+ if (isFirstMessage && generateTitleRef.current) {
2416
+ Promise.resolve(generateTitleRef.current(finalContent)).then((generatedTitle) => {
2317
2417
  if (generatedTitle && generatedTitle.trim()) {
2318
2418
  setSessions(
2319
2419
  (prev) => prev.map(
2320
2420
  (s) => s.id === capturedSessionId ? { ...s, title: generatedTitle.trim(), updatedAt: Date.now() } : s
2321
2421
  )
2322
2422
  );
2323
- onTitleChange?.(capturedSessionId, generatedTitle.trim());
2423
+ onTitleChangeRef.current?.(capturedSessionId, generatedTitle.trim());
2324
2424
  }
2325
2425
  }).catch(() => {
2326
2426
  });
@@ -2648,6 +2748,16 @@ ${currentContextSummary}` },
2648
2748
  abortControllerRef.current = null;
2649
2749
  return;
2650
2750
  }
2751
+ let shouldContinue = continueAfterToolResult;
2752
+ if (onSkillCompleteRef.current) {
2753
+ const decision = onSkillCompleteRef.current(toolName, result);
2754
+ shouldContinue = decision === "continue";
2755
+ }
2756
+ if (!shouldContinue) {
2757
+ setIsLoading(false);
2758
+ abortControllerRef.current = null;
2759
+ return;
2760
+ }
2651
2761
  skipNextSkillParsingRef.current = true;
2652
2762
  const feedbackPrompt = resultType === "error" ? `\uB3C4\uAD6C "${toolName}" \uC2E4\uD589 \uC911 \uC624\uB958 \uBC1C\uC0DD: ${result.content}
2653
2763
 
@@ -2688,6 +2798,16 @@ ${result.content}
2688
2798
  abortControllerRef.current = null;
2689
2799
  return;
2690
2800
  }
2801
+ let shouldContinueSkill = continueAfterToolResult;
2802
+ if (onSkillCompleteRef.current) {
2803
+ const decision = onSkillCompleteRef.current(detectedSkill.name, result);
2804
+ shouldContinueSkill = decision === "continue";
2805
+ }
2806
+ if (!shouldContinueSkill) {
2807
+ setIsLoading(false);
2808
+ abortControllerRef.current = null;
2809
+ return;
2810
+ }
2691
2811
  skipNextSkillParsingRef.current = true;
2692
2812
  const resultPrompt = `\uC2A4\uD0AC "${detectedSkill.name}" \uC2E4\uD589 \uACB0\uACFC:
2693
2813
 
@@ -2727,7 +2847,7 @@ ${result.content}
2727
2847
  );
2728
2848
  if (useExternalStorage && capturedSessionId) {
2729
2849
  const assistantContentForSave = accumulatedContent;
2730
- if (assistantContentForSave && onSaveMessages) {
2850
+ if (assistantContentForSave && onSaveMessagesRef.current) {
2731
2851
  const latestSession = sessionsRef.current.find((s) => s.id === capturedSessionId);
2732
2852
  const latestMessages = latestSession?.messages || [];
2733
2853
  const userMsg = latestMessages.find((m) => m.role === "user" && m.content === finalContent);
@@ -2736,7 +2856,7 @@ ${result.content}
2736
2856
  { role: "user", message: finalContent, ...userMsg?.contentParts && { contentParts: userMsg.contentParts } },
2737
2857
  { role: "assistant", message: assistantContentForSave, ...assistantMsg?.contentParts && { contentParts: assistantMsg.contentParts } }
2738
2858
  ];
2739
- onSaveMessages(capturedSessionId, messagesToSave).catch((saveError) => {
2859
+ onSaveMessagesRef.current(capturedSessionId, messagesToSave).catch((saveError) => {
2740
2860
  console.error("[useChatUI] Failed to save messages:", saveError);
2741
2861
  });
2742
2862
  }
@@ -2764,7 +2884,7 @@ ${result.content}
2764
2884
  return;
2765
2885
  }
2766
2886
  const err = error instanceof Error ? error : new Error("Unknown error");
2767
- onError?.(err);
2887
+ onErrorRef.current?.(err);
2768
2888
  setSessions(
2769
2889
  (prev) => prev.map((s) => {
2770
2890
  if (s.id === capturedSessionId) {
@@ -3037,11 +3157,11 @@ ${formattedParts.join("\n")}
3037
3157
  return;
3038
3158
  }
3039
3159
  console.error("[ChatUI] Regenerate error:", error);
3040
- onError?.(error instanceof Error ? error : new Error("Unknown error"));
3160
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Unknown error"));
3041
3161
  } finally {
3042
3162
  setIsLoading(false);
3043
3163
  }
3044
- }, [currentSession, currentSessionId, isLoading, selectedModel, models, apiEndpoint, apiKey, buildSystemPrompt, onError]);
3164
+ }, [currentSession, currentSessionId, isLoading, selectedModel, models, apiEndpoint, apiKey, buildSystemPrompt]);
3045
3165
  const askOtherModel = useCallback5(async (messageId, targetModel) => {
3046
3166
  if (!currentSession || !currentSessionId || isLoading) return;
3047
3167
  const assistantIndex = currentSession.messages.findIndex((m) => m.id === messageId);
@@ -3193,7 +3313,7 @@ ${currentSession.contextSummary}` },
3193
3313
  return;
3194
3314
  }
3195
3315
  const err = error instanceof Error ? error : new Error("Unknown error");
3196
- onError?.(err);
3316
+ onErrorRef.current?.(err);
3197
3317
  } finally {
3198
3318
  setIsLoading(false);
3199
3319
  setLoadingAlternativeFor(null);
@@ -3262,7 +3382,8 @@ ${currentSession.contextSummary}` },
3262
3382
  getActiveAlternative,
3263
3383
  updatePersonalization,
3264
3384
  savePersonalization,
3265
- models,
3385
+ models: effectiveModels,
3386
+ isModelsLoading,
3266
3387
  // Memory
3267
3388
  globalMemory,
3268
3389
  compressionState,
@@ -3375,8 +3496,8 @@ ${currentSession.contextSummary}` },
3375
3496
  if (!enableProjects) return;
3376
3497
  const projectSessions = sessionsRef.current.filter((s) => s.projectId === projectId);
3377
3498
  for (const sess of projectSessions) {
3378
- if (useExternalStorage && onDeleteSessionCallback) {
3379
- await onDeleteSessionCallback(sess.id).catch(() => {
3499
+ if (useExternalStorage && onDeleteSessionCallbackRef.current) {
3500
+ await onDeleteSessionCallbackRef.current(sess.id).catch(() => {
3380
3501
  });
3381
3502
  }
3382
3503
  removeSessionCache(storageKey, sess.id);
@@ -3736,7 +3857,8 @@ var ChatSidebar = ({
3736
3857
  onNewProject,
3737
3858
  onProjectSettings,
3738
3859
  renderAfterHeader,
3739
- renderFooter
3860
+ renderFooter,
3861
+ isLoading = false
3740
3862
  }) => {
3741
3863
  const sidebarWidth = typeof widthProp === "number" ? `${widthProp}px` : widthProp || "288px";
3742
3864
  const [editingId, setEditingId] = useState7(null);
@@ -3831,11 +3953,10 @@ var ChatSidebar = ({
3831
3953
  style: {
3832
3954
  fontSize: "14px",
3833
3955
  fontWeight: 700,
3834
- textTransform: "uppercase",
3835
3956
  letterSpacing: "-0.01em",
3836
3957
  color: "var(--chatllm-text)"
3837
3958
  },
3838
- children: "Intelligence"
3959
+ children: "AI \uCC44\uD305"
3839
3960
  }
3840
3961
  )
3841
3962
  ] })
@@ -3885,7 +4006,22 @@ var ChatSidebar = ({
3885
4006
  display: "flex",
3886
4007
  flexDirection: "column"
3887
4008
  },
3888
- children: sessions.length === 0 ? (
4009
+ children: isLoading ? (
4010
+ /* Skeleton Loading State */
4011
+ /* @__PURE__ */ jsx3("div", { style: { display: "flex", flexDirection: "column", gap: "8px", padding: "0 4px" }, children: [1, 2, 3, 4].map((i) => /* @__PURE__ */ jsx3(
4012
+ "div",
4013
+ {
4014
+ className: "chatllm-skeleton-pulse",
4015
+ style: {
4016
+ height: "48px",
4017
+ borderRadius: "8px",
4018
+ backgroundColor: "var(--chatllm-bg-tertiary)",
4019
+ opacity: 1 - (i - 1) * 0.15
4020
+ }
4021
+ },
4022
+ i
4023
+ )) })
4024
+ ) : sessions.length === 0 ? (
3889
4025
  /* Empty State */
3890
4026
  /* @__PURE__ */ jsxs2(
3891
4027
  "div",
@@ -4828,6 +4964,16 @@ var ChatInput = ({
4828
4964
  }
4829
4965
  )
4830
4966
  ] }),
4967
+ /* @__PURE__ */ jsx5(
4968
+ "button",
4969
+ {
4970
+ onClick: () => fileInputRef.current?.click(),
4971
+ style: iconButtonStyle,
4972
+ title: "\uD30C\uC77C \uCCA8\uBD80",
4973
+ "aria-label": "\uD30C\uC77C \uCCA8\uBD80",
4974
+ children: /* @__PURE__ */ jsx5(IconSvg, { name: "attachment-line", size: 22, color: "var(--chatllm-text-muted)" })
4975
+ }
4976
+ ),
4831
4977
  /* @__PURE__ */ jsxs4("div", { ref: mainMenuRef, style: { position: "relative" }, children: [
4832
4978
  /* @__PURE__ */ jsx5(
4833
4979
  "button",
@@ -5507,45 +5653,99 @@ var parseTableRow = (row) => {
5507
5653
  return row.split("|").slice(1, -1).map((cell) => cell.trim());
5508
5654
  };
5509
5655
  var MarkdownTable = ({ data }) => {
5656
+ const [copied, setCopied] = React6.useState(false);
5657
+ const [isHovered, setIsHovered] = React6.useState(false);
5658
+ const handleCopy = async () => {
5659
+ const headerLine = data.headers.join(" ");
5660
+ const bodyLines = data.rows.map((row) => row.join(" "));
5661
+ const text = [headerLine, ...bodyLines].join("\n");
5662
+ try {
5663
+ await navigator.clipboard.writeText(text);
5664
+ setCopied(true);
5665
+ setTimeout(() => setCopied(false), 2e3);
5666
+ } catch {
5667
+ console.error("Failed to copy table");
5668
+ }
5669
+ };
5510
5670
  return /* @__PURE__ */ jsxs6(
5511
- "table",
5671
+ "div",
5512
5672
  {
5513
- className: "chatllm-table",
5514
- style: {
5515
- width: "100%",
5516
- borderCollapse: "collapse",
5517
- margin: "12px 0",
5518
- fontSize: "14px"
5519
- },
5673
+ style: { position: "relative", margin: "12px 0" },
5674
+ onMouseEnter: () => setIsHovered(true),
5675
+ onMouseLeave: () => setIsHovered(false),
5520
5676
  children: [
5521
- /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsx7("tr", { children: data.headers.map((header, i) => /* @__PURE__ */ jsx7(
5522
- "th",
5677
+ /* @__PURE__ */ jsxs6(
5678
+ "table",
5523
5679
  {
5680
+ className: "chatllm-table",
5524
5681
  style: {
5525
- border: "1px solid var(--chatllm-border, #e5e7eb)",
5526
- padding: "10px 12px",
5527
- textAlign: data.alignments[i] || "left",
5528
- backgroundColor: "var(--chatllm-bg-secondary, #f9fafb)",
5529
- fontWeight: 600,
5530
- color: "var(--chatllm-text, #374151)"
5682
+ width: "100%",
5683
+ borderCollapse: "collapse",
5684
+ fontSize: "14px"
5531
5685
  },
5532
- children: parseInlineElements(header, `th-${i}`)
5533
- },
5534
- i
5535
- )) }) }),
5536
- /* @__PURE__ */ jsx7("tbody", { children: data.rows.map((row, rowIndex) => /* @__PURE__ */ jsx7("tr", { children: row.map((cell, cellIndex) => /* @__PURE__ */ jsx7(
5537
- "td",
5686
+ children: [
5687
+ /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsx7("tr", { children: data.headers.map((header, i) => /* @__PURE__ */ jsx7(
5688
+ "th",
5689
+ {
5690
+ style: {
5691
+ border: "1px solid var(--chatllm-border, #e5e7eb)",
5692
+ padding: "10px 12px",
5693
+ textAlign: data.alignments[i] || "left",
5694
+ backgroundColor: "var(--chatllm-bg-secondary, #f9fafb)",
5695
+ fontWeight: 600,
5696
+ color: "var(--chatllm-text, #374151)"
5697
+ },
5698
+ children: parseInlineElements(header, `th-${i}`)
5699
+ },
5700
+ i
5701
+ )) }) }),
5702
+ /* @__PURE__ */ jsx7("tbody", { children: data.rows.map((row, rowIndex) => /* @__PURE__ */ jsx7("tr", { children: row.map((cell, cellIndex) => /* @__PURE__ */ jsx7(
5703
+ "td",
5704
+ {
5705
+ style: {
5706
+ border: "1px solid var(--chatllm-border, #e5e7eb)",
5707
+ padding: "10px 12px",
5708
+ textAlign: data.alignments[cellIndex] || "left",
5709
+ color: "var(--chatllm-text, #374151)"
5710
+ },
5711
+ children: parseInlineElements(cell, `td-${rowIndex}-${cellIndex}`)
5712
+ },
5713
+ cellIndex
5714
+ )) }, rowIndex)) })
5715
+ ]
5716
+ }
5717
+ ),
5718
+ /* @__PURE__ */ jsxs6(
5719
+ "button",
5538
5720
  {
5721
+ onClick: handleCopy,
5539
5722
  style: {
5723
+ position: "absolute",
5724
+ top: "4px",
5725
+ right: "4px",
5726
+ padding: "4px 8px",
5727
+ fontSize: "12px",
5728
+ backgroundColor: "var(--chatllm-bg, #ffffff)",
5540
5729
  border: "1px solid var(--chatllm-border, #e5e7eb)",
5541
- padding: "10px 12px",
5542
- textAlign: data.alignments[cellIndex] || "left",
5543
- color: "var(--chatllm-text, #374151)"
5730
+ borderRadius: "6px",
5731
+ cursor: "pointer",
5732
+ color: copied ? "var(--chatllm-success, #22c55e)" : "var(--chatllm-text-muted, #9ca3af)",
5733
+ opacity: isHovered || copied ? 1 : 0,
5734
+ transition: "opacity 0.15s",
5735
+ display: "flex",
5736
+ alignItems: "center",
5737
+ gap: "4px",
5738
+ boxShadow: "0 1px 3px rgba(0,0,0,0.08)"
5544
5739
  },
5545
- children: parseInlineElements(cell, `td-${rowIndex}-${cellIndex}`)
5546
- },
5547
- cellIndex
5548
- )) }, rowIndex)) })
5740
+ children: [
5741
+ /* @__PURE__ */ jsx7("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: copied ? /* @__PURE__ */ jsx7("polyline", { points: "20 6 9 17 4 12" }) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
5742
+ /* @__PURE__ */ jsx7("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
5743
+ /* @__PURE__ */ jsx7("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
5744
+ ] }) }),
5745
+ copied ? "\uBCF5\uC0AC\uB428" : "\uBCF5\uC0AC"
5746
+ ]
5747
+ }
5748
+ )
5549
5749
  ]
5550
5750
  }
5551
5751
  );
@@ -6759,6 +6959,7 @@ var PollCard = ({
6759
6959
  onSkip?.();
6760
6960
  }, [questions, onSubmit, onSkip]);
6761
6961
  useEffect7(() => {
6962
+ if (typeof window === "undefined") return;
6762
6963
  const handleKeyDown = (e) => {
6763
6964
  if (e.key === "Escape") handleSkip();
6764
6965
  };
@@ -7703,7 +7904,7 @@ var MessageBubble = ({
7703
7904
  const displaySources = isAssistant && relevantAlternatives && relevantAlternatives.length > 0 && relevantActiveIndex > 0 ? relevantAlternatives[relevantActiveIndex - 1]?.sources : message.sources;
7704
7905
  const handleMouseUp = () => {
7705
7906
  if (!onQuote) return;
7706
- const selection = window.getSelection();
7907
+ const selection = typeof window !== "undefined" ? window.getSelection() : null;
7707
7908
  const text = selection?.toString().trim();
7708
7909
  if (text && text.length > 0) {
7709
7910
  }
@@ -8318,7 +8519,7 @@ var MessageList = ({
8318
8519
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
8319
8520
  }, [messages]);
8320
8521
  const handleMouseUp = useCallback8(() => {
8321
- const selection = window.getSelection();
8522
+ const selection = typeof window !== "undefined" ? window.getSelection() : null;
8322
8523
  const text = selection?.toString().trim();
8323
8524
  if (text && text.length > 0) {
8324
8525
  const range = selection?.getRangeAt(0);
@@ -8334,7 +8535,7 @@ var MessageList = ({
8334
8535
  }
8335
8536
  } else {
8336
8537
  setTimeout(() => {
8337
- const currentSelection = window.getSelection()?.toString().trim();
8538
+ const currentSelection = typeof window !== "undefined" ? window.getSelection()?.toString().trim() : void 0;
8338
8539
  if (!currentSelection) {
8339
8540
  setSelectionPosition(null);
8340
8541
  }
@@ -8346,7 +8547,9 @@ var MessageList = ({
8346
8547
  onQuote(selectedText);
8347
8548
  setSelectionPosition(null);
8348
8549
  setSelectedText("");
8349
- window.getSelection()?.removeAllRanges();
8550
+ if (typeof window !== "undefined") {
8551
+ window.getSelection()?.removeAllRanges();
8552
+ }
8350
8553
  }
8351
8554
  };
8352
8555
  return /* @__PURE__ */ jsxs15(
@@ -9004,7 +9207,7 @@ var SettingsModal = ({
9004
9207
  "button",
9005
9208
  {
9006
9209
  onClick: () => {
9007
- if (window.confirm("\uBAA8\uB4E0 \uB300\uD654 \uAE30\uB85D\uC744 \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?")) {
9210
+ if (typeof window !== "undefined" && window.confirm("\uBAA8\uB4E0 \uB300\uD654 \uAE30\uB85D\uC744 \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?")) {
9008
9211
  onClearAllData();
9009
9212
  }
9010
9213
  },
@@ -9275,7 +9478,7 @@ var MemoryTabContent = ({ items, contextSummary, onDelete, onClearAll, title = "
9275
9478
  "button",
9276
9479
  {
9277
9480
  onClick: () => {
9278
- if (window.confirm("\uBAA8\uB4E0 AI \uBA54\uBAA8\uB9AC\uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?")) {
9481
+ if (typeof window !== "undefined" && window.confirm("\uBAA8\uB4E0 AI \uBA54\uBAA8\uB9AC\uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?")) {
9279
9482
  onClearAll();
9280
9483
  }
9281
9484
  },
@@ -9522,7 +9725,7 @@ var ProjectSettingsModal = ({
9522
9725
  "button",
9523
9726
  {
9524
9727
  onClick: () => {
9525
- if (window.confirm(`"${project.title}" \uD504\uB85C\uC81D\uD2B8\uC640 \uC18C\uC18D \uB300\uD654\uAC00 \uBAA8\uB450 \uC0AD\uC81C\uB429\uB2C8\uB2E4. \uACC4\uC18D\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`)) {
9728
+ if (typeof window !== "undefined" && window.confirm(`"${project.title}" \uD504\uB85C\uC81D\uD2B8\uC640 \uC18C\uC18D \uB300\uD654\uAC00 \uBAA8\uB450 \uC0AD\uC81C\uB429\uB2C8\uB2E4. \uACC4\uC18D\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`)) {
9526
9729
  onDeleteProject(project.id);
9527
9730
  onClose();
9528
9731
  }
@@ -10027,7 +10230,8 @@ var ChatUIView = ({
10027
10230
  projectSettingsOpen,
10028
10231
  openProjectSettings,
10029
10232
  closeProjectSettings,
10030
- projectMemory
10233
+ projectMemory,
10234
+ isSessionsLoading
10031
10235
  } = state;
10032
10236
  const greeting = currentPersonalization.userProfile.nickname ? `\uC548\uB155\uD558\uC138\uC694, ${currentPersonalization.userProfile.nickname}\uB2D8` : "\uC548\uB155\uD558\uC138\uC694";
10033
10237
  const handleTemplateClick = (template) => {
@@ -10111,7 +10315,8 @@ var ChatUIView = ({
10111
10315
  openProjectSettings();
10112
10316
  } : void 0,
10113
10317
  renderAfterHeader: sidebarRenderAfterHeader,
10114
- renderFooter: sidebarRenderFooter
10318
+ renderFooter: sidebarRenderFooter,
10319
+ isLoading: isSessionsLoading
10115
10320
  }
10116
10321
  ),
10117
10322
  /* @__PURE__ */ jsxs19(
@@ -10283,6 +10488,9 @@ var ChatUIWithHook = ({
10283
10488
  skills,
10284
10489
  tools,
10285
10490
  onToolCall,
10491
+ continueAfterToolResult,
10492
+ onSkillComplete,
10493
+ onLoadModels,
10286
10494
  // Project options
10287
10495
  enableProjects,
10288
10496
  onLoadProjects,
@@ -10322,6 +10530,9 @@ var ChatUIWithHook = ({
10322
10530
  skills,
10323
10531
  tools,
10324
10532
  onToolCall,
10533
+ continueAfterToolResult,
10534
+ onSkillComplete,
10535
+ onLoadModels,
10325
10536
  enableProjects,
10326
10537
  onLoadProjects,
10327
10538
  onCreateProject,