moelove commited on
Commit
fe9f896
·
unverified ·
1 Parent(s): c9d7e50

add streaming support (#1)

Browse files

Signed-off-by: Jintao Zhang <zhangjintao9020@gmail.com>

Files changed (3) hide show
  1. server/index.js +39 -5
  2. src/components/ChatWindow.jsx +51 -11
  3. vite.config.js +2 -2
server/index.js CHANGED
@@ -15,9 +15,18 @@ app.use(express.json());
15
 
16
  app.post('/api/chat', async (req, res) => {
17
  const { messages, apiEndpoint, apiKey, model } = req.body;
 
 
 
 
 
 
18
 
19
  try {
20
- const response = await fetch(`${apiEndpoint}/v1/chat/completions`, {
 
 
 
21
  method: 'POST',
22
  headers: {
23
  'Content-Type': 'application/json',
@@ -25,17 +34,42 @@ app.post('/api/chat', async (req, res) => {
25
  },
26
  body: JSON.stringify({
27
  model: model,
28
- messages: messages
 
29
  })
30
  });
31
 
 
 
 
32
  if (!response.ok) {
33
  const errorData = await response.text();
 
 
 
 
34
  throw new Error(`API error: ${response.status} - ${errorData}`);
35
  }
36
 
37
- const data = await response.json();
38
- res.json(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  } catch (error) {
40
  console.error('Error:', error);
41
  res.status(500).json({ error: error.message });
@@ -54,4 +88,4 @@ if (process.env.NODE_ENV === 'production') {
54
 
55
  app.listen(port, () => {
56
  console.log(`Server running at http://localhost:${port}`);
57
- });
 
15
 
16
  app.post('/api/chat', async (req, res) => {
17
  const { messages, apiEndpoint, apiKey, model } = req.body;
18
+
19
+ console.log('Received chat request with:', {
20
+ apiEndpoint,
21
+ model,
22
+ messageCount: messages.length
23
+ });
24
 
25
  try {
26
+ const apiUrl = `${apiEndpoint}/v1/chat/completions`;
27
+ console.log('Calling API endpoint:', apiUrl);
28
+
29
+ const response = await fetch(apiUrl, {
30
  method: 'POST',
31
  headers: {
32
  'Content-Type': 'application/json',
 
34
  },
35
  body: JSON.stringify({
36
  model: model,
37
+ messages: messages,
38
+ stream: true
39
  })
40
  });
41
 
42
+ console.log('API response status:', response.status);
43
+ console.log('API response headers:', Object.fromEntries(response.headers.entries()));
44
+
45
  if (!response.ok) {
46
  const errorData = await response.text();
47
+ console.error('API error:', {
48
+ status: response.status,
49
+ error: errorData
50
+ });
51
  throw new Error(`API error: ${response.status} - ${errorData}`);
52
  }
53
 
54
+ // Set headers for streaming
55
+ res.setHeader('Content-Type', 'text/event-stream');
56
+ res.setHeader('Cache-Control', 'no-cache');
57
+ res.setHeader('Connection', 'keep-alive');
58
+
59
+ // Set SSE headers
60
+ res.writeHead(200, {
61
+ 'Content-Type': 'text/event-stream',
62
+ 'Cache-Control': 'no-cache',
63
+ 'Connection': 'keep-alive'
64
+ });
65
+
66
+ // Pipe the API response to the client
67
+ response.body.pipe(res).on('error', (err) => {
68
+ console.error('Stream error:', err);
69
+ res.status(500).end();
70
+ }).on('end', () => {
71
+ console.log('Stream completed successfully');
72
+ });
73
  } catch (error) {
74
  console.error('Error:', error);
75
  res.status(500).json({ error: error.message });
 
88
 
89
  app.listen(port, () => {
90
  console.log(`Server running at http://localhost:${port}`);
91
+ });
src/components/ChatWindow.jsx CHANGED
@@ -4,6 +4,8 @@ function ChatWindow({ chat, settings, onUpdateChat }) {
4
  const [input, setInput] = useState('');
5
  const [isLoading, setIsLoading] = useState(false);
6
  const [collapsedThinks, setCollapsedThinks] = useState(new Set());
 
 
7
 
8
  // 添加解析消息的函数
9
  const parseMessage = (content) => {
@@ -29,10 +31,15 @@ function ChatWindow({ chat, settings, onUpdateChat }) {
29
  const handleSendMessage = async () => {
30
  if (!input.trim()) return;
31
 
 
 
 
 
 
32
  const newMessage = {
33
  role: 'user',
34
  content: input,
35
- timestamp: Date.now() // 确保添加时间戳
36
  };
37
 
38
  const updatedChat = {
@@ -41,8 +48,12 @@ function ChatWindow({ chat, settings, onUpdateChat }) {
41
  };
42
  onUpdateChat(updatedChat);
43
  setInput('');
 
 
 
 
 
44
 
45
- // 发送API请求
46
  setIsLoading(true);
47
  try {
48
  const response = await fetch('/api/chat', {
@@ -59,6 +70,7 @@ function ChatWindow({ chat, settings, onUpdateChat }) {
59
  model: settings.model,
60
  apiEndpoint: settings.apiEndpoint,
61
  }),
 
62
  });
63
 
64
  if (!response.ok) {
@@ -66,25 +78,50 @@ function ChatWindow({ chat, settings, onUpdateChat }) {
66
  throw new Error(`HTTP error! status: ${response.status}, message: ${errorData}`);
67
  }
68
 
69
- const data = await response.json();
70
-
71
- // 添加AI回复
72
- const aiMessage = {
73
  role: 'assistant',
74
- content: data.choices[0].message.content,
75
  timestamp: Date.now()
76
  };
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  const finalChat = {
79
  ...updatedChat,
80
  messages: [...updatedChat.messages, aiMessage]
81
  };
82
  onUpdateChat(finalChat);
83
  } catch (error) {
84
- console.error('Failed to send message:', error);
85
- alert(`Failed to send message: ${error.message}`);
 
 
86
  } finally {
87
  setIsLoading(false);
 
 
88
  }
89
  };
90
 
@@ -129,7 +166,10 @@ function ChatWindow({ chat, settings, onUpdateChat }) {
129
  {isLoading && (
130
  <div className="message assistant">
131
  <div className="message-content">
132
- <div className="loading-indicator">Thinking...</div>
 
 
 
133
  </div>
134
  </div>
135
  )}
@@ -153,4 +193,4 @@ function ChatWindow({ chat, settings, onUpdateChat }) {
153
  );
154
  }
155
 
156
- export default ChatWindow;
 
4
  const [input, setInput] = useState('');
5
  const [isLoading, setIsLoading] = useState(false);
6
  const [collapsedThinks, setCollapsedThinks] = useState(new Set());
7
+ const [partialResponse, setPartialResponse] = useState('');
8
+ const [streamController, setStreamController] = useState(null);
9
 
10
  // 添加解析消息的函数
11
  const parseMessage = (content) => {
 
31
  const handleSendMessage = async () => {
32
  if (!input.trim()) return;
33
 
34
+ // Cancel any ongoing stream
35
+ if (streamController) {
36
+ streamController.abort();
37
+ }
38
+
39
  const newMessage = {
40
  role: 'user',
41
  content: input,
42
+ timestamp: Date.now()
43
  };
44
 
45
  const updatedChat = {
 
48
  };
49
  onUpdateChat(updatedChat);
50
  setInput('');
51
+ setPartialResponse('');
52
+
53
+ // Create new AbortController for this stream
54
+ const controller = new AbortController();
55
+ setStreamController(controller);
56
 
 
57
  setIsLoading(true);
58
  try {
59
  const response = await fetch('/api/chat', {
 
70
  model: settings.model,
71
  apiEndpoint: settings.apiEndpoint,
72
  }),
73
+ signal: controller.signal
74
  });
75
 
76
  if (!response.ok) {
 
78
  throw new Error(`HTTP error! status: ${response.status}, message: ${errorData}`);
79
  }
80
 
81
+ const reader = response.body.getReader();
82
+ const decoder = new TextDecoder();
83
+ let aiMessage = {
 
84
  role: 'assistant',
85
+ content: '',
86
  timestamp: Date.now()
87
  };
88
 
89
+ while (true) {
90
+ const { done, value } = await reader.read();
91
+ if (done) break;
92
+
93
+ const chunk = decoder.decode(value);
94
+ const lines = chunk.split('\n').filter(line => line.trim() !== '');
95
+
96
+ for (const line of lines) {
97
+ const message = line.replace(/^data: /, '');
98
+ if (message === '[DONE]') break;
99
+
100
+ try {
101
+ const parsed = JSON.parse(message);
102
+ const content = parsed.choices[0].delta.content || '';
103
+ aiMessage.content += content;
104
+ setPartialResponse(aiMessage.content);
105
+ } catch (error) {
106
+ console.error('Error parsing chunk:', error);
107
+ }
108
+ }
109
+ }
110
+
111
  const finalChat = {
112
  ...updatedChat,
113
  messages: [...updatedChat.messages, aiMessage]
114
  };
115
  onUpdateChat(finalChat);
116
  } catch (error) {
117
+ if (error.name !== 'AbortError') {
118
+ console.error('Failed to send message:', error);
119
+ alert(`Failed to send message: ${error.message}`);
120
+ }
121
  } finally {
122
  setIsLoading(false);
123
+ setPartialResponse('');
124
+ setStreamController(null);
125
  }
126
  };
127
 
 
166
  {isLoading && (
167
  <div className="message assistant">
168
  <div className="message-content">
169
+ <div className="content-text">
170
+ {partialResponse}
171
+ <span className="loading-cursor">|</span>
172
+ </div>
173
  </div>
174
  </div>
175
  )}
 
193
  );
194
  }
195
 
196
+ export default ChatWindow;
vite.config.js CHANGED
@@ -14,9 +14,9 @@ export default defineConfig({
14
  }
15
  },
16
  '/api': {
17
- target: 'http://localhost:3600',
18
  changeOrigin: true
19
  }
20
  }
21
  }
22
- })
 
14
  }
15
  },
16
  '/api': {
17
+ target: 'http://localhost:7860',
18
  changeOrigin: true
19
  }
20
  }
21
  }
22
+ })