@amaster.ai/components-templates 1.4.1 → 1.4.3

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 (25) hide show
  1. package/components/ai-assistant/others.md +13 -3
  2. package/components/ai-assistant/package.json +112 -30
  3. package/components/ai-assistant/template/ai-assistant.tsx +12 -3
  4. package/components/ai-assistant/template/components/chat-assistant-message.tsx +25 -9
  5. package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +1 -6
  6. package/components/ai-assistant/template/components/chat-floating-button.tsx +2 -2
  7. package/components/ai-assistant/template/components/chat-floating-card.tsx +1 -1
  8. package/components/ai-assistant/template/components/chat-header.tsx +16 -14
  9. package/components/ai-assistant/template/components/chat-input.tsx +80 -28
  10. package/components/ai-assistant/template/components/chat-messages.tsx +75 -5
  11. package/components/ai-assistant/template/components/chat-recommends.tsx +3 -1
  12. package/components/ai-assistant/template/components/chat-speech-button.tsx +25 -7
  13. package/components/ai-assistant/template/components/ui-renderer.tsx +10 -1
  14. package/components/ai-assistant/template/hooks/useAssistantStore.tsx +35 -6
  15. package/components/ai-assistant/template/hooks/useConversation.ts +898 -0
  16. package/components/ai-assistant/template/hooks/useDisplayMode.tsx +1 -1
  17. package/components/ai-assistant/template/hooks/useSpeak.ts +89 -6
  18. package/components/ai-assistant/template/hooks/useVoiceInput.ts +1 -1
  19. package/components/ai-assistant/template/i18n.ts +21 -3
  20. package/components/ai-assistant/template/inline-ai-assistant.tsx +21 -9
  21. package/components/ai-assistant/template/types.ts +16 -0
  22. package/package.json +4 -2
  23. package/packages/cli/dist/index.js +26 -3
  24. package/packages/cli/dist/index.js.map +1 -1
  25. package/packages/cli/package.json +1 -1
@@ -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('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)}
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
- }> = ({ hidden, data = getText().defaultRecommendedQuestions, onSend }) => {
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]
@@ -2,6 +2,21 @@ import { Button } from "@/components/ui/button";
2
2
  import { Volume2 } from "lucide-react";
3
3
  import { useSpeak } from "../hooks/useSpeak";
4
4
  import { cn } from "@/lib/utils";
5
+ import { toast } from "@/hooks/use-toast";
6
+ import { getText } from "../i18n";
7
+
8
+ function availableSpeak(text: string) {
9
+ if (!text) return false;
10
+
11
+ // 1. 长度限制
12
+ if (text.length >= 100) return false;
13
+
14
+ // 2. 是否包含复杂 markdown
15
+ const markdownPattern =
16
+ /(```|`|\*\*|__|\* |^- |^\d+\. |\[.+\]\(.+\)|#+ |>|!\[)/m;
17
+
18
+ return !markdownPattern.test(text);
19
+ }
5
20
 
6
21
  export default function TTSReader({
7
22
  text,
@@ -12,7 +27,7 @@ export default function TTSReader({
12
27
  }) {
13
28
  const { speak, stop, speaking } = useSpeak();
14
29
 
15
- if (!text) {
30
+ if (!speaking && !text) {
16
31
  return null;
17
32
  }
18
33
 
@@ -22,20 +37,23 @@ export default function TTSReader({
22
37
  size="sm"
23
38
  className={cn(
24
39
  "hover:bg-transparent p-0 h-6 text-primary/50 hover:text-primary",
40
+ !speaking && "opacity-0 group-hover:opacity-100",
41
+ speaking ? "pointer-events-none " : "",
25
42
  className,
26
43
  )}
27
44
  onClick={() => {
45
+ if (!availableSpeak(text)) {
46
+ toast({
47
+ title: getText().notAvailableSpeak,
48
+ });
49
+ return;
50
+ }
28
51
  try {
29
- if (speaking) {
30
- stop();
31
- } else {
32
- speak(text);
33
- }
52
+ speaking ? stop() : speak(text);
34
53
  } catch (error) {
35
54
  console.error("TTS error:", error);
36
55
  }
37
56
  }}
38
- title={speaking ? "停止朗读" : "朗读文本"}
39
57
  >
40
58
  <Volume2 className={cn(speaking ? "text-primary animate-pulse" : "")} />
41
59
  </Button>
@@ -1,6 +1,8 @@
1
1
  import { Skeleton } from "@/components/ui/skeleton";
2
2
  import { cn } from "@/lib/utils";
3
+ import { LoaderCircle } from "lucide-react";
3
4
  import { lazy, Suspense } from "react";
5
+ import { getText } from "../i18n";
4
6
 
5
7
  const UIRendererLazy = lazy(() => import("./ui-renderer-lazy"));
6
8
 
@@ -25,7 +27,14 @@ export const UIRenderer: React.FC<UIRendererProps> = ({ spec, className }) => {
25
27
  }
26
28
 
27
29
  return (
28
- <Suspense fallback={<Skeleton className={cn('w-full h-[120px] bg-gray-200', className)} />}>
30
+ <Suspense
31
+ fallback={
32
+ <Skeleton className={cn("w-full h-[120px] bg-gray-200 flex items-center justify-center gap-2", className)}>
33
+ <LoaderCircle className="size-4 animate-spin" />
34
+ <span>{getText().loading}...</span>
35
+ </Skeleton>
36
+ }
37
+ >
29
38
  <UIRendererLazy spec={spec} className={className} />
30
39
  </Suspense>
31
40
  );
@@ -1,13 +1,24 @@
1
- import { useState } from "react";
2
- import { useConversationProcessor } from "./useConversationProcessor";
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 = useConversationProcessor();
9
- const { conversations, isLoading, sendMessage, resetConversation } =
10
- chatStreamHook;
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
  };