Spaces:
Runtime error
Runtime error
feat: add summarization profile support and API endpoint for conversation summaries
Browse files- server/index.js +55 -0
- src/App.jsx +13 -2
- src/components/ChatWindow.jsx +38 -1
- src/components/Settings.jsx +116 -37
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 |
-
|
| 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({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
|
|
|
| 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={
|
| 103 |
-
onChange={(e) =>
|
| 104 |
-
...
|
| 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={
|
| 116 |
-
onChange={(e) =>
|
| 117 |
-
...
|
| 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={
|
| 146 |
-
onChange={(e) =>
|
| 147 |
-
...
|
| 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={
|
| 159 |
-
onChange={(e) =>
|
| 160 |
-
...
|
| 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>
|