@sciol/xyzen 0.1.4 → 0.1.6
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/package.json +3 -3
- package/src/app/App.tsx +0 -221
- package/src/assets/react.svg +0 -1
- package/src/components/layouts/XyzenChat.tsx +0 -212
- package/src/components/layouts/XyzenHistory.tsx +0 -117
- package/src/components/layouts/XyzenNodes.tsx +0 -7
- package/src/components/layouts/components/ChatBubble.tsx +0 -140
- package/src/components/layouts/components/ChatInput.tsx +0 -173
- package/src/components/layouts/components/EmptyChat.tsx +0 -117
- package/src/components/layouts/components/WelcomeMessage.tsx +0 -104
- package/src/configs/index.ts +0 -1
- package/src/context/XyzenProvider.tsx +0 -0
- package/src/hooks/useTheme.ts +0 -79
- package/src/index.ts +0 -6
- package/src/lib/Markdown.tsx +0 -98
- package/src/lib/formatDate.ts +0 -32
- package/src/main.tsx +0 -9
- package/src/service/xyzenService.ts +0 -90
- package/src/store/xyzenStore.ts +0 -326
- package/src/types/xyzen.d.ts +0 -0
- package/src/vite-env.d.ts +0 -1
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import Markdown from "@/lib/Markdown";
|
|
2
|
-
|
|
3
|
-
import { motion } from "framer-motion";
|
|
4
|
-
import React, { useEffect, useState } from "react";
|
|
5
|
-
|
|
6
|
-
export interface Message {
|
|
7
|
-
id: string;
|
|
8
|
-
sender: string;
|
|
9
|
-
content: string;
|
|
10
|
-
timestamp: string;
|
|
11
|
-
avatar?: string;
|
|
12
|
-
isCurrentUser?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface ChatBubbleProps {
|
|
16
|
-
message: Message;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const ChatBubble: React.FC<ChatBubbleProps> = ({ message }) => {
|
|
20
|
-
const { isCurrentUser, content, timestamp, avatar } = message;
|
|
21
|
-
const [imageError, setImageError] = useState(false);
|
|
22
|
-
const [userAvatar, setUserAvatar] = useState<string | undefined>(avatar);
|
|
23
|
-
|
|
24
|
-
// 如果是当前用户,从localStorage获取最新的用户头像
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (isCurrentUser) {
|
|
27
|
-
try {
|
|
28
|
-
const userInfoStr = localStorage.getItem("userInfo");
|
|
29
|
-
if (userInfoStr) {
|
|
30
|
-
const userInfo = JSON.parse(userInfoStr);
|
|
31
|
-
if (userInfo.avatar) {
|
|
32
|
-
setUserAvatar(userInfo.avatar);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} catch (e) {
|
|
36
|
-
console.error("Failed to get avatar from localStorage", e);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}, [isCurrentUser]);
|
|
40
|
-
|
|
41
|
-
// Updated time format to include seconds
|
|
42
|
-
const formattedTime = new Date(timestamp).toLocaleTimeString([], {
|
|
43
|
-
hour: "2-digit",
|
|
44
|
-
minute: "2-digit",
|
|
45
|
-
second: "2-digit",
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Different styles for user vs AI messages
|
|
49
|
-
const messageStyles = isCurrentUser
|
|
50
|
-
? "border-l-4 border-blue-400 bg-blue-50/50 dark:border-blue-600 dark:bg-blue-900/20"
|
|
51
|
-
: "border-l-4 border-neutral-300 bg-white dark:border-neutral-600 dark:bg-neutral-800/50";
|
|
52
|
-
|
|
53
|
-
// 获取头像 URL 但避免使用不存在的默认头像文件
|
|
54
|
-
const getAvatarUrl = (avatarPath?: string) => {
|
|
55
|
-
if (!avatarPath) return null;
|
|
56
|
-
|
|
57
|
-
// 如果已经是完整URL就直接使用
|
|
58
|
-
if (avatarPath.startsWith("http")) {
|
|
59
|
-
return avatarPath;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 如果是相对路径,根据环境添加正确的基本URL
|
|
63
|
-
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "";
|
|
64
|
-
return avatarPath.startsWith("/")
|
|
65
|
-
? `${baseUrl}${avatarPath}`
|
|
66
|
-
: `${baseUrl}/${avatarPath}`;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// 渲染头像,使用初始字母作为最后的备用选项
|
|
70
|
-
const renderAvatar = () => {
|
|
71
|
-
// 如果已经知道图像加载失败,或者没有提供头像
|
|
72
|
-
if (imageError || !userAvatar) {
|
|
73
|
-
// 显示用户或AI的首字母作为头像
|
|
74
|
-
const initial = isCurrentUser
|
|
75
|
-
? message.sender?.charAt(0)?.toUpperCase() || "U"
|
|
76
|
-
: "A";
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div
|
|
80
|
-
className={`flex h-6 w-6 items-center justify-center rounded-full ${
|
|
81
|
-
isCurrentUser
|
|
82
|
-
? "bg-blue-500 text-white"
|
|
83
|
-
: "bg-purple-500 text-white"
|
|
84
|
-
}`}
|
|
85
|
-
>
|
|
86
|
-
<span className="text-xs font-medium">{initial}</span>
|
|
87
|
-
</div>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 尝试加载实际头像
|
|
92
|
-
const avatarUrl = getAvatarUrl(userAvatar);
|
|
93
|
-
return (
|
|
94
|
-
<img
|
|
95
|
-
src={avatarUrl || ""}
|
|
96
|
-
alt={isCurrentUser ? "You" : "Assistant"}
|
|
97
|
-
className="h-6 w-6 rounded-full shadow-sm transition-transform duration-200 group-hover:scale-110"
|
|
98
|
-
onError={() => setImageError(true)} // 加载失败时设置状态,不再尝试加载图片
|
|
99
|
-
/>
|
|
100
|
-
);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<motion.div
|
|
105
|
-
initial={{ opacity: 0, y: 20 }}
|
|
106
|
-
animate={{ opacity: 1, y: 0 }}
|
|
107
|
-
transition={{ duration: 0.3, ease: "easeOut" }}
|
|
108
|
-
className="group relative w-full pl-8"
|
|
109
|
-
>
|
|
110
|
-
{/* Timestamp - hidden by default, shown on hover */}
|
|
111
|
-
<div className="absolute -top-6 left-8 z-10 transform opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
|
112
|
-
<span className="rounded px-2 py-1 text-xs text-neutral-500 dark:text-neutral-400">
|
|
113
|
-
{formattedTime}
|
|
114
|
-
</span>
|
|
115
|
-
</div>
|
|
116
|
-
|
|
117
|
-
{/* Avatar - positioned to the left */}
|
|
118
|
-
<div className="absolute left-0 top-1">{renderAvatar()}</div>
|
|
119
|
-
|
|
120
|
-
{/* Message content */}
|
|
121
|
-
<div
|
|
122
|
-
className={`w-full rounded-none ${messageStyles} transition-all duration-200 hover:shadow-sm`}
|
|
123
|
-
>
|
|
124
|
-
<div className="p-3">
|
|
125
|
-
<div
|
|
126
|
-
className={`prose prose-neutral dark:prose-invert prose-sm max-w-none ${
|
|
127
|
-
isCurrentUser
|
|
128
|
-
? "text-sm text-neutral-800 dark:text-neutral-200"
|
|
129
|
-
: "text-sm text-neutral-700 dark:text-neutral-300"
|
|
130
|
-
}`}
|
|
131
|
-
>
|
|
132
|
-
{isCurrentUser ? <p>{content}</p> : <Markdown content={content} />}
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
</motion.div>
|
|
137
|
-
);
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
export default ChatBubble;
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type DragEndEvent,
|
|
3
|
-
type DragMoveEvent,
|
|
4
|
-
DndContext,
|
|
5
|
-
PointerSensor,
|
|
6
|
-
useDraggable,
|
|
7
|
-
useSensor,
|
|
8
|
-
useSensors,
|
|
9
|
-
} from "@dnd-kit/core";
|
|
10
|
-
import { PaperAirplaneIcon } from "@heroicons/react/24/solid";
|
|
11
|
-
import React, { useEffect, useRef, useState } from "react";
|
|
12
|
-
|
|
13
|
-
interface ChatInputProps {
|
|
14
|
-
onSendMessage: (message: string) => void;
|
|
15
|
-
disabled?: boolean;
|
|
16
|
-
placeholder?: string;
|
|
17
|
-
onHeightChange?: (height: number) => void; // New prop to report height changes
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Draggable resize handle component
|
|
21
|
-
const ResizeHandle = () => {
|
|
22
|
-
const { attributes, listeners, setNodeRef } = useDraggable({
|
|
23
|
-
id: "resize-handle",
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div
|
|
28
|
-
ref={setNodeRef}
|
|
29
|
-
{...listeners}
|
|
30
|
-
{...attributes}
|
|
31
|
-
className="absolute -top-2 left-0 right-0 -mx-2 h-0.5 cursor-ns-resize transition-colors hover:bg-indigo-600"
|
|
32
|
-
title="拖动调整高度"
|
|
33
|
-
/>
|
|
34
|
-
);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const ChatInput: React.FC<ChatInputProps> = ({
|
|
38
|
-
onSendMessage,
|
|
39
|
-
disabled = false,
|
|
40
|
-
placeholder = "输入消息...",
|
|
41
|
-
onHeightChange,
|
|
42
|
-
}) => {
|
|
43
|
-
const [inputMessage, setInputMessage] = useState("");
|
|
44
|
-
const [textareaHeight, setTextareaHeight] = useState(() => {
|
|
45
|
-
// Try to get saved height from localStorage
|
|
46
|
-
const savedHeight = localStorage.getItem("chatInputHeight");
|
|
47
|
-
return savedHeight ? parseInt(savedHeight, 10) : 80; // Default 80px
|
|
48
|
-
});
|
|
49
|
-
// 添加一个状态来跟踪输入法的组合状态
|
|
50
|
-
const [isComposing, setIsComposing] = useState(false);
|
|
51
|
-
|
|
52
|
-
// Keep track of initial height when drag starts
|
|
53
|
-
const initialHeightRef = useRef(textareaHeight);
|
|
54
|
-
// Keep track of total delta during drag
|
|
55
|
-
const dragDeltaRef = useRef(0);
|
|
56
|
-
|
|
57
|
-
// Setup dnd sensors with lower activation distance for more responsiveness
|
|
58
|
-
const sensors = useSensors(
|
|
59
|
-
useSensor(PointerSensor, {
|
|
60
|
-
activationConstraint: {
|
|
61
|
-
distance: 0, // Decreased from 1 to improve responsiveness
|
|
62
|
-
},
|
|
63
|
-
}),
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
// Save height to localStorage when it changes and report height changes to parent
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
localStorage.setItem("chatInputHeight", textareaHeight.toString());
|
|
69
|
-
if (onHeightChange) {
|
|
70
|
-
onHeightChange(textareaHeight);
|
|
71
|
-
}
|
|
72
|
-
}, [textareaHeight, onHeightChange]);
|
|
73
|
-
|
|
74
|
-
// Handle drag start to set initial height reference
|
|
75
|
-
const handleDragStart = () => {
|
|
76
|
-
initialHeightRef.current = textareaHeight;
|
|
77
|
-
dragDeltaRef.current = 0;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// Handle drag move to update height in real-time
|
|
81
|
-
const handleDragMove = (event: DragMoveEvent) => {
|
|
82
|
-
const { delta } = event;
|
|
83
|
-
dragDeltaRef.current = delta.y;
|
|
84
|
-
|
|
85
|
-
// Calculate new height based on initial height and total delta
|
|
86
|
-
const newHeight = Math.max(60, initialHeightRef.current - delta.y);
|
|
87
|
-
setTextareaHeight(newHeight);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Handle drag end for final cleanup
|
|
91
|
-
const handleDragEnd = (_: DragEndEvent) => {
|
|
92
|
-
// Final height adjustment already done during move, just ensure it's saved
|
|
93
|
-
const finalHeight = Math.max(
|
|
94
|
-
60,
|
|
95
|
-
initialHeightRef.current - dragDeltaRef.current,
|
|
96
|
-
);
|
|
97
|
-
setTextareaHeight(finalHeight);
|
|
98
|
-
|
|
99
|
-
// Reset the refs
|
|
100
|
-
dragDeltaRef.current = 0;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const handleSendMessage = () => {
|
|
104
|
-
if (!inputMessage.trim()) return;
|
|
105
|
-
onSendMessage(inputMessage);
|
|
106
|
-
setInputMessage("");
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
// 处理输入法组合开始事件
|
|
110
|
-
const handleCompositionStart = () => {
|
|
111
|
-
setIsComposing(true);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// 处理输入法组合结束事件
|
|
115
|
-
const handleCompositionEnd = () => {
|
|
116
|
-
setIsComposing(false);
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// Handle Enter key to send message
|
|
120
|
-
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
121
|
-
// 如果是在输入法组合状态中,不处理回车键事件
|
|
122
|
-
if (e.key === "Enter" && !e.shiftKey && !isComposing) {
|
|
123
|
-
e.preventDefault();
|
|
124
|
-
handleSendMessage();
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<div className="w-full">
|
|
130
|
-
<DndContext
|
|
131
|
-
sensors={sensors}
|
|
132
|
-
onDragStart={handleDragStart}
|
|
133
|
-
onDragMove={handleDragMove}
|
|
134
|
-
onDragEnd={handleDragEnd}
|
|
135
|
-
>
|
|
136
|
-
<div className="group relative">
|
|
137
|
-
{/* Drag handle - only visible on hover */}
|
|
138
|
-
<ResizeHandle />
|
|
139
|
-
|
|
140
|
-
<textarea
|
|
141
|
-
value={inputMessage}
|
|
142
|
-
onChange={(e) => setInputMessage(e.target.value)}
|
|
143
|
-
onKeyDown={handleKeyPress}
|
|
144
|
-
onCompositionStart={handleCompositionStart}
|
|
145
|
-
onCompositionEnd={handleCompositionEnd}
|
|
146
|
-
placeholder={placeholder}
|
|
147
|
-
className="w-full resize-none bg-white py-2 pl-3 pr-12 text-sm placeholder-neutral-500 focus:outline-none dark:bg-black dark:text-white dark:placeholder-neutral-400"
|
|
148
|
-
style={{
|
|
149
|
-
height: `${textareaHeight}px`,
|
|
150
|
-
transition: "none", // Remove transition to make height follow mouse exactly
|
|
151
|
-
}}
|
|
152
|
-
disabled={disabled}
|
|
153
|
-
/>
|
|
154
|
-
<button
|
|
155
|
-
onClick={handleSendMessage}
|
|
156
|
-
disabled={disabled || !inputMessage.trim()}
|
|
157
|
-
className="absolute bottom-4 right-2 rounded-md
|
|
158
|
-
bg-indigo-600 p-1.5 text-white transition-colors
|
|
159
|
-
hover:bg-indigo-700 disabled:bg-neutral-300 disabled:text-neutral-500 dark:disabled:bg-neutral-700 dark:disabled:text-neutral-400"
|
|
160
|
-
aria-label="Send message"
|
|
161
|
-
>
|
|
162
|
-
<PaperAirplaneIcon className="h-4 w-4" />
|
|
163
|
-
</button>
|
|
164
|
-
</div>
|
|
165
|
-
</DndContext>
|
|
166
|
-
<p className="mt-0.5 text-xs text-neutral-500">
|
|
167
|
-
按 Enter 发送,Shift+Enter 换行
|
|
168
|
-
</p>
|
|
169
|
-
</div>
|
|
170
|
-
);
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
export default ChatInput;
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { motion } from "framer-motion";
|
|
2
|
-
import React, { useState } from "react";
|
|
3
|
-
|
|
4
|
-
const EmptyChat: React.FC = () => {
|
|
5
|
-
const [isCreating] = useState(false);
|
|
6
|
-
|
|
7
|
-
// const handleStartNewChat = async () => {
|
|
8
|
-
// setIsCreating(true);
|
|
9
|
-
|
|
10
|
-
// try {
|
|
11
|
-
// // 使用service中的创建默认频道方法,它会处理UUID生成、连接创建以及视图切换
|
|
12
|
-
// await createDefaultChannel();
|
|
13
|
-
// } catch (error) {
|
|
14
|
-
// console.error('Failed to create chat:', error);
|
|
15
|
-
// } finally {
|
|
16
|
-
// setTimeout(() => setIsCreating(false), 1000);
|
|
17
|
-
// }
|
|
18
|
-
// };
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<div className="flex h-full flex-col items-center justify-center space-y-6 p-4 text-center">
|
|
22
|
-
<motion.div
|
|
23
|
-
initial={{ opacity: 0, y: -20 }}
|
|
24
|
-
animate={{ opacity: 1, y: 0 }}
|
|
25
|
-
transition={{ duration: 0.5 }}
|
|
26
|
-
className="rounded-full bg-neutral-100 p-4 dark:bg-neutral-800"
|
|
27
|
-
>
|
|
28
|
-
<svg
|
|
29
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
30
|
-
className="h-8 w-8 text-neutral-500 dark:text-neutral-400"
|
|
31
|
-
fill="none"
|
|
32
|
-
viewBox="0 0 24 24"
|
|
33
|
-
stroke="currentColor"
|
|
34
|
-
>
|
|
35
|
-
<path
|
|
36
|
-
strokeLinecap="round"
|
|
37
|
-
strokeLinejoin="round"
|
|
38
|
-
strokeWidth={1.5}
|
|
39
|
-
d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"
|
|
40
|
-
/>
|
|
41
|
-
</svg>
|
|
42
|
-
</motion.div>
|
|
43
|
-
|
|
44
|
-
<motion.div
|
|
45
|
-
initial={{ opacity: 0 }}
|
|
46
|
-
animate={{ opacity: 1 }}
|
|
47
|
-
transition={{ delay: 0.2, duration: 0.5 }}
|
|
48
|
-
>
|
|
49
|
-
<h3 className="text-lg font-medium text-neutral-900 dark:text-white">
|
|
50
|
-
开始一个新对话
|
|
51
|
-
</h3>
|
|
52
|
-
<p className="mt-2 max-w-md text-sm text-neutral-600 dark:text-neutral-300">
|
|
53
|
-
您可以从列表选择一个专业助手,或者直接开始一个自由对话。
|
|
54
|
-
</p>
|
|
55
|
-
</motion.div>
|
|
56
|
-
|
|
57
|
-
<motion.div
|
|
58
|
-
initial={{ opacity: 0, y: 20 }}
|
|
59
|
-
animate={{ opacity: 1, y: 0 }}
|
|
60
|
-
transition={{ delay: 0.4, duration: 0.5 }}
|
|
61
|
-
className="flex flex-col space-y-3 sm:flex-row sm:space-x-3 sm:space-y-0"
|
|
62
|
-
>
|
|
63
|
-
<button
|
|
64
|
-
onClick={() => {}}
|
|
65
|
-
disabled={isCreating}
|
|
66
|
-
className={`flex items-center justify-center rounded-md ${
|
|
67
|
-
isCreating
|
|
68
|
-
? "bg-indigo-400 dark:bg-indigo-700"
|
|
69
|
-
: "bg-indigo-600 hover:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-600"
|
|
70
|
-
} px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2`}
|
|
71
|
-
>
|
|
72
|
-
{isCreating ? (
|
|
73
|
-
<>
|
|
74
|
-
<svg className="mr-2 h-4 w-4 animate-spin" viewBox="0 0 24 24">
|
|
75
|
-
<circle
|
|
76
|
-
className="opacity-25"
|
|
77
|
-
cx="12"
|
|
78
|
-
cy="12"
|
|
79
|
-
r="10"
|
|
80
|
-
stroke="currentColor"
|
|
81
|
-
strokeWidth="4"
|
|
82
|
-
fill="none"
|
|
83
|
-
/>
|
|
84
|
-
<path
|
|
85
|
-
className="opacity-75"
|
|
86
|
-
fill="currentColor"
|
|
87
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
88
|
-
/>
|
|
89
|
-
</svg>
|
|
90
|
-
创建中...
|
|
91
|
-
</>
|
|
92
|
-
) : (
|
|
93
|
-
<>
|
|
94
|
-
<svg
|
|
95
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
96
|
-
className="mr-2 h-4 w-4"
|
|
97
|
-
fill="none"
|
|
98
|
-
viewBox="0 0 24 24"
|
|
99
|
-
stroke="currentColor"
|
|
100
|
-
>
|
|
101
|
-
<path
|
|
102
|
-
strokeLinecap="round"
|
|
103
|
-
strokeLinejoin="round"
|
|
104
|
-
strokeWidth={2}
|
|
105
|
-
d="M12 4v16m8-8H4"
|
|
106
|
-
/>
|
|
107
|
-
</svg>
|
|
108
|
-
开始新对话
|
|
109
|
-
</>
|
|
110
|
-
)}
|
|
111
|
-
</button>
|
|
112
|
-
</motion.div>
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
export default EmptyChat;
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { motion } from "framer-motion";
|
|
2
|
-
import React from "react";
|
|
3
|
-
|
|
4
|
-
export interface ChatData {
|
|
5
|
-
id: string;
|
|
6
|
-
title: string;
|
|
7
|
-
assistant?: string; // 助手ID
|
|
8
|
-
assistant_name?: string; // 助手名称
|
|
9
|
-
messages_count: number;
|
|
10
|
-
last_message?: {
|
|
11
|
-
content: string;
|
|
12
|
-
timestamp: string;
|
|
13
|
-
};
|
|
14
|
-
created_at: string;
|
|
15
|
-
updated_at: string;
|
|
16
|
-
is_pinned: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface Assistant {
|
|
20
|
-
id: string;
|
|
21
|
-
key?: string; // 助手的唯一标识符
|
|
22
|
-
title: string;
|
|
23
|
-
description: string;
|
|
24
|
-
iconType: string;
|
|
25
|
-
iconColor: string;
|
|
26
|
-
category: string;
|
|
27
|
-
chats?: ChatData[]; // 与该助手的历史对话列表
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface WelcomeMessageProps {
|
|
31
|
-
assistant?: Assistant | null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const WelcomeMessage: React.FC<WelcomeMessageProps> = ({ assistant }) => {
|
|
35
|
-
const iconColor = assistant?.iconColor || "indigo";
|
|
36
|
-
|
|
37
|
-
// Fix dynamic class name issue by mapping to pre-defined classes
|
|
38
|
-
const iconColorMap: Record<string, string> = {
|
|
39
|
-
blue: "bg-blue-100 dark:bg-blue-900/30",
|
|
40
|
-
green: "bg-green-100 dark:bg-green-900/30",
|
|
41
|
-
purple: "bg-purple-100 dark:bg-purple-900/30",
|
|
42
|
-
amber: "bg-amber-100 dark:bg-amber-900/30",
|
|
43
|
-
red: "bg-red-100 dark:bg-red-900/30",
|
|
44
|
-
indigo: "bg-indigo-100 dark:bg-indigo-900/30",
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const iconTextColorMap: Record<string, string> = {
|
|
48
|
-
blue: "text-blue-600 dark:text-blue-400",
|
|
49
|
-
green: "text-green-600 dark:text-green-400",
|
|
50
|
-
purple: "text-purple-600 dark:text-purple-400",
|
|
51
|
-
amber: "text-amber-600 dark:text-amber-400",
|
|
52
|
-
red: "text-red-600 dark:text-red-400",
|
|
53
|
-
indigo: "text-indigo-600 dark:text-indigo-400",
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const bgColorClass = iconColorMap[iconColor] || iconColorMap.indigo;
|
|
57
|
-
const textColorClass = iconTextColorMap[iconColor] || iconTextColorMap.indigo;
|
|
58
|
-
|
|
59
|
-
// Determine title and message based on whether an assistant is selected
|
|
60
|
-
const title = assistant ? `欢迎使用 ${assistant.title}` : "欢迎使用自由对话";
|
|
61
|
-
const description =
|
|
62
|
-
assistant?.description ||
|
|
63
|
-
"您现在可以自由提问任何问题。无需选择特定助手,系统将根据您的问题提供合适的回复。";
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<motion.div
|
|
67
|
-
initial={{ opacity: 0 }}
|
|
68
|
-
animate={{ opacity: 1 }}
|
|
69
|
-
transition={{ duration: 0.6 }}
|
|
70
|
-
className="flex flex-col items-center justify-center space-y-4 p-6 text-center"
|
|
71
|
-
>
|
|
72
|
-
<div className={`rounded-full ${bgColorClass} p-5`}>
|
|
73
|
-
<svg
|
|
74
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
75
|
-
className={`h-10 w-10 ${textColorClass}`}
|
|
76
|
-
fill="none"
|
|
77
|
-
viewBox="0 0 24 24"
|
|
78
|
-
stroke="currentColor"
|
|
79
|
-
>
|
|
80
|
-
<path
|
|
81
|
-
strokeLinecap="round"
|
|
82
|
-
strokeLinejoin="round"
|
|
83
|
-
strokeWidth={1.5}
|
|
84
|
-
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
|
|
85
|
-
/>
|
|
86
|
-
</svg>
|
|
87
|
-
</div>
|
|
88
|
-
<motion.div
|
|
89
|
-
initial={{ y: 20, opacity: 0 }}
|
|
90
|
-
animate={{ y: 0, opacity: 1 }}
|
|
91
|
-
transition={{ delay: 0.2, duration: 0.5 }}
|
|
92
|
-
>
|
|
93
|
-
<h3 className="text-lg font-medium text-neutral-900 dark:text-white">
|
|
94
|
-
{title}
|
|
95
|
-
</h3>
|
|
96
|
-
<p className="mt-2 max-w-md text-sm leading-relaxed text-neutral-600 dark:text-neutral-300">
|
|
97
|
-
{description}
|
|
98
|
-
</p>
|
|
99
|
-
</motion.div>
|
|
100
|
-
</motion.div>
|
|
101
|
-
);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export default WelcomeMessage;
|
package/src/configs/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_BACKEND_URL = "http://localhost:8000";
|
|
File without changes
|
package/src/hooks/useTheme.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
2
|
-
|
|
3
|
-
type Theme = "light" | "dark" | "system";
|
|
4
|
-
|
|
5
|
-
const useTheme = () => {
|
|
6
|
-
const [theme, setTheme] = useState<Theme>("system");
|
|
7
|
-
const [isClient, setIsClient] = useState(false);
|
|
8
|
-
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
setIsClient(true);
|
|
11
|
-
}, []);
|
|
12
|
-
|
|
13
|
-
const applyTheme = useCallback(
|
|
14
|
-
(selectedTheme: Theme) => {
|
|
15
|
-
if (!isClient) return;
|
|
16
|
-
|
|
17
|
-
const root = window.document.documentElement;
|
|
18
|
-
const isDark =
|
|
19
|
-
selectedTheme === "dark" ||
|
|
20
|
-
(selectedTheme === "system" &&
|
|
21
|
-
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
22
|
-
|
|
23
|
-
// 注入CSS以禁用过渡
|
|
24
|
-
const style = document.createElement("style");
|
|
25
|
-
style.innerHTML =
|
|
26
|
-
"*, *::before, *::after { transition: none !important; }";
|
|
27
|
-
document.head.appendChild(style);
|
|
28
|
-
|
|
29
|
-
root.classList.toggle("dark", isDark);
|
|
30
|
-
localStorage.setItem("theme", selectedTheme);
|
|
31
|
-
|
|
32
|
-
// 在短时间后移除样式,以恢复过渡效果
|
|
33
|
-
// 这确保了仅在主题切换的瞬间禁用过渡
|
|
34
|
-
setTimeout(() => {
|
|
35
|
-
if (document.head.contains(style)) {
|
|
36
|
-
document.head.removeChild(style);
|
|
37
|
-
}
|
|
38
|
-
}, 50);
|
|
39
|
-
},
|
|
40
|
-
[isClient],
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (!isClient) return;
|
|
45
|
-
|
|
46
|
-
const savedTheme = localStorage.getItem("theme") as Theme | null;
|
|
47
|
-
const initialTheme = savedTheme || "system";
|
|
48
|
-
setTheme(initialTheme);
|
|
49
|
-
applyTheme(initialTheme);
|
|
50
|
-
}, [isClient, applyTheme]);
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (!isClient) return;
|
|
54
|
-
|
|
55
|
-
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
56
|
-
const handleChange = () => {
|
|
57
|
-
if (theme === "system") {
|
|
58
|
-
applyTheme("system");
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
mediaQuery.addEventListener("change", handleChange);
|
|
63
|
-
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
64
|
-
}, [isClient, theme, applyTheme]);
|
|
65
|
-
|
|
66
|
-
const cycleTheme = () => {
|
|
67
|
-
if (!isClient) return;
|
|
68
|
-
|
|
69
|
-
const themes: Theme[] = ["light", "dark", "system"];
|
|
70
|
-
const currentIndex = themes.indexOf(theme);
|
|
71
|
-
const nextTheme = themes[(currentIndex + 1) % themes.length];
|
|
72
|
-
setTheme(nextTheme);
|
|
73
|
-
applyTheme(nextTheme);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
return { theme, cycleTheme };
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export default useTheme;
|
package/src/index.ts
DELETED