@gendive/chatllm 0.17.0 → 0.17.2

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.
@@ -396,6 +396,11 @@ interface ChatToolDefinition {
396
396
  * @Todo vibecode - MIME 와일드카드 ('image/*'), 확장자 ('.pdf') 지원
397
397
  */
398
398
  acceptedTypes?: string[];
399
+ /**
400
+ * @description 파일 첨부 시 File 객체를 base64 문자열로 자동 변환
401
+ * @Todo vibecode - true 시 execute()에 { name, mimeType, base64, size } 형태로 전달
402
+ */
403
+ autoConvertBase64?: boolean;
399
404
  }
400
405
  interface ChatMessage {
401
406
  id: string;
@@ -541,6 +546,11 @@ interface SkillConfig<TParams = Record<string, unknown>> {
541
546
  * @example ['image/*', 'application/pdf', '.docx']
542
547
  */
543
548
  acceptedTypes?: string[];
549
+ /**
550
+ * @description 파일 첨부 시 File 객체를 base64 문자열로 자동 변환
551
+ * @Todo vibecode - true 시 execute()에 { name, mimeType, base64, size } 형태로 전달
552
+ */
553
+ autoConvertBase64?: boolean;
544
554
  }
545
555
  /**
546
556
  * @description 스킬 실행 상태 (메시지에 첨부)
@@ -576,6 +586,14 @@ interface ModelConfig {
576
586
  name: string;
577
587
  provider: ProviderType;
578
588
  description?: string;
589
+ /** @Todo vibecode - UI 표시용 아이콘 (URL 또는 아이콘 이름) */
590
+ icon?: string;
591
+ /** @Todo vibecode - 최대 출력 토큰 수 */
592
+ maxTokens?: number;
593
+ /** @Todo vibecode - 컨텍스트 윈도우 크기 (토큰) */
594
+ contextWindow?: number;
595
+ /** @Todo vibecode - 호스트 커스텀 필드 허용 */
596
+ [key: string]: unknown;
579
597
  }
