@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.
@@ -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" />