moelove commited on
Commit
2316bca
·
1 Parent(s): 434a486

feat: add summarization profile support and API endpoint for conversation summaries

Browse files
server/index.js CHANGED
@@ -13,6 +13,61 @@ const __dirname = path.dirname(__filename)
13
  app.use(cors());
14
  app.use(express.json());
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  app.post('/api/chat', async (req, res) => {
17
  const { messages, apiEndpoint, apiKey, model } = req.body;
18
 
 
13
  app.use(cors());
14
  app.use(express.json());
15
 
16
+ app.post('/api/summarize', async (req, res) => {
17
+ const { content, apiEndpoint, apiKey, model } = req.body;
18
+
19
+ console.log('Received summarize request with:', {
20
+ apiEndpoint,
21
+ model,
22
+ contentLength: content.length
23
+ });
24
+
25
+ try {
26
+ let apiUrl;
27
+ if (apiEndpoint.endsWith('#')) {
28
+ apiUrl = apiEndpoint.slice(0, -1);
29
+ } else if (apiEndpoint.endsWith('/')) {
30
+ apiUrl = `${apiEndpoint}chat/completions`;
31
+ } else {
32
+ apiUrl = `${apiEndpoint}/v1/chat/completions`;
33
+ }
34
+ console.log('Calling API endpoint:', apiUrl);
35
+
36
+ const response = await fetch(apiUrl, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ 'Authorization': `Bearer ${apiKey}`
41
+ },
42
+ body: JSON.stringify({
43
+ model: model,
44
+ messages: [{
45
+ role: 'user',
46
+ content: `Summarize this conversation in 3-5 words: ${content}`
47
+ }],
48
+ temperature: 0.2,
49
+ max_tokens: 20
50
+ })
51
+ });
52
+
53
+ if (!response.ok) {
54
+ const errorData = await response.text();
55
+ console.error('API error:', {
56
+ status: response.status,
57
+ error: errorData
58
+ });
59
+ throw new Error(`API error: ${response.status} - ${errorData}`);
60
+ }
61
+
62
+ const data = await response.json();
63
+ const summary = data.choices[0].message.content.trim();
64
+ res.json({ summary });
65
+ } catch (error) {
66
+ console.error('Error:', error);
67
+ res.status(500).json({ error: error.message });
68
+ }
69
+ });
70
+
71
  app.post('/api/chat', async (req, res) => {
72
  const { messages, apiEndpoint, apiKey, model } = req.body;
73
 
src/App.jsx CHANGED
@@ -15,6 +15,14 @@ function App() {
15
  }
16
  ]);
17
 
 
 
 
 
 
 
 
 
18
  const [activeProfileId, setActiveProfileId] = useLocalStorage('activeProfileId', 'default');
19
 
20
  const [chats, setChats] = useLocalStorage('chats', []);