580
598
  interface PromptTemplate {
581
599
  id: string;
@@ -396,6 +396,11 @@ interface ChatToolDefinition {
396
396
  * @Todo vibecode - MIME 와일드카드 ('image/*'), 확장자 ('.pdf') 지원
397
397
  */
398
398
  acceptedTypes?: string[];
399
+ /**
400
+ * @description 파일 첨부 시 File 객체를 base64 문자열로 자동 변환
401
+ * @Todo vibecode - true 시 execute()에 { name, mimeType, base64, size } 형태로 전달
402
+ */
403
+ autoConvertBase64?: boolean;
399
404
  }
400
405
  interface ChatMessage {
401
406
  id: string;
@@ -541,6 +546,11 @@ interface SkillConfig<TParams = Record<string, unknown>> {
541
546
  * @example ['image/*', 'application/pdf', '.docx']
542
547
  */
543
548
  acceptedTypes?: string[];
549
+ /**
550
+ * @description 파일 첨부 시 File 객체를 base64 문자열로 자동 변환
551
+ * @Todo vibecode - true 시 execute()에 { name, mimeType, base64, size } 형태로 전달
552
+ */
553
+ autoConvertBase64?: boolean;
544
554
  }
545
555
  /**
546
556
  * @description 스킬 실행 상태 (메시지에 첨부)
@@ -576,6 +586,14 @@ interface ModelConfig {
576
586
  name: string;
577
587
  provider: ProviderType;
578
588
  description?: string;
589
+ /** @Todo vibecode - UI 표시용 아이콘 (URL 또는 아이콘 이름) */
590
+ icon?: string;
591
+ /** @Todo vibecode - 최대 출력 토큰 수 */
592
+ maxTokens?: number;
593
+ /** @Todo vibecode - 컨텍스트 윈도우 크기 (토큰) */
594
+ contextWindow?: number;
595
+ /** @Todo vibecode - 호스트 커스텀 필드 허용 */
596
+ [key: string]: unknown;
579
597
  }
580
598
  interface PromptTemplate {
581
599
  id: string;
@@ -1199,7 +1199,7 @@ var useProject = (options) => {
1199
1199
  const currentProject = projects.find((p) => p.id === currentProjectId) || null;
1200
1200
  (0, import_react4.useEffect)(() => {
1201
1201
  if (!enabled || useExternalStorage || !initializedRef.current) return;
1202
- if (projects.length > 0) {
1202
+ if (projects.length > 0 && typeof window !== "undefined") {
1203
1203
  localStorage.setItem(storageKey, JSON.stringify(projects));
1204
1204
  }
1205
1205
  }, [enabled, projects, storageKey, useExternalStorage]);
@@ -1233,7 +1233,7 @@ var useProject = (options) => {
1233
1233
  }
1234
1234
  } else {
1235
1235
  try {
1236
- const saved = localStorage.getItem(storageKey);
1236
+ const saved = typeof window !== "undefined" ? localStorage.getItem(storageKey) : null;
1237
1237
  if (saved) {
1238
1238
  const parsed = JSON.parse(saved);
1239
1239
  if (!parsed.find((p) => p.id === DEFAULT_PROJECT_ID)) {
@@ -1540,6 +1540,7 @@ var convertToolsToSkills = (tools, onToolCall) => {
1540
1540
  label: tool.label || tool.name,
1541
1541
  icon: tool.icon,
1542
1542
  acceptedTypes: tool.acceptedTypes,
1543
+ autoConvertBase64: tool.autoConvertBase64,
1543
1544
  parameters: {
1544
1545
  type: "object",
1545
1546
  properties: Object.fromEntries(
@@ -1613,6 +1614,20 @@ var DEFAULT_KEEP_RECENT = 6;
1613
1614
  var DEFAULT_RECOMPRESSION_THRESHOLD = 10;
1614
1615
  var DEFAULT_TOKEN_LIMIT = 8e3;
1615
1616
  var generateId3 = (prefix) => `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1617
+ var fileToBase64 = (file) => new Promise((resolve, reject) => {
1618
+ const reader = new FileReader();
1619
+ reader.onload = () => resolve(reader.result.split(",")[1] || "");
1620
+ reader.onerror = reject;
1621
+ reader.readAsDataURL(file);
1622
+ });
1623
+ var convertAttachmentsToBase64 = async (attachments) => Promise.all(
1624
+ attachments.map(async (att) => ({
1625
+ name: att.name,
1626
+ mimeType: att.mimeType,
1627
+ base64: await fileToBase64(att.file),
1628
+ size: att.size
1629
+ }))
1630
+ );
1616
1631
  var generateTitle = (messages) => {
1617
1632
  const firstUserMessage = messages.find((m) => m.role === "user");
1618
1633
  if (!firstUserMessage) return "\uC0C8 \uB300\uD654";
@@ -1835,11 +1850,13 @@ var useChatUI = (options) => {
1835
1850
  }).finally(() => {
1836
1851
  setIsSessionsLoading(false);
1837
1852
  });
1838
- const savedPersonalization2 = localStorage.getItem(`${storageKey}_personalization`);
1839
- if (savedPersonalization2) {
1840
- try {
1841
- setPersonalization(JSON.parse(savedPersonalization2));
1842
- } catch {
1853
+ if (typeof window !== "undefined") {
1854
+ const savedPersonalization2 = localStorage.getItem(`${storageKey}_personalization`);
1855
+ if (savedPersonalization2) {
1856
+ try {
1857
+ setPersonalization(JSON.parse(savedPersonalization2));
1858
+ } catch {
1859
+ }
1843
1860
  }
1844
1861
  }
1845
1862
  return;
@@ -1847,6 +1864,7 @@ var useChatUI = (options) => {
1847
1864
  if (enableProjects) {
1848
1865
  migrateSessionsToProjects(storageKey);
1849
1866
  }
1867
+ if (typeof window === "undefined") return;
1850
1868
  const saved = localStorage.getItem(storageKey);
1851
1869
  if (saved) {
1852
1870
  try {
@@ -2335,7 +2353,8 @@ ${newConversation}
2335
2353
  }, []);
2336
2354
  const sendMessage = (0, import_react5.useCallback)(async (content, options2) => {
2337
2355
  const messageContent = content || input;
2338
- if (!messageContent.trim() || isLoading) return;
2356
+ if (!messageContent.trim() && attachments.length === 0 || isLoading) return;
2357
+ setIsLoading(true);
2339
2358
  let sessionId = currentSessionId;
2340
2359
  if (!sessionId) {
2341
2360
  if (useExternalStorage && onCreateSessionRef.current) {
@@ -2426,7 +2445,8 @@ ${finalContent}`;
2426
2445
  });
2427
2446
  if (matchedFiles.length === 0) continue;
2428
2447
  try {
2429
- const result = await skillConfig.execute({ files: matchedFiles, userMessage: finalContent });
2448
+ const filesToPass = skillConfig.autoConvertBase64 ? await convertAttachmentsToBase64(matchedFiles) : matchedFiles;
2449
+ const result = await skillConfig.execute({ files: filesToPass, userMessage: finalContent });
2430
2450
  attachmentResults.push({
2431
2451
  type: "tool_result",
2432
2452
  toolName: skillName,
@@ -2444,6 +2464,20 @@ ${finalContent}`;
2444
2464
  }
2445
2465
  }
2446
2466
  }
2467
+ let shouldContinueAfterAttachment = continueAfterToolResult;
2468
+ if (attachmentResults.length > 0) {
2469
+ for (const part of attachmentResults) {
2470
+ if (part.type === "tool_result" && onSkillCompleteRef.current) {
2471
+ const decision = onSkillCompleteRef.current(part.toolName, {
2472
+ content: part.result.content,
2473
+ metadata: part.result.metadata,
2474
+ sources: part.result.sources
2475
+ });
2476
+ shouldContinueAfterAttachment = decision === "continue";
2477
+ if (!shouldContinueAfterAttachment) break;
2478
+ }
2479
+ }
2480
+ }
2447
2481
  const assistantMessageId = generateId3("msg");
2448
2482
  const assistantMessage = {
2449
2483
  id: assistantMessageId,
@@ -2491,7 +2525,10 @@ ${finalContent}`;
2491
2525
  }).catch(() => {
2492
2526
  });
2493
2527
  }
2494
- setIsLoading(true);
2528
+ if (attachmentResults.length > 0 && !shouldContinueAfterAttachment) {
2529
+ setIsLoading(false);
2530
+ return;
2531
+ }
2495
2532
  abortControllerRef.current = new AbortController();
2496
2533
  try {
2497
2534
  let messagesToSend = [...existingMessages, userMessage];
@@ -2550,6 +2587,20 @@ ${currentContextSummary}` },
2550
2587
  } else {
2551
2588
  chatMessages = messagesToSend.map((m) => ({ role: m.role, content: m.content }));
2552
2589
  }
2590
+ if (attachmentResults.length > 0 && shouldContinueAfterAttachment) {
2591
+ const attachmentContext = attachmentResults.filter((part) => part.type === "tool_result").map((part) => `[${part.label || part.toolName} \uACB0\uACFC]
2592
+ ${part.result.content}`).join("\n\n");
2593
+ if (attachmentContext) {
2594
+ chatMessages.push({
2595
+ role: "user",
2596
+ content: `\uD30C\uC77C \uBD84\uC11D \uACB0\uACFC:
2597
+
2598
+ ${attachmentContext}
2599
+
2600
+ \uC704 \uACB0\uACFC\uB97C \uCC38\uACE0\uD558\uC5EC \uB2F5\uBCC0\uD574\uC8FC\uC138\uC694.`
2601
+ });
2602
+ }
2603
+ }
2553
2604
  console.log("[ChatUI] Messages to send:", chatMessages.length, chatMessages.map((m) => ({ role: m.role, content: m.content.slice(0, 50) })));
2554
2605
  const baseSystemPrompt = buildSystemPrompt();
2555
2606
  const combinedSystemPrompt = [baseSystemPrompt, actionPrompt].filter(Boolean).join("\n\n");
@@ -2557,8 +2608,8 @@ ${currentContextSummary}` },
2557
2608
  const modelConfig = models.find((m) => m.id === selectedModel);
2558
2609
  const provider = modelConfig?.provider || "ollama";
2559
2610
  let response;
2560
- if (onSendMessage) {
2561
- const result = await onSendMessage({
2611
+ if (onSendMessageRef.current) {
2612
+ const result = await onSendMessageRef.current({
2562
2613
  messages: messagesForApi,
2563
2614
  model: selectedModel,
2564
2615
  provider,
@@ -2983,14 +3034,12 @@ ${result.content}
2983
3034
  keepRecentMessages,
2984
3035
  buildSystemPrompt,
2985
3036
  compressContext,
2986
- onSendMessage,
2987
- onError,
2988
- generateTitleCallback,
2989
- onTitleChange,
2990
3037
  useExternalStorage,
2991
- onSaveMessages,
2992
3038
  handleSkillCall,
2993
- resolvedSkills
3039
+ resolvedSkills,
3040
+ /** @Todo vibecode - attachments, continueAfterToolResult를 deps에 추가하여 stale closure 방지 */
3041
+ attachments,
3042
+ continueAfterToolResult
2994
3043
  ]);
2995
3044
  const handlePollSubmit = (0, import_react5.useCallback)(
2996
3045
  (messageId, responses) => {
@@ -3258,8 +3307,8 @@ ${currentSession.contextSummary}` },
3258
3307
  const provider = modelConfig?.provider || "ollama";
3259
3308
  let responseContent = "";
3260
3309
  let responseSources;
3261
- if (onSendMessage) {
3262
- const result = await onSendMessage({
3310
+ if (onSendMessageRef.current) {
3311
+ const result = await onSendMessageRef.current({
3263
3312
  messages: messagesForApi,
3264
3313
  model: targetModel,
3265
3314
  provider,
@@ -3537,7 +3586,25 @@ ${currentSession.contextSummary}` },
3537
3586
  systemPrompt: `\uC0AC\uC6A9\uC790\uAC00 "${toolConfig.label || skillName}" \uB3C4\uAD6C\uB97C \uC0AC\uC6A9\uD558\uB824\uACE0 \uD569\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC758 \uBA54\uC2DC\uC9C0\uB97C \uBC14\uD0D5\uC73C\uB85C \uBC18\uB4DC\uC2DC <skill_use name="${skillName}"> \uD0DC\uADF8\uB97C \uC0AC\uC6A9\uD558\uC5EC \uC774 \uB3C4\uAD6C\uB97C \uD638\uCD9C\uD558\uC138\uC694. \uB3C4\uAD6C\uB97C \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uACE0 \uD14D\uC2A4\uD2B8\uB85C\uB9CC \uB2F5\uBCC0\uD558\uC9C0 \uB9C8\uC138\uC694.`
3538
3587
  });
3539
3588
  } else {
3540
- executeManualSkill(skillName, { query: input });
3589
+ const currentQuery = input;
3590
+ executeManualSkill(skillName, { query: currentQuery }).then((result) => {
3591
+ if (!result || !result.content) return;
3592
+ let shouldContinue = continueAfterToolResult;
3593
+ if (onSkillCompleteRef.current) {
3594
+ const decision = onSkillCompleteRef.current(skillName, result);
3595
+ shouldContinue = decision === "continue";
3596
+ }
3597
+ if (!shouldContinue) return;
3598
+ skipNextSkillParsingRef.current = true;
3599
+ const resultPrompt = `\uC2A4\uD0AC "${skillName}" \uC2E4\uD589 \uACB0\uACFC:
3600
+
3601
+ ${result.content}
3602
+
3603
+ \uC704 \uACB0\uACFC\uB97C \uBC14\uD0D5\uC73C\uB85C \uC0AC\uC6A9\uC790\uC758 \uC6D0\uB798 \uC9C8\uBB38\uC5D0 \uB2F5\uBCC0\uD574\uC8FC\uC138\uC694. skill_use \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.`;
3604
+ setTimeout(() => {
3605
+ sendMessage(resultPrompt, { hiddenUserMessage: true });
3606
+ }, 100);
3607
+ });
3541
3608
  }
3542
3609
  },
3543
3610
  // Project
@@ -3968,6 +4035,7 @@ var ChatSidebar = ({
3968
4035
  className: `chatllm-sidebar chatllm-sidebar-transition ${theme ? `chatllm-root ${themeClass}` : ""}`,
3969
4036
  style: {
3970
4037
  width: isOpen ? sidebarWidth : "0",
4038
+ height: "100%",
3971
4039
  flexShrink: 0,
3972
4040
  backgroundColor: "var(--chatllm-sidebar-bg)",
3973
4041
  borderRight: isOpen ? "1px solid var(--chatllm-border)" : "none",
@@ -10371,6 +10439,7 @@ var ChatUIView = ({
10371
10439
  currentProjectId,
10372
10440
  onSelectProject: projects.length > 0 ? selectProject : void 0,
10373
10441
  onNewProject: projects.length > 0 ? () => {
10442
+ if (typeof window === "undefined") return;
10374
10443
  const title = window.prompt("\uD504\uB85C\uC81D\uD2B8 \uC774\uB984\uC744 \uC785\uB825\uD558\uC138\uC694");
10375
10444
  if (title?.trim()) {
10376
10445
  createProject({ title: title.trim() });