@sciol/xyzen 0.1.4 → 0.1.5

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,98 +0,0 @@
1
- import React, { useState } from "react";
2
- import ReactMarkdown from "react-markdown";
3
- import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
4
- import rehypeHighlight from "rehype-highlight";
5
- import rehypeKatex from "rehype-katex";
6
- import rehypeRaw from "rehype-raw";
7
- import remarkGfm from "remark-gfm";
8
- import remarkMath from "remark-math";
9
-
10
- import "katex/dist/katex.css";
11
- import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
12
-
13
- interface MarkdownProps {
14
- content: string;
15
- }
16
-
17
- const Markdown: React.FC<MarkdownProps> = function Markdown(props) {
18
- const { content = "" } = props;
19
- const [copiedCode, setCopiedCode] = useState<string | null>(null);
20
-
21
- const copyToClipboard = (code: string) => {
22
- navigator.clipboard.writeText(code).then(() => {
23
- setCopiedCode(code);
24
- setTimeout(() => {
25
- setCopiedCode(null);
26
- }, 2000);
27
- });
28
- };
29
-
30
- const MarkdownComponents = {
31
- code({
32
- inline,
33
- className,
34
- children,
35
- ...props
36
- }: React.ComponentPropsWithoutRef<"code"> & { inline?: boolean }) {
37
- const match = /language-(\w+)/.exec(className || "");
38
- const code = String(children).replace(/\n$/, "");
39
-
40
- return !inline && match ? (
41
- <div style={{ position: "relative" }}>
42
- <button
43
- onClick={() => copyToClipboard(code)}
44
- style={{
45
- position: "absolute",
46
- top: "5px",
47
- right: "5px",
48
- padding: "4px 8px",
49
- backgroundColor: "#282c34",
50
- color: "white",
51
- border: "1px solid #444",
52
- borderRadius: "4px",
53
- cursor: "pointer",
54
- fontSize: "12px",
55
- zIndex: 2,
56
- opacity: 0.8,
57
- transition: "opacity 0.2s",
58
- }}
59
- onMouseEnter={(e) => {
60
- e.currentTarget.style.opacity = "1";
61
- }}
62
- onMouseLeave={(e) => {
63
- e.currentTarget.style.opacity = "0.8";
64
- }}
65
- >
66
- {copiedCode === code ? (
67
- <span className=" text-green-400">Copied!</span>
68
- ) : (
69
- "Copy"
70
- )}
71
- </button>
72
- <SyntaxHighlighter style={oneDark} language={match[1]} PreTag="div">
73
- {code}
74
- </SyntaxHighlighter>
75
- </div>
76
- ) : (
77
- <code className={className} {...props}>
78
- {children}
79
- </code>
80
- );
81
- },
82
- };
83
- return (
84
- <ReactMarkdown
85
- components={MarkdownComponents}
86
- remarkPlugins={[remarkMath, remarkGfm]}
87
- rehypePlugins={[
88
- rehypeKatex,
89
- rehypeRaw,
90
- [rehypeHighlight, { ignoreMissing: true }],
91
- ]}
92
- >
93
- {content}
94
- </ReactMarkdown>
95
- );
96
- };
97
-
98
- export default Markdown;
@@ -1,32 +0,0 @@
1
- import { format, formatDistanceToNow, isToday, isYesterday } from "date-fns";
2
- import { zhCN } from "date-fns/locale";
3
-
4
- export function formatTime(dateString: string): string {
5
- const date = new Date(dateString);
6
- const now = new Date();
7
-
8
- // 使用 formatDistanceToNow 显示相对时间,并添加中文支持
9
- // addSuffix: true 会添加 "前" 或 "后"
10
- const relativeTime = formatDistanceToNow(date, {
11
- addSuffix: true,
12
- locale: zhCN,
13
- });
14
-
15
- // 如果是一天内,直接返回相对时间,例如 "约5小时前"
16
- if (now.getTime() - date.getTime() < 24 * 60 * 60 * 1000) {
17
- return relativeTime;
18
- }
19
-
20
- // 如果是昨天
21
- if (isYesterday(date)) {
22
- return `昨天 ${format(date, "HH:mm")}`;
23
- }
24
-
25
- // 如果是今天(理论上被前一个if覆盖,但作为保险)
26
- if (isToday(date)) {
27
- return format(date, "HH:mm");
28
- }
29
-
30
- // 如果是更早的时间,显示具体日期
31
- return format(date, "yyyy-MM-dd");
32
- }
package/src/main.tsx DELETED
@@ -1,9 +0,0 @@
1
- import { Xyzen } from "@/app/App";
2
- import { StrictMode } from "react";
3
- import { createRoot } from "react-dom/client";
4
-
5
- createRoot(document.getElementById("root")!).render(
6
- <StrictMode>
7
- <Xyzen />
8
- </StrictMode>,
9
- );
@@ -1,90 +0,0 @@
1
- import { type Message } from "@/store/xyzenStore";
2
-
3
- interface StatusChangePayload {
4
- connected: boolean;
5
- error: string | null;
6
- }
7
-
8
- type ServiceCallback<T> = (payload: T) => void;
9
-
10
- class XyzenService {
11
- private ws: WebSocket | null = null;
12
- private onMessageCallback: ServiceCallback<Message> | null = null;
13
- private onStatusChangeCallback: ServiceCallback<StatusChangePayload> | null =
14
- null;
15
- private backendUrl = "";
16
-
17
- public setBackendUrl(url: string) {
18
- this.backendUrl = url;
19
- }
20
-
21
- public connect(
22
- sessionId: string,
23
- topicId: string,
24
- onMessage: ServiceCallback<Message>,
25
- onStatusChange: ServiceCallback<StatusChangePayload>,
26
- ) {
27
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
28
- console.log("WebSocket is already connected.");
29
- return;
30
- }
31
-
32
- this.onMessageCallback = onMessage;
33
- this.onStatusChangeCallback = onStatusChange;
34
-
35
- const wsUrl = `${this.backendUrl.replace(
36
- /^http(s?):\/\//,
37
- "ws$1://",
38
- )}/ws/v1/chat/sessions/${sessionId}/topics/${topicId}`;
39
- this.ws = new WebSocket(wsUrl);
40
-
41
- this.ws.onopen = () => {
42
- console.log("XyzenService: WebSocket connected");
43
- this.onStatusChangeCallback?.({ connected: true, error: null });
44
- };
45
-
46
- this.ws.onmessage = (event) => {
47
- try {
48
- const messageData = JSON.parse(event.data);
49
- this.onMessageCallback?.(messageData);
50
- } catch (error) {
51
- console.error("XyzenService: Failed to parse message data:", error);
52
- }
53
- };
54
-
55
- this.ws.onclose = () => {
56
- console.log("XyzenService: WebSocket disconnected");
57
- this.onStatusChangeCallback?.({
58
- connected: false,
59
- error: "Connection closed.",
60
- });
61
- };
62
-
63
- this.ws.onerror = (error) => {
64
- console.error("XyzenService: WebSocket error:", error);
65
- this.onStatusChangeCallback?.({
66
- connected: false,
67
- error: "A connection error occurred.",
68
- });
69
- };
70
- }
71
-
72
- public sendMessage(message: string) {
73
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
74
- this.ws.send(JSON.stringify({ message }));
75
- } else {
76
- console.error("XyzenService: WebSocket is not connected.");
77
- }
78
- }
79
-
80
- public disconnect() {
81
- if (this.ws) {
82
- this.ws.close();
83
- this.ws = null;
84
- }
85
- }
86
- }
87
-
88
- // Export a singleton instance of the service
89
- const xyzenService = new XyzenService();
90
- export default xyzenService;
@@ -1,326 +0,0 @@
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
package/src/vite-env.d.ts DELETED
@@ -1 +0,0 @@
1
- /// <reference types="vite/client" />