@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.
Files changed (40) hide show
  1. package/README.md +193 -0
  2. package/bin/amaster.js +2 -0
  3. package/components/ai-assistant/example.md +34 -0
  4. package/components/ai-assistant/package.json +34 -0
  5. package/components/ai-assistant/template/ai-assistant.tsx +88 -0
  6. package/components/ai-assistant/template/components/Markdown.tsx +70 -0
  7. package/components/ai-assistant/template/components/chat-assistant-message.tsx +190 -0
  8. package/components/ai-assistant/template/components/chat-banner.tsx +17 -0
  9. package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +70 -0
  10. package/components/ai-assistant/template/components/chat-floating-button.tsx +56 -0
  11. package/components/ai-assistant/template/components/chat-floating-card.tsx +43 -0
  12. package/components/ai-assistant/template/components/chat-header.tsx +66 -0
  13. package/components/ai-assistant/template/components/chat-input.tsx +143 -0
  14. package/components/ai-assistant/template/components/chat-messages.tsx +81 -0
  15. package/components/ai-assistant/template/components/chat-recommends.tsx +36 -0
  16. package/components/ai-assistant/template/components/chat-speech-button.tsx +43 -0
  17. package/components/ai-assistant/template/components/chat-user-message.tsx +26 -0
  18. package/components/ai-assistant/template/components/ui-renderer-lazy.tsx +307 -0
  19. package/components/ai-assistant/template/components/ui-renderer.tsx +34 -0
  20. package/components/ai-assistant/template/components/voice-input.tsx +43 -0
  21. package/components/ai-assistant/template/hooks/useAssistantStore.tsx +36 -0
  22. package/components/ai-assistant/template/hooks/useAutoScroll.ts +90 -0
  23. package/components/ai-assistant/template/hooks/useConversationProcessor.ts +649 -0
  24. package/components/ai-assistant/template/hooks/useDisplayMode.tsx +74 -0
  25. package/components/ai-assistant/template/hooks/useDraggable.ts +125 -0
  26. package/components/ai-assistant/template/hooks/usePosition.ts +206 -0
  27. package/components/ai-assistant/template/hooks/useSpeak.ts +50 -0
  28. package/components/ai-assistant/template/hooks/useVoiceInput.ts +172 -0
  29. package/components/ai-assistant/template/i18n.ts +114 -0
  30. package/components/ai-assistant/template/index.ts +6 -0
  31. package/components/ai-assistant/template/inline-ai-assistant.tsx +78 -0
  32. package/components/ai-assistant/template/mock/mock-data.ts +643 -0
  33. package/components/ai-assistant/template/types.ts +72 -0
  34. package/index.js +13 -0
  35. package/package.json +67 -0
  36. package/packages/cli/dist/index.d.ts +3 -0
  37. package/packages/cli/dist/index.d.ts.map +1 -0
  38. package/packages/cli/dist/index.js +335 -0
  39. package/packages/cli/dist/index.js.map +1 -0
  40. 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;