@@ -10,8 +10,13 @@ import { ModelSettingsInterface } from './components/ModelSettings';
1010import { WaitingIndicator } from './components/WaitingIndicator' ;
1111import { ToastProvider } from './context/ToastContext' ;
1212import { storageService } from './services/StorageService' ;
13- import { Message , Model } from './types/types' ;
13+ import { ChatEntry , ChatHistory , Message , Model } from './types/types' ;
1414import { 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
1621function 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 >
0 commit comments