@amaster.ai/components-templates 1.5.0 → 1.8.0
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/README.md +12 -8
- package/components/ai-assistant/amaster.config.json +3 -0
- package/components/ai-assistant/package.json +3 -3
- package/components/ai-assistant/template/components/chat-assistant-message.tsx +1 -1
- package/components/ai-assistant/template/components/chat-banner.tsx +1 -1
- package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +6 -6
- package/components/ai-assistant/template/components/chat-floating-button.tsx +4 -5
- package/components/ai-assistant/template/components/chat-floating-card.tsx +3 -3
- package/components/ai-assistant/template/components/chat-header.tsx +5 -5
- package/components/ai-assistant/template/components/chat-input.tsx +36 -24
- package/components/ai-assistant/template/components/chat-messages.tsx +11 -3
- package/components/ai-assistant/template/components/chat-recommends.tsx +12 -14
- package/components/ai-assistant/template/components/chat-user-message.tsx +1 -1
- package/components/ai-assistant/template/components/ui-renderer.tsx +1 -1
- package/components/ai-assistant/template/components/voice-input.tsx +10 -2
- package/components/ai-assistant/template/hooks/useConversation.ts +0 -23
- package/components/ai-assistant/template/hooks/useVoiceInput.ts +18 -20
- package/components/ai-assistant/template/inline-ai-assistant.tsx +1 -1
- package/components/ai-assistant-taro/amaster.config.json +3 -0
- package/components/ai-assistant-taro/package.json +94 -0
- package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +176 -0
- package/components/ai-assistant-taro/template/components/ChatHeader.tsx +27 -0
- package/components/ai-assistant-taro/template/components/ChatInput.tsx +233 -0
- package/components/ai-assistant-taro/template/components/ChatMessages.tsx +126 -0
- package/components/ai-assistant-taro/template/components/ChatUserMessage.tsx +25 -0
- package/components/ai-assistant-taro/template/components/VoiceInput.tsx +169 -0
- package/components/ai-assistant-taro/template/components/markdown.tsx +362 -0
- package/components/ai-assistant-taro/template/hooks/useConversation.ts +905 -0
- package/components/ai-assistant-taro/template/hooks/useSafeArea.ts +20 -0
- package/components/ai-assistant-taro/template/hooks/useVoiceInput.ts +204 -0
- package/components/ai-assistant-taro/template/i18n.ts +157 -0
- package/components/ai-assistant-taro/template/index.config.ts +10 -0
- package/components/ai-assistant-taro/template/index.tsx +83 -0
- package/components/ai-assistant-taro/template/types.ts +74 -0
- package/package.json +5 -2
- package/packages/cli/dist/index.js +14 -3
- package/packages/cli/dist/index.js.map +1 -1
- package/packages/cli/package.json +1 -1
- package/components/ai-assistant/example.md +0 -34
- package/components/ai-assistant/others.md +0 -16
package/README.md
CHANGED
|
@@ -137,12 +137,14 @@ npm run create:component
|
|
|
137
137
|
|
|
138
138
|
1. 在 `components/` 目录下创建新组件目录
|
|
139
139
|
2. 添加 `package.json` 配置文件
|
|
140
|
-
3.
|
|
140
|
+
3. 添加 `amaster.config.json` 配置文件(配置目标目录)
|
|
141
|
+
4. 在 `template/` 目录下放置模板文件
|
|
141
142
|
|
|
142
143
|
```
|
|
143
144
|
components/
|
|
144
145
|
└── your-component/
|
|
145
|
-
├── package.json #
|
|
146
|
+
├── package.json # 组件依赖配置
|
|
147
|
+
├── amaster.config.json # 组件模板配置
|
|
146
148
|
└── template/ # 模板文件
|
|
147
149
|
├── index.ts
|
|
148
150
|
└── ...
|
|
@@ -155,12 +157,6 @@ components/
|
|
|
155
157
|
"name": "your-component",
|
|
156
158
|
"version": "1.0.0",
|
|
157
159
|
"description": "组件描述",
|
|
158
|
-
"template": {
|
|
159
|
-
"name": "your-component",
|
|
160
|
-
"targetDir": "src/components/your-component",
|
|
161
|
-
"files": ["**/*"],
|
|
162
|
-
"ignore": ["node_modules", "*.log", ".DS_Store"]
|
|
163
|
-
},
|
|
164
160
|
"dependencies": {
|
|
165
161
|
"some-lib": "^1.0.0"
|
|
166
162
|
},
|
|
@@ -173,6 +169,14 @@ components/
|
|
|
173
169
|
}
|
|
174
170
|
```
|
|
175
171
|
|
|
172
|
+
### 组件 amaster.config.json 配置
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"targetDir": "src/components/your-component"
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
176
180
|
## 发布
|
|
177
181
|
|
|
178
182
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amaster-react-project",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite --force",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@a2a-js/sdk": "^0.3.7",
|
|
20
|
-
"@amaster.ai/client": "1.1.0-beta.
|
|
21
|
-
"@amaster.ai/vite-plugins": "1.1.0-beta.
|
|
20
|
+
"@amaster.ai/client": "1.1.0-beta.61",
|
|
21
|
+
"@amaster.ai/vite-plugins": "1.1.0-beta.61",
|
|
22
22
|
"@fontsource-variable/inter": "^5.2.8",
|
|
23
23
|
"@fortawesome/fontawesome-free": "^6.1.1",
|
|
24
24
|
"@hookform/resolvers": "^5.2.2",
|
|
@@ -195,7 +195,7 @@ const ChatAssistantMessage: React.FC<
|
|
|
195
195
|
)}
|
|
196
196
|
>
|
|
197
197
|
{showAvatar && (
|
|
198
|
-
<MessageSquare className="h-3.5 w-3.5 text-
|
|
198
|
+
<MessageSquare className="h-3.5 w-3.5 text-primary-foreground" strokeWidth={2} />
|
|
199
199
|
)}
|
|
200
200
|
</div>
|
|
201
201
|
<MessageContentRenderer message={message} {...rest} />
|
|
@@ -16,22 +16,22 @@ const ChatDisplayModeSwitcher: React.FC<{
|
|
|
16
16
|
}[] = [
|
|
17
17
|
{
|
|
18
18
|
mode: "floating",
|
|
19
|
-
icon: <Layers2 className="
|
|
19
|
+
icon: <Layers2 className="size-4" strokeWidth={1.75} />,
|
|
20
20
|
title: getText().displayMode.floating,
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
mode: "side-right",
|
|
24
|
-
icon: <Sidebar className="
|
|
24
|
+
icon: <Sidebar className="size-4 rotate-180" strokeWidth={1.75} />,
|
|
25
25
|
title: getText().displayMode.sideRight,
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
mode: "side-left",
|
|
29
|
-
icon: <Sidebar className="
|
|
29
|
+
icon: <Sidebar className="size-4 " strokeWidth={1.75} />,
|
|
30
30
|
title: getText().displayMode.sideLeft,
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
mode: "fullscreen",
|
|
34
|
-
icon: <Maximize className="
|
|
34
|
+
icon: <Maximize className="size-4" strokeWidth={1.75} />,
|
|
35
35
|
title: getText().displayMode.fullscreen,
|
|
36
36
|
},
|
|
37
37
|
];
|
|
@@ -39,7 +39,7 @@ const ChatDisplayModeSwitcher: React.FC<{
|
|
|
39
39
|
return (
|
|
40
40
|
<HoverCard openDelay={0}>
|
|
41
41
|
<HoverCardTrigger asChild>
|
|
42
|
-
<Button variant="ghost" size="icon" className="
|
|
42
|
+
<Button variant="ghost" size="icon" className="size-8">
|
|
43
43
|
{modes.find((m) => m.mode === displayMode)?.icon}
|
|
44
44
|
</Button>
|
|
45
45
|
</HoverCardTrigger>
|
|
@@ -48,7 +48,7 @@ const ChatDisplayModeSwitcher: React.FC<{
|
|
|
48
48
|
<Button
|
|
49
49
|
key={mode}
|
|
50
50
|
variant="ghost"
|
|
51
|
-
className="justify-start
|
|
51
|
+
className="justify-start"
|
|
52
52
|
onClick={() => onChange(mode)}
|
|
53
53
|
size="sm"
|
|
54
54
|
disabled={mode === displayMode}
|
|
@@ -10,7 +10,6 @@ interface ChatFloatingButtonProps {
|
|
|
10
10
|
style: React.CSSProperties;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
const ChatFloatingButton: React.FC<ChatFloatingButtonProps> = ({
|
|
15
14
|
onClick,
|
|
16
15
|
onMouseDown,
|
|
@@ -32,14 +31,14 @@ const ChatFloatingButton: React.FC<ChatFloatingButtonProps> = ({
|
|
|
32
31
|
className={`
|
|
33
32
|
group relative h-full w-full rounded-full
|
|
34
33
|
bg-gradient-to-br from-primary to-primary/40
|
|
35
|
-
text-
|
|
34
|
+
text-primary-foreground
|
|
36
35
|
border-0
|
|
37
36
|
transition-all duration-300 ease-out select-none
|
|
38
37
|
cursor-pointer
|
|
39
38
|
${
|
|
40
39
|
isDragging
|
|
41
|
-
? "shadow-
|
|
42
|
-
: "shadow-
|
|
40
|
+
? "shadow-2xl scale-105"
|
|
41
|
+
: "shadow-lg hover:shadow-2xl hover:scale-105"
|
|
43
42
|
}
|
|
44
43
|
`}
|
|
45
44
|
>
|
|
@@ -53,4 +52,4 @@ const ChatFloatingButton: React.FC<ChatFloatingButtonProps> = ({
|
|
|
53
52
|
);
|
|
54
53
|
};
|
|
55
54
|
|
|
56
|
-
export default ChatFloatingButton;
|
|
55
|
+
export default ChatFloatingButton;
|
|
@@ -23,14 +23,14 @@ const ChatFloatingCard: React.FC<ChatFloatingCardProps> = ({
|
|
|
23
23
|
<Card
|
|
24
24
|
ref={containerRef}
|
|
25
25
|
className={cn(
|
|
26
|
-
`flex flex-col overflow-hidden bg-
|
|
26
|
+
`flex flex-col overflow-hidden bg-card border border-border z-50`,
|
|
27
27
|
{
|
|
28
28
|
"fixed inset-0 w-auto h-auto animate-in fade-in-0 zoom-in-[0.98] duration-300 origin-top-left rounded-none": displayMode === "fullscreen",
|
|
29
29
|
"fixed top-0 right-0 w-[420px] h-full rounded-none": displayMode === "side-right",
|
|
30
30
|
"fixed top-0 left-0 w-[420px] h-full rounded-none": displayMode === "side-left",
|
|
31
31
|
"rounded-2xl": displayMode === "floating",
|
|
32
|
-
"shadow-
|
|
33
|
-
"shadow-
|
|
32
|
+
"shadow-2xl": displayMode === "floating" && isDragging,
|
|
33
|
+
"shadow-lg": displayMode === "floating" && !isDragging,
|
|
34
34
|
}
|
|
35
35
|
)}
|
|
36
36
|
style={displayMode !== "floating" ? {} : style}
|
|
@@ -27,14 +27,14 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|
|
27
27
|
return (
|
|
28
28
|
<div
|
|
29
29
|
className={cn(
|
|
30
|
-
"flex items-center justify-between px-4",
|
|
30
|
+
"flex items-center justify-between px-4 py-1 pr-2",
|
|
31
31
|
!disabledDrag && onMouseDown
|
|
32
32
|
? isDragging
|
|
33
33
|
? "cursor-grabbing"
|
|
34
34
|
: "cursor-grab"
|
|
35
35
|
: "",
|
|
36
36
|
{
|
|
37
|
-
'border-b border-
|
|
37
|
+
'border-b border-border': displayMode === "floating",
|
|
38
38
|
},
|
|
39
39
|
)}
|
|
40
40
|
onMouseDown={!disabledDrag ? onMouseDown : undefined}
|
|
@@ -42,7 +42,7 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|
|
42
42
|
>
|
|
43
43
|
<div className="flex items-center gap-2.5">
|
|
44
44
|
<div className="flex flex-col">
|
|
45
|
-
<span className="text-sm font-semibold text-
|
|
45
|
+
<span className="text-sm font-semibold text-foreground">
|
|
46
46
|
{getText().assistantName}
|
|
47
47
|
</span>
|
|
48
48
|
</div>
|
|
@@ -55,9 +55,9 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|
|
55
55
|
variant="ghost"
|
|
56
56
|
size="icon"
|
|
57
57
|
onClick={onClose}
|
|
58
|
-
className="
|
|
58
|
+
className="size-8"
|
|
59
59
|
>
|
|
60
|
-
<X className="
|
|
60
|
+
<X className="size-4" strokeWidth={1.75} />
|
|
61
61
|
</Button>
|
|
62
62
|
)}
|
|
63
63
|
</div>
|
|
@@ -9,7 +9,7 @@ import type React from "react";
|
|
|
9
9
|
import { Button } from "@/components/ui/button";
|
|
10
10
|
import { getText } from "../i18n";
|
|
11
11
|
import type { Conversation, IDisplayMode } from "../types";
|
|
12
|
-
import { useMemo, useState } from "react";
|
|
12
|
+
import { useMemo, useRef, useState } from "react";
|
|
13
13
|
import { cn } from "@/lib/utils";
|
|
14
14
|
import {
|
|
15
15
|
Tooltip,
|
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
} from "@/components/ui/tooltip";
|
|
20
20
|
import VoiceInputButton from "./voice-input";
|
|
21
21
|
|
|
22
|
-
const NewConvButton: React.FC<{ onNew?: () => void; disabled?: boolean }> = ({
|
|
22
|
+
const NewConvButton: React.FC<{ onNew?: () => void; disabled?: boolean }> = ({
|
|
23
|
+
onNew,
|
|
24
|
+
disabled,
|
|
25
|
+
}) => {
|
|
23
26
|
if (!onNew) return null;
|
|
24
27
|
return (
|
|
25
28
|
<TooltipProvider>
|
|
@@ -32,7 +35,7 @@ const NewConvButton: React.FC<{ onNew?: () => void; disabled?: boolean }> = ({ o
|
|
|
32
35
|
size="icon"
|
|
33
36
|
onClick={onNew}
|
|
34
37
|
disabled={disabled}
|
|
35
|
-
className="h-8 w-8
|
|
38
|
+
className="h-8 w-8 cursor-pointer"
|
|
36
39
|
>
|
|
37
40
|
<MessageCirclePlus className="size-5" />
|
|
38
41
|
</Button>
|
|
@@ -49,12 +52,11 @@ const SubmitButton: React.FC<{
|
|
|
49
52
|
}> = ({ disabled, starting, onClick }) => {
|
|
50
53
|
return (
|
|
51
54
|
<Button
|
|
52
|
-
type="button"
|
|
53
55
|
onClick={onClick}
|
|
54
56
|
disabled={disabled || starting}
|
|
55
57
|
size="icon"
|
|
56
58
|
className={cn(
|
|
57
|
-
"size-8 text-
|
|
59
|
+
"rounded-xl size-8 text-primary-foreground transition-all duration-200 cursor-pointer shadow-sm hover:shadow-2xl",
|
|
58
60
|
"bg-gradient-to-r from-primary to-primary/80",
|
|
59
61
|
"hover:from-primary/90 hover:to-primary/70",
|
|
60
62
|
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|
@@ -76,20 +78,10 @@ const StopButton: React.FC<{ onClick: () => void; disabled?: boolean }> = ({
|
|
|
76
78
|
}) => {
|
|
77
79
|
return (
|
|
78
80
|
<Button
|
|
79
|
-
type="button"
|
|
80
81
|
onClick={onClick}
|
|
81
82
|
size="icon"
|
|
82
83
|
disabled={disabled}
|
|
83
|
-
className="
|
|
84
|
-
size-8
|
|
85
|
-
bg-foreground
|
|
86
|
-
hover:bg-foreground/50
|
|
87
|
-
text-white
|
|
88
|
-
rounded-xl
|
|
89
|
-
transition-all duration-200
|
|
90
|
-
cursor-pointer
|
|
91
|
-
shadow-[0_2px_8px_rgba(239,68,68,0.3)]
|
|
92
|
-
hover:shadow-[0_4px_12px_rgba(239,68,68,0.4)]"
|
|
84
|
+
className="size-8 bg-success hover:bg-success/50 text-success-foreground rounded-xl transition-all duration-200 cursor-pointer shadow-sm hover:shadow-2xl"
|
|
93
85
|
>
|
|
94
86
|
<Square className="h-3 w-3 fill-current" strokeWidth={2} />
|
|
95
87
|
</Button>
|
|
@@ -117,15 +109,17 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
117
109
|
onNew,
|
|
118
110
|
onCancel,
|
|
119
111
|
starting,
|
|
120
|
-
displayMode
|
|
112
|
+
displayMode,
|
|
121
113
|
}) => {
|
|
122
114
|
const hasConversations = conversations.length > 0;
|
|
123
115
|
const lastConv =
|
|
124
116
|
conversations.length > 0 ? conversations[conversations.length - 1] : null;
|
|
125
117
|
const lastIsDivider = useMemo(() => {
|
|
126
118
|
if (!lastConv) return false;
|
|
127
|
-
return
|
|
119
|
+
return lastConv.system?.level === "newConversation";
|
|
128
120
|
}, [lastConv]);
|
|
121
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
122
|
+
const [speaking, setSpeaking] = useState(false);
|
|
129
123
|
|
|
130
124
|
const [focus, setFocus] = useState(false);
|
|
131
125
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
@@ -135,47 +129,65 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
135
129
|
}
|
|
136
130
|
};
|
|
137
131
|
|
|
132
|
+
const textareaScrollToBottom = () => {
|
|
133
|
+
if (textareaRef.current) {
|
|
134
|
+
textareaRef.current.scrollTop = textareaRef.current.scrollHeight;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
138
|
return (
|
|
139
139
|
<div className="py-3 px-4">
|
|
140
140
|
<div
|
|
141
141
|
className={cn(
|
|
142
|
-
"w-full rounded-xl bg-
|
|
142
|
+
"w-full rounded-xl bg-card transition-all duration-200 p-3 border border-primary/80",
|
|
143
143
|
{
|
|
144
144
|
"border-primary ring-2 ring-primary/20": focus,
|
|
145
145
|
},
|
|
146
146
|
)}
|
|
147
147
|
>
|
|
148
148
|
<textarea
|
|
149
|
+
ref={textareaRef}
|
|
149
150
|
value={inputValue}
|
|
150
151
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
151
152
|
onInputChange(e.target.value)
|
|
152
153
|
}
|
|
153
|
-
onKeyPress={isLoading ? undefined : handleKeyPress}
|
|
154
|
+
onKeyPress={isLoading || speaking ? undefined : handleKeyPress}
|
|
154
155
|
placeholder={getText().typePlaceholder}
|
|
155
156
|
onFocus={() => setFocus(true)}
|
|
156
157
|
onBlur={() => setFocus(false)}
|
|
157
|
-
className="w-full text-sm text-
|
|
158
|
+
className="w-full text-sm text-foreground placeholder:text-muted-foreground outline-none resize-none leading-5 bg-card"
|
|
158
159
|
rows={4}
|
|
160
|
+
disabled={speaking}
|
|
161
|
+
readOnly={speaking}
|
|
159
162
|
/>
|
|
160
163
|
<div className="flex items-center justify-between gap-2 mt-1">
|
|
161
164
|
<div className="flex items-center gap-1">
|
|
162
|
-
{onNew &&
|
|
165
|
+
{onNew && (
|
|
166
|
+
<NewConvButton
|
|
167
|
+
onNew={onNew}
|
|
168
|
+
disabled={
|
|
169
|
+
!hasConversations || lastIsDivider || starting || isLoading
|
|
170
|
+
}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
163
173
|
</div>
|
|
164
174
|
<div className="flex items-center gap-2">
|
|
165
175
|
<VoiceInputButton
|
|
166
176
|
onChange={(text) => {
|
|
167
177
|
onInputChange(text);
|
|
178
|
+
textareaScrollToBottom();
|
|
168
179
|
setFocus(true);
|
|
169
180
|
}}
|
|
170
181
|
value={inputValue}
|
|
171
182
|
disabled={isLoading || starting}
|
|
183
|
+
onRunningChange={setSpeaking}
|
|
172
184
|
/>
|
|
173
185
|
{isLoading ? (
|
|
174
186
|
<StopButton onClick={onCancel || (() => {})} />
|
|
175
187
|
) : (
|
|
176
188
|
<SubmitButton
|
|
177
189
|
onClick={() => onSendMessage()}
|
|
178
|
-
disabled={!inputValue.trim() || isLoading}
|
|
190
|
+
disabled={!inputValue.trim() || isLoading || speaking}
|
|
179
191
|
starting={starting}
|
|
180
192
|
/>
|
|
181
193
|
)}
|
|
@@ -184,7 +196,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
184
196
|
</div>
|
|
185
197
|
|
|
186
198
|
{(conversations.length > 0 || displayMode !== "inline") && (
|
|
187
|
-
<p className="text-[10px] text-
|
|
199
|
+
<p className="text-[10px] text-muted-foreground/50 mt-2 text-center">
|
|
188
200
|
{getText().footerAiWarning}
|
|
189
201
|
</p>
|
|
190
202
|
)}
|
|
@@ -71,9 +71,16 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
|
|
|
71
71
|
},
|
|
72
72
|
className,
|
|
73
73
|
)}
|
|
74
|
+
style={{
|
|
75
|
+
maskImage:
|
|
76
|
+
"linear-gradient(to bottom, transparent, black 10px, black calc(100% - 10px), transparent)",
|
|
77
|
+
WebkitMaskImage:
|
|
78
|
+
"linear-gradient(to bottom, transparent, black 10px, black calc(100% - 10px), transparent)",
|
|
79
|
+
maskSize: "80% 100%",
|
|
80
|
+
}}
|
|
74
81
|
data-role="chat-messages"
|
|
75
82
|
>
|
|
76
|
-
{isLoadingHistory ? (
|
|
83
|
+
{isLoadingHistory && convLength > 0 ? (
|
|
77
84
|
<div
|
|
78
85
|
key="loading-history"
|
|
79
86
|
className="flex justify-center items-center gap-2 text-center"
|
|
@@ -94,7 +101,8 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
|
|
|
94
101
|
const historyId = conversation.historyId || "";
|
|
95
102
|
const lastHistoryId = conversations[index - 1]?.historyId || "";
|
|
96
103
|
let addDivider =
|
|
97
|
-
index > 0 && historyId !== lastHistoryId ||
|
|
104
|
+
(index > 0 && historyId !== lastHistoryId) ||
|
|
105
|
+
conversation.system?.level === "newConversation";
|
|
98
106
|
|
|
99
107
|
return (
|
|
100
108
|
<div key={conversation.taskId} className="flex flex-col gap-4">
|
|
@@ -148,4 +156,4 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
|
|
|
148
156
|
);
|
|
149
157
|
};
|
|
150
158
|
|
|
151
|
-
export default ChatMessages;
|
|
159
|
+
export default ChatMessages;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button";
|
|
1
2
|
import { getText } from "../i18n";
|
|
2
3
|
|
|
3
4
|
const ChatRecommends: React.FC<{
|
|
@@ -5,31 +6,28 @@ const ChatRecommends: React.FC<{
|
|
|
5
6
|
data?: string[];
|
|
6
7
|
onSend: (prompt: string) => void;
|
|
7
8
|
disabled?: boolean;
|
|
8
|
-
}> = ({
|
|
9
|
+
}> = ({
|
|
10
|
+
hidden,
|
|
11
|
+
data = getText().defaultRecommendedQuestions,
|
|
12
|
+
onSend,
|
|
13
|
+
disabled,
|
|
14
|
+
}) => {
|
|
9
15
|
if (hidden || !data || data.length === 0) return null;
|
|
10
16
|
return (
|
|
11
17
|
<div className="flex flex-wrap gap-2 pt-2 px-4">
|
|
12
18
|
{data.map((prompt, index) => (
|
|
13
|
-
<
|
|
14
|
-
type="button"
|
|
19
|
+
<Button
|
|
15
20
|
key={prompt}
|
|
21
|
+
variant="outline"
|
|
16
22
|
onClick={() => onSend(prompt)}
|
|
17
23
|
disabled={disabled}
|
|
18
24
|
className="
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
text-[#374151] hover:text-[#111827]
|
|
22
|
-
rounded-full
|
|
23
|
-
border border-[#D1D5DB]
|
|
24
|
-
transition-all duration-200
|
|
25
|
-
cursor-pointer
|
|
26
|
-
animate-in fade-in-0 slide-in-from-bottom-1
|
|
27
|
-
text-nowrap
|
|
28
|
-
"
|
|
25
|
+
text-xs px-2 py-1 h-auto rounded-full cursor-pointer text-nowrap
|
|
26
|
+
transition-all duration-200 animate-in fade-in-0 slide-in-from-bottom-1"
|
|
29
27
|
style={{ animationDelay: `${index * 50}ms` }}
|
|
30
28
|
>
|
|
31
29
|
{prompt}
|
|
32
|
-
</
|
|
30
|
+
</Button>
|
|
33
31
|
))}
|
|
34
32
|
</div>
|
|
35
33
|
);
|
|
@@ -12,7 +12,7 @@ const ChatUserMessage: React.FC<{
|
|
|
12
12
|
className={`
|
|
13
13
|
px-4 py-2.5 rounded-2xl
|
|
14
14
|
transition-all duration-200 min-w-0 overflow-hidden max-w-full
|
|
15
|
-
bg-
|
|
15
|
+
bg-primary/10 text-foreground rounded-br-md
|
|
16
16
|
`}
|
|
17
17
|
>
|
|
18
18
|
<div className="text-sm leading-relaxed whitespace-pre-wrap break-words">
|
|
@@ -29,7 +29,7 @@ export const UIRenderer: React.FC<UIRendererProps> = ({ spec, className }) => {
|
|
|
29
29
|
return (
|
|
30
30
|
<Suspense
|
|
31
31
|
fallback={
|
|
32
|
-
<Skeleton className={cn("w-full h-[120px] bg-
|
|
32
|
+
<Skeleton className={cn("w-full h-[120px] bg-primary/20 flex items-center justify-center gap-2", className)}>
|
|
33
33
|
<LoaderCircle className="size-4 animate-spin" />
|
|
34
34
|
<span>{getText().loading}...</span>
|
|
35
35
|
</Skeleton>
|
|
@@ -3,12 +3,14 @@ import { Mic, StopCircle } from "lucide-react";
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { cn } from "@/lib/utils";
|
|
5
5
|
import { useVoiceInput } from "../hooks/useVoiceInput";
|
|
6
|
+
import { useEffect } from "react";
|
|
6
7
|
|
|
7
8
|
const VoiceInputButton: React.FC<{
|
|
8
9
|
onChange: (text: string) => void;
|
|
9
10
|
disabled?: boolean;
|
|
10
11
|
value?: string;
|
|
11
|
-
|
|
12
|
+
onRunningChange?: (running: boolean) => void;
|
|
13
|
+
}> = ({ onChange, disabled, value, onRunningChange }) => {
|
|
12
14
|
const {
|
|
13
15
|
start,
|
|
14
16
|
stop,
|
|
@@ -23,13 +25,19 @@ const VoiceInputButton: React.FC<{
|
|
|
23
25
|
value,
|
|
24
26
|
});
|
|
25
27
|
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (onRunningChange) {
|
|
30
|
+
onRunningChange(running);
|
|
31
|
+
}
|
|
32
|
+
}, [running, onRunningChange]);
|
|
33
|
+
|
|
26
34
|
return (
|
|
27
35
|
<Button
|
|
28
36
|
variant="ghost"
|
|
29
37
|
onClick={stoppable ? stop : status === "idle" ? start : undefined}
|
|
30
38
|
disabled={finalDisabled}
|
|
31
39
|
className={cn("h-8 w-8 rounded-lg cursor-pointer text-xs ", {
|
|
32
|
-
"bg-gradient-to-r from-
|
|
40
|
+
"bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70 text-primary-foreground animate-pulse":
|
|
33
41
|
running,
|
|
34
42
|
"w-auto": statusText,
|
|
35
43
|
})}
|
|
@@ -391,7 +391,6 @@ export function useConversation() {
|
|
|
391
391
|
try {
|
|
392
392
|
const partData = JSON.parse(json);
|
|
393
393
|
if (partData.root && partData.elements) {
|
|
394
|
-
debugger
|
|
395
394
|
fillData({
|
|
396
395
|
kind: 'ui-render',
|
|
397
396
|
taskId,
|
|
@@ -768,28 +767,6 @@ export function useConversation() {
|
|
|
768
767
|
const controller = abortControllerRef.current;
|
|
769
768
|
forceStopRef.current = false;
|
|
770
769
|
|
|
771
|
-
// mock
|
|
772
|
-
if (userContent === "MAGA") {
|
|
773
|
-
const { generateMockUIStream } = await import("../mock/mock-data");
|
|
774
|
-
const cancelMock = generateMockUIStream(
|
|
775
|
-
taskId || generateId(),
|
|
776
|
-
(chunk) => {
|
|
777
|
-
if (forceStopRef.current) return;
|
|
778
|
-
hasResponse = true;
|
|
779
|
-
processLiveData(chunk as any);
|
|
780
|
-
if (chunk.result.final) {
|
|
781
|
-
setIsLoading(false);
|
|
782
|
-
}
|
|
783
|
-
},
|
|
784
|
-
);
|
|
785
|
-
abortControllerRef.current?.signal.addEventListener(
|
|
786
|
-
"abort",
|
|
787
|
-
cancelMock,
|
|
788
|
-
);
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// real chat
|
|
793
770
|
try {
|
|
794
771
|
const stream = client.copilot.chat(
|
|
795
772
|
userContent ? [{ role: "user", content: userContent }] : [],
|
|
@@ -8,10 +8,7 @@ type Status =
|
|
|
8
8
|
| "idle"
|
|
9
9
|
| "starting"
|
|
10
10
|
| "ready"
|
|
11
|
-
| "speaking"
|
|
12
|
-
| "recording"
|
|
13
11
|
| "stopping"
|
|
14
|
-
| "ended"
|
|
15
12
|
| "error"
|
|
16
13
|
| "closed";
|
|
17
14
|
|
|
@@ -30,10 +27,10 @@ export const useVoiceInput = ({
|
|
|
30
27
|
const AsrClientRef = useRef<ASRClient | null>(null);
|
|
31
28
|
const runningRef = useRef(false);
|
|
32
29
|
const stoppable = useMemo(() => {
|
|
33
|
-
return ["ready"
|
|
30
|
+
return ["ready"].includes(status);
|
|
34
31
|
}, [status]);
|
|
35
32
|
const disabledClick = useMemo(() => {
|
|
36
|
-
return ["starting", "
|
|
33
|
+
return ["starting", "stopping", "closed", "error"].includes(
|
|
37
34
|
status,
|
|
38
35
|
);
|
|
39
36
|
}, [status]);
|
|
@@ -53,13 +50,6 @@ export const useVoiceInput = ({
|
|
|
53
50
|
onReady() {
|
|
54
51
|
setStatus("ready");
|
|
55
52
|
},
|
|
56
|
-
onSpeechStart() {
|
|
57
|
-
setStatus((status) => (status !== "stopping" ? "recording" : status));
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
onSpeechEnd() {
|
|
61
|
-
setStatus((status) => (status !== "stopping" ? "recording" : status));
|
|
62
|
-
},
|
|
63
53
|
onTranscript(text, isFinal) {
|
|
64
54
|
if (!isFinal) {
|
|
65
55
|
temp = text;
|
|
@@ -70,10 +60,21 @@ export const useVoiceInput = ({
|
|
|
70
60
|
onChange(result);
|
|
71
61
|
}
|
|
72
62
|
},
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
onAudioBufferCommitted() {
|
|
64
|
+
console.debug("音频缓冲区已提交");
|
|
75
65
|
},
|
|
76
|
-
onError() {
|
|
66
|
+
onError(error) {
|
|
67
|
+
if (error.message === "NO_DEVICES_AVAILABLE") {
|
|
68
|
+
toast({
|
|
69
|
+
title: getText().voiceInputError.microphoneAccessDenied,
|
|
70
|
+
variant: "destructive",
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
toast({
|
|
74
|
+
title: getText().voiceInputError.unknownError,
|
|
75
|
+
variant: "destructive",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
77
78
|
reset();
|
|
78
79
|
},
|
|
79
80
|
onClose() {
|
|
@@ -89,7 +90,7 @@ export const useVoiceInput = ({
|
|
|
89
90
|
AsrClientRef.current = asrClient;
|
|
90
91
|
} catch (error) {
|
|
91
92
|
const message = error.message;
|
|
92
|
-
const showError = (title: string) => toast({title})
|
|
93
|
+
const showError = (title: string) => toast({ title });
|
|
93
94
|
if (message.includes("Microphone access denied")) {
|
|
94
95
|
showError(getText().voiceInputError.microphoneAccessDenied);
|
|
95
96
|
} else if (message.includes("No speech detected")) {
|
|
@@ -147,15 +148,12 @@ export const useVoiceInput = ({
|
|
|
147
148
|
}
|
|
148
149
|
}, [status]);
|
|
149
150
|
|
|
150
|
-
const statusTextMap = {
|
|
151
|
+
const statusTextMap: Record<Status, string> = {
|
|
151
152
|
// idle: t('translation:voiceInputStatus.idle'),
|
|
152
153
|
idle: "",
|
|
153
154
|
starting: getText().voiceInputStatus.starting,
|
|
154
155
|
ready: getText().voiceInputStatus.ready,
|
|
155
|
-
speaking: getText().voiceInputStatus.speaking,
|
|
156
|
-
recording: getText().voiceInputStatus.recording,
|
|
157
156
|
stopping: getText().voiceInputStatus.stopping,
|
|
158
|
-
ended: getText().voiceInputStatus.ended,
|
|
159
157
|
error: getText().voiceInputStatus.error,
|
|
160
158
|
closed: getText().voiceInputStatus.closed,
|
|
161
159
|
};
|
|
@@ -45,7 +45,7 @@ const InlineAIAssistant: React.FC<InlineAIAssistantProps> = ({
|
|
|
45
45
|
return (
|
|
46
46
|
<div
|
|
47
47
|
className={cn(
|
|
48
|
-
"w-full h-full max-h-screen relative flex flex-col justify-center overflow-hidden max-w-
|
|
48
|
+
"w-full h-full max-h-screen relative flex flex-col justify-center overflow-hidden max-w-6xl mx-auto",
|
|
49
49
|
className,
|
|
50
50
|
)}
|
|
51
51
|
style={style}
|