@amaster.ai/components-templates 1.4.1 → 1.4.2
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/components/ai-assistant/others.md +13 -3
- package/components/ai-assistant/package.json +1 -1
- package/components/ai-assistant/template/ai-assistant.tsx +12 -3
- package/components/ai-assistant/template/components/chat-assistant-message.tsx +21 -4
- package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +1 -6
- package/components/ai-assistant/template/components/chat-floating-button.tsx +2 -2
- package/components/ai-assistant/template/components/chat-floating-card.tsx +1 -1
- package/components/ai-assistant/template/components/chat-header.tsx +16 -14
- package/components/ai-assistant/template/components/chat-input.tsx +80 -28
- package/components/ai-assistant/template/components/chat-messages.tsx +75 -5
- package/components/ai-assistant/template/components/chat-recommends.tsx +3 -1
- package/components/ai-assistant/template/hooks/useAssistantStore.tsx +35 -6
- package/components/ai-assistant/template/hooks/useConversation.ts +856 -0
- package/components/ai-assistant/template/hooks/useDisplayMode.tsx +1 -1
- package/components/ai-assistant/template/i18n.ts +14 -2
- package/components/ai-assistant/template/inline-ai-assistant.tsx +21 -9
- package/components/ai-assistant/template/types.ts +16 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
## tailwind.config.js
|
|
2
2
|
|
|
3
|
-
content 添加
|
|
3
|
+
content 添加 "./node_modules/@json-render/shadcn/dist/**/*.js"
|
|
4
|
+
```
|
|
5
|
+
{
|
|
6
|
+
content: [
|
|
7
|
+
"./node_modules/@json-render/shadcn/dist/**/*.js"
|
|
8
|
+
]
|
|
9
|
+
}
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## src/lib/client 保证是新的
|
|
13
|
+
```
|
|
14
|
+
import { createClient } from "@amaster.ai/client";
|
|
15
|
+
export const client = createClient({});
|
|
4
16
|
```
|
|
5
|
-
"./node_modules/@json-render/shadcn/dist/**/*.js"
|
|
6
|
-
```
|
|
@@ -10,8 +10,10 @@ import ChatDisplayModeSwitcher from "./components/chat-display-mode-switcher";
|
|
|
10
10
|
import { useDisplayMode } from "./hooks/useDisplayMode";
|
|
11
11
|
import InlineAIAssistant from "./inline-ai-assistant";
|
|
12
12
|
|
|
13
|
-
const AIAssistant: React.FC
|
|
14
|
-
|
|
13
|
+
const AIAssistant: React.FC<{
|
|
14
|
+
greeting?: string;
|
|
15
|
+
recommends?: string[];
|
|
16
|
+
}> = ({ greeting, recommends }) => {
|
|
15
17
|
const [isOpen, setIsOpen] = useState(false);
|
|
16
18
|
const [displayMode, setDisplayMode] = useDisplayMode(isOpen);
|
|
17
19
|
const isFullscreen = displayMode === "fullscreen" && isOpen;
|
|
@@ -67,6 +69,7 @@ const AIAssistant: React.FC = () => {
|
|
|
67
69
|
<ChatHeader
|
|
68
70
|
disabledDrag={displayMode !== "floating"}
|
|
69
71
|
isDragging={isDragging}
|
|
72
|
+
displayMode={displayMode}
|
|
70
73
|
onMouseDown={handleDragStart}
|
|
71
74
|
onTouchStart={handleTouchStart}
|
|
72
75
|
onClose={handleClose}
|
|
@@ -77,7 +80,13 @@ const AIAssistant: React.FC = () => {
|
|
|
77
80
|
/>
|
|
78
81
|
</ChatHeader>
|
|
79
82
|
|
|
80
|
-
<InlineAIAssistant
|
|
83
|
+
<InlineAIAssistant
|
|
84
|
+
showBanner={false}
|
|
85
|
+
className="flex-1"
|
|
86
|
+
greeting={greeting || getText().greeting}
|
|
87
|
+
recommends={recommends}
|
|
88
|
+
displayMode={displayMode}
|
|
89
|
+
/>
|
|
81
90
|
</ChatFloatingCard>
|
|
82
91
|
)}
|
|
83
92
|
</>
|
|
@@ -116,7 +116,6 @@ const ChatToolMessage: React.FC<
|
|
|
116
116
|
<span className="flex-1 truncate flex-shrink-0">
|
|
117
117
|
{message.toolName || getText().unknownTool}
|
|
118
118
|
</span>
|
|
119
|
-
{/* {message.toolDescription && <div className="flex-1 truncate max-w-1/2">{message.toolDescription}</div>} */}
|
|
120
119
|
</div>
|
|
121
120
|
);
|
|
122
121
|
};
|
|
@@ -129,11 +128,25 @@ const ChatErrorMessage: React.FC<{ message: TextMessage }> = ({ message }) => {
|
|
|
129
128
|
);
|
|
130
129
|
};
|
|
131
130
|
|
|
131
|
+
export const ChatDivider = () => {
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex items-center justify-center gap-4 w-full my-4">
|
|
134
|
+
<div className="h-px bg-border flex-1" />
|
|
135
|
+
<span className="text-xs text-muted-foreground">
|
|
136
|
+
{getText().newConversation}
|
|
137
|
+
</span>
|
|
138
|
+
<div className="h-px bg-border flex-1" />
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
132
143
|
const ChatUIRenderMessage: React.FC<{ message: UIRenderMessage }> = ({
|
|
133
144
|
message,
|
|
134
145
|
}) => {
|
|
135
146
|
return (
|
|
136
|
-
<
|
|
147
|
+
<div className="flex-1 overflow-hidden">
|
|
148
|
+
<UIRenderer spec={message.spec} className="mb-2" />
|
|
149
|
+
</div>
|
|
137
150
|
);
|
|
138
151
|
};
|
|
139
152
|
|
|
@@ -154,7 +167,9 @@ const MessageContentRenderer: React.FC<
|
|
|
154
167
|
case "error":
|
|
155
168
|
return <ChatErrorMessage message={message as TextMessage} {...rest} />;
|
|
156
169
|
case "ui-render":
|
|
157
|
-
return
|
|
170
|
+
return (
|
|
171
|
+
<ChatUIRenderMessage message={message as UIRenderMessage} {...rest} />
|
|
172
|
+
);
|
|
158
173
|
default:
|
|
159
174
|
return <div>{getText().unknownTool}</div>;
|
|
160
175
|
}
|
|
@@ -166,15 +181,17 @@ const ChatAssistantMessage: React.FC<
|
|
|
166
181
|
showAvatar?: boolean;
|
|
167
182
|
} & MessageCommonProps
|
|
168
183
|
> = ({ message, showAvatar, ...rest }) => {
|
|
184
|
+
|
|
169
185
|
return (
|
|
170
186
|
<div
|
|
187
|
+
data-conversation-message-id={(message as any).messageId}
|
|
171
188
|
className={cn(
|
|
172
189
|
"flex gap-3 animate-in fade-in-0 slide-in-from-bottom-2 duration-300 overflow-hidden w-full chat-assistant-message &+.chat-assistant-message:mt-4 group",
|
|
173
190
|
)}
|
|
174
191
|
>
|
|
175
192
|
<div
|
|
176
193
|
className={cn(
|
|
177
|
-
"flex-shrink-0 h-7 w-7 rounded-full bg-gradient-to-br from-
|
|
194
|
+
"flex-shrink-0 h-7 w-7 rounded-full bg-gradient-to-br from-primary to-primary/40 flex items-center justify-center text-left",
|
|
178
195
|
!showAvatar && "invisible",
|
|
179
196
|
)}
|
|
180
197
|
>
|
|
@@ -3,12 +3,7 @@ import { getText } from "../i18n";
|
|
|
3
3
|
import { HoverCard } from "@radix-ui/react-hover-card";
|
|
4
4
|
import { HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
|
-
|
|
7
|
-
export type IDisplayMode =
|
|
8
|
-
| "fullscreen"
|
|
9
|
-
| "floating"
|
|
10
|
-
| "side-left"
|
|
11
|
-
| "side-right";
|
|
6
|
+
import type { IDisplayMode } from "../types";
|
|
12
7
|
|
|
13
8
|
const ChatDisplayModeSwitcher: React.FC<{
|
|
14
9
|
displayMode: IDisplayMode;
|
|
@@ -31,7 +31,7 @@ const ChatFloatingButton: React.FC<ChatFloatingButtonProps> = ({
|
|
|
31
31
|
size="lg"
|
|
32
32
|
className={`
|
|
33
33
|
group relative h-full w-full rounded-full
|
|
34
|
-
bg-gradient-to-br from-
|
|
34
|
+
bg-gradient-to-br from-primary to-primary/40
|
|
35
35
|
text-white
|
|
36
36
|
border-0
|
|
37
37
|
transition-all duration-300 ease-out select-none
|
|
@@ -43,7 +43,7 @@ const ChatFloatingButton: React.FC<ChatFloatingButtonProps> = ({
|
|
|
43
43
|
}
|
|
44
44
|
`}
|
|
45
45
|
>
|
|
46
|
-
<span className="absolute inset-0 rounded-full bg-
|
|
46
|
+
<span className="absolute inset-0 rounded-full bg-primary animate-ping opacity-20" />
|
|
47
47
|
<MessageSquare
|
|
48
48
|
className="relative h-5 w-5 pointer-events-none"
|
|
49
49
|
strokeWidth={1.75}
|
|
@@ -2,7 +2,7 @@ import type React from "react";
|
|
|
2
2
|
import { useRef } from "react";
|
|
3
3
|
import { Card } from "@/components/ui/card";
|
|
4
4
|
import { cn } from "@/lib/utils";
|
|
5
|
-
import { IDisplayMode } from "
|
|
5
|
+
import { IDisplayMode } from "../types";
|
|
6
6
|
|
|
7
7
|
interface ChatFloatingCardProps {
|
|
8
8
|
displayMode: IDisplayMode;
|
|
@@ -2,8 +2,11 @@ import { MessageSquare, X } from "lucide-react";
|
|
|
2
2
|
import type React from "react";
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { getText } from "../i18n";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { IDisplayMode } from "../types";
|
|
5
7
|
|
|
6
8
|
interface ChatHeaderProps {
|
|
9
|
+
displayMode?: IDisplayMode;
|
|
7
10
|
disabledDrag?: boolean;
|
|
8
11
|
isDragging?: boolean;
|
|
9
12
|
onMouseDown?: (e: React.MouseEvent) => void;
|
|
@@ -13,6 +16,7 @@ interface ChatHeaderProps {
|
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|
19
|
+
displayMode,
|
|
16
20
|
disabledDrag,
|
|
17
21
|
isDragging,
|
|
18
22
|
onMouseDown,
|
|
@@ -22,27 +26,25 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|
|
22
26
|
}) => {
|
|
23
27
|
return (
|
|
24
28
|
<div
|
|
25
|
-
className={
|
|
26
|
-
flex items-center justify-between px-4
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
className={cn(
|
|
30
|
+
"flex items-center justify-between px-4",
|
|
31
|
+
!disabledDrag && onMouseDown
|
|
32
|
+
? isDragging
|
|
33
|
+
? "cursor-grabbing"
|
|
34
|
+
: "cursor-grab"
|
|
35
|
+
: "",
|
|
36
|
+
{
|
|
37
|
+
'border-b border-[#E5E7EB] bg-white': displayMode === "floating",
|
|
38
|
+
},
|
|
39
|
+
)}
|
|
31
40
|
onMouseDown={!disabledDrag ? onMouseDown : undefined}
|
|
32
41
|
onTouchStart={!disabledDrag ? onTouchStart : undefined}
|
|
33
42
|
>
|
|
34
43
|
<div className="flex items-center gap-2.5">
|
|
35
|
-
<div className="flex items-center justify-center h-8 w-8 rounded-full bg-gradient-to-br from-[#6366F1] to-[#8B5CF6]">
|
|
36
|
-
<MessageSquare className="h-4 w-4 text-white" strokeWidth={2} />
|
|
37
|
-
</div>
|
|
38
44
|
<div className="flex flex-col">
|
|
39
45
|
<span className="text-sm font-semibold text-[#111827]">
|
|
40
|
-
|
|
46
|
+
{getText().assistantName}
|
|
41
47
|
</span>
|
|
42
|
-
<div className="flex items-center gap-1.5">
|
|
43
|
-
<span className="h-1.5 w-1.5 rounded-full bg-[#10B981] animate-pulse" />
|
|
44
|
-
<span className="text-xs text-[#6B7280]">{getText().online}</span>
|
|
45
|
-
</div>
|
|
46
48
|
</div>
|
|
47
49
|
</div>
|
|
48
50
|
<div className="flex items-center gap-1">
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ArrowUp,
|
|
3
|
+
LoaderCircle,
|
|
4
|
+
MessageCirclePlus,
|
|
5
|
+
RotateCw,
|
|
6
|
+
Square,
|
|
7
|
+
} from "lucide-react";
|
|
2
8
|
import type React from "react";
|
|
3
9
|
import { Button } from "@/components/ui/button";
|
|
4
10
|
import { getText } from "../i18n";
|
|
5
|
-
import type { Conversation } from "../types";
|
|
6
|
-
import { useState } from "react";
|
|
11
|
+
import type { Conversation, IDisplayMode } from "../types";
|
|
12
|
+
import { useMemo, useState } from "react";
|
|
7
13
|
import { cn } from "@/lib/utils";
|
|
8
14
|
import {
|
|
9
15
|
Tooltip,
|
|
@@ -13,18 +19,19 @@ import {
|
|
|
13
19
|
} from "@/components/ui/tooltip";
|
|
14
20
|
import VoiceInputButton from "./voice-input";
|
|
15
21
|
|
|
16
|
-
const
|
|
17
|
-
if (!
|
|
22
|
+
const NewConvButton: React.FC<{ onNew?: () => void; disabled?: boolean }> = ({ onNew, disabled }) => {
|
|
23
|
+
if (!onNew) return null;
|
|
18
24
|
return (
|
|
19
25
|
<TooltipProvider>
|
|
20
26
|
<Tooltip delayDuration={0}>
|
|
21
|
-
<TooltipContent>{getText().
|
|
27
|
+
<TooltipContent>{getText().newConversation}</TooltipContent>
|
|
22
28
|
<TooltipTrigger asChild>
|
|
23
29
|
<Button
|
|
24
30
|
type="button"
|
|
25
31
|
variant="ghost"
|
|
26
32
|
size="icon"
|
|
27
|
-
onClick={
|
|
33
|
+
onClick={onNew}
|
|
34
|
+
disabled={disabled}
|
|
28
35
|
className="h-8 w-8 text-[#6B7280] hover:text-[#374151] hover:bg-[#F3F4F6] rounded-lg transition-colors duration-200 cursor-pointer"
|
|
29
36
|
>
|
|
30
37
|
<MessageCirclePlus className="size-5" />
|
|
@@ -35,29 +42,56 @@ const ResetButton: React.FC<{ onReset?: () => void }> = ({ onReset }) => {
|
|
|
35
42
|
);
|
|
36
43
|
};
|
|
37
44
|
|
|
38
|
-
const SubmitButton: React.FC<{
|
|
39
|
-
disabled
|
|
45
|
+
const SubmitButton: React.FC<{
|
|
46
|
+
disabled: boolean;
|
|
47
|
+
starting?: boolean;
|
|
48
|
+
onClick: () => void;
|
|
49
|
+
}> = ({ disabled, starting, onClick }) => {
|
|
50
|
+
return (
|
|
51
|
+
<Button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={onClick}
|
|
54
|
+
disabled={disabled || starting}
|
|
55
|
+
size="icon"
|
|
56
|
+
className={cn(
|
|
57
|
+
"size-8 text-white rounded-xl transition-all duration-200 cursor-pointer shadow-[0_2px_8px_rgba(99,102,241,0.3)] hover:shadow-[0_4px_12px_rgba(99,102,241,0.4)]",
|
|
58
|
+
"bg-gradient-to-r from-primary to-primary/80",
|
|
59
|
+
"hover:from-primary/90 hover:to-primary/70",
|
|
60
|
+
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|
61
|
+
starting && "bg-gray-600",
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{starting ? (
|
|
65
|
+
<LoaderCircle className="h-4 w-4 animate-spin" strokeWidth={2} />
|
|
66
|
+
) : (
|
|
67
|
+
<ArrowUp className="h-4 w-4" strokeWidth={2} />
|
|
68
|
+
)}
|
|
69
|
+
</Button>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const StopButton: React.FC<{ onClick: () => void; disabled?: boolean }> = ({
|
|
40
74
|
onClick,
|
|
75
|
+
disabled,
|
|
41
76
|
}) => {
|
|
42
77
|
return (
|
|
43
78
|
<Button
|
|
44
79
|
type="button"
|
|
45
80
|
onClick={onClick}
|
|
46
|
-
disabled={disabled}
|
|
47
81
|
size="icon"
|
|
82
|
+
disabled={disabled}
|
|
48
83
|
className="
|
|
49
84
|
size-8
|
|
50
|
-
bg-
|
|
51
|
-
hover:
|
|
85
|
+
bg-foreground
|
|
86
|
+
hover:bg-foreground/50
|
|
52
87
|
text-white
|
|
53
88
|
rounded-xl
|
|
54
89
|
transition-all duration-200
|
|
55
90
|
cursor-pointer
|
|
56
|
-
|
|
57
|
-
shadow-[
|
|
58
|
-
hover:shadow-[0_4px_12px_rgba(99,102,241,0.4)]"
|
|
91
|
+
shadow-[0_2px_8px_rgba(239,68,68,0.3)]
|
|
92
|
+
hover:shadow-[0_4px_12px_rgba(239,68,68,0.4)]"
|
|
59
93
|
>
|
|
60
|
-
<
|
|
94
|
+
<Square className="h-3 w-3 fill-current" strokeWidth={2} />
|
|
61
95
|
</Button>
|
|
62
96
|
);
|
|
63
97
|
};
|
|
@@ -68,7 +102,10 @@ interface ChatInputProps {
|
|
|
68
102
|
inputValue: string;
|
|
69
103
|
onInputChange: (value: string) => void;
|
|
70
104
|
onSendMessage: () => void;
|
|
71
|
-
|
|
105
|
+
onNew?: () => void;
|
|
106
|
+
onCancel?: () => void;
|
|
107
|
+
starting?: boolean;
|
|
108
|
+
displayMode?: IDisplayMode;
|
|
72
109
|
}
|
|
73
110
|
|
|
74
111
|
const ChatInput: React.FC<ChatInputProps> = ({
|
|
@@ -77,9 +114,19 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
77
114
|
inputValue,
|
|
78
115
|
onInputChange,
|
|
79
116
|
onSendMessage,
|
|
80
|
-
|
|
117
|
+
onNew,
|
|
118
|
+
onCancel,
|
|
119
|
+
starting,
|
|
120
|
+
displayMode
|
|
81
121
|
}) => {
|
|
82
|
-
const hasConversations = conversations.length >
|
|
122
|
+
const hasConversations = conversations.length > 0;
|
|
123
|
+
const lastConv =
|
|
124
|
+
conversations.length > 0 ? conversations[conversations.length - 1] : null;
|
|
125
|
+
const lastIsDivider = useMemo(() => {
|
|
126
|
+
if (!lastConv) return false;
|
|
127
|
+
return (lastConv.system?.level === "newConversation");
|
|
128
|
+
}, [lastConv]);
|
|
129
|
+
|
|
83
130
|
const [focus, setFocus] = useState(false);
|
|
84
131
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
85
132
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
@@ -92,9 +139,9 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
92
139
|
<div className="py-3 px-4">
|
|
93
140
|
<div
|
|
94
141
|
className={cn(
|
|
95
|
-
"w-full rounded-xl bg-white transition-all duration-200 p-3 border border-
|
|
142
|
+
"w-full rounded-xl bg-white transition-all duration-200 p-3 border border-primary/80",
|
|
96
143
|
{
|
|
97
|
-
"border-
|
|
144
|
+
"border-primary ring-2 ring-primary/20": focus,
|
|
98
145
|
},
|
|
99
146
|
)}
|
|
100
147
|
>
|
|
@@ -112,7 +159,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
112
159
|
/>
|
|
113
160
|
<div className="flex items-center justify-between gap-2 mt-1">
|
|
114
161
|
<div className="flex items-center gap-1">
|
|
115
|
-
<
|
|
162
|
+
{onNew && <NewConvButton onNew={onNew} disabled={!hasConversations || lastIsDivider || starting || isLoading} />}
|
|
116
163
|
</div>
|
|
117
164
|
<div className="flex items-center gap-2">
|
|
118
165
|
<VoiceInputButton
|
|
@@ -121,17 +168,22 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
121
168
|
setFocus(true);
|
|
122
169
|
}}
|
|
123
170
|
value={inputValue}
|
|
124
|
-
disabled={isLoading}
|
|
125
|
-
/>
|
|
126
|
-
<SubmitButton
|
|
127
|
-
onClick={() => onSendMessage()}
|
|
128
|
-
disabled={!inputValue.trim() || isLoading}
|
|
171
|
+
disabled={isLoading || starting}
|
|
129
172
|
/>
|
|
173
|
+
{isLoading ? (
|
|
174
|
+
<StopButton onClick={onCancel || (() => {})} />
|
|
175
|
+
) : (
|
|
176
|
+
<SubmitButton
|
|
177
|
+
onClick={() => onSendMessage()}
|
|
178
|
+
disabled={!inputValue.trim() || isLoading}
|
|
179
|
+
starting={starting}
|
|
180
|
+
/>
|
|
181
|
+
)}
|
|
130
182
|
</div>
|
|
131
183
|
</div>
|
|
132
184
|
</div>
|
|
133
185
|
|
|
134
|
-
{conversations.length > 0 && (
|
|
186
|
+
{(conversations.length > 0 || displayMode !== "inline") && (
|
|
135
187
|
<p className="text-[10px] text-[#9CA3AF] mt-2 text-center">
|
|
136
188
|
{getText().footerAiWarning}
|
|
137
189
|
</p>
|
|
@@ -1,36 +1,106 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
|
+
import { useEffect, useRef, useCallback } from "react";
|
|
2
3
|
import type { Conversation } from "../types";
|
|
3
|
-
import ChatAssistantMessage from "./chat-assistant-message";
|
|
4
|
+
import ChatAssistantMessage, { ChatDivider } from "./chat-assistant-message";
|
|
4
5
|
import ChatUserMessage from "./chat-user-message";
|
|
5
6
|
import { cn } from "@/lib/utils";
|
|
7
|
+
import { LoaderCircle } from "lucide-react";
|
|
8
|
+
import { getText } from "../i18n";
|
|
6
9
|
|
|
7
10
|
interface ChatMessagesProps {
|
|
8
11
|
conversations: Conversation[];
|
|
9
12
|
isLoading: boolean;
|
|
13
|
+
isLoadingHistory?: boolean;
|
|
14
|
+
hasMoreHistory?: boolean;
|
|
10
15
|
scrollAreaRef: React.RefObject<HTMLDivElement>;
|
|
11
16
|
messagesEndRef: React.RefObject<HTMLDivElement>;
|
|
12
17
|
className?: string;
|
|
18
|
+
onLoadMore?: () => void;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
const ChatMessages: React.FC<ChatMessagesProps> = ({
|
|
16
22
|
conversations,
|
|
17
23
|
isLoading,
|
|
24
|
+
isLoadingHistory,
|
|
25
|
+
hasMoreHistory,
|
|
18
26
|
scrollAreaRef,
|
|
19
27
|
messagesEndRef,
|
|
20
|
-
className
|
|
28
|
+
className,
|
|
29
|
+
onLoadMore,
|
|
21
30
|
}) => {
|
|
31
|
+
const isNearTopRef = useRef(false);
|
|
32
|
+
const lastScrollTopRef = useRef(0);
|
|
33
|
+
|
|
34
|
+
const handleScroll = useCallback(() => {
|
|
35
|
+
const scrollArea = scrollAreaRef.current;
|
|
36
|
+
if (!scrollArea || !onLoadMore || !hasMoreHistory || isLoadingHistory)
|
|
37
|
+
return;
|
|
38
|
+
|
|
39
|
+
const scrollTop = scrollArea.scrollTop;
|
|
40
|
+
|
|
41
|
+
const isAtTop = scrollTop < 50;
|
|
42
|
+
const wasNearTop = isNearTopRef.current;
|
|
43
|
+
|
|
44
|
+
if (isAtTop && !wasNearTop && scrollTop < lastScrollTopRef.current) {
|
|
45
|
+
onLoadMore();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
isNearTopRef.current = isAtTop;
|
|
49
|
+
lastScrollTopRef.current = scrollTop;
|
|
50
|
+
}, [scrollAreaRef, onLoadMore, hasMoreHistory, isLoadingHistory]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const scrollArea = scrollAreaRef.current;
|
|
54
|
+
if (!scrollArea) return;
|
|
55
|
+
|
|
56
|
+
scrollArea.addEventListener("scroll", handleScroll, { passive: true });
|
|
57
|
+
return () => {
|
|
58
|
+
scrollArea.removeEventListener("scroll", handleScroll);
|
|
59
|
+
};
|
|
60
|
+
}, [handleScroll, scrollAreaRef]);
|
|
61
|
+
|
|
62
|
+
const convLength = conversations.length;
|
|
63
|
+
|
|
22
64
|
return (
|
|
23
65
|
<div
|
|
24
66
|
ref={scrollAreaRef}
|
|
25
|
-
className={cn(
|
|
26
|
-
|
|
27
|
-
|
|
67
|
+
className={cn(
|
|
68
|
+
"text-sm flex flex-col gap-4 w-full py-4 pb-0 min-h-0 overflow-x-hidden overflow-auto max-w-full min-w-0 px-4",
|
|
69
|
+
{
|
|
70
|
+
"flex-1": convLength > 0,
|
|
71
|
+
},
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
28
74
|
data-role="chat-messages"
|
|
29
75
|
>
|
|
76
|
+
{isLoadingHistory ? (
|
|
77
|
+
<div
|
|
78
|
+
key="loading-history"
|
|
79
|
+
className="flex justify-center items-center gap-2 text-center"
|
|
80
|
+
>
|
|
81
|
+
<LoaderCircle className="size-4 animate-spin" />
|
|
82
|
+
<span>{getText().loadingHistory}</span>
|
|
83
|
+
</div>
|
|
84
|
+
) : hasMoreHistory ? (
|
|
85
|
+
<div className="flex justify-center items-center gap-2 text-center">
|
|
86
|
+
<span>
|
|
87
|
+
{hasMoreHistory ? getText().loadMore : getText().noMoreHistory}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
) : null}
|
|
91
|
+
|
|
30
92
|
{conversations.map((conversation, index) => {
|
|
31
93
|
const len = conversation.messages.length;
|
|
94
|
+
const historyId = conversation.historyId || "";
|
|
95
|
+
const lastHistoryId = conversations[index - 1]?.historyId || "";
|
|
96
|
+
let addDivider =
|
|
97
|
+
index > 0 && historyId !== lastHistoryId || conversation.system?.level === "newConversation";
|
|
98
|
+
|
|
32
99
|
return (
|
|
33
100
|
<div key={conversation.taskId} className="flex flex-col gap-4">
|
|
101
|
+
{addDivider && (
|
|
102
|
+
<ChatDivider key={`${conversation.taskId}-divider`} />
|
|
103
|
+
)}
|
|
34
104
|
{conversation.messages.map((message, msgIndex) => {
|
|
35
105
|
const key = message.messageId || `${index}-${msgIndex}`;
|
|
36
106
|
const isNewest = msgIndex === len - 1;
|
|
@@ -4,7 +4,8 @@ const ChatRecommends: React.FC<{
|
|
|
4
4
|
hidden?: boolean;
|
|
5
5
|
data?: string[];
|
|
6
6
|
onSend: (prompt: string) => void;
|
|
7
|
-
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}> = ({ hidden, data = getText().defaultRecommendedQuestions, onSend, disabled }) => {
|
|
8
9
|
if (hidden || !data || data.length === 0) return null;
|
|
9
10
|
return (
|
|
10
11
|
<div className="flex flex-wrap gap-2 pt-2 px-4">
|
|
@@ -13,6 +14,7 @@ const ChatRecommends: React.FC<{
|
|
|
13
14
|
type="button"
|
|
14
15
|
key={prompt}
|
|
15
16
|
onClick={() => onSend(prompt)}
|
|
17
|
+
disabled={disabled}
|
|
16
18
|
className="
|
|
17
19
|
text-xs px-2 py-1.5
|
|
18
20
|
bg-[#F3F4F6] hover:bg-[#E5E7EB]
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import { useConversation } from "./useConversation";
|
|
3
3
|
import { useAutoScroll } from "./useAutoScroll";
|
|
4
4
|
|
|
5
5
|
export const useAssistantStore = () => {
|
|
6
6
|
const [inputValue, setInputValue] = useState("");
|
|
7
7
|
|
|
8
|
-
const chatStreamHook =
|
|
9
|
-
const {
|
|
10
|
-
|
|
8
|
+
const chatStreamHook = useConversation();
|
|
9
|
+
const {
|
|
10
|
+
conversations,
|
|
11
|
+
isLoading,
|
|
12
|
+
isLoadingHistory,
|
|
13
|
+
historyState,
|
|
14
|
+
sendMessage,
|
|
15
|
+
resetConversation,
|
|
16
|
+
startNewConversation,
|
|
17
|
+
loadHistory,
|
|
18
|
+
loadMoreHistory,
|
|
19
|
+
starting,
|
|
20
|
+
cancelChat,
|
|
21
|
+
} = chatStreamHook;
|
|
11
22
|
|
|
12
23
|
const autoScrollHook = useAutoScroll(conversations, isLoading);
|
|
13
24
|
const { scrollAreaRef, messagesEndRef, scrollToBottom } = autoScrollHook;
|
|
@@ -22,15 +33,33 @@ export const useAssistantStore = () => {
|
|
|
22
33
|
await sendMessage(message.trim());
|
|
23
34
|
};
|
|
24
35
|
|
|
36
|
+
const handleNewConversation = useCallback(async () => {
|
|
37
|
+
await startNewConversation();
|
|
38
|
+
}, [startNewConversation]);
|
|
39
|
+
|
|
40
|
+
const handleCancelChat = useCallback(async () => {
|
|
41
|
+
await cancelChat();
|
|
42
|
+
}, [cancelChat]);
|
|
43
|
+
|
|
44
|
+
const handleLoadMore = useCallback(async () => {
|
|
45
|
+
await loadMoreHistory();
|
|
46
|
+
}, [loadMoreHistory]);
|
|
47
|
+
|
|
25
48
|
return {
|
|
26
49
|
conversations,
|
|
27
50
|
isLoading,
|
|
51
|
+
isLoadingHistory,
|
|
52
|
+
historyState,
|
|
28
53
|
inputValue,
|
|
29
54
|
setInputValue,
|
|
30
55
|
sendMessage: handleSendMessage,
|
|
31
56
|
reset: resetConversation,
|
|
57
|
+
startNewConversation: handleNewConversation,
|
|
58
|
+
cancelChat: handleCancelChat,
|
|
59
|
+
loadMoreHistory: handleLoadMore,
|
|
32
60
|
scrollAreaRef,
|
|
33
61
|
messagesEndRef,
|
|
34
|
-
scrollToBottom
|
|
62
|
+
scrollToBottom,
|
|
63
|
+
starting,
|
|
35
64
|
};
|
|
36
65
|
};
|