@nextclaw/agent-chat-ui 0.2.14 → 0.2.16
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.
- package/dist/index.d.ts +9 -1
- package/dist/index.js +155 -51
- package/package.json +5 -4
package/dist/index.d.ts
CHANGED
|
@@ -48,6 +48,11 @@ type ChatToolbarSelectOption = {
|
|
|
48
48
|
label: string;
|
|
49
49
|
description?: string;
|
|
50
50
|
};
|
|
51
|
+
type ChatToolbarSelectGroup = {
|
|
52
|
+
key: string;
|
|
53
|
+
label?: string;
|
|
54
|
+
options: ChatToolbarSelectOption[];
|
|
55
|
+
};
|
|
51
56
|
type ChatToolbarSelect = {
|
|
52
57
|
key: string;
|
|
53
58
|
value?: string;
|
|
@@ -55,6 +60,7 @@ type ChatToolbarSelect = {
|
|
|
55
60
|
selectedLabel?: string;
|
|
56
61
|
icon?: ChatToolbarIcon;
|
|
57
62
|
options: ChatToolbarSelectOption[];
|
|
63
|
+
groups?: ChatToolbarSelectGroup[];
|
|
58
64
|
disabled?: boolean;
|
|
59
65
|
loading?: boolean;
|
|
60
66
|
emptyLabel?: string;
|
|
@@ -185,6 +191,8 @@ type ChatMessageViewModel = {
|
|
|
185
191
|
type ChatMessageTexts = {
|
|
186
192
|
copyCodeLabel: string;
|
|
187
193
|
copiedCodeLabel: string;
|
|
194
|
+
copyMessageLabel: string;
|
|
195
|
+
copiedMessageLabel: string;
|
|
188
196
|
typingLabel: string;
|
|
189
197
|
};
|
|
190
198
|
type ChatMessageListProps = {
|
|
@@ -267,4 +275,4 @@ declare function resolveChatComposerSlashTrigger(nodes: ChatComposerNode[], sele
|
|
|
267
275
|
end: number;
|
|
268
276
|
} | null;
|
|
269
277
|
|
|
270
|
-
export { type ChatComposerNode, type ChatComposerSelection, type ChatComposerTextNode, type ChatComposerTokenKind, type ChatComposerTokenNode, type ChatInlineHint, ChatInputBar, type ChatInputBarActionsProps, type ChatInputBarHandle, type ChatInputBarProps, type ChatInputBarToolbarProps, ChatMessageList, type ChatMessageListProps, type ChatMessagePartViewModel, type ChatMessageRole, type ChatMessageTexts, type ChatMessageViewModel, type ChatSelectedItem, type ChatSkillPickerOption, type ChatSkillPickerProps, type ChatSlashItem, type ChatSlashMenuProps, type ChatTexts, type ChatToolPartViewModel, type ChatToolbarAccessory, type ChatToolbarAccessoryIcon, type ChatToolbarIcon, type ChatToolbarSelect, type ChatToolbarSelectOption, copyText, createChatComposerNodesFromText, createChatComposerTextNode, createChatComposerTokenNode, createEmptyChatComposerNodes, extractChatComposerTokenKeys, normalizeChatComposerNodes, removeChatComposerTokenNodes, replaceChatComposerRange, resolveChatComposerSlashTrigger, serializeChatComposerDocument, serializeChatComposerPlainText, useActiveItemScroll, useCopyFeedback, useElementWidth, useStickyBottomScroll };
|
|
278
|
+
export { type ChatComposerNode, type ChatComposerSelection, type ChatComposerTextNode, type ChatComposerTokenKind, type ChatComposerTokenNode, type ChatInlineHint, ChatInputBar, type ChatInputBarActionsProps, type ChatInputBarHandle, type ChatInputBarProps, type ChatInputBarToolbarProps, ChatMessageList, type ChatMessageListProps, type ChatMessagePartViewModel, type ChatMessageRole, type ChatMessageTexts, type ChatMessageViewModel, type ChatSelectedItem, type ChatSkillPickerOption, type ChatSkillPickerProps, type ChatSlashItem, type ChatSlashMenuProps, type ChatTexts, type ChatToolPartViewModel, type ChatToolbarAccessory, type ChatToolbarAccessoryIcon, type ChatToolbarIcon, type ChatToolbarSelect, type ChatToolbarSelectGroup, type ChatToolbarSelectOption, copyText, createChatComposerNodesFromText, createChatComposerTextNode, createChatComposerTokenNode, createEmptyChatComposerNodes, extractChatComposerTokenKeys, normalizeChatComposerNodes, removeChatComposerTokenNodes, replaceChatComposerRange, resolveChatComposerSlashTrigger, serializeChatComposerDocument, serializeChatComposerPlainText, useActiveItemScroll, useCopyFeedback, useElementWidth, useStickyBottomScroll };
|
package/dist/index.js
CHANGED
|
@@ -100,6 +100,7 @@ import * as SelectPrimitive from "@radix-ui/react-select";
|
|
|
100
100
|
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
101
101
|
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
102
102
|
var ChatSelect = SelectPrimitive.Root;
|
|
103
|
+
var ChatSelectGroup = SelectPrimitive.Group;
|
|
103
104
|
var ChatSelectValue = SelectPrimitive.Value;
|
|
104
105
|
var ChatSelectTrigger = React3.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
105
106
|
SelectPrimitive.Trigger,
|
|
@@ -146,6 +147,8 @@ var ChatSelectContent = React3.forwardRef(({ className, children, position = "po
|
|
|
146
147
|
}
|
|
147
148
|
) }));
|
|
148
149
|
ChatSelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
150
|
+
var ChatSelectLabel = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx3(SelectPrimitive.Label, { ref, className: cn("px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-gray-500", className), ...props }));
|
|
151
|
+
ChatSelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
149
152
|
var ChatSelectItem = React3.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
150
153
|
SelectPrimitive.Item,
|
|
151
154
|
{
|
|
@@ -162,6 +165,8 @@ var ChatSelectItem = React3.forwardRef(({ className, children, ...props }, ref)
|
|
|
162
165
|
}
|
|
163
166
|
));
|
|
164
167
|
ChatSelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
168
|
+
var ChatSelectSeparator = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx3(SelectPrimitive.Separator, { ref, className: cn("-mx-1 my-1 h-px bg-gray-100", className), ...props }));
|
|
169
|
+
ChatSelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|
165
170
|
|
|
166
171
|
// src/components/chat/default-skin/tooltip.tsx
|
|
167
172
|
import * as React4 from "react";
|
|
@@ -193,7 +198,10 @@ var ChatUiPrimitives = {
|
|
|
193
198
|
Input: ChatInput,
|
|
194
199
|
Select: ChatSelect,
|
|
195
200
|
SelectContent: ChatSelectContent,
|
|
201
|
+
SelectGroup: ChatSelectGroup,
|
|
196
202
|
SelectItem: ChatSelectItem,
|
|
203
|
+
SelectLabel: ChatSelectLabel,
|
|
204
|
+
SelectSeparator: ChatSelectSeparator,
|
|
197
205
|
SelectTrigger: ChatSelectTrigger,
|
|
198
206
|
SelectValue: ChatSelectValue,
|
|
199
207
|
Tooltip: ChatTooltip,
|
|
@@ -602,7 +610,9 @@ function resolveContentWidth(key) {
|
|
|
602
610
|
return "";
|
|
603
611
|
}
|
|
604
612
|
function ToolbarSelect({ item }) {
|
|
605
|
-
const { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } = ChatUiPrimitives;
|
|
613
|
+
const { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } = ChatUiPrimitives;
|
|
614
|
+
const groups = item.groups?.filter((group) => group.options.length > 0) ?? (item.options.length > 0 ? [{ key: `${item.key}-default`, options: item.options }] : []);
|
|
615
|
+
const hasOptions = groups.some((group) => group.options.length > 0);
|
|
606
616
|
return /* @__PURE__ */ jsxs5(Select, { value: item.value, onValueChange: item.onValueChange, disabled: item.disabled, children: [
|
|
607
617
|
/* @__PURE__ */ jsx9(
|
|
608
618
|
SelectTrigger,
|
|
@@ -615,15 +625,21 @@ function ToolbarSelect({ item }) {
|
|
|
615
625
|
}
|
|
616
626
|
),
|
|
617
627
|
/* @__PURE__ */ jsxs5(SelectContent, { className: resolveContentWidth(item.key), children: [
|
|
618
|
-
|
|
628
|
+
!hasOptions ? item.loading ? /* @__PURE__ */ jsxs5("div", { className: "space-y-2 px-3 py-2", children: [
|
|
619
629
|
/* @__PURE__ */ jsx9("div", { className: "h-3 w-36 animate-pulse rounded bg-gray-200" }),
|
|
620
630
|
/* @__PURE__ */ jsx9("div", { className: "h-3 w-28 animate-pulse rounded bg-gray-200" }),
|
|
621
631
|
/* @__PURE__ */ jsx9("div", { className: "h-3 w-32 animate-pulse rounded bg-gray-200" })
|
|
622
632
|
] }) : item.emptyLabel ? /* @__PURE__ */ jsx9("div", { className: "px-3 py-2 text-xs text-gray-500", children: item.emptyLabel }) : null : null,
|
|
623
|
-
|
|
624
|
-
/* @__PURE__ */ jsx9(
|
|
625
|
-
/* @__PURE__ */
|
|
626
|
-
|
|
633
|
+
groups.map((group, groupIndex) => /* @__PURE__ */ jsxs5("div", { children: [
|
|
634
|
+
groupIndex > 0 ? /* @__PURE__ */ jsx9(SelectSeparator, {}) : null,
|
|
635
|
+
/* @__PURE__ */ jsxs5(SelectGroup, { children: [
|
|
636
|
+
group.label ? /* @__PURE__ */ jsx9(SelectLabel, { children: group.label }) : null,
|
|
637
|
+
group.options.map((option) => /* @__PURE__ */ jsx9(SelectItem, { value: option.value, className: "py-2", children: option.description ? /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 flex-col gap-0.5", children: [
|
|
638
|
+
/* @__PURE__ */ jsx9("span", { className: "truncate text-xs font-semibold text-gray-800", children: option.label }),
|
|
639
|
+
/* @__PURE__ */ jsx9("span", { className: "truncate text-[11px] text-gray-500", children: option.description })
|
|
640
|
+
] }) : /* @__PURE__ */ jsx9("span", { className: "truncate text-xs font-semibold text-gray-800", children: option.label }) }, option.value))
|
|
641
|
+
] })
|
|
642
|
+
] }, group.key))
|
|
627
643
|
] })
|
|
628
644
|
] });
|
|
629
645
|
}
|
|
@@ -1939,8 +1955,8 @@ function ChatMessageAvatar({ role }) {
|
|
|
1939
1955
|
"div",
|
|
1940
1956
|
{
|
|
1941
1957
|
"data-testid": "chat-message-avatar-assistant",
|
|
1942
|
-
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full
|
|
1943
|
-
children: /* @__PURE__ */ jsx12(Bot, { className: "h-
|
|
1958
|
+
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-b from-gray-700 to-gray-950 text-white shadow-md ring-1 ring-inset ring-white/20",
|
|
1959
|
+
children: /* @__PURE__ */ jsx12(Bot, { className: "h-[18px] w-[18px] text-white/95", strokeWidth: 2.5 })
|
|
1944
1960
|
}
|
|
1945
1961
|
);
|
|
1946
1962
|
}
|
|
@@ -2508,19 +2524,64 @@ function ChatMessageFile({
|
|
|
2508
2524
|
);
|
|
2509
2525
|
}
|
|
2510
2526
|
|
|
2527
|
+
// src/components/chat/hooks/use-reasoning-block-open-state.ts
|
|
2528
|
+
import { useEffect as useEffect6, useRef as useRef5, useState as useState6 } from "react";
|
|
2529
|
+
function useReasoningBlockOpenState(params) {
|
|
2530
|
+
const { isInProgress } = params;
|
|
2531
|
+
const [isOpen, setIsOpen] = useState6(isInProgress);
|
|
2532
|
+
const [keepOpenAfterCompletion, setKeepOpenAfterCompletion] = useState6(false);
|
|
2533
|
+
const previousInProgressRef = useRef5(isInProgress);
|
|
2534
|
+
useEffect6(() => {
|
|
2535
|
+
const wasInProgress = previousInProgressRef.current;
|
|
2536
|
+
if (!wasInProgress && isInProgress) {
|
|
2537
|
+
setIsOpen(true);
|
|
2538
|
+
setKeepOpenAfterCompletion(false);
|
|
2539
|
+
} else if (wasInProgress && !isInProgress && !keepOpenAfterCompletion) {
|
|
2540
|
+
setIsOpen(false);
|
|
2541
|
+
}
|
|
2542
|
+
previousInProgressRef.current = isInProgress;
|
|
2543
|
+
}, [isInProgress, keepOpenAfterCompletion]);
|
|
2544
|
+
function onSummaryClick(event) {
|
|
2545
|
+
event.preventDefault();
|
|
2546
|
+
const nextOpen = !isOpen;
|
|
2547
|
+
setIsOpen(nextOpen);
|
|
2548
|
+
if (!nextOpen) {
|
|
2549
|
+
setKeepOpenAfterCompletion(false);
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
if (isInProgress) {
|
|
2553
|
+
setKeepOpenAfterCompletion(true);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
return {
|
|
2557
|
+
isOpen,
|
|
2558
|
+
onSummaryClick
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2511
2562
|
// src/components/chat/ui/chat-message-list/chat-reasoning-block.tsx
|
|
2512
2563
|
import { jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2513
|
-
function ChatReasoningBlock(
|
|
2514
|
-
|
|
2515
|
-
|
|
2564
|
+
function ChatReasoningBlock({ label, text, isUser, isInProgress }) {
|
|
2565
|
+
const { isOpen, onSummaryClick } = useReasoningBlockOpenState({
|
|
2566
|
+
isInProgress
|
|
2567
|
+
});
|
|
2568
|
+
return /* @__PURE__ */ jsxs9("details", { className: "mt-2", open: isOpen, children: [
|
|
2569
|
+
/* @__PURE__ */ jsx16(
|
|
2570
|
+
"summary",
|
|
2571
|
+
{
|
|
2572
|
+
className: cn("cursor-pointer text-xs", isUser ? "text-primary-100" : "text-gray-500"),
|
|
2573
|
+
onClick: onSummaryClick,
|
|
2574
|
+
children: label
|
|
2575
|
+
}
|
|
2576
|
+
),
|
|
2516
2577
|
/* @__PURE__ */ jsx16(
|
|
2517
2578
|
"pre",
|
|
2518
2579
|
{
|
|
2519
2580
|
className: cn(
|
|
2520
|
-
"mt-2 whitespace-pre-wrap break-
|
|
2521
|
-
|
|
2581
|
+
"mt-2 w-fit max-w-[500px] whitespace-pre-wrap break-all rounded-lg p-2 text-[11px]",
|
|
2582
|
+
isUser ? "bg-primary-700/60" : "bg-gray-100"
|
|
2522
2583
|
),
|
|
2523
|
-
children:
|
|
2584
|
+
children: text
|
|
2524
2585
|
}
|
|
2525
2586
|
)
|
|
2526
2587
|
] });
|
|
@@ -2528,7 +2589,7 @@ function ChatReasoningBlock(props) {
|
|
|
2528
2589
|
|
|
2529
2590
|
// src/components/chat/ui/chat-message-list/tool-card/tool-card-views.tsx
|
|
2530
2591
|
import { Terminal, FileText, Code2, Search as Search2, Globe } from "lucide-react";
|
|
2531
|
-
import { useState as
|
|
2592
|
+
import { useState as useState7, useEffect as useEffect7, useRef as useRef6 } from "react";
|
|
2532
2593
|
|
|
2533
2594
|
// src/components/chat/ui/chat-message-list/tool-card/tool-card-root.tsx
|
|
2534
2595
|
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
@@ -2614,17 +2675,17 @@ function TerminalExecutionView({ card }) {
|
|
|
2614
2675
|
const output = card.output?.trim() ?? "";
|
|
2615
2676
|
const isRunning = card.statusTone === "running";
|
|
2616
2677
|
const hasContent = !!(card.summary?.trim() || output.length > 0);
|
|
2617
|
-
const wasEmptyRef =
|
|
2618
|
-
const [expanded, setExpanded] =
|
|
2619
|
-
const [hasUserToggled, setHasUserToggled] =
|
|
2620
|
-
const prevRunningRef =
|
|
2621
|
-
|
|
2678
|
+
const wasEmptyRef = useRef6(!hasContent);
|
|
2679
|
+
const [expanded, setExpanded] = useState7(hasContent && (isRunning || card.statusTone === "error"));
|
|
2680
|
+
const [hasUserToggled, setHasUserToggled] = useState7(false);
|
|
2681
|
+
const prevRunningRef = useRef6(isRunning);
|
|
2682
|
+
useEffect7(() => {
|
|
2622
2683
|
if (wasEmptyRef.current && hasContent && isRunning) {
|
|
2623
2684
|
setExpanded(true);
|
|
2624
2685
|
wasEmptyRef.current = false;
|
|
2625
2686
|
}
|
|
2626
2687
|
}, [hasContent, isRunning]);
|
|
2627
|
-
|
|
2688
|
+
useEffect7(() => {
|
|
2628
2689
|
if (prevRunningRef.current && !isRunning && !hasUserToggled) {
|
|
2629
2690
|
setExpanded(false);
|
|
2630
2691
|
}
|
|
@@ -2665,10 +2726,10 @@ function TerminalExecutionView({ card }) {
|
|
|
2665
2726
|
function FileOperationView({ card }) {
|
|
2666
2727
|
const output = card.output?.trim() ?? "";
|
|
2667
2728
|
const isRunning = card.statusTone === "running";
|
|
2668
|
-
const [expanded, setExpanded] =
|
|
2669
|
-
const [hasUserToggled, setHasUserToggled] =
|
|
2670
|
-
const prevRunningRef =
|
|
2671
|
-
|
|
2729
|
+
const [expanded, setExpanded] = useState7(false);
|
|
2730
|
+
const [hasUserToggled, setHasUserToggled] = useState7(false);
|
|
2731
|
+
const prevRunningRef = useRef6(isRunning);
|
|
2732
|
+
useEffect7(() => {
|
|
2672
2733
|
if (prevRunningRef.current && !isRunning && !hasUserToggled) {
|
|
2673
2734
|
setExpanded(false);
|
|
2674
2735
|
}
|
|
@@ -2717,11 +2778,11 @@ function FileOperationView({ card }) {
|
|
|
2717
2778
|
}
|
|
2718
2779
|
function SearchSnippetView({ card }) {
|
|
2719
2780
|
const isRunning = card.statusTone === "running";
|
|
2720
|
-
const [expanded, setExpanded] =
|
|
2721
|
-
const [hasUserToggled, setHasUserToggled] =
|
|
2722
|
-
const prevRunningRef =
|
|
2781
|
+
const [expanded, setExpanded] = useState7(isRunning);
|
|
2782
|
+
const [hasUserToggled, setHasUserToggled] = useState7(false);
|
|
2783
|
+
const prevRunningRef = useRef6(isRunning);
|
|
2723
2784
|
const output = card.output?.trim() ?? "";
|
|
2724
|
-
|
|
2785
|
+
useEffect7(() => {
|
|
2725
2786
|
if (prevRunningRef.current && !isRunning && !hasUserToggled) {
|
|
2726
2787
|
setExpanded(false);
|
|
2727
2788
|
}
|
|
@@ -2749,11 +2810,11 @@ function SearchSnippetView({ card }) {
|
|
|
2749
2810
|
function GenericToolCard({ card }) {
|
|
2750
2811
|
const output = card.output?.trim() ?? "";
|
|
2751
2812
|
const isRunning = card.statusTone === "running";
|
|
2752
|
-
const [expanded, setExpanded] =
|
|
2753
|
-
const [hasUserToggled, setHasUserToggled] =
|
|
2754
|
-
const prevRunningRef =
|
|
2813
|
+
const [expanded, setExpanded] = useState7(isRunning);
|
|
2814
|
+
const [hasUserToggled, setHasUserToggled] = useState7(false);
|
|
2815
|
+
const prevRunningRef = useRef6(isRunning);
|
|
2755
2816
|
const showOutputSection = card.kind === "result" || card.hasResult;
|
|
2756
|
-
|
|
2817
|
+
useEffect7(() => {
|
|
2757
2818
|
if (prevRunningRef.current && !isRunning && !hasUserToggled) {
|
|
2758
2819
|
setExpanded(false);
|
|
2759
2820
|
}
|
|
@@ -2825,12 +2886,13 @@ var ChatMessage = memo(function ChatMessage2(props) {
|
|
|
2825
2886
|
const { message, texts } = props;
|
|
2826
2887
|
const { role } = message;
|
|
2827
2888
|
const isUser = role === "user";
|
|
2889
|
+
const isMessageInProgress = message.status === "pending" || message.status === "streaming";
|
|
2828
2890
|
return /* @__PURE__ */ jsx23(
|
|
2829
2891
|
"div",
|
|
2830
2892
|
{
|
|
2831
2893
|
className: cn(
|
|
2832
|
-
"inline-block w-fit max-w-full rounded-2xl border px-4
|
|
2833
|
-
isUser ? "border-primary bg-primary text-white" : role === "assistant" ? "border-gray-200 bg-white text-gray-900" : "border-orange-200/80 bg-orange-50/70 text-gray-900"
|
|
2894
|
+
"inline-block w-fit max-w-full rounded-2xl border px-4 shadow-sm",
|
|
2895
|
+
isUser ? "border-primary bg-primary py-3 text-white" : role === "assistant" ? "border-gray-200 bg-white pb-3 pt-4 text-gray-900" : "border-orange-200/80 bg-orange-50/70 py-3 text-gray-900"
|
|
2834
2896
|
),
|
|
2835
2897
|
children: /* @__PURE__ */ jsx23("div", { className: "space-y-2", children: message.parts.map((part, index) => {
|
|
2836
2898
|
if (part.type === "markdown") {
|
|
@@ -2850,7 +2912,8 @@ var ChatMessage = memo(function ChatMessage2(props) {
|
|
|
2850
2912
|
{
|
|
2851
2913
|
label: part.label,
|
|
2852
2914
|
text: part.text,
|
|
2853
|
-
isUser
|
|
2915
|
+
isUser,
|
|
2916
|
+
isInProgress: isMessageInProgress && index === message.parts.length - 1
|
|
2854
2917
|
},
|
|
2855
2918
|
`reasoning-${index}`
|
|
2856
2919
|
);
|
|
@@ -2904,8 +2967,38 @@ function ChatMessageMeta(props) {
|
|
|
2904
2967
|
);
|
|
2905
2968
|
}
|
|
2906
2969
|
|
|
2970
|
+
// src/components/chat/ui/chat-message-list/chat-message-action-copy.tsx
|
|
2971
|
+
import { useMemo as useMemo6 } from "react";
|
|
2972
|
+
import { Check as Check5, Copy as Copy2 } from "lucide-react";
|
|
2973
|
+
import { jsx as jsx24 } from "react/jsx-runtime";
|
|
2974
|
+
function ChatMessageActionCopy({
|
|
2975
|
+
message,
|
|
2976
|
+
texts
|
|
2977
|
+
}) {
|
|
2978
|
+
const messageText = useMemo6(() => {
|
|
2979
|
+
return message.parts.map((part) => {
|
|
2980
|
+
if (part.type === "markdown") return part.text;
|
|
2981
|
+
if (part.type === "unknown") return part.text;
|
|
2982
|
+
return "";
|
|
2983
|
+
}).filter((text) => !!text && text.trim().length > 0).join("\n\n");
|
|
2984
|
+
}, [message.parts]);
|
|
2985
|
+
const { copied, copy } = useCopyFeedback({ text: messageText });
|
|
2986
|
+
if (!messageText) return null;
|
|
2987
|
+
return /* @__PURE__ */ jsx24(
|
|
2988
|
+
"button",
|
|
2989
|
+
{
|
|
2990
|
+
type: "button",
|
|
2991
|
+
onClick: () => void copy(),
|
|
2992
|
+
className: "text-gray-400 hover:text-gray-600 transition-colors p-1 rounded-md hover:bg-gray-100 flex items-center justify-center",
|
|
2993
|
+
"aria-label": copied ? texts.copiedMessageLabel : texts.copyMessageLabel,
|
|
2994
|
+
title: copied ? texts.copiedMessageLabel : texts.copyMessageLabel,
|
|
2995
|
+
children: copied ? /* @__PURE__ */ jsx24(Check5, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx24(Copy2, { className: "h-3.5 w-3.5" })
|
|
2996
|
+
}
|
|
2997
|
+
);
|
|
2998
|
+
}
|
|
2999
|
+
|
|
2907
3000
|
// src/components/chat/ui/chat-message-list/chat-message-list.tsx
|
|
2908
|
-
import { jsx as
|
|
3001
|
+
import { Fragment as Fragment4, jsx as jsx25, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2909
3002
|
var INVISIBLE_ONLY_TEXT_PATTERN = /\u200B|\u200C|\u200D|\u2060|\uFEFF/g;
|
|
2910
3003
|
function hasRenderableText(value) {
|
|
2911
3004
|
const trimmed = value.trim();
|
|
@@ -2922,6 +3015,13 @@ function hasRenderableMessageContent(message) {
|
|
|
2922
3015
|
return true;
|
|
2923
3016
|
});
|
|
2924
3017
|
}
|
|
3018
|
+
function ChatMessageTypingFooter() {
|
|
3019
|
+
return /* @__PURE__ */ jsx25("div", { className: "flex items-center gap-2 px-1 py-0.5 text-[11px] text-gray-400", children: /* @__PURE__ */ jsxs15("div", { className: "flex space-x-1 items-center h-full", children: [
|
|
3020
|
+
/* @__PURE__ */ jsx25("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse" }),
|
|
3021
|
+
/* @__PURE__ */ jsx25("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse [animation-delay:200ms]" }),
|
|
3022
|
+
/* @__PURE__ */ jsx25("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse [animation-delay:400ms]" })
|
|
3023
|
+
] }) });
|
|
3024
|
+
}
|
|
2925
3025
|
function ChatMessageList(props) {
|
|
2926
3026
|
const visibleMessages = props.messages.filter(hasRenderableMessageContent);
|
|
2927
3027
|
const hasRenderableAssistantDraft = visibleMessages.some(
|
|
@@ -2930,24 +3030,28 @@ function ChatMessageList(props) {
|
|
|
2930
3030
|
return /* @__PURE__ */ jsxs15("div", { className: cn("space-y-5", props.className), children: [
|
|
2931
3031
|
visibleMessages.map((message) => {
|
|
2932
3032
|
const isUser = message.role === "user";
|
|
3033
|
+
const isGenerating = !isUser && (message.status === "streaming" || message.status === "pending");
|
|
2933
3034
|
return /* @__PURE__ */ jsxs15("div", { className: cn("flex gap-3", isUser ? "justify-end" : "justify-start"), children: [
|
|
2934
|
-
!isUser ? /* @__PURE__ */
|
|
3035
|
+
!isUser ? /* @__PURE__ */ jsx25(ChatMessageAvatar, { role: message.role }) : null,
|
|
2935
3036
|
/* @__PURE__ */ jsxs15("div", { className: cn("w-fit max-w-[92%] space-y-2", isUser && "flex flex-col items-end"), children: [
|
|
2936
|
-
/* @__PURE__ */
|
|
2937
|
-
/* @__PURE__ */
|
|
3037
|
+
/* @__PURE__ */ jsx25(ChatMessage, { message, texts: props.texts }),
|
|
3038
|
+
/* @__PURE__ */ jsx25("div", { className: cn("flex items-center gap-2", isUser && "justify-end"), children: isGenerating ? /* @__PURE__ */ jsx25(ChatMessageTypingFooter, {}) : /* @__PURE__ */ jsxs15(Fragment4, { children: [
|
|
3039
|
+
/* @__PURE__ */ jsx25(ChatMessageMeta, { roleLabel: message.roleLabel, timestampLabel: message.timestampLabel, isUser }),
|
|
3040
|
+
!isUser ? /* @__PURE__ */ jsx25(ChatMessageActionCopy, { message, texts: props.texts }) : null
|
|
3041
|
+
] }) })
|
|
2938
3042
|
] }),
|
|
2939
|
-
isUser ? /* @__PURE__ */
|
|
3043
|
+
isUser ? /* @__PURE__ */ jsx25(ChatMessageAvatar, { role: message.role }) : null
|
|
2940
3044
|
] }, message.id);
|
|
2941
3045
|
}),
|
|
2942
3046
|
props.isSending && !hasRenderableAssistantDraft ? /* @__PURE__ */ jsxs15("div", { className: "flex justify-start gap-3", children: [
|
|
2943
|
-
/* @__PURE__ */
|
|
2944
|
-
/* @__PURE__ */
|
|
3047
|
+
/* @__PURE__ */ jsx25(ChatMessageAvatar, { role: "assistant" }),
|
|
3048
|
+
/* @__PURE__ */ jsx25("div", { className: "rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 shadow-sm", children: props.texts.typingLabel })
|
|
2945
3049
|
] }) : null
|
|
2946
3050
|
] });
|
|
2947
3051
|
}
|
|
2948
3052
|
|
|
2949
3053
|
// src/components/chat/hooks/use-sticky-bottom-scroll.ts
|
|
2950
|
-
import { useEffect as
|
|
3054
|
+
import { useEffect as useEffect8, useLayoutEffect as useLayoutEffect2, useRef as useRef7 } from "react";
|
|
2951
3055
|
var DEFAULT_STICKY_THRESHOLD_PX = 10;
|
|
2952
3056
|
function scrollElementToBottom(element) {
|
|
2953
3057
|
element.scrollTop = element.scrollHeight;
|
|
@@ -2971,11 +3075,11 @@ function queueScrollToBottom(params) {
|
|
|
2971
3075
|
});
|
|
2972
3076
|
}
|
|
2973
3077
|
function useStickyBottomScroll(params) {
|
|
2974
|
-
const isStickyRef =
|
|
2975
|
-
const isProgrammaticScrollRef =
|
|
2976
|
-
const previousResetKeyRef =
|
|
2977
|
-
const pendingInitialScrollRef =
|
|
2978
|
-
const scheduledScrollFrameRef =
|
|
3078
|
+
const isStickyRef = useRef7(true);
|
|
3079
|
+
const isProgrammaticScrollRef = useRef7(false);
|
|
3080
|
+
const previousResetKeyRef = useRef7(null);
|
|
3081
|
+
const pendingInitialScrollRef = useRef7(false);
|
|
3082
|
+
const scheduledScrollFrameRef = useRef7(null);
|
|
2979
3083
|
const onScroll = () => {
|
|
2980
3084
|
if (isProgrammaticScrollRef.current) {
|
|
2981
3085
|
isProgrammaticScrollRef.current = false;
|
|
@@ -2988,7 +3092,7 @@ function useStickyBottomScroll(params) {
|
|
|
2988
3092
|
const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
|
|
2989
3093
|
isStickyRef.current = distanceFromBottom <= (params.stickyThresholdPx ?? DEFAULT_STICKY_THRESHOLD_PX);
|
|
2990
3094
|
};
|
|
2991
|
-
|
|
3095
|
+
useEffect8(() => {
|
|
2992
3096
|
if (previousResetKeyRef.current === params.resetKey) {
|
|
2993
3097
|
return;
|
|
2994
3098
|
}
|
|
@@ -2996,7 +3100,7 @@ function useStickyBottomScroll(params) {
|
|
|
2996
3100
|
isStickyRef.current = true;
|
|
2997
3101
|
pendingInitialScrollRef.current = true;
|
|
2998
3102
|
}, [params.resetKey]);
|
|
2999
|
-
|
|
3103
|
+
useEffect8(() => {
|
|
3000
3104
|
const scheduledScrollFrame = scheduledScrollFrameRef.current;
|
|
3001
3105
|
return () => {
|
|
3002
3106
|
if (scheduledScrollFrame !== null) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/agent-chat-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable Nextclaw agent chat UI primitives and default skin.",
|
|
6
6
|
"type": "module",
|
|
@@ -45,15 +45,16 @@
|
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@testing-library/react": "^16.3.0",
|
|
48
|
+
"@types/node": "^20.19.33",
|
|
48
49
|
"@types/react": "^18.3.12",
|
|
49
50
|
"@types/react-dom": "^18.3.1",
|
|
50
|
-
"@vitejs/plugin-react": "^
|
|
51
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
51
52
|
"jsdom": "^25.0.1",
|
|
52
53
|
"prettier": "^3.3.3",
|
|
53
54
|
"tsup": "^8.3.5",
|
|
54
55
|
"typescript": "^5.6.3",
|
|
55
|
-
"vite": "^
|
|
56
|
-
"vitest": "^
|
|
56
|
+
"vite": "^8.0.3",
|
|
57
|
+
"vitest": "^4.1.2"
|
|
57
58
|
},
|
|
58
59
|
"scripts": {
|
|
59
60
|
"build": "tsup src/index.ts --format esm --dts --out-dir dist",
|