@amaster.ai/components-templates 1.3.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 +193 -0
- package/bin/amaster.js +2 -0
- package/components/ai-assistant/example.md +34 -0
- package/components/ai-assistant/package.json +34 -0
- package/components/ai-assistant/template/ai-assistant.tsx +88 -0
- package/components/ai-assistant/template/components/Markdown.tsx +70 -0
- package/components/ai-assistant/template/components/chat-assistant-message.tsx +190 -0
- package/components/ai-assistant/template/components/chat-banner.tsx +17 -0
- package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +70 -0
- package/components/ai-assistant/template/components/chat-floating-button.tsx +56 -0
- package/components/ai-assistant/template/components/chat-floating-card.tsx +43 -0
- package/components/ai-assistant/template/components/chat-header.tsx +66 -0
- package/components/ai-assistant/template/components/chat-input.tsx +143 -0
- package/components/ai-assistant/template/components/chat-messages.tsx +81 -0
- package/components/ai-assistant/template/components/chat-recommends.tsx +36 -0
- package/components/ai-assistant/template/components/chat-speech-button.tsx +43 -0
- package/components/ai-assistant/template/components/chat-user-message.tsx +26 -0
- package/components/ai-assistant/template/components/ui-renderer-lazy.tsx +307 -0
- package/components/ai-assistant/template/components/ui-renderer.tsx +34 -0
- package/components/ai-assistant/template/components/voice-input.tsx +43 -0
- package/components/ai-assistant/template/hooks/useAssistantStore.tsx +36 -0
- package/components/ai-assistant/template/hooks/useAutoScroll.ts +90 -0
- package/components/ai-assistant/template/hooks/useConversationProcessor.ts +649 -0
- package/components/ai-assistant/template/hooks/useDisplayMode.tsx +74 -0
- package/components/ai-assistant/template/hooks/useDraggable.ts +125 -0
- package/components/ai-assistant/template/hooks/usePosition.ts +206 -0
- package/components/ai-assistant/template/hooks/useSpeak.ts +50 -0
- package/components/ai-assistant/template/hooks/useVoiceInput.ts +172 -0
- package/components/ai-assistant/template/i18n.ts +114 -0
- package/components/ai-assistant/template/index.ts +6 -0
- package/components/ai-assistant/template/inline-ai-assistant.tsx +78 -0
- package/components/ai-assistant/template/mock/mock-data.ts +643 -0
- package/components/ai-assistant/template/types.ts +72 -0
- package/index.js +13 -0
- package/package.json +67 -0
- package/packages/cli/dist/index.d.ts +3 -0
- package/packages/cli/dist/index.d.ts.map +1 -0
- package/packages/cli/dist/index.js +335 -0
- package/packages/cli/dist/index.js.map +1 -0
- package/packages/cli/package.json +35 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Layers2, Maximize, Move, Sidebar } from "lucide-react";
|
|
2
|
+
import { getText } from "../i18n";
|
|
3
|
+
import { HoverCard } from "@radix-ui/react-hover-card";
|
|
4
|
+
import { HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
|
|
7
|
+
export type IDisplayMode =
|
|
8
|
+
| "fullscreen"
|
|
9
|
+
| "floating"
|
|
10
|
+
| "side-left"
|
|
11
|
+
| "side-right";
|
|
12
|
+
|
|
13
|
+
const ChatDisplayModeSwitcher: React.FC<{
|
|
14
|
+
displayMode: IDisplayMode;
|
|
15
|
+
onChange: (mode: IDisplayMode) => void;
|
|
16
|
+
}> = ({ displayMode, onChange }) => {
|
|
17
|
+
const modes: {
|
|
18
|
+
mode: IDisplayMode;
|
|
19
|
+
icon: React.ReactNode;
|
|
20
|
+
title: string;
|
|
21
|
+
}[] = [
|
|
22
|
+
{
|
|
23
|
+
mode: "floating",
|
|
24
|
+
icon: <Layers2 className="h-4 w-4" strokeWidth={1.75} />,
|
|
25
|
+
title: getText().displayMode.floating,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
mode: "side-right",
|
|
29
|
+
icon: <Sidebar className="h-4 w-4 rotate-180" strokeWidth={1.75} />,
|
|
30
|
+
title: getText().displayMode.sideRight,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
mode: "side-left",
|
|
34
|
+
icon: <Sidebar className="h-4 w-4 " strokeWidth={1.75} />,
|
|
35
|
+
title: getText().displayMode.sideLeft,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
mode: "fullscreen",
|
|
39
|
+
icon: <Maximize className="h-4 w-4" strokeWidth={1.75} />,
|
|
40
|
+
title: getText().displayMode.fullscreen,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<HoverCard openDelay={0}>
|
|
46
|
+
<HoverCardTrigger asChild>
|
|
47
|
+
<Button variant="ghost" size="icon" className="hover:bg-transparent text-black/50 hover:text-black">
|
|
48
|
+
{modes.find((m) => m.mode === displayMode)?.icon}
|
|
49
|
+
</Button>
|
|
50
|
+
</HoverCardTrigger>
|
|
51
|
+
<HoverCardContent align="end" className="flex flex-col w-fit p-1">
|
|
52
|
+
{modes.map(({ mode, icon, title }) => (
|
|
53
|
+
<Button
|
|
54
|
+
key={mode}
|
|
55
|
+
variant="ghost"
|
|
56
|
+
className="justify-start hover:bg-gray-200 text-black/80 hover:text-black data-[state=open]:bg-transparent"
|
|
57
|
+
onClick={() => onChange(mode)}
|
|
58
|
+
size="sm"
|
|
59
|
+
disabled={mode === displayMode}
|
|
60
|
+
>
|
|
61
|
+
{icon}
|
|
62
|
+
{title}
|
|
63
|
+
</Button>
|
|
64
|
+
))}
|
|
65
|
+
</HoverCardContent>
|
|
66
|
+
</HoverCard>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default ChatDisplayModeSwitcher;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { MessageSquare } from "lucide-react";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
|
|
5
|
+
interface ChatFloatingButtonProps {
|
|
6
|
+
onClick: () => void;
|
|
7
|
+
onMouseDown: (e: React.MouseEvent) => void;
|
|
8
|
+
onTouchStart: (e: React.TouchEvent) => void;
|
|
9
|
+
isDragging: boolean;
|
|
10
|
+
style: React.CSSProperties;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const ChatFloatingButton: React.FC<ChatFloatingButtonProps> = ({
|
|
15
|
+
onClick,
|
|
16
|
+
onMouseDown,
|
|
17
|
+
onTouchStart,
|
|
18
|
+
isDragging,
|
|
19
|
+
style,
|
|
20
|
+
}) => {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className={`fixed z-50 flex items-center justify-center ${isDragging ? "cursor-grabbing" : "cursor-grab"}`}
|
|
24
|
+
style={style}
|
|
25
|
+
>
|
|
26
|
+
<Button
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={() => !isDragging && onClick()}
|
|
29
|
+
onMouseDown={onMouseDown}
|
|
30
|
+
onTouchStart={onTouchStart}
|
|
31
|
+
size="lg"
|
|
32
|
+
className={`
|
|
33
|
+
group relative h-full w-full rounded-full
|
|
34
|
+
bg-gradient-to-br from-[#6366F1] to-[#8B5CF6]
|
|
35
|
+
text-white
|
|
36
|
+
border-0
|
|
37
|
+
transition-all duration-300 ease-out select-none
|
|
38
|
+
cursor-pointer
|
|
39
|
+
${
|
|
40
|
+
isDragging
|
|
41
|
+
? "shadow-[0_8px_30px_rgba(99,102,241,0.5)] scale-105"
|
|
42
|
+
: "shadow-[0_4px_20px_rgba(99,102,241,0.35)] hover:shadow-[0_8px_30px_rgba(99,102,241,0.5)] hover:scale-105"
|
|
43
|
+
}
|
|
44
|
+
`}
|
|
45
|
+
>
|
|
46
|
+
<span className="absolute inset-0 rounded-full bg-[#6366F1] animate-ping opacity-20" />
|
|
47
|
+
<MessageSquare
|
|
48
|
+
className="relative h-5 w-5 pointer-events-none"
|
|
49
|
+
strokeWidth={1.75}
|
|
50
|
+
/>
|
|
51
|
+
</Button>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default ChatFloatingButton;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { Card } from "@/components/ui/card";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
import { IDisplayMode } from "./chat-display-mode-switcher";
|
|
6
|
+
|
|
7
|
+
interface ChatFloatingCardProps {
|
|
8
|
+
displayMode: IDisplayMode;
|
|
9
|
+
isDragging: boolean;
|
|
10
|
+
style: React.CSSProperties;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ChatFloatingCard: React.FC<ChatFloatingCardProps> = ({
|
|
15
|
+
displayMode,
|
|
16
|
+
isDragging,
|
|
17
|
+
style,
|
|
18
|
+
children,
|
|
19
|
+
}) => {
|
|
20
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Card
|
|
24
|
+
ref={containerRef}
|
|
25
|
+
className={cn(
|
|
26
|
+
`flex flex-col overflow-hidden bg-[#F9FAFB] border border-[#E5E7EB] z-50`,
|
|
27
|
+
{
|
|
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
|
+
"fixed top-0 right-0 w-[420px] h-full rounded-none": displayMode === "side-right",
|
|
30
|
+
"fixed top-0 left-0 w-[420px] h-full rounded-none": displayMode === "side-left",
|
|
31
|
+
"rounded-2xl": displayMode === "floating",
|
|
32
|
+
"shadow-[0_25px_50px_-12px_rgba(0,0,0,0.25)]": displayMode === "floating" && isDragging,
|
|
33
|
+
"shadow-[0_10px_40px_-10px_rgba(0,0,0,0.1),0_4px_6px_-4px_rgba(0,0,0,0.1)]": displayMode === "floating" && !isDragging,
|
|
34
|
+
}
|
|
35
|
+
)}
|
|
36
|
+
style={displayMode !== "floating" ? {} : style}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</Card>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default ChatFloatingCard;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { MessageSquare, X } from "lucide-react";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { getText } from "../i18n";
|
|
5
|
+
|
|
6
|
+
interface ChatHeaderProps {
|
|
7
|
+
disabledDrag?: boolean;
|
|
8
|
+
isDragging?: boolean;
|
|
9
|
+
onMouseDown?: (e: React.MouseEvent) => void;
|
|
10
|
+
onTouchStart?: (e: React.TouchEvent) => void;
|
|
11
|
+
onClose?: () => void;
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|
16
|
+
disabledDrag,
|
|
17
|
+
isDragging,
|
|
18
|
+
onMouseDown,
|
|
19
|
+
onTouchStart,
|
|
20
|
+
onClose,
|
|
21
|
+
children,
|
|
22
|
+
}) => {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={`
|
|
26
|
+
flex items-center justify-between px-4 py-3
|
|
27
|
+
border-b border-[#E5E7EB]
|
|
28
|
+
bg-white
|
|
29
|
+
${!disabledDrag && onMouseDown ? (isDragging ? "cursor-grabbing" : "cursor-grab") : ""}
|
|
30
|
+
`}
|
|
31
|
+
onMouseDown={!disabledDrag ? onMouseDown : undefined}
|
|
32
|
+
onTouchStart={!disabledDrag ? onTouchStart : undefined}
|
|
33
|
+
>
|
|
34
|
+
<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
|
+
<div className="flex flex-col">
|
|
39
|
+
<span className="text-sm font-semibold text-[#111827]">
|
|
40
|
+
AI Assistant
|
|
41
|
+
</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
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="flex items-center gap-1">
|
|
49
|
+
{children}
|
|
50
|
+
{onClose && (
|
|
51
|
+
<Button
|
|
52
|
+
type="button"
|
|
53
|
+
variant="ghost"
|
|
54
|
+
size="icon"
|
|
55
|
+
onClick={onClose}
|
|
56
|
+
className="h-8 w-8 text-[#6B7280] hover:text-[#374151] hover:bg-[#F3F4F6] rounded-lg transition-colors duration-200 cursor-pointer"
|
|
57
|
+
>
|
|
58
|
+
<X className="h-4 w-4" strokeWidth={1.75} />
|
|
59
|
+
</Button>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default ChatHeader;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { ArrowUp, MessageCirclePlus } from "lucide-react";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { getText } from "../i18n";
|
|
5
|
+
import type { Conversation } from "../types";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
import {
|
|
9
|
+
Tooltip,
|
|
10
|
+
TooltipContent,
|
|
11
|
+
TooltipProvider,
|
|
12
|
+
TooltipTrigger,
|
|
13
|
+
} from "@/components/ui/tooltip";
|
|
14
|
+
import VoiceInputButton from "./voice-input";
|
|
15
|
+
|
|
16
|
+
const ResetButton: React.FC<{ onReset?: () => void }> = ({ onReset }) => {
|
|
17
|
+
if (!onReset) return null;
|
|
18
|
+
return (
|
|
19
|
+
<TooltipProvider>
|
|
20
|
+
<Tooltip delayDuration={0}>
|
|
21
|
+
<TooltipContent>{getText().resetConversation}</TooltipContent>
|
|
22
|
+
<TooltipTrigger asChild>
|
|
23
|
+
<Button
|
|
24
|
+
type="button"
|
|
25
|
+
variant="ghost"
|
|
26
|
+
size="icon"
|
|
27
|
+
onClick={onReset}
|
|
28
|
+
className="h-8 w-8 text-[#6B7280] hover:text-[#374151] hover:bg-[#F3F4F6] rounded-lg transition-colors duration-200 cursor-pointer"
|
|
29
|
+
>
|
|
30
|
+
<MessageCirclePlus className="size-5" />
|
|
31
|
+
</Button>
|
|
32
|
+
</TooltipTrigger>
|
|
33
|
+
</Tooltip>
|
|
34
|
+
</TooltipProvider>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const SubmitButton: React.FC<{ disabled: boolean; onClick: () => void }> = ({
|
|
39
|
+
disabled,
|
|
40
|
+
onClick,
|
|
41
|
+
}) => {
|
|
42
|
+
return (
|
|
43
|
+
<Button
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={onClick}
|
|
46
|
+
disabled={disabled}
|
|
47
|
+
size="icon"
|
|
48
|
+
className="
|
|
49
|
+
size-8
|
|
50
|
+
bg-gradient-to-r from-[#6366F1] to-[#8B5CF6]
|
|
51
|
+
hover:from-[#4F46E5] hover:to-[#7C3AED]
|
|
52
|
+
text-white
|
|
53
|
+
rounded-xl
|
|
54
|
+
transition-all duration-200
|
|
55
|
+
cursor-pointer
|
|
56
|
+
disabled:opacity-40 disabled:cursor-not-allowed
|
|
57
|
+
shadow-[0_2px_8px_rgba(99,102,241,0.3)]
|
|
58
|
+
hover:shadow-[0_4px_12px_rgba(99,102,241,0.4)]"
|
|
59
|
+
>
|
|
60
|
+
<ArrowUp className="h-4 w-4" strokeWidth={2} />
|
|
61
|
+
</Button>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
interface ChatInputProps {
|
|
66
|
+
conversations: Conversation[];
|
|
67
|
+
isLoading: boolean;
|
|
68
|
+
inputValue: string;
|
|
69
|
+
onInputChange: (value: string) => void;
|
|
70
|
+
onSendMessage: () => void;
|
|
71
|
+
onReset?: () => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const ChatInput: React.FC<ChatInputProps> = ({
|
|
75
|
+
conversations,
|
|
76
|
+
isLoading,
|
|
77
|
+
inputValue,
|
|
78
|
+
onInputChange,
|
|
79
|
+
onSendMessage,
|
|
80
|
+
onReset,
|
|
81
|
+
}) => {
|
|
82
|
+
const hasConversations = conversations.length > 1;
|
|
83
|
+
const [focus, setFocus] = useState(false);
|
|
84
|
+
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
85
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
onSendMessage();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="py-3 px-4">
|
|
93
|
+
<div
|
|
94
|
+
className={cn(
|
|
95
|
+
"w-full rounded-xl bg-white transition-all duration-200 p-3 border border-[#6366F1]/80",
|
|
96
|
+
{
|
|
97
|
+
"border-[#6366F1] ring-2 ring-[#6366F1]/20": focus
|
|
98
|
+
},
|
|
99
|
+
)}
|
|
100
|
+
>
|
|
101
|
+
<textarea
|
|
102
|
+
value={inputValue}
|
|
103
|
+
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
104
|
+
onInputChange(e.target.value)
|
|
105
|
+
}
|
|
106
|
+
onKeyPress={isLoading ? undefined : handleKeyPress}
|
|
107
|
+
placeholder={getText().typePlaceholder}
|
|
108
|
+
onFocus={() => setFocus(true)}
|
|
109
|
+
onBlur={() => setFocus(false)}
|
|
110
|
+
className="w-full text-sm text-[#111827] placeholder:text-[#9CA3AF] outline-none resize-none leading-5 disabled:bg-transparent"
|
|
111
|
+
rows={4}
|
|
112
|
+
/>
|
|
113
|
+
<div className="flex items-center justify-between gap-2 mt-1">
|
|
114
|
+
<div className="flex items-center gap-1">
|
|
115
|
+
<ResetButton onReset={hasConversations ? onReset : undefined} />
|
|
116
|
+
</div>
|
|
117
|
+
<div className="flex items-center gap-2">
|
|
118
|
+
<VoiceInputButton
|
|
119
|
+
onChange={(text) => {
|
|
120
|
+
onInputChange(text);
|
|
121
|
+
setFocus(true);
|
|
122
|
+
}}
|
|
123
|
+
value={inputValue}
|
|
124
|
+
disabled={isLoading}
|
|
125
|
+
/>
|
|
126
|
+
<SubmitButton
|
|
127
|
+
onClick={() => onSendMessage()}
|
|
128
|
+
disabled={!inputValue.trim() || isLoading}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{conversations.length > 0 && (
|
|
135
|
+
<p className="text-[10px] text-[#9CA3AF] mt-2 text-center">
|
|
136
|
+
{getText().footerAiWarning}
|
|
137
|
+
</p>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export default ChatInput;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { Conversation } from "../types";
|
|
3
|
+
import ChatAssistantMessage from "./chat-assistant-message";
|
|
4
|
+
import ChatUserMessage from "./chat-user-message";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
interface ChatMessagesProps {
|
|
8
|
+
conversations: Conversation[];
|
|
9
|
+
isLoading: boolean;
|
|
10
|
+
scrollAreaRef: React.RefObject<HTMLDivElement>;
|
|
11
|
+
messagesEndRef: React.RefObject<HTMLDivElement>;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ChatMessages: React.FC<ChatMessagesProps> = ({
|
|
16
|
+
conversations,
|
|
17
|
+
isLoading,
|
|
18
|
+
scrollAreaRef,
|
|
19
|
+
messagesEndRef,
|
|
20
|
+
className
|
|
21
|
+
}) => {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
ref={scrollAreaRef}
|
|
25
|
+
className={cn('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', {
|
|
26
|
+
'flex-1': conversations.length > 0,
|
|
27
|
+
}, className)}
|
|
28
|
+
data-role="chat-messages"
|
|
29
|
+
>
|
|
30
|
+
{conversations.map((conversation, index) => {
|
|
31
|
+
const len = conversation.messages.length;
|
|
32
|
+
return (
|
|
33
|
+
<div key={conversation.taskId} className="flex flex-col gap-4">
|
|
34
|
+
{conversation.messages.map((message, msgIndex) => {
|
|
35
|
+
const key = message.messageId || `${index}-${msgIndex}`;
|
|
36
|
+
const isNewest = msgIndex === len - 1;
|
|
37
|
+
if (message.role === "assistant") {
|
|
38
|
+
return (
|
|
39
|
+
<ChatAssistantMessage
|
|
40
|
+
key={key}
|
|
41
|
+
message={message}
|
|
42
|
+
isNewest={isNewest}
|
|
43
|
+
isLoading={isLoading}
|
|
44
|
+
showAvatar={
|
|
45
|
+
msgIndex === 0 ||
|
|
46
|
+
conversation.messages[msgIndex - 1].role !== "assistant"
|
|
47
|
+
}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return <ChatUserMessage key={key} message={message} />;
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
|
|
57
|
+
{isLoading && (
|
|
58
|
+
<div
|
|
59
|
+
key="loading"
|
|
60
|
+
className="flex flex-col gap-2 px-2 -mt-2 sticky bottom-2"
|
|
61
|
+
>
|
|
62
|
+
<ChatAssistantMessage
|
|
63
|
+
key="loading"
|
|
64
|
+
message={{
|
|
65
|
+
kind: "text-content",
|
|
66
|
+
messageId: "loading",
|
|
67
|
+
role: "assistant",
|
|
68
|
+
content: "",
|
|
69
|
+
}}
|
|
70
|
+
isLoading={true}
|
|
71
|
+
showAvatar={false}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
<div ref={messagesEndRef} className="mt-auto" />
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default ChatMessages;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getText } from "../i18n";
|
|
2
|
+
|
|
3
|
+
const ChatRecommends: React.FC<{
|
|
4
|
+
hidden?: boolean;
|
|
5
|
+
data?: string[];
|
|
6
|
+
onSend: (prompt: string) => void;
|
|
7
|
+
}> = ({ hidden, data = getText().defaultRecommendedQuestions, onSend }) => {
|
|
8
|
+
if (hidden || !data || data.length === 0) return null;
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex flex-wrap gap-2 pt-2 px-4">
|
|
11
|
+
{data.map((prompt, index) => (
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
key={prompt}
|
|
15
|
+
onClick={() => onSend(prompt)}
|
|
16
|
+
className="
|
|
17
|
+
text-xs px-2 py-1.5
|
|
18
|
+
bg-[#F3F4F6] hover:bg-[#E5E7EB]
|
|
19
|
+
text-[#374151] hover:text-[#111827]
|
|
20
|
+
rounded-full
|
|
21
|
+
border border-[#D1D5DB]
|
|
22
|
+
transition-all duration-200
|
|
23
|
+
cursor-pointer
|
|
24
|
+
animate-in fade-in-0 slide-in-from-bottom-1
|
|
25
|
+
text-nowrap
|
|
26
|
+
"
|
|
27
|
+
style={{ animationDelay: `${index * 50}ms` }}
|
|
28
|
+
>
|
|
29
|
+
{prompt}
|
|
30
|
+
</button>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default ChatRecommends;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button";
|
|
2
|
+
import { Volume2 } from "lucide-react";
|
|
3
|
+
import { useSpeak } from "../hooks/useSpeak";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
export default function TTSReader({
|
|
7
|
+
text,
|
|
8
|
+
className,
|
|
9
|
+
}: {
|
|
10
|
+
text: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}) {
|
|
13
|
+
const { speak, stop, speaking } = useSpeak();
|
|
14
|
+
|
|
15
|
+
if (!text) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Button
|
|
21
|
+
variant="ghost"
|
|
22
|
+
size="sm"
|
|
23
|
+
className={cn(
|
|
24
|
+
"hover:bg-transparent p-0 h-6 text-primary/50 hover:text-primary",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
onClick={() => {
|
|
28
|
+
try {
|
|
29
|
+
if (speaking) {
|
|
30
|
+
stop();
|
|
31
|
+
} else {
|
|
32
|
+
speak(text);
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("TTS error:", error);
|
|
36
|
+
}
|
|
37
|
+
}}
|
|
38
|
+
title={speaking ? "停止朗读" : "朗读文本"}
|
|
39
|
+
>
|
|
40
|
+
<Volume2 className={cn(speaking ? "text-primary animate-pulse" : "")} />
|
|
41
|
+
</Button>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Markdown from "./Markdown";
|
|
2
|
+
import type { MessagesItem, TextMessage } from "../types";
|
|
3
|
+
|
|
4
|
+
const ChatUserMessage: React.FC<{
|
|
5
|
+
message: MessagesItem;
|
|
6
|
+
}> = ({ message }) => {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
className={`flex gap-3 animate-in fade-in-0 slide-in-from-bottom-2 duration-300 overflow-hidden w-full flex-row-reverse`}
|
|
10
|
+
>
|
|
11
|
+
<div
|
|
12
|
+
className={`
|
|
13
|
+
px-4 py-2.5 rounded-2xl
|
|
14
|
+
transition-all duration-200 min-w-0 overflow-hidden max-w-full
|
|
15
|
+
bg-[#E0E7FF] text-[#1E1B4B] rounded-br-md
|
|
16
|
+
`}
|
|
17
|
+
>
|
|
18
|
+
<div className="text-sm leading-relaxed whitespace-pre-wrap break-words">
|
|
19
|
+
<Markdown content={(message as TextMessage).content || "..."} />
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default ChatUserMessage;
|