Skip to content

Commit 3511b8d

Browse files
add saving, export and import of multiple chat histories
1 parent 5dc896e commit 3511b8d

File tree

9 files changed

+789
-114
lines changed

9 files changed

+789
-114
lines changed

package-lock.json

Lines changed: 386 additions & 41 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
"private": true,
44
"version": "1.0.0",
55
"type": "module",
6+
"homepage": "https://github.com/josephgodwinkimani/ollama-coder",
7+
"bugs": {
8+
"url": "https://github.com/josephgodwinkimani/ollama-coder/issues"
9+
},
10+
"repository": {
11+
"type": "git",
12+
"url": "https://github.com/josephgodwinkimani/ollama-coder.git"
13+
},
14+
"authors": [
15+
{
16+
"name": "Godwin Kimani"
17+
}
18+
],
619
"scripts": {
720
"dev": "vite",
821
"build": "tsc -b && vite build",
@@ -38,9 +51,12 @@
3851
"marked": "^15.0.7",
3952
"prismjs": "^1.29.0",
4053
"react": "^19.0.0",
41-
"react-dom": "^19.0.0"
54+
"react-dom": "^19.0.0",
55+
"react-select": "5.10.0",
56+
"uuid": "11.1.0"
4257
},
4358
"devDependencies": {
59+
"@types/uuid": "10.0.0",
4460
"@eslint/js": "^9.20.0",
4561
"@types/node": "^22.13.2",
4662
"@types/prismjs": "^1.26.5",
@@ -69,4 +85,4 @@
6985
"typescript-eslint": "^8.22.0",
7086
"vite": "^6.1.0"
7187
}
72-
}
88+
}

src/App.tsx

Lines changed: 199 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ import { ModelSettingsInterface } from './components/ModelSettings';
1010
import { WaitingIndicator } from './components/WaitingIndicator';
1111
import { ToastProvider } from './context/ToastContext';
1212
import { storageService } from './services/StorageService';
13-
import { Message, Model } from './types/types';
13+
import { ChatEntry, ChatHistory, Message, Model } from './types/types';
1414
import { modelStorage } from './utils/modelStorage';
15+
import { v4 as uuidv4 } from 'uuid';
16+
import Select from 'react-select';
17+
import { ExportButton } from './components/ExportButton';
18+
import { exportChatHistories } from './utils/exportUtils';
19+
import { ImportButton } from './components/ImportButton';
1520

