@schlenkr/quassel 0.5.4 → 0.5.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.
@@ -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",
@@ -175,6 +177,8 @@ function createChatScroll(onFollowChange = () => {
175
177
  }, getThreshold = () => defaultJumpToEndThreshold) {
176
178
  const atEnd = (viewport) => viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop <= getThreshold();
177
179
  let following = true;
180
+ let held = false;
181
+ let heldTop = 0;
178
182
  let lastTop = 0;
179
183
  let lastHeight = 0;
180
184
  let lastClientHeight = 0;
@@ -185,6 +189,7 @@ function createChatScroll(onFollowChange = () => {
185
189
  lastClientHeight = viewport.clientHeight;
186
190
  };
187
191
  const follow = (value) => {
192
+ if (value) held = false;
188
193
  if (following === value) return;
189
194
  following = value;
190
195
  onFollowChange(value);
@@ -192,19 +197,23 @@ function createChatScroll(onFollowChange = () => {
192
197
  const layout = (viewport, dragging = false) => {
193
198
  if (viewport.clientHeight <= 0) return;
194
199
  if (movedUp(viewport, dragging) && !atEnd(viewport)) follow(false);
195
- if (atEnd(viewport)) follow(true);
200
+ if (viewport.scrollHeight <= viewport.clientHeight || atEnd(viewport) && !held) follow(true);
196
201
  if (following) viewport.scrollTop = Math.max(0, viewport.scrollHeight - viewport.clientHeight);
197
202
  remember(viewport);
198
203
  };
199
204
  return {
200
205
  layout,
201
206
  pause(viewport) {
202
- if (viewport.clientHeight > 0) follow(atEnd(viewport));
207
+ if (viewport.clientHeight <= 0 || viewport.scrollHeight <= viewport.clientHeight) return;
208
+ follow(false);
209
+ held = true;
210
+ heldTop = viewport.scrollTop;
203
211
  },
204
212
  scroll(viewport, dragging = false) {
205
213
  if (viewport.clientHeight <= 0) return;
206
- if (atEnd(viewport)) follow(true);
214
+ if (atEnd(viewport) && (!held || viewport.scrollTop > heldTop)) follow(true);
207
215
  else if (movedUp(viewport, dragging) && !atEnd(viewport)) follow(false);
216
+ heldTop = viewport.scrollTop;
208
217
  remember(viewport);
209
218
  },
210
219
  jump(viewport) {
@@ -456,6 +465,15 @@ async function copyChatText(text, unavailable) {
456
465
  if (!navigator.clipboard) throw new Error(unavailable);
457
466
  await navigator.clipboard.writeText(text);
458
467
  }
468
+ async function copyChatHtml(element, text, unavailable) {
469
+ if (!navigator.clipboard) throw new Error(unavailable);
470
+ if (typeof ClipboardItem === "undefined" || !navigator.clipboard.write) return navigator.clipboard.writeText(text);
471
+ const clone = element.cloneNode(true);
472
+ clone.querySelectorAll("button").forEach((button) => button.remove());
473
+ clone.querySelectorAll("[class]").forEach((node) => node.removeAttribute("class"));
474
+ const blob = (content, type) => new Blob([content], { type });
475
+ await navigator.clipboard.write([new ClipboardItem({ "text/html": blob(clone.innerHTML, "text/html"), "text/plain": blob(markdownPlainText(text), "text/plain") })]);
476
+ }
459
477
 
460
478
  // packages/quassel/src/Markdown.tsx
461
479
  import { jsx as jsx8, jsxs } from "react/jsx-runtime";
@@ -1041,37 +1059,46 @@ function useElapsed(start) {
1041
1059
  }
1042
1060
 
1043
1061
  // packages/quassel/src/MessageActions.tsx
1044
- import { CheckIcon as CheckIcon3, CopyIcon as CopyIcon2, PencilIcon, RotateCcwIcon } from "lucide-react";
1062
+ import { CheckIcon as CheckIcon3, ClipboardTypeIcon, CopyIcon as CopyIcon2, PencilIcon, RotateCcwIcon } from "lucide-react";
1045
1063
  import { useState as useState3 } from "react";
1046
1064
  import { Fragment, jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
1047
1065
  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"
1066
+ "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",
1067
+ "qsl:pointer-events-none qsl:group-hover/message:pointer-events-auto qsl:focus-within:pointer-events-auto",
1068
+ "qsl:[@media(hover:none)]:pointer-events-auto"
1052
1069
  );
1053
- function MessageActions({ message, options, texts }) {
1070
+ var visibleClasses = "qsl:group-hover/message:opacity-100 qsl:focus-within:opacity-100 qsl:[@media(hover:none)]:opacity-100";
1071
+ var lightClasses = "qsl:group-hover/message:opacity-70 qsl:focus-within:opacity-70 qsl:[@media(hover:none)]:opacity-70";
1072
+ function MessageActions({ message, options, texts, contentRef, side = message.role === "user" ? "end" : "start" }) {
1054
1073
  const action = useChatAction();
1055
1074
  const { Button: Button2 } = useQuasselComponents();
1056
1075
  const [lastAction, setLastAction] = useState3();
1057
- const copy = typeof options?.copy === "function" ? options.copy(message) : options?.copy ?? message.role === "user";
1076
+ const enabled = (option, fallback) => typeof option === "function" ? option(message) : option ?? fallback;
1077
+ const copy = enabled(options?.copy, message.role === "user" || message.role === "assistant") && !!message.text.trim();
1078
+ const copyFormatted = copy && !!contentRef && enabled(options?.copyFormatted, message.role === "assistant");
1058
1079
  const actions = [
1059
- ...copy && message.text.trim() ? [{ id: "copy", label: texts.copyMessage, icon: /* @__PURE__ */ jsx12(CopyIcon2, {}), onClick: () => copyChatText(message.text, texts.clipboardUnavailable) }] : [],
1080
+ ...copy ? [{ id: "copy", label: texts.copyMessage, icon: /* @__PURE__ */ jsx12(CopyIcon2, {}), onClick: () => copyChatText(message.text, texts.clipboardUnavailable) }] : [],
1081
+ ...copyFormatted ? [{
1082
+ id: "copy-formatted",
1083
+ label: texts.copyFormatted ?? defaultTexts.copyFormatted,
1084
+ icon: /* @__PURE__ */ jsx12(ClipboardTypeIcon, {}),
1085
+ onClick: () => contentRef.current ? copyChatHtml(contentRef.current, message.text, texts.clipboardUnavailable) : copyChatText(message.text, texts.clipboardUnavailable)
1086
+ }] : [],
1060
1087
  ...options?.edit && message.role === "user" ? [{ id: "edit", label: texts.editMessage, icon: /* @__PURE__ */ jsx12(PencilIcon, {}), onClick: options.edit }] : [],
1061
1088
  ...options?.retry && message.role === "assistant" ? [{ id: "retry", label: texts.retryMessage, icon: /* @__PURE__ */ jsx12(RotateCcwIcon, {}), onClick: options.retry }] : [],
1062
1089
  ...options?.custom?.(message) ?? []
1063
1090
  ];
1064
1091
  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";
1092
+ 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) => {
1093
+ const copied = ["copy", "copy-formatted"].includes(item.id) && lastAction === item.id && action.status === "done";
1067
1094
  const label = copied ? texts.copied : item.label;
1068
1095
  return /* @__PURE__ */ jsx12(
1069
1096
  Button2,
1070
1097
  {
1071
1098
  "aria-label": label,
1072
- className: "qsl:text-inherit",
1099
+ className: cn("qsl:text-inherit", item.icon && "qsl:size-5"),
1073
1100
  disabled: item.disabled || action.status === "pending",
1074
- size: item.icon ? "icon-sm" : "sm",
1101
+ size: item.icon ? "icon-xs" : "xs",
1075
1102
  title: label,
1076
1103
  variant: "ghost",
1077
1104
  onClick: () => {
@@ -1731,6 +1758,7 @@ function Bubble({
1731
1758
  messageActions
1732
1759
  }) {
1733
1760
  const stepSpacing = dense ? denseStepClasses : stepClasses;
1761
+ const textRef = useRef3(null);
1734
1762
  if (message.role === "thinking" || message.role === "tool" && message.tool) {
1735
1763
  if (detailMode === "compact") {
1736
1764
  return /* @__PURE__ */ jsx13(TraceLine, { className: stepSpacing, elapsedAfter, expandable, message, texts, toolArgumentsText });
@@ -1777,38 +1805,43 @@ function Bubble({
1777
1805
  "qsl:mb-[5px] qsl:table qsl:text-label qsl:leading-[1.45] qsl:font-semibold",
1778
1806
  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
1807
  ), "data-chat": "sender", children: sender }),
1780
- /* @__PURE__ */ jsx13(Markdown, { text: message.text, streaming: message.role === "assistant" && !message.closed }),
1808
+ /* @__PURE__ */ jsx13("div", { "data-chat": "message-text", ref: textRef, children: /* @__PURE__ */ jsx13(Markdown, { text: message.text, streaming: message.role === "assistant" && !message.closed }) }),
1781
1809
  /* @__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 })
1810
+ 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
1811
  ] });
1785
1812
  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 });
1813
+ 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: [
1814
+ content,
1815
+ /* @__PURE__ */ jsx13(MessageActions, { contentRef: textRef, message, options: messageActions, side, texts })
1816
+ ] });
1787
1817
  const fullBackground = message.role === "user" && bubbleOptions?.userBackgroundFullWidth !== false;
1788
1818
  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
- );
1819
+ return /* @__PURE__ */ jsxs6("div", { className: cn(stepSpacing, "qsl:group/message"), children: [
1820
+ /* @__PURE__ */ jsxs6(
1821
+ "div",
1822
+ {
1823
+ "data-chat-text-cursor": cursor,
1824
+ className: cn("qsl:relative qsl:flex", side === "end" ? "qsl:justify-end" : "qsl:justify-start"),
1825
+ "data-message": message.role === "user" && !message.bubble && variant === "default" ? "user" : "bubble",
1826
+ "data-side": side,
1827
+ "data-background-width": fullBackground ? "full" : "content",
1828
+ children: [
1829
+ 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 } }),
1830
+ /* @__PURE__ */ jsx13(
1831
+ "div",
1832
+ {
1833
+ 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)]"),
1834
+ "data-chat": "bubble-content",
1835
+ "data-tone": message.bubble ? "on-color" : void 0,
1836
+ style: { background: fullBackground ? void 0 : message.bubble?.color, maxWidth: bubbleOptions?.maxWidth },
1837
+ children: content
1838
+ }
1839
+ )
1840
+ ]
1841
+ }
1842
+ ),
1843
+ /* @__PURE__ */ jsx13(MessageActions, { contentRef: textRef, message, options: messageActions, side, texts })
1844
+ ] });
1812
1845
  }
1813
1846
  function ActionContent({
1814
1847
  action,
@@ -2602,6 +2635,7 @@ export {
2602
2635
  attachmentDownloadUrl,
2603
2636
  attachmentMediaType,
2604
2637
  compactToolLine,
2638
+ copyChatHtml,
2605
2639
  copyChatText,
2606
2640
  createChatScroll,
2607
2641
  createChatSendScope,