@gendive/chatllm 0.21.5 → 0.21.6

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.
@@ -2761,6 +2761,11 @@ ${projectMemoryContext}`);
2761
2761
  - \uB370\uC774\uD130 \uBE44\uAD50/\uCD94\uC774 \u2192 html (Chart.js CDN \uC0AC\uC6A9, <artifact> \uD0DC\uADF8)
2762
2762
  - \uC544\uC774\uCF58/\uB85C\uACE0/\uC77C\uB7EC\uC2A4\uD2B8 \u2192 svg
2763
2763
 
2764
+ **mermaid \uC8FC\uC758\uC0AC\uD56D:**
2765
+ - \uB178\uB4DC \uB77C\uBCA8\uC5D0 \uAD04\uD638 () \uC0AC\uC6A9 \uAE08\uC9C0 \u2192 \uB300\uAD04\uD638 [] \uC0AC\uC6A9: B[\uC0C1\uD0DC \uAD00\uB9AC useState] \u2705 B[\uC0C1\uD0DC \uAD00\uB9AC (useState)] \u274C
2766
+ - \uB178\uB4DC \uB77C\uBCA8\uC5D0 \uD2B9\uC218\uBB38\uC790\uAC00 \uC788\uC73C\uBA74 \uD070\uB530\uC634\uD45C\uB85C \uAC10\uC2F8\uAE30: B["API \uD638\uCD9C \u2192 \uC751\uB2F5"]
2767
+ - \uC18C\uAD04\uD638 ()\uB294 mermaid \uBB38\uBC95\uC5D0\uC11C \uB465\uADFC \uB178\uB4DC\uB97C \uC758\uBBF8\uD558\uBBC0\uB85C \uB77C\uBCA8 \uD14D\uC2A4\uD2B8\uC5D0 \uC808\uB300 \uD3EC\uD568\uD558\uC9C0 \uB9C8\uC138\uC694
2768
+
2764
2769
  **mermaid \uC608\uC2DC:**
2765
2770
  \`\`\`mermaid
2766
2771
  graph TD
@@ -2785,7 +2790,7 @@ new Chart(document.getElementById('chart'), {
2785
2790
  - html \uC2DC\uAC01\uD654\uB294 \uBC18\uB4DC\uC2DC <artifact language="html"> \uD0DC\uADF8\uB85C \uAC10\uC2F8\uC57C \uD568 (\`\`\`html \uCF54\uB4DC\uBE14\uB85D \uC0AC\uC6A9 \uAE08\uC9C0)
2786
2791
  - \uC0AC\uC6A9\uC790\uAC00 \uC2DC\uAC01\uD654\uB97C \uC694\uCCAD\uD558\uC9C0 \uC54A\uC544\uB3C4 \uC801\uC808\uD558\uBA74 \uC790\uB3D9\uC73C\uB85C \uD3EC\uD568
2787
2792
  - \uC2DC\uAC01\uD654 \uC55E\uB4A4\uC5D0 \uAC04\uACB0\uD55C \uD14D\uC2A4\uD2B8 \uC124\uBA85 \uD3EC\uD568
2788
- - mermaid \uBB38\uBC95 \uC624\uB958 \uC8FC\uC758 (\uD2B9\uD788 \uAD04\uD638, \uB530\uC634\uD45C \uC774\uC2A4\uCF00\uC774\uD504)`);
2793
+ - mermaid \uB178\uB4DC \uB77C\uBCA8\uC5D0 \uC18C\uAD04\uD638 () \uC808\uB300 \uC0AC\uC6A9 \uAE08\uC9C0 \u2014 \uD30C\uC2F1 \uC5D0\uB7EC \uC6D0\uC778`);
2789
2794
  if (shouldIncludeSkills) {
2790
2795
  const skillsPrompt = buildSkillsPrompt();
2791
2796
  if (skillsPrompt) {
@@ -3118,18 +3123,29 @@ ${newConversation}
3118
3123
  setIsSessionLoading(true);
3119
3124
  try {
3120
3125
  const sessionDetail = await onLoadSessionRef.current(id);
3121
- let loadedMessages = sessionDetail.messages.map((m, idx) => ({
3122
- id: m.id || generateId5("msg"),
3123
- role: typeof m.role === "string" ? m.role.toLowerCase() : m.role,
3124
- // API는 message 필드, 내부는 content 필드 사용
3125
- content: m.content || m.message || "",
3126
- timestamp: m.created_at ? new Date(m.created_at).getTime() : Date.now() - (sessionDetail.messages.length - idx) * 1e3,
3127
- model: m.model,
3128
- alternatives: m.alternatives,
3129
- sources: m.sources,
3130
- /** @Todo vibecode - 백엔드에서 전달받은 contentParts 복원 (이미지/파일 등) */
3131
- ...m.contentParts && { contentParts: m.contentParts }
3132
- }));
3126
+ let loadedMessages = sessionDetail.messages.map((m, idx) => {
3127
+ const msg = {
3128
+ id: m.id || generateId5("msg"),
3129
+ role: typeof m.role === "string" ? m.role.toLowerCase() : m.role,
3130
+ // API는 message 필드, 내부는 content 필드 사용
3131
+ content: m.content || m.message || "",
3132
+ timestamp: m.created_at ? new Date(m.created_at).getTime() : Date.now() - (sessionDetail.messages.length - idx) * 1e3,
3133
+ model: m.model,
3134
+ alternatives: m.alternatives,
3135
+ sources: m.sources,
3136
+ /** @Todo vibecode - 백엔드에서 전달받은 contentParts 복원 (이미지/파일 등) */
3137
+ ...m.contentParts && { contentParts: m.contentParts }
3138
+ };
3139
+ if (!msg.contentParts && msg.role === "assistant" && hasArtifactTag(msg.content)) {
3140
+ const { artifacts, cleanContent } = parseArtifactsFromContent(msg.content);
3141
+ if (artifacts.length > 0) {
3142
+ const textPart = cleanContent.trim() ? [{ type: "text", content: cleanContent }] : [];
3143
+ msg.content = cleanContent;
3144
+ msg.contentParts = [...textPart, ...artifacts];
3145
+ }
3146
+ }
3147
+ return msg;
3148
+ });
3133
3149
  let resolvedTitle = sessionDetail.title;
3134
3150
  let resolvedSessionContext = sessionDetail.sessionContext;
3135
3151
  if (loadedMessages.length === 0) {
@@ -10203,9 +10219,39 @@ var MermaidArtifact = ({ code }) => {
10203
10219
  }
10204
10220
  );
10205
10221
  };
10206
- var ArtifactActions = ({ code, language }) => {
10222
+ var svgToPngBlob = (svgString, scale = 2) => {
10223
+ return new Promise((resolve) => {
10224
+ const parser = new DOMParser();
10225
+ const doc = parser.parseFromString(svgString, "image/svg+xml");
10226
+ const svgEl = doc.querySelector("svg");
10227
+ if (!svgEl) return resolve(null);
10228
+ const w = parseInt(svgEl.getAttribute("width") || "800", 10);
10229
+ const h = parseInt(svgEl.getAttribute("height") || "600", 10);
10230
+ const canvas = document.createElement("canvas");
10231
+ canvas.width = w * scale;
10232
+ canvas.height = h * scale;
10233
+ const ctx = canvas.getContext("2d");
10234
+ if (!ctx) return resolve(null);
10235
+ ctx.scale(scale, scale);
10236
+ const blob = new Blob([new XMLSerializer().serializeToString(svgEl)], { type: "image/svg+xml;charset=utf-8" });
10237
+ const url = URL.createObjectURL(blob);
10238
+ const img = new Image();
10239
+ img.onload = () => {
10240
+ ctx.drawImage(img, 0, 0, w, h);
10241
+ URL.revokeObjectURL(url);
10242
+ canvas.toBlob((pngBlob) => resolve(pngBlob), "image/png");
10243
+ };
10244
+ img.onerror = () => {
10245
+ URL.revokeObjectURL(url);
10246
+ resolve(null);
10247
+ };
10248
+ img.src = url;
10249
+ });
10250
+ };
10251
+ var ArtifactActions = ({ code, language, containerRef }) => {
10207
10252
  const [showCode, setShowCode] = useState15(false);
10208
10253
  const [copied, setCopied] = useState15(false);
10254
+ const [saving, setSaving] = useState15(false);
10209
10255
  const handleCopy = async () => {
10210
10256
  try {
10211
10257
  await navigator.clipboard.writeText(code);
@@ -10214,6 +10260,39 @@ var ArtifactActions = ({ code, language }) => {
10214
10260
  } catch {
10215
10261
  }
10216
10262
  };
10263
+ const handleSaveImage = async () => {
10264
+ setSaving(true);
10265
+ try {
10266
+ let blob = null;
10267
+ if (language === "svg" || language === "mermaid") {
10268
+ const svgEl = containerRef?.current?.querySelector("svg");
10269
+ if (svgEl) {
10270
+ blob = await svgToPngBlob(svgEl.outerHTML);
10271
+ }
10272
+ } else if (language === "html") {
10273
+ const htmlBlob = new Blob([code], { type: "text/html;charset=utf-8" });
10274
+ const url = URL.createObjectURL(htmlBlob);
10275
+ const a = document.createElement("a");
10276
+ a.href = url;
10277
+ a.download = `artifact.html`;
10278
+ a.click();
10279
+ URL.revokeObjectURL(url);
10280
+ setSaving(false);
10281
+ return;
10282
+ }
10283
+ if (blob) {
10284
+ const url = URL.createObjectURL(blob);
10285
+ const a = document.createElement("a");
10286
+ a.href = url;
10287
+ a.download = `artifact.png`;
10288
+ a.click();
10289
+ URL.revokeObjectURL(url);
10290
+ }
10291
+ } catch {
10292
+ } finally {
10293
+ setSaving(false);
10294
+ }
10295
+ };
10217
10296
  const langLabel = language === "mermaid" ? "Mermaid" : language === "svg" ? "SVG" : "HTML";
10218
10297
  return /* @__PURE__ */ jsxs13("div", { children: [
10219
10298
  /* @__PURE__ */ jsxs13(
@@ -10279,6 +10358,30 @@ var ArtifactActions = ({ code, language }) => {
10279
10358
  copied ? "\uBCF5\uC0AC\uB428" : "\uBCF5\uC0AC"
10280
10359
  ]
10281
10360
  }
10361
+ ),
10362
+ /* @__PURE__ */ jsxs13(
10363
+ "button",
10364
+ {
10365
+ onClick: handleSaveImage,
10366
+ disabled: saving,
10367
+ style: {
10368
+ display: "flex",
10369
+ alignItems: "center",
10370
+ gap: "4px",
10371
+ padding: "4px 8px",
10372
+ fontSize: "12px",
10373
+ color: "var(--chatllm-text-muted, #64748b)",
10374
+ backgroundColor: "transparent",
10375
+ border: "none",
10376
+ borderRadius: "4px",
10377
+ cursor: saving ? "wait" : "pointer",
10378
+ opacity: saving ? 0.5 : 1
10379
+ },
10380
+ children: [
10381
+ /* @__PURE__ */ jsx14(IconSvg, { name: "download-line", size: 12, color: "var(--chatllm-text-muted, #64748b)" }),
10382
+ language === "html" ? "HTML \uC800\uC7A5" : "\uC774\uBBF8\uC9C0 \uC800\uC7A5"
10383
+ ]
10384
+ }
10282
10385
  )
10283
10386
  ]
10284
10387
  }
@@ -10313,6 +10416,7 @@ var ArtifactActions = ({ code, language }) => {
10313
10416
  ] });
10314
10417
  };
10315
10418
  var ArtifactCard = ({ part, index = 0 }) => {
10419
+ const contentRef = useRef9(null);
10316
10420
  return /* @__PURE__ */ jsxs13(
10317
10421
  "div",
10318
10422
  {
@@ -10352,12 +10456,12 @@ var ArtifactCard = ({ part, index = 0 }) => {
10352
10456
  ]
10353
10457
  }
10354
10458
  ),
10355
- /* @__PURE__ */ jsxs13("div", { style: { padding: part.language === "html" ? 0 : "12px" }, children: [
10459
+ /* @__PURE__ */ jsxs13("div", { ref: contentRef, style: { padding: part.language === "html" ? 0 : "12px" }, children: [
10356
10460
  part.language === "html" && /* @__PURE__ */ jsx14(HtmlArtifact, { code: part.code }),
10357
10461
  part.language === "svg" && /* @__PURE__ */ jsx14(SvgArtifact, { code: part.code }),
10358
10462
  part.language === "mermaid" && /* @__PURE__ */ jsx14(MermaidArtifact, { code: part.code })
10359
10463
  ] }),
10360
- /* @__PURE__ */ jsx14(ArtifactActions, { code: part.code, language: part.language })
10464
+ /* @__PURE__ */ jsx14(ArtifactActions, { code: part.code, language: part.language, containerRef: contentRef })
10361
10465
  ]
10362
10466
  }
10363
10467
  );
@@ -15196,6 +15300,7 @@ var useDragResize = (options) => {
15196
15300
  const dragStartRef = useRef11(null);
15197
15301
  const isDraggingRef = useRef11(false);
15198
15302
  const wasDragRef = useRef11(false);
15303
+ const userDraggedRef = useRef11(false);
15199
15304
  const dragDeltaRef = useRef11({ dx: 0, dy: 0 });
15200
15305
  const fabElRef = useRef11(null);
15201
15306
  const dragCleanupRef = useRef11(null);
@@ -15262,6 +15367,7 @@ var useDragResize = (options) => {
15262
15367
  if (!isDraggingRef.current && distance > DRAG_THRESHOLD) {
15263
15368
  isDraggingRef.current = true;
15264
15369
  wasDragRef.current = true;
15370
+ userDraggedRef.current = true;
15265
15371
  setIsDragging(true);
15266
15372
  fabElRef.current?.classList.add("chatllm-fab-dragging");
15267
15373
  }
@@ -15501,7 +15607,11 @@ var useDragResize = (options) => {
15501
15607
  const newVw = window.innerWidth;
15502
15608
  const newVh = window.innerHeight;
15503
15609
  setViewport({ width: newVw, height: newVh });
15504
- setFabPos((prev) => clampToViewport(prev.x, prev.y, newVw, newVh));
15610
+ if (!userDraggedRef.current) {
15611
+ setFabPos(getInitialFabPosition(initialPosition));
15612
+ } else {
15613
+ setFabPos((prev) => clampToViewport(prev.x, prev.y, newVw, newVh));
15614
+ }
15505
15615
  };
15506
15616
  window.addEventListener("resize", handleResize);
15507
15617
  return () => window.removeEventListener("resize", handleResize);
@@ -17101,23 +17211,39 @@ var ChatFloatingWidget = ({
17101
17211
  return [chatTab, ...customTabs];
17102
17212
  }, [tabs]);
17103
17213
  const [unreadBadge, setUnreadBadge] = useState28(0);
17104
- const seenMessageIdsRef = useRef17(new Set(chatState.messages.map((m) => m.id)));
17214
+ const seenMessageIdsRef = useRef17(/* @__PURE__ */ new Set());
17105
17215
  useEffect19(() => {
17106
- if (!isOpen) {
17216
+ if (seenMessageIdsRef.current.size === 0 && chatState.messages.length > 0) {
17217
+ for (const m of chatState.messages) {
17218
+ seenMessageIdsRef.current.add(m.id);
17219
+ }
17220
+ }
17221
+ }, [chatState.messages]);
17222
+ useEffect19(() => {
17223
+ if (isOpen) {
17224
+ for (const m of chatState.messages) {
17225
+ seenMessageIdsRef.current.add(m.id);
17226
+ }
17227
+ } else {
17107
17228
  const newAssistant = chatState.messages.filter(
17108
17229
  (m) => m.role === "assistant" && !seenMessageIdsRef.current.has(m.id)
17109
17230
  );
17110
17231
  if (newAssistant.length > 0) {
17111
17232
  setUnreadBadge((prev) => prev + newAssistant.length);
17233
+ for (const m of newAssistant) {
17234
+ seenMessageIdsRef.current.add(m.id);
17235
+ }
17112
17236
  }
17113
17237
  }
17114
- for (const m of chatState.messages) {
17115
- seenMessageIdsRef.current.add(m.id);
17116
- }
17117
17238
  }, [chatState.messages, isOpen]);
17118
17239
  useEffect19(() => {
17119
- if (isOpen) setUnreadBadge(0);
17120
- }, [isOpen]);
17240
+ if (isOpen) {
17241
+ setUnreadBadge(0);
17242
+ for (const m of chatState.messages) {
17243
+ seenMessageIdsRef.current.add(m.id);
17244
+ }
17245
+ }
17246
+ }, [isOpen, chatState.messages]);
17121
17247
  const totalBadge = useMemo7(() => {
17122
17248
  return tabs.reduce((sum, t) => sum + (t.badge || 0), 0) + unreadBadge;
17123
17249
  }, [tabs, unreadBadge]);
@@ -17553,6 +17679,57 @@ var useDeepResearch = (options) => {
17553
17679
  };
17554
17680
  };
17555
17681
 
17682
+ // src/react/utils/conversationSearchAdapter.ts
17683
+ var formatSearchResults = (results) => {
17684
+ if (results.length === 0) {
17685
+ return "\uAD00\uB828 \uB300\uD654\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.";
17686
+ }
17687
+ const formatted = results.map((r, i) => {
17688
+ const header = r.sessionTitle ? `[${r.sessionTitle}]` : "";
17689
+ const time = r.timestamp ? ` (${new Date(r.timestamp).toLocaleDateString("ko-KR")})` : "";
17690
+ return `[${i + 1}] ${header}${time} (${r.role}): ${r.content}`;
17691
+ }).join("\n\n");
17692
+ return `\uAC80\uC0C9 \uACB0\uACFC (${results.length}\uAC74):
17693
+
17694
+ ${formatted}`;
17695
+ };
17696
+ var createConversationSearchSkill = (options) => {
17697
+ const { onSearch, label, disabled } = options;
17698
+ return {
17699
+ description: '\uC774\uC804 \uB300\uD654 \uB0B4\uC6A9\uC744 \uAC80\uC0C9\uD569\uB2C8\uB2E4. \uBC18\uB4DC\uC2DC \uD638\uCD9C\uD574\uC57C \uD558\uB294 \uC0C1\uD669: \uC0AC\uC6A9\uC790\uAC00 "\uC804\uC5D0", "\uC9C0\uB09C\uBC88\uC5D0", "\uC608\uC804\uC5D0", "\uC544\uAE4C", "\uC774\uC804\uC5D0", "\uB2E4\uC2DC \uC54C\uB824\uC918", "\uB610", "\uADF8\uB54C" \uB4F1 \uACFC\uAC70 \uB300\uD654\uB97C \uCC38\uC870\uD558\uB294 \uD45C\uD604\uC744 \uC0AC\uC6A9\uD560 \uB54C. \uACFC\uAC70\uC5D0 \uB17C\uC758\uD588\uB358 \uC8FC\uC81C\uB97C \uB2E4\uC2DC \uBB3C\uC5B4\uBCFC \uB54C\uB3C4 \uD638\uCD9C\uD558\uC138\uC694.',
17700
+ trigger: "auto",
17701
+ label: label || "\uB300\uD654 \uAC80\uC0C9",
17702
+ icon: "search-line",
17703
+ disabled,
17704
+ persist: false,
17705
+ parameters: {
17706
+ type: "object",
17707
+ properties: {
17708
+ query: {
17709
+ type: "string",
17710
+ description: "\uAC80\uC0C9 \uD0A4\uC6CC\uB4DC \uB610\uB294 \uBB38\uC7A5 (\uC0AC\uC6A9\uC790 \uC758\uB3C4\uB97C \uBC18\uC601\uD55C \uAC80\uC0C9\uC5B4)"
17711
+ },
17712
+ limit: {
17713
+ type: "number",
17714
+ description: "\uCD5C\uB300 \uACB0\uACFC \uC218 (\uAE30\uBCF8: 5)"
17715
+ }
17716
+ },
17717
+ required: ["query"]
17718
+ },
17719
+ execute: async (params) => {
17720
+ const { query, limit } = params;
17721
+ if (!query) {
17722
+ return { content: "\uAC80\uC0C9\uC5B4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4." };
17723
+ }
17724
+ const results = await onSearch(query, { limit: limit || 5 });
17725
+ return {
17726
+ content: formatSearchResults(results),
17727
+ metadata: { resultCount: results.length }
17728
+ };
17729
+ }
17730
+ };
17731
+ };
17732
+
17556
17733
  // src/react/components/EmptyState.tsx
17557
17734
  import { jsx as jsx31, jsxs as jsxs30 } from "react/jsx-runtime";
17558
17735
  var EmptyState = ({
@@ -18068,6 +18245,7 @@ export {
18068
18245
  convertSkillsToOpenAITools,
18069
18246
  convertToolsToSkills,
18070
18247
  createAdvancedResearchSkill,
18248
+ createConversationSearchSkill,
18071
18249
  createDeepResearchSkill,
18072
18250
  migrateSessionsToProjects,
18073
18251
  useChatUI,