1621
function App() {
1722
const [models, setModels] = useState<Model[]>([]);
@@ -28,6 +33,8 @@ function App() {
2833
const [dateTime, setDateTime] = useState('');
2934
const [waitStartTime, setWaitStartTime] = useState<number | null>(null);
3035
const [waitEndTime, setWaitEndTime] = useState<number | null>(null);
36+
const [chatHistories, setChatHistories] = useState<ChatHistory[]>([]);
37+
const [currentChatId, setCurrentChatId] = useState<string>(uuidv4());
3138

3239
useEffect(() => {
3340
const initialize = async () => {
@@ -49,7 +56,7 @@ function App() {
4956
};
5057

5158
// Update time every second
52-
const updateDateTime = () => {
59+
const updateDateTime: () => void = () => {
5360
console.log(dateTime);
5461
const now = new Date();
5562
const formattedDate = formatDateTime(now);
@@ -67,7 +74,7 @@ function App() {
6774
};
6875
}, []);
6976

70-
const loadModels = async () => {
77+
const loadModels: () => Promise<void> = async () => {
7178
try {
7279
const response = await ollamaApi.listModels();
7380
setModels(response?.models as Model[]);
@@ -82,13 +89,15 @@ function App() {
8289
}
8390
};
8491

85-
const handleSettingsChange = (settings: ModelSettingsInterface) => {
92+
const handleSettingsChange: (settings: ModelSettingsInterface) => void = (
93+
settings: ModelSettingsInterface
94+
) => {
8695
setModelSettings(settings);
8796
// Save settings to localStorage
8897
localStorage.setItem('modelSettings', JSON.stringify(settings));
8998
};
9099

91-
const formatDateTime = (date: Date): string => {
100+
const formatDateTime: (date: Date) => string = (date: Date): string => {
92101
const pad = (num: number): string => num.toString().padStart(2, '0');
93102

94103
const year = date.getUTCFullYear();
@@ -101,29 +110,49 @@ function App() {
101110
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
102111
};
103112

104-
const loadModelSettings = () => {
113+
const loadModelSettings: () => void = () => {
105114
const savedSettings = localStorage.getItem('modelSettings');
106115
if (savedSettings) {
107116
setModelSettings(JSON.parse(savedSettings));
108117
}
109118
};
110119

111-
const loadChatHistory = async () => {
120+
const loadChatHistory: () => Promise<void> = async () => {
112121
try {
113-
const chat = await storageService.loadLatestChat();
114-
if (chat && chat?.messages && chat?.messages?.length > 0) {
115-
console.log('Loading chat history:', chat);
116-
setMessages(chat?.messages);
117-
if (chat?.model) {
118-
setSelectedModel(chat?.model);
119-
}
122+
const histories = await storageService.loadAllChats();
123+
if (!Array.isArray(histories)) {
124+
console.error('Invalid chat histories format');
125+
return;
126+
}
127+
128+
const historiesWithTitles = histories
129+
.filter(chat => chat && typeof chat === 'object')
130+
.map(chat => ({
131+
...chat,
132+
id: String(chat.id || uuidv4()), // Convert ID to string
133+
title: getFirstQuestionPreview(Array.isArray(chat.messages) ? chat.messages : []),
134+
}));
135+
136+
console.log(historiesWithTitles);
137+
setChatHistories(historiesWithTitles);
138+
139+
// Load latest chat if exists
140+
const latestChat = historiesWithTitles[historiesWithTitles.length - 1];
141+
if (latestChat && Array.isArray(latestChat.messages)) {
142+
setMessages(latestChat.messages);
143+
setSelectedModel(latestChat.model || '');
144+
setCurrentChatId(latestChat.id);
120145
}
121146
} catch (error) {
122-
console.error('Error loading chat history:', error);
147+
console.error('Error loading chat histories:', error);
148+
setChatHistories([]); // Reset to empty array on error
123149
}
124150
};
125151

126-
const handleFileContent = (content: string, filename: string) => {
152+
const handleFileContent: (content: string, filename: string) => void = (
153+
content: string,
154+
filename: string
155+
) => {
127156
// Create a message with the file content
128157
const fileMessage = `Using this file named "${filename}" with the content:\n\`\`\`${filename}\n${content}\n\`\`\`\n`;
129158

@@ -132,7 +161,10 @@ function App() {
132161
setFileContent(fileMessage);
133162
};
134163

135-
const calculateWaitTime = (startTime: number, endTime: number): string => {
164+
const calculateWaitTime: (startTime: number, endTime: number) => string = (
165+
startTime: number,
166+
endTime: number
167+
): string => {
136168
const elapsedSeconds = Math.floor((endTime - startTime) / 1000);
137169
const minutes = Math.floor(elapsedSeconds / 60);
138170
const seconds = elapsedSeconds % 60;
@@ -178,29 +210,75 @@ function App() {
178210
const newMessages = [...updatedMessages, assistantMessage];
179211
setMessages(newMessages);
180212

213+
const chatHistory: ChatHistory = {
214+
id: currentChatId,
215+
title: getFirstQuestionPreview(newMessages),
216+
messages: newMessages,
217+
model: selectedModel,
218+
timestamp: new Date().toISOString(),
219+
};
220+
221+
const chatEntry: ChatEntry = {
222+
...chatHistory,
223+
id: parseInt(currentChatId) || Date.now(),
224+
};
225+
226+
await storageService.saveChat(chatHistory.messages, chatHistory.model, chatEntry);
227+
setChatHistories(prev => [...prev.filter(ch => ch.id !== currentChatId), chatHistory]);
228+
181229
// Save to storage
182-
await storageService.saveChat(newMessages, selectedModel);
230+
await storageService.saveChat(newMessages, selectedModel, {
231+
...chatHistory,
232+
id: parseInt(currentChatId) || Date.now(),
233+
});
183234
console.log(waitEndTime);
184235

185236
// Play sound
186-
const audio = new Audio('/sounds/notification-sound-3-262896.mp3');
237+
const audio: HTMLAudioElement = new Audio('/sounds/notification-sound-3-262896.mp3');
187238
audio.play();
188239
} catch (error) {
189240
console.error('Error sending message:', error);
241+
// Play sound
242+
const audio: HTMLAudioElement = new Audio('/sounds/windows-error-sound-effect-35894.mp3');
243+
audio.play();
190244
} finally {
191245
console.log(waitEndTime);
192246
setIsLoading(false);
193247
setWaitStartTime(null);
194248
setWaitEndTime(null);
249+
}
250+
};
195251

196-
// Play sound
197-
const audio = new Audio('/sounds/windows-error-sound-effect-35894.mp3');
198-
audio.play();
252+
const getFirstQuestionPreview: (messages: Message[]) => string = (
253+
messages: Message[]
254+
): string => {
255+
const firstQuestion = messages.find(m => m.role === 'user')?.content || '';
256+
const words = firstQuestion.split(' ').slice(0, 10).join(' ');
257+
return words + (firstQuestion.split(' ').length > 10 ? '...' : '');
258+
};
259+
260+
const handleChatSelect: (
261+
selectedOption: {
262+
value: string;
263+
label: string;
264+
} | null
265+
) => void = (selectedOption: { value: string; label: string } | null) => {
266+
if (!selectedOption) return;
267+
268+
const selectedChat = chatHistories.find(ch => ch.id === selectedOption.value);
269+
if (selectedChat) {
270+
setMessages(selectedChat.messages);
271+
setSelectedModel(selectedChat.model);
272+
setCurrentChatId(selectedChat.id);
199273
}
200274
};
201275

202-
// Add this function in your App component
203-
const handleClearAll = () => {
276+
const handleNewChat: () => void = () => {
277+
setMessages([]);
278+
setCurrentChatId(uuidv4());
279+
};
280+
281+
const handleClearAll: () => void = () => {
204282
// Clear messages
205283
setMessages([]);
206284

@@ -222,6 +300,71 @@ function App() {
222300

223301
// Clear file content if any
224302
setFileContent('');
303+
setChatHistories([]);
304+
setCurrentChatId(uuidv4());
305+
};
306+
307+
const handleExport: () => void = () => {
308+
if (chatHistories.length > 0) {
309+
exportChatHistories(chatHistories);
310+
}
311+
};
312+
313+
const handleImport: (file: File) => Promise<void> = async (file: File) => {
314+
try {
315+
const fileContent: string = await file.text();
316+
const importedHistories: ChatHistory[] = JSON.parse(fileContent) as ChatHistory[];
317+
318+
if (!Array.isArray(importedHistories)) {
319+
throw new Error('Invalid chat history format');
320+
}
321+
322+
// Validate and process imported histories
323+
const validHistories: {
324+
id: string;
325+
timestamp: string;
326+
title: string;
327+
messages: Message[];
328+
model: string;
329+
}[] = importedHistories
330+
.filter(
331+
chat =>
332+
chat &&
333+
typeof chat === 'object' &&
334+
Array.isArray(chat.messages) &&
335+
typeof chat.model === 'string'
336+
)
337+
.map(chat => ({
338+
...chat,
339+
id: uuidv4(), // Generate new IDs to avoid conflicts
340+
timestamp: chat.timestamp || new Date().toISOString(),
341+
}));
342+
343+
if (validHistories.length === 0) {
344+
throw new Error('No valid chat histories found in file');
345+
}
346+
347+
// Merge with existing histories
348+
setChatHistories(prev => [...prev, ...validHistories]);
349+
350+
// Save merged histories to storage
351+
const allHistories: ChatHistory[] = [...chatHistories, ...validHistories];
352+
localStorage.setItem('ollama-chats', JSON.stringify(allHistories));
353+
354+
// Update current chat to the first imported chat
355+
const firstImported: {
356+
id: string;
357+
timestamp: string;
358+
title: string;
359+
messages: Message[];
360+
model: string;
361+
} = validHistories[0];
362+
setMessages(firstImported.messages);
363+
setSelectedModel(firstImported.model);
364+
setCurrentChatId(firstImported.id);
365+
} catch (error) {
366+
console.error('Error importing chat histories:', error);
367+
}
225368
};
226369

227370
return (
@@ -234,7 +377,38 @@ function App() {
234377
</a>
235378
</div>
236379
<div className="header-info">
237-
<div className="info-line">
380+
<div
381+
className="info-line"
382+
style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}
383+
>
384+
<Select
385+
value={
386+
chatHistories.length > 0
387+
? {
388+
value: currentChatId,
389+
label:
390+
chatHistories.find(ch => ch.id === currentChatId)?.title || 'New Chat',
391+
}
392+
: null
393+
}
394+
onChange={handleChatSelect}
395+
options={chatHistories.map(chat => ({
396+
value: chat.id,
397+
label: chat.title,
398+
}))}
399+
placeholder="Select chat history..."
400+
styles={{
401+
container: base => ({
402+
...base,
403+
minWidth: '300px',
404+
}),
405+
}}
406+
/>
407+
<button onClick={handleNewChat} className="new-chat-button">
408+
New Chat
409+
</button>
410+
<ExportButton onExport={handleExport} disabled={chatHistories.length === 0} />
411+
<ImportButton onImport={handleImport} />
238412
<ClearDataButton onClear={handleClearAll} />
239413
</div>
240414
</div>

src/components/ExportButton.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
3+
interface ExportButtonProps {
4+
onExport: () => void;
5+
disabled?: boolean;
6+
}
7+
8+
export const ExportButton: React.FC<ExportButtonProps> = ({ onExport, disabled }) => {
9+
return (
10+
<button
11+
onClick={onExport}
12+
className="export-button"
13+
disabled={disabled}
14+
title="Export chat histories"
15+
>
16+
Export Chats
17+
</button>
18+
);
19+
};

0 commit comments

Comments
 (0)