@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.
@@ -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;
@@ -1 +0,0 @@
1
- export const DEFAULT_BACKEND_URL = "http://localhost:8000";
File without changes
@@ -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
@@ -1,6 +0,0 @@
1
- "use client";
2
-
3
- export { Xyzen, type XyzenProps } from "@/app/App";
4
- export { default as useTheme } from "@/hooks/useTheme";
5
- export { default as xyzenService } from "@/service/xyzenService";
6
- export { useXyzen } from "@/store/xyzenStore";