@gendive/chatllm 0.6.7 → 0.6.9

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.
@@ -2016,97 +2016,80 @@ var ImageWithCopyButton = ({ src, alt, imageKey }) => {
2016
2016
  const [isHovered, setIsHovered] = React4.useState(false);
2017
2017
  const [copyState, setCopyState] = React4.useState("idle");
2018
2018
  const imgRef = React4.useRef(null);
2019
- const handleCopyImage = async () => {
2020
- setCopyState("copying");
2021
- try {
2022
- if (!navigator.clipboard || !navigator.clipboard.write) {
2023
- await navigator.clipboard.writeText(src);
2024
- setCopyState("copied");
2025
- setTimeout(() => setCopyState("idle"), 2e3);
2026
- return;
2027
- }
2028
- const img = imgRef.current;
2029
- if (img && img.complete && img.naturalWidth > 0) {
2030
- try {
2031
- const canvas = document.createElement("canvas");
2032
- canvas.width = img.naturalWidth;
2033
- canvas.height = img.naturalHeight;
2034
- const ctx = canvas.getContext("2d");
2035
- if (ctx) {
2036
- ctx.drawImage(img, 0, 0);
2037
- const blob = await new Promise((resolve) => {
2038
- canvas.toBlob(resolve, "image/png");
2039
- });
2040
- if (blob) {
2041
- await navigator.clipboard.write([
2042
- new ClipboardItem({ "image/png": blob })
2043
- ]);
2044
- setCopyState("copied");
2045
- setTimeout(() => setCopyState("idle"), 2e3);
2046
- return;
2047
- }
2048
- }
2049
- } catch {
2050
- console.log("Canvas method failed (CORS), trying fetch...");
2019
+ const getImageBlob = async () => {
2020
+ const img = imgRef.current;
2021
+ if (img && img.complete && img.naturalWidth > 0) {
2022
+ try {
2023
+ const canvas = document.createElement("canvas");
2024
+ canvas.width = img.naturalWidth;
2025
+ canvas.height = img.naturalHeight;
2026
+ const ctx = canvas.getContext("2d");
2027
+ if (ctx) {
2028
+ ctx.drawImage(img, 0, 0);
2029
+ const blob = await new Promise((resolve) => {
2030
+ canvas.toBlob(resolve, "image/png");
2031
+ });
2032
+ if (blob) return blob;
2051
2033
  }
2034
+ } catch {
2035
+ console.log("Canvas method failed (CORS), trying fetch...");
2052
2036
  }
2037
+ }
2038
+ if (!src.startsWith("data:")) {
2053
2039
  try {
2054
2040
  const response = await fetch(src, { mode: "cors" });
2055
2041
  if (response.ok) {
2056
2042
  const blob = await response.blob();
2057
- if (blob.type === "image/png") {
2058
- await navigator.clipboard.write([
2059
- new ClipboardItem({ "image/png": blob })
2060
- ]);
2061
- setCopyState("copied");
2062
- setTimeout(() => setCopyState("idle"), 2e3);
2063
- return;
2064
- }
2043
+ if (blob.type === "image/png") return blob;
2065
2044
  const tempImg = new Image();
2066
2045
  const blobUrl = URL.createObjectURL(blob);
2067
- await new Promise((resolve, reject) => {
2068
- tempImg.onload = async () => {
2069
- try {
2070
- const canvas = document.createElement("canvas");
2071
- canvas.width = tempImg.naturalWidth;
2072
- canvas.height = tempImg.naturalHeight;
2073
- const ctx = canvas.getContext("2d");
2074
- if (!ctx) {
2075
- reject(new Error("Failed to get canvas context"));
2076
- return;
2077
- }
2046
+ return new Promise((resolve) => {
2047
+ tempImg.onload = () => {
2048
+ const canvas = document.createElement("canvas");
2049
+ canvas.width = tempImg.naturalWidth;
2050
+ canvas.height = tempImg.naturalHeight;
2051
+ const ctx = canvas.getContext("2d");
2052
+ if (ctx) {
2078
2053
  ctx.drawImage(tempImg, 0, 0);
2079
- const pngBlob = await new Promise((res) => {
2080
- canvas.toBlob(res, "image/png");
2081
- });
2054
+ canvas.toBlob((pngBlob) => {
2055
+ URL.revokeObjectURL(blobUrl);
2056
+ resolve(pngBlob);
2057
+ }, "image/png");
2058
+ } else {
2082
2059
  URL.revokeObjectURL(blobUrl);
2083
- if (pngBlob) {
2084
- await navigator.clipboard.write([
2085
- new ClipboardItem({ "image/png": pngBlob })
2086
- ]);
2087
- setCopyState("copied");
2088
- setTimeout(() => setCopyState("idle"), 2e3);
2089
- resolve();
2090
- } else {
2091
- reject(new Error("Failed to create PNG blob"));
2092
- }
2093
- } catch (err) {
2094
- reject(err);
2060
+ resolve(null);
2095
2061
  }
2096
2062
  };
2097
2063
  tempImg.onerror = () => {
2098
2064
  URL.revokeObjectURL(blobUrl);
2099
- reject(new Error("Failed to load image"));
2065
+ resolve(null);
2100
2066
  };
2101
2067
  tempImg.src = blobUrl;
2102
2068
  });
2103
- return;
2104
2069
  }
2105
2070
  } catch {
2106
- console.log("Fetch method failed, using URL fallback");
2071
+ console.log("Fetch method failed");
2072
+ }
2073
+ }
2074
+ return null;
2075
+ };
2076
+ const handleCopy = async () => {
2077
+ setCopyState("copying");
2078
+ try {
2079
+ if (!navigator.clipboard?.write) {
2080
+ await navigator.clipboard.writeText(src);
2081
+ setCopyState("copied");
2082
+ setTimeout(() => setCopyState("idle"), 2e3);
2083
+ return;
2084
+ }
2085
+ const blob = await getImageBlob();
2086
+ if (blob) {
2087
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
2088
+ setCopyState("copied");
2089
+ } else {
2090
+ await navigator.clipboard.writeText(src);
2091
+ setCopyState("copied");
2107
2092
  }
2108
- await navigator.clipboard.writeText(src);
2109
- setCopyState("copied");
2110
2093
  setTimeout(() => setCopyState("idle"), 2e3);
2111
2094
  } catch (error) {
2112
2095
  console.error("Failed to copy image:", error);
@@ -2114,27 +2097,34 @@ var ImageWithCopyButton = ({ src, alt, imageKey }) => {
2114
2097
  setTimeout(() => setCopyState("idle"), 2e3);
2115
2098
  }
2116
2099
  };
2117
- const getCopyButtonText = () => {
2118
- switch (copyState) {
2119
- case "copying":
2120
- return "\uBCF5\uC0AC \uC911...";
2121
- case "copied":
2122
- return "\uBCF5\uC0AC\uB428!";
2123
- case "error":
2124
- return "\uBCF5\uC0AC \uC2E4\uD328";
2125
- default:
2126
- return "\uC774\uBBF8\uC9C0 \uBCF5\uC0AC";
2100
+ const handleDownload = async () => {
2101
+ try {
2102
+ const blob = await getImageBlob();
2103
+ const url = blob ? URL.createObjectURL(blob) : src;
2104
+ const link = document.createElement("a");
2105
+ link.href = url;
2106
+ link.download = alt || `image-${Date.now()}.png`;
2107
+ document.body.appendChild(link);
2108
+ link.click();
2109
+ document.body.removeChild(link);
2110
+ if (blob) URL.revokeObjectURL(url);
2111
+ } catch (error) {
2112
+ console.error("Failed to download image:", error);
2113
+ window.open(src, "_blank");
2127
2114
  }
2128
2115
  };
2129
- const getCopyButtonColor = () => {
2130
- switch (copyState) {
2131
- case "copied":
2132
- return "var(--chatllm-success, #22c55e)";
2133
- case "error":
2134
- return "var(--chatllm-error, #ef4444)";
2135
- default:
2136
- return "var(--chatllm-text, #ffffff)";
2137
- }
2116
+ const toolbarButtonStyle = {
2117
+ display: "flex",
2118
+ alignItems: "center",
2119
+ justifyContent: "center",
2120
+ width: "36px",
2121
+ height: "36px",
2122
+ backgroundColor: "transparent",
2123
+ border: "none",
2124
+ borderRadius: "8px",
2125
+ cursor: "pointer",
2126
+ color: "var(--chatllm-text-muted, #9ca3af)",
2127
+ transition: "background-color 0.2s, color 0.2s"
2138
2128
  };
2139
2129
  return /* @__PURE__ */ jsxs5(
2140
2130
  "div",
@@ -2154,7 +2144,7 @@ var ImageWithCopyButton = ({ src, alt, imageKey }) => {
2154
2144
  src,
2155
2145
  alt,
2156
2146
  className: "chatllm-image",
2157
- crossOrigin: "anonymous",
2147
+ crossOrigin: src.startsWith("data:") ? void 0 : "anonymous",
2158
2148
  style: {
2159
2149
  maxWidth: "100%",
2160
2150
  borderRadius: "8px",
@@ -2164,50 +2154,71 @@ var ImageWithCopyButton = ({ src, alt, imageKey }) => {
2164
2154
  }
2165
2155
  ),
2166
2156
  /* @__PURE__ */ jsxs5(
2167
- "button",
2157
+ "div",
2168
2158
  {
2169
- onClick: handleCopyImage,
2170
- disabled: copyState === "copying",
2171
2159
  style: {
2172
2160
  position: "absolute",
2173
- top: "8px",
2174
- right: "8px",
2161
+ bottom: "0",
2162
+ left: "0",
2163
+ right: "0",
2175
2164
  display: "flex",
2176
2165
  alignItems: "center",
2177
- gap: "6px",
2178
- padding: "6px 12px",
2179
- backgroundColor: "rgba(0, 0, 0, 0.7)",
2180
- color: getCopyButtonColor(),
2181
- border: "none",
2182
- borderRadius: "6px",
2183
- fontSize: "12px",
2184
- fontWeight: 500,
2185
- cursor: copyState === "copying" ? "wait" : "pointer",
2186
- opacity: isHovered || copyState !== "idle" ? 1 : 0,
2187
- transform: isHovered || copyState !== "idle" ? "translateY(0)" : "translateY(-4px)",
2166
+ gap: "4px",
2167
+ padding: "8px 12px",
2168
+ backgroundColor: "var(--chatllm-bg, #ffffff)",
2169
+ borderTop: "1px solid var(--chatllm-border-light, #e5e7eb)",
2170
+ borderRadius: "0 0 8px 8px",
2171
+ opacity: isHovered ? 1 : 0,
2172
+ transform: isHovered ? "translateY(0)" : "translateY(8px)",
2188
2173
  transition: "opacity 0.2s ease, transform 0.2s ease",
2189
- backdropFilter: "blur(4px)",
2190
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.2)"
2174
+ pointerEvents: isHovered ? "auto" : "none"
2191
2175
  },
2192
2176
  children: [
2193
2177
  /* @__PURE__ */ jsx6(
2194
- "svg",
2178
+ "button",
2195
2179
  {
2196
- width: "14",
2197
- height: "14",
2198
- viewBox: "0 0 24 24",
2199
- fill: "none",
2200
- stroke: "currentColor",
2201
- strokeWidth: "2",
2202
- strokeLinecap: "round",
2203
- strokeLinejoin: "round",
2204
- children: copyState === "copied" ? /* @__PURE__ */ jsx6("polyline", { points: "20 6 9 17 4 12" }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
2180
+ onClick: handleCopy,
2181
+ disabled: copyState === "copying",
2182
+ style: {
2183
+ ...toolbarButtonStyle,
2184
+ color: copyState === "copied" ? "var(--chatllm-success, #22c55e)" : copyState === "error" ? "var(--chatllm-error, #ef4444)" : "var(--chatllm-text-muted, #9ca3af)"
2185
+ },
2186
+ title: copyState === "copied" ? "\uBCF5\uC0AC\uB428!" : copyState === "error" ? "\uBCF5\uC0AC \uC2E4\uD328" : "\uBCF5\uC0AC\uD558\uAE30",
2187
+ onMouseOver: (e) => {
2188
+ e.currentTarget.style.backgroundColor = "var(--chatllm-bg-hover, #f3f4f6)";
2189
+ e.currentTarget.style.color = "var(--chatllm-text, #1f2937)";
2190
+ },
2191
+ onMouseOut: (e) => {
2192
+ e.currentTarget.style.backgroundColor = "transparent";
2193
+ e.currentTarget.style.color = copyState === "copied" ? "var(--chatllm-success, #22c55e)" : "var(--chatllm-text-muted, #9ca3af)";
2194
+ },
2195
+ children: /* @__PURE__ */ jsx6("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: copyState === "copied" ? /* @__PURE__ */ jsx6("polyline", { points: "20 6 9 17 4 12" }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
2205
2196
  /* @__PURE__ */ jsx6("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
2206
2197
  /* @__PURE__ */ jsx6("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
2207
- ] })
2198
+ ] }) })
2208
2199
  }
2209
2200
  ),
2210
- getCopyButtonText()
2201
+ /* @__PURE__ */ jsx6(
2202
+ "button",
2203
+ {
2204
+ onClick: handleDownload,
2205
+ style: toolbarButtonStyle,
2206
+ title: "\uB2E4\uC6B4\uB85C\uB4DC",
2207
+ onMouseOver: (e) => {
2208
+ e.currentTarget.style.backgroundColor = "var(--chatllm-bg-hover, #f3f4f6)";
2209
+ e.currentTarget.style.color = "var(--chatllm-text, #1f2937)";
2210
+ },
2211
+ onMouseOut: (e) => {
2212
+ e.currentTarget.style.backgroundColor = "transparent";
2213
+ e.currentTarget.style.color = "var(--chatllm-text-muted, #9ca3af)";
2214
+ },
2215
+ children: /* @__PURE__ */ jsxs5("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2216
+ /* @__PURE__ */ jsx6("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
2217
+ /* @__PURE__ */ jsx6("polyline", { points: "7 10 12 15 17 10" }),
2218
+ /* @__PURE__ */ jsx6("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
2219
+ ] })
2220
+ }
2221
+ )
2211
2222
  ]
2212
2223
  }
2213
2224
  )