@schlenkr/quassel 0.5.4 → 0.5.5

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.
@@ -1,8 +1,11 @@
1
+ import { type RefObject } from "react";
1
2
  import type { MessageActionsOptions } from "./options";
2
- import type { ChatTexts } from "./texts";
3
+ import { type ChatTexts } from "./texts";
3
4
  import type { Message } from "./types";
4
- export declare function MessageActions({ message, options, texts }: {
5
+ export declare function MessageActions({ message, options, texts, contentRef, side }: {
5
6
  message: Message;
6
7
  options?: MessageActionsOptions;
7
8
  texts: ChatTexts;
9
+ contentRef?: RefObject<HTMLElement | null>;
10
+ side?: "start" | "end";
8
11
  }): import("react").JSX.Element;
package/chat.css CHANGED
@@ -79,9 +79,6 @@
79
79
  .qsl\:z-10 {
80
80
  z-index: 10;
81
81
  }
82
- .qsl\:z-\[1\] {
83
- z-index: 1;
84
- }
85
82
  .qsl\:z-\[5\] {
86
83
  z-index: 5;
87
84
  }
@@ -170,6 +167,10 @@
170
167
  width: calc(var(--qsl-spacing) * 4);
171
168
  height: calc(var(--qsl-spacing) * 4);
172
169
  }
170
+ .qsl\:size-5 {
171
+ width: calc(var(--qsl-spacing) * 5);
172
+ height: calc(var(--qsl-spacing) * 5);
173
+ }
173
174
  .qsl\:size-6 {
174
175
  width: calc(var(--qsl-spacing) * 6);
175
176
  height: calc(var(--qsl-spacing) * 6);
@@ -186,6 +187,9 @@
186
187
  width: calc(var(--qsl-spacing) * 9);
187
188
  height: calc(var(--qsl-spacing) * 9);
188
189
  }
190
+ .qsl\:h-5 {
191
+ height: calc(var(--qsl-spacing) * 5);
192
+ }
189
193
  .qsl\:h-6 {
190
194
  height: calc(var(--qsl-spacing) * 6);
191
195
  }
@@ -395,6 +399,9 @@
395
399
  .qsl\:gap-0 {
396
400
  gap: 0px;
397
401
  }
402
+ .qsl\:gap-0\.5 {
403
+ gap: calc(var(--qsl-spacing) * 0.5);
404
+ }
398
405
  .qsl\:gap-1 {
399
406
  gap: var(--qsl-spacing);
400
407
  }
@@ -890,13 +897,6 @@
890
897
  .qsl\:opacity-100 {
891
898
  opacity: 100%;
892
899
  }
893
- .qsl\:shadow-\[0_2px_6px_color-mix\(in_srgb\,var\(--qsl-foreground\)_15\%\,transparent\)\] {
894
- --tw-shadow: 0 2px 6px var(--tw-shadow-color, var(--qsl-foreground));
895
- @supports (color: color-mix(in lab, red, red)) {
896
- --tw-shadow: 0 2px 6px var(--tw-shadow-color, color-mix(in srgb,var(--qsl-foreground) 15%,transparent));
897
- }
898
- box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
899
- }
900
900
  .qsl\:shadow-bar {
901
901
  --tw-shadow: var(--qsl-bar-shadow);
902
902
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -951,6 +951,11 @@
951
951
  transition-timing-function: var(--tw-ease, var(--qsl-default-transition-timing-function));
952
952
  transition-duration: var(--tw-duration, var(--qsl-default-transition-duration));
953
953
  }
954
+ .qsl\:transition-opacity {
955
+ transition-property: opacity;
956
+ transition-timing-function: var(--tw-ease, var(--qsl-default-transition-timing-function));
957
+ transition-duration: var(--tw-duration, var(--qsl-default-transition-duration));
958
+ }
954
959
  .qsl\:transition-transform {
955
960
  transition-property: transform, translate, scale, rotate;
956
961
  transition-timing-function: var(--tw-ease, var(--qsl-default-transition-timing-function));
@@ -1000,10 +1005,13 @@
1000
1005
  max-width: var(--qsl-chat-content-max-width,none);
1001
1006
  }
1002
1007
  @media (hover: hover) {
1003
- .qsl\:group-hover\:pointer-events-auto:is(:where(.qsl\:group):hover *) {
1008
+ .qsl\:group-hover\/message\:pointer-events-auto:is(:where(.qsl\:group\/message):hover *) {
1004
1009
  pointer-events: auto;
1005
1010
  }
1006
- .qsl\:group-hover\:opacity-100:is(:where(.qsl\:group):hover *) {
1011
+ .qsl\:group-hover\/message\:opacity-70:is(:where(.qsl\:group\/message):hover *) {
1012
+ opacity: 70%;
1013
+ }
1014
+ .qsl\:group-hover\/message\:opacity-100:is(:where(.qsl\:group\/message):hover *) {
1007
1015
  opacity: 100%;
1008
1016
  }
1009
1017
  }
@@ -1031,6 +1039,9 @@
1031
1039
  .qsl\:focus-within\:border-primary:focus-within {
1032
1040
  border-color: var(--qsl-primary);
1033
1041
  }
1042
+ .qsl\:focus-within\:opacity-70:focus-within {
1043
+ opacity: 70%;
1044
+ }
1034
1045
  .qsl\:focus-within\:opacity-100:focus-within {
1035
1046
  opacity: 100%;
1036
1047
  }
@@ -1604,6 +1615,9 @@
1604
1615
  .qsl\:\[\@media\(hover\:none\)\]\:pointer-events-auto {
1605
1616
  pointer-events: auto;
1606
1617
  }
1618
+ .qsl\:\[\@media\(hover\:none\)\]\:opacity-70 {
1619
+ opacity: 70%;
1620
+ }
1607
1621
  .qsl\:\[\@media\(hover\:none\)\]\:opacity-100 {
1608
1622
  opacity: 100%;
1609
1623
  }
package/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export { PendingActionCard } from "./PendingActionCard";
14
14
  export { StepPopover } from "./StepPopover";
15
15
  export { WorkingScenes, type WorkingScenesProps } from "./WorkingScenes";
16
16
  export { announce } from "./announce";
17
- export { useChatAction, copyChatText } from "./useChatAction";
17
+ export { useChatAction, copyChatText, copyChatHtml } from "./useChatAction";
18
18
  export { createChatScroll } from "./chat-scroll";
19
19
  export { ChatSendContext, createChatSendScope, type ChatSendScope } from "./ChatSendContext";
20
20
  export { messageDate, timestampDay, timestampLabel } from "./timestamps";
package/index.js CHANGED
@@ -57,7 +57,8 @@ var englishTexts = {
57
57
  showLatestExchange: "Show latest reply between inputs",
58
58
  showTimestamps: "Show timestamps",
59
59
  hideTimestamps: "Hide timestamps",
60
- copyMessage: "Copy message",
60
+ copyMessage: "Copy as Markdown",
61
+ copyFormatted: "Copy with formatting",
61
62
  editMessage: "Edit message",
62
63
  retryMessage: "Request answer again",
63
64
  copyCode: "Copy code",
@@ -126,7 +127,8 @@ var germanTexts = {
126
127
  showLatestExchange: "Letzte Antwort zwischen den Eingaben anzeigen",
127
128
  showTimestamps: "Zeitstempel anzeigen",
128
129
  hideTimestamps: "Zeitstempel ausblenden",
129
- copyMessage: "Nachricht kopieren",
130
+ copyMessage: "Als Markdown kopieren",
131
+ copyFormatted: "Mit Formatierung kopieren",
130
132
  editMessage: "Nachricht bearbeiten",
131
133
  retryMessage: "Antwort erneut anfordern",
132
134
  copyCode: "Code kopieren",
@@ -456,6 +458,15 @@ async function copyChatText(text, unavailable) {
456
458
  if (!navigator.clipboard) throw new Error(unavailable);
457
459
  await navigator.clipboard.writeText(text);
458
460
  }
461
+ async function copyChatHtml(element, text, unavailable) {
462
+ if (!navigator.clipboard) throw new Error(unavailable);
463
+ if (typeof ClipboardItem === "undefined" || !navigator.clipboard.write) return navigator.clipboard.writeText(text);
464
+ const clone = element.cloneNode(true);
465
+ clone.querySelectorAll("button").forEach((button) => button.remove());
466
+ clone.querySelectorAll("[class]").forEach((node) => node.removeAttribute("class"));
467
+ const blob = (content, type) => new Blob([content], { type });
468
+ await navigator.clipboard.write([new ClipboardItem({ "text/html": blob(clone.innerHTML, "text/html"), "text/plain": blob(markdownPlainText(text), "text/plain") })]);
469
+ }
459
470
 
460
471
  // packages/quassel/src/Markdown.tsx
461
472
  import { jsx as jsx8, jsxs } from "react/jsx-runtime";
@@ -1041,37 +1052,46 @@ function useElapsed(start) {
1041
1052
  }
1042
1053
 
1043
1054
  // packages/quassel/src/MessageActions.tsx
1044
- import { CheckIcon as CheckIcon3, CopyIcon as CopyIcon2, PencilIcon, RotateCcwIcon } from "lucide-react";
1055
+ import { CheckIcon as CheckIcon3, ClipboardTypeIcon, CopyIcon as CopyIcon2, PencilIcon, RotateCcwIcon } from "lucide-react";
1045
1056
  import { useState as useState3 } from "react";
1046
1057
  import { Fragment, jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
1047
1058
  var actionClasses = cn(
1048
- "qsl:absolute qsl:top-1 qsl:right-1 qsl:z-[1] qsl:flex qsl:flex-wrap qsl:rounded-sm qsl:bg-secondary qsl:opacity-0",
1049
- "qsl:shadow-[0_2px_6px_color-mix(in_srgb,var(--qsl-foreground)_15%,transparent)]",
1050
- "qsl:pointer-events-none qsl:group-hover:pointer-events-auto qsl:group-hover:opacity-100 qsl:focus-within:pointer-events-auto qsl:focus-within:opacity-100",
1051
- "qsl:[@media(hover:none)]:pointer-events-auto qsl:[@media(hover:none)]:opacity-100"
1059
+ "qsl:mt-0.5 qsl:flex qsl:h-5 qsl:items-center qsl:gap-0.5 qsl:text-muted-foreground qsl:opacity-0 qsl:transition-opacity",
1060
+ "qsl:pointer-events-none qsl:group-hover/message:pointer-events-auto qsl:focus-within:pointer-events-auto",
1061
+ "qsl:[@media(hover:none)]:pointer-events-auto"
1052
1062
  );
1053
- function MessageActions({ message, options, texts }) {
1063
+ var visibleClasses = "qsl:group-hover/message:opacity-100 qsl:focus-within:opacity-100 qsl:[@media(hover:none)]:opacity-100";
1064
+ var lightClasses = "qsl:group-hover/message:opacity-70 qsl:focus-within:opacity-70 qsl:[@media(hover:none)]:opacity-70";
1065
+ function MessageActions({ message, options, texts, contentRef, side = message.role === "user" ? "end" : "start" }) {
1054
1066
  const action = useChatAction();
1055
1067
  const { Button: Button2 } = useQuasselComponents();
1056
1068
  const [lastAction, setLastAction] = useState3();
1057
- const copy = typeof options?.copy === "function" ? options.copy(message) : options?.copy ?? message.role === "user";
1069
+ const enabled = (option, fallback) => typeof option === "function" ? option(message) : option ?? fallback;
1070
+ const copy = enabled(options?.copy, message.role === "user" || message.role === "assistant") && !!message.text.trim();
1071
+ const copyFormatted = copy && !!contentRef && enabled(options?.copyFormatted, message.role === "assistant");
1058
1072
  const actions = [
1059
- ...copy && message.text.trim() ? [{ id: "copy", label: texts.copyMessage, icon: /* @__PURE__ */ jsx12(CopyIcon2, {}), onClick: () => copyChatText(message.text, texts.clipboardUnavailable) }] : [],
1073
+ ...copy ? [{ id: "copy", label: texts.copyMessage, icon: /* @__PURE__ */ jsx12(CopyIcon2, {}), onClick: () => copyChatText(message.text, texts.clipboardUnavailable) }] : [],
1074
+ ...copyFormatted ? [{
1075
+ id: "copy-formatted",
1076
+ label: texts.copyFormatted ?? defaultTexts.copyFormatted,
1077
+ icon: /* @__PURE__ */ jsx12(ClipboardTypeIcon, {}),
1078
+ onClick: () => contentRef.current ? copyChatHtml(contentRef.current, message.text, texts.clipboardUnavailable) : copyChatText(message.text, texts.clipboardUnavailable)
1079
+ }] : [],
1060
1080
  ...options?.edit && message.role === "user" ? [{ id: "edit", label: texts.editMessage, icon: /* @__PURE__ */ jsx12(PencilIcon, {}), onClick: options.edit }] : [],
1061
1081
  ...options?.retry && message.role === "assistant" ? [{ id: "retry", label: texts.retryMessage, icon: /* @__PURE__ */ jsx12(RotateCcwIcon, {}), onClick: options.retry }] : [],
1062
1082
  ...options?.custom?.(message) ?? []
1063
1083
  ];
1064
1084
  return /* @__PURE__ */ jsxs5(Fragment, { children: [
1065
- actions.length > 0 && /* @__PURE__ */ jsx12("div", { className: actionClasses, children: actions.map((item) => {
1066
- const copied = item.id === "copy" && lastAction === item.id && action.status === "done";
1085
+ actions.length > 0 && /* @__PURE__ */ jsx12("div", { className: cn(actionClasses, message.role === "user" ? visibleClasses : lightClasses, side === "end" && "qsl:justify-end"), "data-chat": "message-actions", children: actions.map((item) => {
1086
+ const copied = ["copy", "copy-formatted"].includes(item.id) && lastAction === item.id && action.status === "done";
1067
1087
  const label = copied ? texts.copied : item.label;
1068
1088
  return /* @__PURE__ */ jsx12(
1069
1089
  Button2,
1070
1090
  {
1071
1091
  "aria-label": label,
1072
- className: "qsl:text-inherit",
1092
+ className: cn("qsl:text-inherit", item.icon && "qsl:size-5"),
1073
1093
  disabled: item.disabled || action.status === "pending",
1074
- size: item.icon ? "icon-sm" : "sm",
1094
+ size: item.icon ? "icon-xs" : "xs",
1075
1095
  title: label,
1076
1096
  variant: "ghost",
1077
1097
  onClick: () => {
@@ -1731,6 +1751,7 @@ function Bubble({
1731
1751
  messageActions
1732
1752
  }) {
1733
1753
  const stepSpacing = dense ? denseStepClasses : stepClasses;
1754
+ const textRef = useRef3(null);
1734
1755
  if (message.role === "thinking" || message.role === "tool" && message.tool) {
1735
1756
  if (detailMode === "compact") {
1736
1757
  return /* @__PURE__ */ jsx13(TraceLine, { className: stepSpacing, elapsedAfter, expandable, message, texts, toolArgumentsText });
@@ -1777,38 +1798,43 @@ function Bubble({
1777
1798
  "qsl:mb-[5px] qsl:table qsl:text-label qsl:leading-[1.45] qsl:font-semibold",
1778
1799
  useBubble && message.bubble && "qsl:rounded-full qsl:border qsl:border-white/30 qsl:bg-black/15 qsl:px-[7px] qsl:py-px qsl:tracking-[0.02em]"
1779
1800
  ), "data-chat": "sender", children: sender }),
1780
- /* @__PURE__ */ jsx13(Markdown, { text: message.text, streaming: message.role === "assistant" && !message.closed }),
1801
+ /* @__PURE__ */ jsx13("div", { "data-chat": "message-text", ref: textRef, children: /* @__PURE__ */ jsx13(Markdown, { text: message.text, streaming: message.role === "assistant" && !message.closed }) }),
1781
1802
  /* @__PURE__ */ jsx13(MessageAttachments, { message, texts }),
1782
- message.steered && /* @__PURE__ */ jsx13("small", { className: "qsl:mt-1 qsl:block qsl:text-label qsl:leading-[1.45] qsl:opacity-70", "data-chat": "steered", children: texts.steered }),
1783
- /* @__PURE__ */ jsx13(MessageActions, { message, options: messageActions, texts })
1803
+ message.steered && /* @__PURE__ */ jsx13("small", { className: "qsl:mt-1 qsl:block qsl:text-label qsl:leading-[1.45] qsl:opacity-70", "data-chat": "steered", children: texts.steered })
1784
1804
  ] });
1785
1805
  const cursor = message.role === "assistant" && message.textCursor ? JSON.stringify(message.textCursor) : void 0;
1786
- if (!useBubble) return /* @__PURE__ */ jsx13("div", { "data-chat-text-cursor": cursor, className: cn(stepSpacing, "qsl:group qsl:relative qsl:w-full qsl:leading-[var(--qsl-chat-line-height,1.65)] qsl:text-foreground"), "data-message": "answer", children: content });
1806
+ if (!useBubble) return /* @__PURE__ */ jsxs6("div", { "data-chat-text-cursor": cursor, className: cn(stepSpacing, "qsl:group/message qsl:relative qsl:w-full qsl:leading-[var(--qsl-chat-line-height,1.65)] qsl:text-foreground"), "data-message": "answer", children: [
1807
+ content,
1808
+ /* @__PURE__ */ jsx13(MessageActions, { contentRef: textRef, message, options: messageActions, side, texts })
1809
+ ] });
1787
1810
  const fullBackground = message.role === "user" && bubbleOptions?.userBackgroundFullWidth !== false;
1788
1811
  const surfaceClasses = message.bubble ? "qsl:rounded-lg" : "qsl:rounded-[var(--qsl-bubble-radius,10px_10px_4px_10px)] qsl:border qsl:border-border qsl:bg-secondary qsl:in-data-[tone=material]:rounded-[var(--qsl-bubble-radius,9px)] qsl:in-data-[tone=material]:border-glass-edge/20 qsl:in-data-[tone=material]:bg-primary/15";
1789
- return /* @__PURE__ */ jsxs6(
1790
- "div",
1791
- {
1792
- "data-chat-text-cursor": cursor,
1793
- className: cn(stepSpacing, "qsl:relative qsl:flex", side === "end" ? "qsl:justify-end" : "qsl:justify-start"),
1794
- "data-message": message.role === "user" && !message.bubble && variant === "default" ? "user" : "bubble",
1795
- "data-side": side,
1796
- "data-background-width": fullBackground ? "full" : "content",
1797
- children: [
1798
- fullBackground && /* @__PURE__ */ jsx13("div", { "aria-hidden": "true", className: cn("qsl:pointer-events-none qsl:absolute qsl:inset-0", surfaceClasses), "data-chat": "bubble-background", style: { background: message.bubble?.color } }),
1799
- /* @__PURE__ */ jsx13(
1800
- "div",
1801
- {
1802
- className: cn(bubbleBodyClasses, "qsl:group", !fullBackground && surfaceClasses, fullBackground && !message.bubble && "qsl:border qsl:border-transparent", message.bubble ? "qsl:text-white qsl:[--qsl-scroll-cover:var(--qsl-primary)]" : "qsl:text-foreground qsl:[--qsl-scroll-cover:var(--qsl-secondary)]"),
1803
- "data-chat": "bubble-content",
1804
- "data-tone": message.bubble ? "on-color" : void 0,
1805
- style: { background: fullBackground ? void 0 : message.bubble?.color, maxWidth: bubbleOptions?.maxWidth },
1806
- children: content
1807
- }
1808
- )
1809
- ]
1810
- }
1811
- );
1812
+ return /* @__PURE__ */ jsxs6("div", { className: cn(stepSpacing, "qsl:group/message"), children: [
1813
+ /* @__PURE__ */ jsxs6(
1814
+ "div",
1815
+ {
1816
+ "data-chat-text-cursor": cursor,
1817
+ className: cn("qsl:relative qsl:flex", side === "end" ? "qsl:justify-end" : "qsl:justify-start"),
1818
+ "data-message": message.role === "user" && !message.bubble && variant === "default" ? "user" : "bubble",
1819
+ "data-side": side,
1820
+ "data-background-width": fullBackground ? "full" : "content",
1821
+ children: [
1822
+ fullBackground && /* @__PURE__ */ jsx13("div", { "aria-hidden": "true", className: cn("qsl:pointer-events-none qsl:absolute qsl:inset-0", surfaceClasses), "data-chat": "bubble-background", style: { background: message.bubble?.color } }),
1823
+ /* @__PURE__ */ jsx13(
1824
+ "div",
1825
+ {
1826
+ className: cn(bubbleBodyClasses, !fullBackground && surfaceClasses, fullBackground && !message.bubble && "qsl:border qsl:border-transparent", message.bubble ? "qsl:text-white qsl:[--qsl-scroll-cover:var(--qsl-primary)]" : "qsl:text-foreground qsl:[--qsl-scroll-cover:var(--qsl-secondary)]"),
1827
+ "data-chat": "bubble-content",
1828
+ "data-tone": message.bubble ? "on-color" : void 0,
1829
+ style: { background: fullBackground ? void 0 : message.bubble?.color, maxWidth: bubbleOptions?.maxWidth },
1830
+ children: content
1831
+ }
1832
+ )
1833
+ ]
1834
+ }
1835
+ ),
1836
+ /* @__PURE__ */ jsx13(MessageActions, { contentRef: textRef, message, options: messageActions, side, texts })
1837
+ ] });
1812
1838
  }
1813
1839
  function ActionContent({
1814
1840
  action,
@@ -2602,6 +2628,7 @@ export {
2602
2628
  attachmentDownloadUrl,
2603
2629
  attachmentMediaType,
2604
2630
  compactToolLine,
2631
+ copyChatHtml,
2605
2632
  copyChatText,
2606
2633
  createChatScroll,
2607
2634
  createChatSendScope,