@@ -124,6 +132,7 @@ function App() {
124
  <ChatWindow
125
  chat={chats.find(c => c.id === currentChatId)}
126
  profile={activeProfile}
 
127
  onUpdateChat={(updatedChat) => {
128
  setChats(chats.map(c =>
129
  c.id === updatedChat.id ? updatedChat : c
@@ -151,7 +160,7 @@ function App() {
151
  >
152
  ×
153
  </button>
154
- <Settings
155
  profiles={profiles}
156
  activeProfileId={activeProfileId}
157
  onSaveProfiles={(newProfiles) => {
@@ -161,6 +170,8 @@ function App() {
161
  setActiveProfileId(newProfiles[0]?.id || null);
162
  }
163
  }}
 
 
164
  onChangeActiveProfile={setActiveProfileId}
165
  onCloseSettings={() => setShowSettings(false)}
166
  />
@@ -182,4 +193,4 @@ function App() {
182
  );
183
  }
184
 
185
- export default App;
 
15
  }
16
  ]);
17
 
18
+ const [summarizationProfile, setSummarizationProfile] = useLocalStorage('summarizationProfile', {
19
+ id: 'default-summarization-profile',
20
+ name: 'Summarization Profile',
21
+ apiEndpoint: '',
22
+ apiKey: '',
23
+ model: 'DeepSeek-R1'
24
+ });
25
+
26
  const [activeProfileId, setActiveProfileId] = useLocalStorage('activeProfileId', 'default');
27
 
28
  const [chats, setChats] = useLocalStorage('chats', []);
 
132
  <ChatWindow
133
  chat={chats.find(c => c.id === currentChatId)}
134
  profile={activeProfile}
135
+ summarizationProfile={summarizationProfile}
136
  onUpdateChat={(updatedChat) => {
137
  setChats(chats.map(c =>
138
  c.id === updatedChat.id ? updatedChat : c
 
160
  >
161
  ×
162
  </button>
163
+ <Settings
164
  profiles={profiles}
165
  activeProfileId={activeProfileId}
166
  onSaveProfiles={(newProfiles) => {
 
170
  setActiveProfileId(newProfiles[0]?.id || null);
171
  }
172
  }}
173
+ onSaveSummarizationProfile={setSummarizationProfile}
174
+ summarizationProfile={summarizationProfile}
175
  onChangeActiveProfile={setActiveProfileId}
176
  onCloseSettings={() => setShowSettings(false)}
177
  />
 
193
  );
194
  }
195
 
196
+ export default App;
src/components/ChatWindow.jsx CHANGED
@@ -1,7 +1,7 @@
1
  import React, { useState, useRef, useEffect } from 'react';
2
  import ReactMarkdown from 'react-markdown';
3
 
4
- function ChatWindow({ chat, profile, onUpdateChat }) {
5
  const [input, setInput] = useState('');
6
  const [isLoading, setIsLoading] = useState(false);
7
  const [collapsedThinks, setCollapsedThinks] = useState(new Set());
@@ -125,6 +125,43 @@ function ChatWindow({ chat, profile, onUpdateChat }) {
125
  messages: [...updatedChat.messages, aiMessage]
126
  };
127
  onUpdateChat(finalChat);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  } catch (error) {
129
  if (error.name !== 'AbortError') {
130
  console.error('Failed to send message:', error);
 
1
  import React, { useState, useRef, useEffect } from 'react';
2
  import ReactMarkdown from 'react-markdown';
3
 
4
+ function ChatWindow({ chat, profile, summarizationProfile, onUpdateChat }) {
5
  const [input, setInput] = useState('');
6
  const [isLoading, setIsLoading] = useState(false);
7
  const [collapsedThinks, setCollapsedThinks] = useState(new Set());
 
125
  messages: [...updatedChat.messages, aiMessage]
126
  };
127
  onUpdateChat(finalChat);
128
+
129
+ // Generate summary title after first message exchange
130
+ if (finalChat.messages.length === 2) {
131
+ try {
132
+ const summaryResponse = await fetch('/api/summarize', {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ },
137
+ body: JSON.stringify({
138
+ content: finalChat.messages.map(msg => msg.content).join('\n'),
139
+ apiKey: summarizationProfile?.apiKey || profile.apiKey || '',
140
+ model: summarizationProfile?.model || profile.model || 'gpt-3.5-turbo',
141
+ apiEndpoint: summarizationProfile?.apiEndpoint || profile.apiEndpoint || 'https://api.openai.com/v1'
142
+ })
143
+ });
144
+
145
+ if (!summaryResponse.ok) {
146
+ throw new Error(`Summary API error: ${summaryResponse.status}`);
147
+ }
148
+
149
+ const { summary } = await summaryResponse.json();
150
+ const updatedChatWithTitle = {
151
+ ...finalChat,
152
+ title: summary
153
+ };
154
+ onUpdateChat(updatedChatWithTitle);
155
+ } catch (error) {
156
+ console.error('Failed to generate summary:', error);
157
+ // Fallback to default title if summarization fails
158
+ const updatedChatWithTitle = {
159
+ ...finalChat,
160
+ title: 'New Conversation'
161
+ };
162
+ onUpdateChat(updatedChatWithTitle);
163
+ }
164
+ }
165
  } catch (error) {
166
  if (error.name !== 'AbortError') {
167
  console.error('Failed to send message:', error);
src/components/Settings.jsx CHANGED
@@ -1,11 +1,27 @@
1
  import { useState } from 'react';
2
 
3
- function Settings({ profiles, activeProfileId, onSaveProfiles, onChangeActiveProfile, onCloseSettings }) {
 
 
 
 
 
 
 
 
4
  const [localProfiles, setLocalProfiles] = useState(profiles);
 
 
 
 
 
 
 
5
  const [editingProfileId, setEditingProfileId] = useState(activeProfileId);
6
  const [isHintExpanded, setIsHintExpanded] = useState(false);
7
 
8
  const editingProfile = localProfiles.find(p => p.id === editingProfileId) || localProfiles[0];
 
9
 
10
  const handleProfileChange = (updatedProfile) => {
11
  setLocalProfiles(localProfiles.map(p =>
@@ -13,6 +29,10 @@ function Settings({ profiles, activeProfileId, onSaveProfiles, onChangeActivePro
13
  ));
14
  };
15
 
 
 
 
 
16
  const handleAddProfile = () => {
17
  const newId = `profile-${Date.now()}`;
18
  const newProfile = {
@@ -29,7 +49,6 @@ function Settings({ profiles, activeProfileId, onSaveProfiles, onChangeActivePro
29
  };
30
 
31
  const handleDeleteProfile = (profileId) => {
32
- // Prevent deleting the last profile
33
  if (localProfiles.length <= 1) {
34
  alert("Cannot delete the last profile");
35
  return;
@@ -38,7 +57,6 @@ function Settings({ profiles, activeProfileId, onSaveProfiles, onChangeActivePro
38
  const updatedProfiles = localProfiles.filter(p => p.id !== profileId);
39
  setLocalProfiles(updatedProfiles);
40
 
41
- // If we're deleting the currently edited profile, switch to another one
42
  if (editingProfileId === profileId) {
43
  setEditingProfileId(updatedProfiles[0].id);
44
  }
@@ -47,6 +65,7 @@ function Settings({ profiles, activeProfileId, onSaveProfiles, onChangeActivePro
47
  const handleSubmit = (e) => {
48
  e.preventDefault();
49
  onSaveProfiles(localProfiles);
 
50
  onChangeActiveProfile(editingProfileId);
51
  onCloseSettings();
52
  };
@@ -57,7 +76,7 @@ function Settings({ profiles, activeProfileId, onSaveProfiles, onChangeActivePro
57
 
58
  <div className="profiles-section">
59
  <div className="profiles-header">
60
- <h3>Profiles</h3>
61
  <button
62
  type="button"
63
  className="add-profile-button"
@@ -90,18 +109,17 @@ function Settings({ profiles, activeProfileId, onSaveProfiles, onChangeActivePro
90
  ))}
91
  </div>
92
  </div>
93
-
94
- <form onSubmit={handleSubmit}>
 
95
  <div className="current-profile-section">
96
- <h3>Edit Profile: {editingProfile.name}</h3>
97
-
98
  <div className="setting-item">
99
  <label>Profile Name:</label>
100
  <input
101
  type="text"
102
- value={editingProfile.name}
103
- onChange={(e) => handleProfileChange({
104
- ...editingProfile,
105
  name: e.target.value
106
  })}
107
  placeholder="Enter profile name"
@@ -112,39 +130,22 @@ function Settings({ profiles, activeProfileId, onSaveProfiles, onChangeActivePro
112
  <label>API Endpoint:</label>
113
  <input
114
  type="text"
115
- value={editingProfile.apiEndpoint}
116
- onChange={(e) => handleProfileChange({
117
- ...editingProfile,
118
  apiEndpoint: e.target.value
119
  })}
120
  placeholder="Enter API endpoint"
121
  />
122
- <div className="setting-hint">
123
- <button
124
- type="button"
125
- className="hint-toggle"
126
- onClick={() => setIsHintExpanded(!isHintExpanded)}
127
- >
128
- {isHintExpanded ? 'Hide' : 'Show'} API Endpoint Format Examples
129
- </button>
130
- <div className={`hint-content ${isHintExpanded ? 'expanded' : ''}`}>
131
- <p>API Endpoint format examples:</p>
132
- <ul>
133
- <li>Ends with / → /chat/completions will be appended</li>
134
- <li>Ends with # → # will be removed</li>
135
- <li>Other cases → /v1/chat/completions will be appended</li>
136
- </ul>
137
- </div>
138
- </div>
139
  </div>
140
 
141
  <div className="setting-item">
142
  <label>API Key:</label>
143
  <input
144
  type="password"
145
- value={editingProfile.apiKey}
146
- onChange={(e) => handleProfileChange({
147
- ...editingProfile,
148
  apiKey: e.target.value
149
  })}
150
  placeholder="Enter your API key"
@@ -155,15 +156,93 @@ function Settings({ profiles, activeProfileId, onSaveProfiles, onChangeActivePro
155
  <label>Model:</label>
156
  <input
157
  type="text"
158
- value={editingProfile.model}
159
- onChange={(e) => handleProfileChange({
160
- ...editingProfile,
161
  model: e.target.value
162
  })}
163
  placeholder="Enter model name (e.g., DeepSeek-R1)"
164
  />
165
  </div>
166
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  <div className="settings-actions">
169
  <button type="submit" className="save-button">Save Settings</button>
 
1
  import { useState } from 'react';
2
 
3
+ function Settings({
4
+ profiles,
5
+ activeProfileId,
6
+ onSaveProfiles,
7
+ onChangeActiveProfile,
8
+ onCloseSettings,
9
+ onSaveSummarizationProfile,
10
+ summarizationProfile
11
+ }) {
12
  const [localProfiles, setLocalProfiles] = useState(profiles);
13
+ const [localSummarizationProfile, setLocalSummarizationProfile] = useState(summarizationProfile || {
14
+ id: 'default-summarization-profile',
15
+ name: 'Summarization Profile',
16
+ apiEndpoint: '',
17
+ apiKey: '',
18
+ model: 'DeepSeek-R1'
19
+ });
20
  const [editingProfileId, setEditingProfileId] = useState(activeProfileId);
21
  const [isHintExpanded, setIsHintExpanded] = useState(false);
22
 
23
  const editingProfile = localProfiles.find(p => p.id === editingProfileId) || localProfiles[0];
24
+ const editingSummarizationProfile = localSummarizationProfile;
25
 
26
  const handleProfileChange = (updatedProfile) => {
27
  setLocalProfiles(localProfiles.map(p =>
 
29
  ));
30
  };
31
 
32
+ const handleSummarizationProfileChange = (updatedProfile) => {
33
+ setLocalSummarizationProfile(updatedProfile);
34
+ };
35
+
36
  const handleAddProfile = () => {
37
  const newId = `profile-${Date.now()}`;
38
  const newProfile = {
 
49
  };
50
 
51
  const handleDeleteProfile = (profileId) => {
 
52
  if (localProfiles.length <= 1) {
53
  alert("Cannot delete the last profile");
54
  return;
 
57
  const updatedProfiles = localProfiles.filter(p => p.id !== profileId);
58
  setLocalProfiles(updatedProfiles);
59
 
 
60
  if (editingProfileId === profileId) {
61
  setEditingProfileId(updatedProfiles[0].id);
62
  }
 
65
  const handleSubmit = (e) => {
66
  e.preventDefault();
67
  onSaveProfiles(localProfiles);
68
+ onSaveSummarizationProfile(localSummarizationProfile);
69
  onChangeActiveProfile(editingProfileId);
70
  onCloseSettings();
71
  };
 
76
 
77
  <div className="profiles-section">
78
  <div className="profiles-header">
79
+ <h3>Chat Profiles</h3>
80
  <button
81
  type="button"
82
  className="add-profile-button"
 
109
  ))}
110
  </div>
111
  </div>
112
+
113
+ <div className="profiles-section">
114
+ <h3>Summarization Profile</h3>
115
  <div className="current-profile-section">
 
 
116
  <div className="setting-item">
117
  <label>Profile Name:</label>
118
  <input
119
  type="text"
120
+ value={localSummarizationProfile.name}
121
+ onChange={(e) => handleSummarizationProfileChange({
122
+ ...localSummarizationProfile,
123
  name: e.target.value
124
  })}
125
  placeholder="Enter profile name"
 
130
  <label>API Endpoint:</label>
131
  <input
132
  type="text"
133
+ value={localSummarizationProfile.apiEndpoint}
134
+ onChange={(e) => handleSummarizationProfileChange({
135
+ ...localSummarizationProfile,
136
  apiEndpoint: e.target.value
137
  })}
138
  placeholder="Enter API endpoint"
139
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  </div>
141
 
142
  <div className="setting-item">
143
  <label>API Key:</label>
144
  <input
145
  type="password"
146
+ value={localSummarizationProfile.apiKey}
147
+ onChange={(e) => handleSummarizationProfileChange({
148
+ ...localSummarizationProfile,
149
  apiKey: e.target.value
150
  })}
151
  placeholder="Enter your API key"
 
156
  <label>Model:</label>
157
  <input
158
  type="text"
159
+ value={localSummarizationProfile.model}
160
+ onChange={(e) => handleSummarizationProfileChange({
161
+ ...localSummarizationProfile,
162
  model: e.target.value
163
  })}
164
  placeholder="Enter model name (e.g., DeepSeek-R1)"
165
  />
166
  </div>
167
  </div>
168
+ </div>
169
+
170
+ <form onSubmit={handleSubmit}>
171
+ {editingProfile && (
172
+ <div className="current-profile-section">
173
+ <h3>Edit Chat Profile: {editingProfile.name}</h3>
174
+
175
+ <div className="setting-item">
176
+ <label>Profile Name:</label>
177
+ <input
178
+ type="text"
179
+ value={editingProfile.name}
180
+ onChange={(e) => handleProfileChange({
181
+ ...editingProfile,
182
+ name: e.target.value
183
+ })}
184
+ placeholder="Enter profile name"
185
+ />
186
+ </div>
187
+
188
+ <div className="setting-item">
189
+ <label>API Endpoint:</label>
190
+ <input
191
+ type="text"
192
+ value={editingProfile.apiEndpoint}
193
+ onChange={(e) => handleProfileChange({
194
+ ...editingProfile,
195
+ apiEndpoint: e.target.value
196
+ })}
197
+ placeholder="Enter API endpoint"
198
+ />
199
+ <div className="setting-hint">
200
+ <button
201
+ type="button"
202
+ className="hint-toggle"
203
+ onClick={() => setIsHintExpanded(!isHintExpanded)}
204
+ >
205
+ {isHintExpanded ? 'Hide' : 'Show'} API Endpoint Format Examples
206
+ </button>
207
+ <div className={`hint-content ${isHintExpanded ? 'expanded' : ''}`}>
208
+ <p>API Endpoint format examples:</p>
209
+ <ul>
210
+ <li>Ends with / → /chat/completions will be appended</li>
211
+ <li>Ends with # → # will be removed</li>
212
+ <li>Other cases → /v1/chat/completions will be appended</li>
213
+ </ul>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ <div className="setting-item">
219
+ <label>API Key:</label>
220
+ <input
221
+ type="password"
222
+ value={editingProfile.apiKey}
223
+ onChange={(e) => handleProfileChange({
224
+ ...editingProfile,
225
+ apiKey: e.target.value
226
+ })}
227
+ placeholder="Enter your API key"
228
+ />
229
+ </div>
230
+
231
+ <div className="setting-item">
232
+ <label>Model:</label>
233
+ <input
234
+ type="text"
235
+ value={editingProfile.model}
236
+ onChange={(e) => handleProfileChange({
237
+ ...editingProfile,
238
+ model: e.target.value
239
+ })}
240
+ placeholder="Enter model name (e.g., DeepSeek-R1)"
241
+ />
242
+ </div>
243
+ </div>
244
+ )}
245
+
246
 
247
  <div className="settings-actions">
248
  <button type="submit" className="save-button">Save Settings</button>