@gendive/chatllm 0.6.5 → 0.6.7

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.
@@ -273,6 +273,10 @@ interface HeaderProps {
273
273
  onSettingsOpen: () => void;
274
274
  onSidebarToggle: () => void;
275
275
  sidebarOpen: boolean;
276
+ /** 모델 선택기 표시 여부 */
277
+ showModelSelector?: boolean;
278
+ /** 설정 버튼 표시 여부 */
279
+ showSettings?: boolean;
276
280
  }
277
281
  interface EmptyStateProps {
278
282
  greeting: string;
@@ -273,6 +273,10 @@ interface HeaderProps {
273
273
  onSettingsOpen: () => void;
274
274
  onSidebarToggle: () => void;
275
275
  sidebarOpen: boolean;
276
+ /** 모델 선택기 표시 여부 */
277
+ showModelSelector?: boolean;
278
+ /** 설정 버튼 표시 여부 */
279
+ showSettings?: boolean;
276
280
  }
277
281
  interface EmptyStateProps {
278
282
  greeting: string;
@@ -1073,7 +1073,9 @@ var ChatHeader = ({
1073
1073
  onModelChange,
1074
1074
  onSettingsOpen,
1075
1075
  onSidebarToggle,
1076
- sidebarOpen
1076
+ sidebarOpen,
1077
+ showModelSelector = true,
1078
+ showSettings = true
1077
1079
  }) => {
1078
1080
  const [modelDropdownOpen, setModelDropdownOpen] = (0, import_react2.useState)(false);
1079
1081
  const currentModel = models.find((m) => m.id === model);
@@ -1130,7 +1132,7 @@ var ChatHeader = ({
1130
1132
  )
1131
1133
  ] }),
1132
1134
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
1133
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { position: "relative" }, children: [
1135
+ showModelSelector && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { position: "relative" }, children: [
1134
1136
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1135
1137
  "button",
1136
1138
  {
@@ -1297,7 +1299,7 @@ var ChatHeader = ({
1297
1299
  )
1298
1300
  ] })
1299
1301
  ] }),
1300
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1302
+ showSettings && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1301
1303
  "button",
