fixffsd
This commit is contained in:
		
							parent
							
								
									240d8ece6a
								
							
						
					
					
						commit
						512e140b80
					
				| @ -78,6 +78,18 @@ export function setupIpcHandlers() { | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   // Ollama Operations
 | ||||
|   ipcMain.handle('check-ollama', async () => { | ||||
|     try { | ||||
|       const status = await (await import('../services/ollamaService')).ollamaService.checkOllamaInstallation(); | ||||
|       return status; | ||||
|     } catch (error) { | ||||
|       const err = error as Error; | ||||
|       console.error('Error checking Ollama:', err); | ||||
|       throw err; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   // Model Operations
 | ||||
|   ipcMain.handle('check-model', async (_: unknown, modelName: string) => { | ||||
|     try { | ||||
|  | ||||
| @ -161,44 +161,6 @@ ipcMain.handle('window-close', (event) => { | ||||
|   window?.close(); | ||||
| }); | ||||
| 
 | ||||
| ipcMain.handle('check-ollama', async () => { | ||||
|   const checkInstalled = () => { | ||||
|     return new Promise((resolve) => { | ||||
|       const check = spawn('ollama', ['--version']); | ||||
|       check.on('close', (code) => { | ||||
|         resolve(code === 0); | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const checkRunning = async () => { | ||||
|     try { | ||||
|       const response = await fetch('http://localhost:11434/api/version'); | ||||
|       return response.ok; | ||||
|     } catch (error) { | ||||
|       return false; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const startOllama = () => { | ||||
|     return new Promise<void>((resolve) => { | ||||
|       const start = spawn('ollama', ['serve']); | ||||
|       // Wait a bit for the server to start
 | ||||
|       setTimeout(resolve, 2000); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const installed = await checkInstalled(); | ||||
|   let running = await checkRunning(); | ||||
| 
 | ||||
|   if (installed && !running) { | ||||
|     await startOllama(); | ||||
|     running = await checkRunning(); | ||||
|   } | ||||
| 
 | ||||
|   return { installed, running }; | ||||
| }); | ||||
| 
 | ||||
| ipcMain.handle('open-external', (_, url) => { | ||||
|   return require('electron').shell.openExternal(url); | ||||
| }); | ||||
|  | ||||
| @ -65,7 +65,8 @@ contextBridge.exposeInMainWorld('electron', { | ||||
|     ipcRenderer.removeListener(channel, callback); | ||||
|   }, | ||||
| 
 | ||||
|   checkOllama: () => ipcRenderer.invoke('check-ollama'), | ||||
|   checkOllama: async (): Promise<{ installed: boolean; running: boolean }> =>  | ||||
|     ipcRenderer.invoke('check-ollama'), | ||||
|   openExternal: (url: string) => ipcRenderer.invoke('open-external', url), | ||||
|    | ||||
|   // Model Operations
 | ||||
|  | ||||
| @ -1,5 +1,15 @@ | ||||
| import { ServiceError } from '../types'; | ||||
| import { net } from 'electron'; | ||||
| import { platform } from 'os'; | ||||
| const { exec } = require('child_process'); | ||||
| const { promisify } = require('util'); | ||||
| 
 | ||||
| const execAsync = promisify(exec); | ||||
| 
 | ||||
| interface OllamaStatus { | ||||
|   installed: boolean; | ||||
|   running: boolean; | ||||
| } | ||||
| 
 | ||||
| interface OllamaModel { | ||||
|   name: string; | ||||
| @ -40,12 +50,31 @@ class OllamaService { | ||||
|   private baseUrl: string = 'http://127.0.0.1:11434'; | ||||
|   private _lastProgress: number | null = null; | ||||
| 
 | ||||
|   async checkOllamaInstallation(): Promise<OllamaStatus> { | ||||
|     try { | ||||
|       // Check if ollama binary exists
 | ||||
|       const cmd = platform() === 'win32' ? 'where ollama' : 'which ollama'; | ||||
|       await execAsync(cmd); | ||||
|        | ||||
|       // Check if Ollama server is running by attempting to connect to the API
 | ||||
|       try { | ||||
|         await this.makeRequest<any>('/api/tags'); | ||||
|         return { installed: true, running: true }; | ||||
|       } catch (error) { | ||||
|         return { installed: true, running: false }; | ||||
|       } | ||||
|     } catch (error) { | ||||
|       return { installed: false, running: false }; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async makeRequest<T>( | ||||
|     path: string, | ||||
|     method: string = 'GET', | ||||
|     body?: any, | ||||
|     onChunk?: (chunk: string) => void | ||||
|   ): Promise<T> { | ||||
|     let accumulatedContent = ''; // Add accumulator for chat content
 | ||||
|     return new Promise((resolve, reject) => { | ||||
|       try { | ||||
|         const url = new URL(path, this.baseUrl); | ||||
| @ -88,8 +117,11 @@ class OllamaService { | ||||
|                    | ||||
|                   try { | ||||
|                     const parsed = JSON.parse(line); | ||||
|                     if (path === '/api/chat' && parsed.message?.content && onChunk) { | ||||
|                       onChunk(parsed.message.content); | ||||
|                     if (path === '/api/chat' && parsed.message?.content) { | ||||
|                       accumulatedContent += parsed.message.content; | ||||
|                       if (onChunk) { | ||||
|                         onChunk(parsed.message.content); | ||||
|                       } | ||||
|                     } else if (path === '/api/pull' && onChunk) { | ||||
|                       if (parsed.status === 'success') { | ||||
|                         onChunk('downloading: 100% complete'); | ||||
| @ -118,28 +150,26 @@ class OllamaService { | ||||
| 
 | ||||
|           response.on('end', () => { | ||||
|             try { | ||||
|               if (path === '/api/chat' || path === '/api/pull') { | ||||
|                 // Handle any remaining data in the streaming buffer
 | ||||
|               if (path === '/api/chat') { | ||||
|                 // Process any remaining data in the buffer for chat
 | ||||
|                 if (streamBuffer.trim()) { | ||||
|                   try { | ||||
|                     const parsed = JSON.parse(streamBuffer); | ||||
|                     if (path === '/api/chat' && parsed.message?.content && onChunk) { | ||||
|                       onChunk(parsed.message.content); | ||||
|                     } else if (path === '/api/pull' && onChunk) { | ||||
|                       if (parsed.status === 'success') { | ||||
|                         onChunk('downloading: 100% complete'); | ||||
|                       } else if (parsed.total && parsed.completed !== undefined) { | ||||
|                         const percentage = ((parsed.completed / parsed.total) * 100).toFixed(1); | ||||
|                         onChunk(`downloading: ${percentage}% complete`); | ||||
|                       } else if (parsed.status) { | ||||
|                         onChunk(parsed.status); | ||||
|                       } | ||||
|                     if (parsed.message?.content) { | ||||
|                       accumulatedContent += parsed.message.content; | ||||
|                     } | ||||
|                   } catch (e) { | ||||
|                     console.warn('Failed to parse final chunk:', { buffer: streamBuffer, error: e }); | ||||
|                     console.warn('Failed to parse final chat chunk:', { buffer: streamBuffer, error: e }); | ||||
|                   } | ||||
|                 } | ||||
|                 // Resolve streaming endpoints with success response
 | ||||
|                 // Resolve with the complete accumulated content
 | ||||
|                 resolve({  | ||||
|                   message: {  | ||||
|                     content: accumulatedContent  | ||||
|                   }  | ||||
|                 } as T); | ||||
|               } else if (path === '/api/pull') { | ||||
|                 // For pull, resolve with success
 | ||||
|                 resolve({ success: true } as T); | ||||
|               } else { | ||||
|                 // For non-streaming endpoints, parse the accumulated response
 | ||||
| @ -216,11 +246,7 @@ class OllamaService { | ||||
|         onChunk | ||||
|       ); | ||||
| 
 | ||||
|       if (!response?.message) { | ||||
|         throw new Error('Invalid response format from Ollama'); | ||||
|       } | ||||
| 
 | ||||
|       return response; | ||||
|       return response as OllamaChatResponse; | ||||
|     } catch (error) { | ||||
|       const errorMessage = error instanceof Error ? error.message : 'Failed to chat with Ollama'; | ||||
|       console.error('Chat error:', { | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import React, { useEffect, useRef } from 'react'; | ||||
| import { Box, Typography, Paper, Avatar } from '@mui/material'; | ||||
| import React, { useEffect, useRef, useState, useMemo } from 'react'; | ||||
| import { Box, Typography, Paper, Avatar, IconButton } from '@mui/material'; | ||||
| import ChevronRightIcon from '@mui/icons-material/ChevronRight'; | ||||
| import type { DocumentMetadata } from '../../../electron/types'; | ||||
| import ReactMarkdown from 'react-markdown'; | ||||
| import rehypeHighlight from 'rehype-highlight'; | ||||
| @ -61,8 +62,8 @@ export default function MessageList({ messages }: MessageListProps) { | ||||
|             flexDirection: message.isUser ? 'row-reverse' : 'row', | ||||
|             alignItems: 'flex-start', | ||||
|             gap: 2, | ||||
|             maxWidth: '80%', | ||||
|             alignSelf: message.isUser ? 'flex-end' : 'flex-start', | ||||
|             maxWidth: message.isUser ? '80%' : '100%', | ||||
|             alignSelf: message.isUser ? 'flex-end' : 'stretch', | ||||
|           }} | ||||
|         > | ||||
|           <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}> | ||||
| @ -91,7 +92,7 @@ export default function MessageList({ messages }: MessageListProps) { | ||||
|             component={Paper} | ||||
|             elevation={1} | ||||
|             sx={{ | ||||
|             p: 2, | ||||
|             p: 2.5, | ||||
|             flex: 1, | ||||
|             bgcolor: message.isUser ? 'primary.main' : 'background.paper', | ||||
|             color: message.isUser ? 'primary.contrastText' : 'text.primary', | ||||
| @ -154,15 +155,70 @@ export default function MessageList({ messages }: MessageListProps) { | ||||
|               } | ||||
|             } | ||||
|           }}> | ||||
|             <ReactMarkdown | ||||
|               className="markdown-body" | ||||
|               rehypePlugins={[rehypeHighlight]} | ||||
|             > | ||||
|               {message.text} | ||||
|             </ReactMarkdown> | ||||
|             {message.text.includes('<think>') ? ( | ||||
|               message.text.split(/<think>|<\/think>/).map((segment, index) => { | ||||
|                 if (index % 2 === 1) { // This is a thinking section
 | ||||
|                   // Calculate thinking time - assume 1 character = 0.1s
 | ||||
|                   const thinkingTime = (segment.length * 0.1).toFixed(1); | ||||
|                    | ||||
|                   return ( | ||||
|                     <div key={index} className="thinking-section"> | ||||
|                       <div  | ||||
|                         className="thinking-header"  | ||||
|                         onClick={() => { | ||||
|                           const content = document.getElementById(`thinking-content-${message.id}-${index}`); | ||||
|                           const indicator = document.getElementById(`thinking-indicator-${message.id}-${index}`); | ||||
|                           if (content && indicator) { | ||||
|                             content.classList.toggle('open'); | ||||
|                             indicator.classList.toggle('open'); | ||||
|                           } | ||||
|                         }} | ||||
|                       > | ||||
|                         <div className="thinking-header-left"> | ||||
|                           <ChevronRightIcon  | ||||
|                             id={`thinking-indicator-${message.id}-${index}`} | ||||
|                             className="indicator"  | ||||
|                           /> | ||||
|                           <span>Reasoning</span> | ||||
|                         </div> | ||||
|                         <span className="timestamp">(thought for {thinkingTime}s)</span> | ||||
|                       </div> | ||||
|                       <div  | ||||
|                         id={`thinking-content-${message.id}-${index}`} | ||||
|                         className="thinking-content" | ||||
|                       > | ||||
|                         {segment} | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   ); | ||||
|                 } else if (segment.includes('Sorry, there was an error processing your request.')) { | ||||
|                   return ( | ||||
|                     <div key={index} className="error-message"> | ||||
|                       {segment} | ||||
|                     </div> | ||||
|                   ); | ||||
|                 } | ||||
|                 return ( | ||||
|                   <ReactMarkdown | ||||
|                     key={index} | ||||
|                     className="markdown-body" | ||||
|                     rehypePlugins={[rehypeHighlight]} | ||||
|                   > | ||||
|                     {segment} | ||||
|                   </ReactMarkdown> | ||||
|                 ); | ||||
|               }) | ||||
|             ) : ( | ||||
|               <ReactMarkdown | ||||
|                 className="markdown-body" | ||||
|                 rehypePlugins={[rehypeHighlight]} | ||||
|               > | ||||
|                 {message.text} | ||||
|               </ReactMarkdown> | ||||
|             )} | ||||
|           </Box> | ||||
|           {message.sources && message.sources.length > 0 && ( | ||||
|             <Box sx={{ mt: 2, pt: 1, borderTop: '1px solid', borderColor: 'divider' }}> | ||||
|             <Box sx={{ mt: 1, pt: 0, borderTop: '1px solid', borderColor: 'divider' }}> | ||||
|               <Typography  | ||||
|                 variant="caption"  | ||||
|                 color="text.secondary" | ||||
|  | ||||
| @ -14,3 +14,94 @@ html, body { | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| .error-message { | ||||
|   width: 100%; | ||||
|   background-color: rgba(211, 47, 47, 0.1); | ||||
|   padding: 8px 12px; | ||||
|   border-radius: 4px; | ||||
|   margin-bottom: 8px; | ||||
|   color: #d32f2f; | ||||
|   border: 1px solid rgba(211, 47, 47, 0.2); | ||||
| } | ||||
| 
 | ||||
| [data-theme="dark"] .error-message { | ||||
|   background-color: rgba(211, 47, 47, 0.15); | ||||
|   color: #ef5350; | ||||
|   border-color: rgba(211, 47, 47, 0.3); | ||||
| } | ||||
| 
 | ||||
| .thinking-section { | ||||
|   border-radius: 8px; | ||||
|   margin: 0 0 8px 0; | ||||
|   overflow: hidden; | ||||
|   background-color: #1976d2; | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| [data-theme="dark"] .thinking-section { | ||||
|   background-color: #1565c0; | ||||
| } | ||||
| 
 | ||||
| .thinking-header { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: space-between; | ||||
|   padding: 12px 16px; | ||||
|   cursor: pointer; | ||||
|   user-select: none; | ||||
|   color: rgba(255, 255, 255, 0.9); | ||||
|   font-size: 0.9rem; | ||||
|   background-color: rgba(0, 0, 0, 0.1); | ||||
|   transition: background-color 0.2s ease; | ||||
| } | ||||
| 
 | ||||
| .thinking-header:hover { | ||||
|   background-color: rgba(0, 0, 0, 0.2); | ||||
| } | ||||
| 
 | ||||
| [data-theme="dark"] .thinking-header { | ||||
|   color: rgba(255, 255, 255, 0.9); | ||||
|   background-color: rgba(0, 0, 0, 0.2); | ||||
| } | ||||
| 
 | ||||
| [data-theme="dark"] .thinking-header:hover { | ||||
|   background-color: rgba(0, 0, 0, 0.3); | ||||
| } | ||||
| 
 | ||||
| .thinking-header-left { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 8px; | ||||
| } | ||||
| 
 | ||||
| .thinking-header .indicator { | ||||
|   width: 20px; | ||||
|   height: 20px; | ||||
|   transition: transform 0.3s ease; | ||||
|   color: rgba(255, 255, 255, 0.9); | ||||
| } | ||||
| 
 | ||||
| .thinking-header .indicator.open { | ||||
|   transform: rotate(90deg); | ||||
| } | ||||
| 
 | ||||
| .thinking-header .timestamp { | ||||
|   font-size: 0.85rem; | ||||
|   color: rgba(255, 255, 255, 0.7); | ||||
| } | ||||
| 
 | ||||
| .thinking-content { | ||||
|   max-height: 0; | ||||
|   overflow: hidden; | ||||
|   transition: max-height 0.3s ease-out; | ||||
|   padding: 0 16px; | ||||
|   color: rgba(255, 255, 255, 0.9); | ||||
|   line-height: 1.5; | ||||
|   background-color: rgba(0, 0, 0, 0.1); | ||||
| } | ||||
| 
 | ||||
| .thinking-content.open { | ||||
|   max-height: 2000px; | ||||
|   padding: 16px; | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user