@sciol/xyzen 0.1.3 → 0.1.4
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/dist/xyzen.es.js +138 -127
- package/dist/xyzen.umd.js +1 -1
- package/package.json +3 -2
- package/src/app/App.tsx +221 -0
- package/src/assets/react.svg +1 -0
- package/src/components/layouts/XyzenChat.tsx +212 -0
- package/src/components/layouts/XyzenHistory.tsx +117 -0
- package/src/components/layouts/XyzenNodes.tsx +7 -0
- package/src/components/layouts/components/ChatBubble.tsx +140 -0
- package/src/components/layouts/components/ChatInput.tsx +173 -0
- package/src/components/layouts/components/EmptyChat.tsx +117 -0
- package/src/components/layouts/components/WelcomeMessage.tsx +104 -0
- package/src/configs/index.ts +1 -0
- package/src/context/XyzenProvider.tsx +0 -0
- package/src/hooks/useTheme.ts +79 -0
- package/src/index.ts +6 -0
- package/src/lib/Markdown.tsx +98 -0
- package/src/lib/formatDate.ts +32 -0
- package/src/main.tsx +9 -0
- package/src/service/xyzenService.ts +90 -0
- package/src/store/xyzenStore.ts +326 -0
- package/src/types/xyzen.d.ts +0 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import xyzenService from "@/service/xyzenService";
|
|
2
|
+
import { create } from "zustand";
|
|
3
|
+
import { persist } from "zustand/middleware";
|
|
4
|
+
import { immer } from "zustand/middleware/immer";
|
|
5
|
+
|
|
6
|
+
// 定义应用中的核心类型
|
|
7
|
+
export interface Message {
|
|
8
|
+
id: string;
|
|
9
|
+
content: string;
|
|
10
|
+
sender: "user" | "assistant" | "system";
|
|
11
|
+
timestamp: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChatChannel {
|
|
15
|
+
id: string; // This will now be the Topic ID
|
|
16
|
+
sessionId: string; // The session this topic belongs to
|
|
17
|
+
title: string;
|
|
18
|
+
messages: Message[];
|
|
19
|
+
assistantId?: string;
|
|
20
|
+
connected: boolean;
|
|
21
|
+
error: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ChatHistoryItem {
|
|
25
|
+
id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
updatedAt: string;
|
|
28
|
+
assistantTitle: string;
|
|
29
|
+
lastMessage?: string;
|
|
30
|
+
isPinned: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Assistant {
|
|
34
|
+
id: string;
|
|
35
|
+
title: string;
|
|
36
|
+
description: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface User {
|
|
40
|
+
username: string;
|
|
41
|
+
avatar: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type Theme = "light" | "dark" | "system";
|
|
45
|
+
|
|
46
|
+
// Add types for API response
|
|
47
|
+
interface TopicResponse {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
updated_at: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface SessionResponse {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
username: string;
|
|
57
|
+
topics: TopicResponse[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface XyzenState {
|
|
61
|
+
backendUrl: string;
|
|
62
|
+
isXyzenOpen: boolean;
|
|
63
|
+
panelWidth: number;
|
|
64
|
+
activeChatChannel: string | null;
|
|
65
|
+
user: User | null;
|
|
66
|
+
activeTabIndex: number;
|
|
67
|
+
theme: Theme;
|
|
68
|
+
|
|
69
|
+
chatHistory: ChatHistoryItem[];
|
|
70
|
+
chatHistoryLoading: boolean;
|
|
71
|
+
channels: Record<string, ChatChannel>;
|
|
72
|
+
assistants: Assistant[];
|
|
73
|
+
|
|
74
|
+
toggleXyzen: () => void;
|
|
75
|
+
openXyzen: () => void;
|
|
76
|
+
closeXyzen: () => void;
|
|
77
|
+
setPanelWidth: (width: number) => void;
|
|
78
|
+
setActiveChatChannel: (channelUUID: string | null) => void;
|
|
79
|
+
setTabIndex: (index: number) => void;
|
|
80
|
+
setTheme: (theme: Theme) => void;
|
|
81
|
+
setBackendUrl: (url: string) => void;
|
|
82
|
+
|
|
83
|
+
fetchChatHistory: () => Promise<void>;
|
|
84
|
+
togglePinChat: (chatId: string) => void;
|
|
85
|
+
connectToChannel: (sessionId: string, topicId: string) => void;
|
|
86
|
+
disconnectFromChannel: () => void;
|
|
87
|
+
sendMessage: (message: string) => void;
|
|
88
|
+
createDefaultChannel: () => Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Mock Data ---
|
|
92
|
+
const mockUser: User = {
|
|
93
|
+
username: "Harvey",
|
|
94
|
+
avatar: `https://i.pravatar.cc/40?u=harvey`,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const mockAssistants: Assistant[] = [
|
|
98
|
+
{
|
|
99
|
+
id: "asst_1",
|
|
100
|
+
title: "通用助理",
|
|
101
|
+
description: "我可以回答各种问题。",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "asst_2",
|
|
105
|
+
title: "代码助手",
|
|
106
|
+
description: "我可以帮助你处理代码相关的任务。",
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
// --- End Mock Data ---
|
|
111
|
+
|
|
112
|
+
export const useXyzen = create<XyzenState>()(
|
|
113
|
+
persist(
|
|
114
|
+
immer((set, get) => ({
|
|
115
|
+
// --- State ---
|
|
116
|
+
backendUrl: "",
|
|
117
|
+
isXyzenOpen: false,
|
|
118
|
+
panelWidth: 380,
|
|
119
|
+
activeChatChannel: null,
|
|
120
|
+
user: mockUser,
|
|
121
|
+
activeTabIndex: 0,
|
|
122
|
+
theme: "system",
|
|
123
|
+
chatHistory: [],
|
|
124
|
+
chatHistoryLoading: true,
|
|
125
|
+
channels: {},
|
|
126
|
+
assistants: mockAssistants,
|
|
127
|
+
|
|
128
|
+
// --- Actions ---
|
|
129
|
+
toggleXyzen: () => set((state) => ({ isXyzenOpen: !state.isXyzenOpen })),
|
|
130
|
+
openXyzen: () => set({ isXyzenOpen: true }),
|
|
131
|
+
closeXyzen: () => set({ isXyzenOpen: false }),
|
|
132
|
+
setPanelWidth: (width) => set({ panelWidth: width }),
|
|
133
|
+
setActiveChatChannel: (channelId) =>
|
|
134
|
+
set({ activeChatChannel: channelId }),
|
|
135
|
+
setTabIndex: (index) => set({ activeTabIndex: index }),
|
|
136
|
+
setTheme: (theme) => set({ theme }),
|
|
137
|
+
setBackendUrl: (url) => {
|
|
138
|
+
set({ backendUrl: url });
|
|
139
|
+
xyzenService.setBackendUrl(url);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// --- Async Actions ---
|
|
143
|
+
fetchChatHistory: async () => {
|
|
144
|
+
set({ chatHistoryLoading: true });
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(`${get().backendUrl}/api/v1/sessions/`);
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw new Error("Failed to fetch chat history");
|
|
149
|
+
}
|
|
150
|
+
const history: SessionResponse[] = await response.json();
|
|
151
|
+
|
|
152
|
+
// Transform the fetched data into the format expected by the store
|
|
153
|
+
const channels: Record<string, ChatChannel> = {};
|
|
154
|
+
const chatHistory: ChatHistoryItem[] = history.flatMap(
|
|
155
|
+
(session: SessionResponse) =>
|
|
156
|
+
session.topics.map((topic: TopicResponse) => {
|
|
157
|
+
channels[topic.id] = {
|
|
158
|
+
id: topic.id,
|
|
159
|
+
sessionId: session.id,
|
|
160
|
+
title: topic.name,
|
|
161
|
+
messages: [], // Messages will be fetched on demand or via WebSocket
|
|
162
|
+
connected: false,
|
|
163
|
+
error: null,
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
id: topic.id,
|
|
167
|
+
title: topic.name,
|
|
168
|
+
updatedAt: topic.updated_at,
|
|
169
|
+
assistantTitle: "通用助理", // Placeholder
|
|
170
|
+
lastMessage: "", // Placeholder
|
|
171
|
+
isPinned: false, // Placeholder
|
|
172
|
+
};
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
set({
|
|
177
|
+
chatHistory,
|
|
178
|
+
channels,
|
|
179
|
+
chatHistoryLoading: false,
|
|
180
|
+
activeChatChannel:
|
|
181
|
+
chatHistory.length > 0 ? chatHistory[0].id : null,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (chatHistory.length > 0) {
|
|
185
|
+
const activeChannel = channels[chatHistory[0].id];
|
|
186
|
+
get().connectToChannel(activeChannel.sessionId, activeChannel.id);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error("Failed to fetch chat history:", error);
|
|
190
|
+
set({ chatHistoryLoading: false });
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
togglePinChat: (chatId: string) => {
|
|
195
|
+
set((state) => {
|
|
196
|
+
const chat = state.chatHistory.find(
|
|
197
|
+
(c: ChatHistoryItem) => c.id === chatId,
|
|
198
|
+
);
|
|
199
|
+
if (chat) {
|
|
200
|
+
chat.isPinned = !chat.isPinned;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
connectToChannel: (sessionId, topicId) => {
|
|
206
|
+
xyzenService.connect(
|
|
207
|
+
sessionId,
|
|
208
|
+
topicId,
|
|
209
|
+
(incomingMessage) => {
|
|
210
|
+
// onMessage callback
|
|
211
|
+
set((state) => {
|
|
212
|
+
const channel = state.channels[topicId];
|
|
213
|
+
if (channel) {
|
|
214
|
+
const newMsg: Message = {
|
|
215
|
+
id: `msg-${Date.now()}`, // Or use an ID from the server
|
|
216
|
+
sender: incomingMessage.sender,
|
|
217
|
+
content: incomingMessage.content,
|
|
218
|
+
timestamp: new Date().toISOString(),
|
|
219
|
+
};
|
|
220
|
+
channel.messages.push(newMsg);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
(status) => {
|
|
225
|
+
// onStatusChange callback
|
|
226
|
+
set((state) => {
|
|
227
|
+
const channel = state.channels[topicId];
|
|
228
|
+
if (channel) {
|
|
229
|
+
channel.connected = status.connected;
|
|
230
|
+
channel.error = status.error;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
disconnectFromChannel: () => {
|
|
238
|
+
xyzenService.disconnect();
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
sendMessage: (message) => {
|
|
242
|
+
const channelId = get().activeChatChannel;
|
|
243
|
+
if (!channelId) return;
|
|
244
|
+
|
|
245
|
+
const userMessage: Message = {
|
|
246
|
+
id: `msg-${Date.now()}`,
|
|
247
|
+
sender: "user",
|
|
248
|
+
content: message,
|
|
249
|
+
timestamp: new Date().toISOString(),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
set((state) => {
|
|
253
|
+
state.channels[channelId]?.messages.push(userMessage);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
xyzenService.sendMessage(message);
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
createDefaultChannel: async () => {
|
|
260
|
+
try {
|
|
261
|
+
// Step 1: Call the backend to create a new session and a default topic
|
|
262
|
+
const response = await fetch(`${get().backendUrl}/api/v1/sessions/`, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: {
|
|
265
|
+
"Content-Type": "application/json",
|
|
266
|
+
},
|
|
267
|
+
body: JSON.stringify({
|
|
268
|
+
name: "New Session",
|
|
269
|
+
username: get().user?.username || "default_user", // Get username from state
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
throw new Error("Failed to create a new session on the backend.");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const newSession = await response.json();
|
|
278
|
+
const newTopic = newSession.topics[0]; // Assuming the first topic is the default one
|
|
279
|
+
|
|
280
|
+
if (!newTopic) {
|
|
281
|
+
throw new Error("Backend did not return a default topic.");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const newChannel: ChatChannel = {
|
|
285
|
+
id: newTopic.id,
|
|
286
|
+
sessionId: newSession.id,
|
|
287
|
+
title: newTopic.name,
|
|
288
|
+
messages: [],
|
|
289
|
+
connected: false,
|
|
290
|
+
error: null,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const newHistoryItem: ChatHistoryItem = {
|
|
294
|
+
id: newTopic.id,
|
|
295
|
+
title: newTopic.name,
|
|
296
|
+
updatedAt: new Date().toISOString(),
|
|
297
|
+
assistantTitle: "通用助理", // Or derive from session/topic
|
|
298
|
+
lastMessage: "",
|
|
299
|
+
isPinned: false,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
set((state) => {
|
|
303
|
+
state.channels[newTopic.id] = newChannel;
|
|
304
|
+
state.chatHistory.unshift(newHistoryItem);
|
|
305
|
+
state.activeChatChannel = newTopic.id;
|
|
306
|
+
state.activeTabIndex = 0; // Switch to the chat tab
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Step 2: Connect to the WebSocket with the real IDs
|
|
310
|
+
get().connectToChannel(newSession.id, newTopic.id);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error("Error creating default channel:", error);
|
|
313
|
+
// Optionally, update the state to show an error to the user
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
})),
|
|
317
|
+
{
|
|
318
|
+
name: "xyzen-storage", // local storage key
|
|
319
|
+
partialize: (state) => ({
|
|
320
|
+
panelWidth: state.panelWidth,
|
|
321
|
+
isXyzenOpen: state.isXyzenOpen,
|
|
322
|
+
theme: state.theme,
|
|
323
|
+
}), // only persist panelWidth and isXyzenOpen
|
|
324
|
+
},
|
|
325
|
+
),
|
|
326
|
+
);
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|