1302
1304
  {
1303
1305
  onClick: onSettingsOpen,
@@ -1882,18 +1884,11 @@ var parseInlineElements = (text, key) => {
1882
1884
  if (match) {
1883
1885
  elements.push(
1884
1886
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1885
- "img",
1887
+ ImageWithCopyButton,
1886
1888
  {
1887
1889
  src: match[2],
1888
1890
  alt: match[1] || "",
1889
- className: "chatllm-image",
1890
- style: {
1891
- maxWidth: "100%",
1892
- borderRadius: "8px",
1893
- margin: "8px 0",
1894
- display: "block"
1895
- },
1896
- loading: "lazy"
1891
+ imageKey: `${key}-image-${index}`
1897
1892
  },
1898
1893
  `${key}-image-${index}`
1899
1894
  )
@@ -2066,6 +2061,210 @@ var CodeBlock = ({ language, code }) => {
2066
2061
  }
2067
2062
  );
2068
2063
  };
2064
+ var ImageWithCopyButton = ({ src, alt, imageKey }) => {
2065
+ const [isHovered, setIsHovered] = import_react5.default.useState(false);
2066
+ const [copyState, setCopyState] = import_react5.default.useState("idle");
2067
+ const imgRef = import_react5.default.useRef(null);
2068
+ const handleCopyImage = async () => {
2069
+ setCopyState("copying");
2070
+ try {
2071
+ if (!navigator.clipboard || !navigator.clipboard.write) {
2072
+ await navigator.clipboard.writeText(src);
2073
+ setCopyState("copied");
2074
+ setTimeout(() => setCopyState("idle"), 2e3);
2075
+ return;
2076
+ }
2077
+ const img = imgRef.current;
2078
+ if (img && img.complete && img.naturalWidth > 0) {
2079
+ try {
2080
+ const canvas = document.createElement("canvas");
2081
+ canvas.width = img.naturalWidth;
2082
+ canvas.height = img.naturalHeight;
2083
+ const ctx = canvas.getContext("2d");
2084
+ if (ctx) {
2085
+ ctx.drawImage(img, 0, 0);
2086
+ const blob = await new Promise((resolve) => {
2087
+ canvas.toBlob(resolve, "image/png");
2088
+ });
2089
+ if (blob) {
2090
+ await navigator.clipboard.write([
2091
+ new ClipboardItem({ "image/png": blob })
2092
+ ]);
2093
+ setCopyState("copied");
2094
+ setTimeout(() => setCopyState("idle"), 2e3);
2095
+ return;
2096
+ }
2097
+ }
2098
+ } catch {
2099
+ console.log("Canvas method failed (CORS), trying fetch...");
2100
+ }
2101
+ }
2102
+ try {
2103
+ const response = await fetch(src, { mode: "cors" });
2104
+ if (response.ok) {
2105
+ const blob = await response.blob();
2106
+ if (blob.type === "image/png") {
2107
+ await navigator.clipboard.write([
2108
+ new ClipboardItem({ "image/png": blob })
2109
+ ]);
2110
+ setCopyState("copied");
2111
+ setTimeout(() => setCopyState("idle"), 2e3);
2112
+ return;
2113
+ }
2114
+ const tempImg = new Image();
2115
+ const blobUrl = URL.createObjectURL(blob);
2116
+ await new Promise((resolve, reject) => {
2117
+ tempImg.onload = async () => {
2118
+ try {
2119
+ const canvas = document.createElement("canvas");
2120
+ canvas.width = tempImg.naturalWidth;
2121
+ canvas.height = tempImg.naturalHeight;
2122
+ const ctx = canvas.getContext("2d");
2123
+ if (!ctx) {
2124
+ reject(new Error("Failed to get canvas context"));
2125
+ return;
2126
+ }
2127
+ ctx.drawImage(tempImg, 0, 0);
2128
+ const pngBlob = await new Promise((res) => {
2129
+ canvas.toBlob(res, "image/png");
2130
+ });
2131
+ URL.revokeObjectURL(blobUrl);
2132
+ if (pngBlob) {
2133
+ await navigator.clipboard.write([
2134
+ new ClipboardItem({ "image/png": pngBlob })
2135
+ ]);
2136
+ setCopyState("copied");
2137
+ setTimeout(() => setCopyState("idle"), 2e3);
2138
+ resolve();
2139
+ } else {
2140
+ reject(new Error("Failed to create PNG blob"));
2141
+ }
2142
+ } catch (err) {
2143
+ reject(err);
2144
+ }
2145
+ };
2146
+ tempImg.onerror = () => {
2147
+ URL.revokeObjectURL(blobUrl);
2148
+ reject(new Error("Failed to load image"));
2149
+ };
2150
+ tempImg.src = blobUrl;
2151
+ });
2152
+ return;
2153
+ }
2154
+ } catch {
2155
+ console.log("Fetch method failed, using URL fallback");
2156
+ }
2157
+ await navigator.clipboard.writeText(src);
2158
+ setCopyState("copied");
2159
+ setTimeout(() => setCopyState("idle"), 2e3);
2160
+ } catch (error) {
2161
+ console.error("Failed to copy image:", error);
2162
+ setCopyState("error");
2163
+ setTimeout(() => setCopyState("idle"), 2e3);
2164
+ }
2165
+ };
2166
+ const getCopyButtonText = () => {
2167
+ switch (copyState) {
2168
+ case "copying":
2169
+ return "\uBCF5\uC0AC \uC911...";
2170
+ case "copied":
2171
+ return "\uBCF5\uC0AC\uB428!";
2172
+ case "error":
2173
+ return "\uBCF5\uC0AC \uC2E4\uD328";
2174
+ default:
2175
+ return "\uC774\uBBF8\uC9C0 \uBCF5\uC0AC";
2176
+ }
2177
+ };
2178
+ const getCopyButtonColor = () => {
2179
+ switch (copyState) {
2180
+ case "copied":
2181
+ return "var(--chatllm-success, #22c55e)";
2182
+ case "error":
2183
+ return "var(--chatllm-error, #ef4444)";
2184
+ default:
2185
+ return "var(--chatllm-text, #ffffff)";
2186
+ }
2187
+ };
2188
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2189
+ "div",
2190
+ {
2191
+ style: {
2192
+ position: "relative",
2193
+ display: "inline-block",
2194
+ margin: "8px 0"
2195
+ },
2196
+ onMouseEnter: () => setIsHovered(true),
2197
+ onMouseLeave: () => setIsHovered(false),
2198
+ children: [
2199
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2200
+ "img",
2201
+ {
2202
+ ref: imgRef,
2203
+ src,
2204
+ alt,
2205
+ className: "chatllm-image",
2206
+ crossOrigin: "anonymous",
2207
+ style: {
2208
+ maxWidth: "100%",
2209
+ borderRadius: "8px",
2210
+ display: "block"
2211
+ },
2212
+ loading: "lazy"
2213
+ }
2214
+ ),
2215
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2216
+ "button",
2217
+ {
2218
+ onClick: handleCopyImage,
2219
+ disabled: copyState === "copying",
2220
+ style: {
2221
+ position: "absolute",
2222
+ top: "8px",
2223
+ right: "8px",
2224
+ display: "flex",
2225
+ alignItems: "center",
2226
+ gap: "6px",
2227
+ padding: "6px 12px",
2228
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
2229
+ color: getCopyButtonColor(),
2230
+ border: "none",
2231
+ borderRadius: "6px",
2232
+ fontSize: "12px",
2233
+ fontWeight: 500,
2234
+ cursor: copyState === "copying" ? "wait" : "pointer",
2235
+ opacity: isHovered || copyState !== "idle" ? 1 : 0,
2236
+ transform: isHovered || copyState !== "idle" ? "translateY(0)" : "translateY(-4px)",
2237
+ transition: "opacity 0.2s ease, transform 0.2s ease",
2238
+ backdropFilter: "blur(4px)",
2239
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.2)"
2240
+ },
2241
+ children: [
2242
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2243
+ "svg",
2244
+ {
2245
+ width: "14",
2246
+ height: "14",
2247
+ viewBox: "0 0 24 24",
2248
+ fill: "none",
2249
+ stroke: "currentColor",
2250
+ strokeWidth: "2",
2251
+ strokeLinecap: "round",
2252
+ strokeLinejoin: "round",
2253
+ children: copyState === "copied" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polyline", { points: "20 6 9 17 4 12" }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
2254
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
2255
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
2256
+ ] })
2257
+ }
2258
+ ),
2259
+ getCopyButtonText()
2260
+ ]
2261
+ }
2262
+ )
2263
+ ]
2264
+ },
2265
+ imageKey
2266
+ );
2267
+ };
2069
2268
  var ChoiceButtons = ({ choices, onChoiceClick }) => {
2070
2269
  const [hoveredIndex, setHoveredIndex] = import_react5.default.useState(null);
2071
2270
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -4372,7 +4571,9 @@ var ChatUI = ({
4372
4571
  onModelChange: setModel,
4373
4572
  onSettingsOpen: openSettings,
4374
4573
  onSidebarToggle: toggleSidebar,
4375
- sidebarOpen
4574
+ sidebarOpen,
4575
+ showModelSelector,
4576
+ showSettings
4376
4577
  }
4377
4578
  ),
4378
4579
  messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(