This commit is contained in:
Damien 2025-02-04 13:38:48 -05:00
parent 086e311aa4
commit 1cebb80b08
10 changed files with 378 additions and 163 deletions

1
.gitignore vendored
View File

@ -39,3 +39,4 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
/electron-file-search/datahound-win32-x64

9
Modelfile Normal file
View File

@ -0,0 +1,9 @@
FROM deepseek-r1:1.5b
# Disable GPU usage
PARAMETER num_gpu 0
# Increase context length
PARAMETER max_tokens 16384
# Set temperature to 0.6
PARAMETER temperature 0.6

View File

@ -148,7 +148,7 @@ export class LLMService {
case 'ollama': case 'ollama':
const ollamaResponse = await ollamaService.chat({ const ollamaResponse = await ollamaService.chat({
model: this.#config.model || 'phi4:latest', model: this.#config.model || 'llama2:7b',
messages: [{ role: 'user', content: question }], messages: [{ role: 'user', content: question }],
temperature: this.#config.temperature, temperature: this.#config.temperature,
onChunk, onChunk,
@ -195,7 +195,7 @@ export class LLMService {
const cleanConfig = { const cleanConfig = {
provider: newConfig.provider, provider: newConfig.provider,
apiKey: newConfig.apiKey ?? null, apiKey: newConfig.apiKey ?? null,
model: newConfig.model ?? (newConfig.provider === 'ollama' ? 'phi4' : null), model: newConfig.model ?? (newConfig.provider === 'ollama' ? 'llama2:7b' : null),
baseUrl: newConfig.provider === 'ollama' ? (newConfig.baseUrl ?? 'http://localhost:11434') : (newConfig.baseUrl ?? null), baseUrl: newConfig.provider === 'ollama' ? (newConfig.baseUrl ?? 'http://localhost:11434') : (newConfig.baseUrl ?? null),
temperature: typeof newConfig.temperature === 'number' ? newConfig.temperature : 0.7 temperature: typeof newConfig.temperature === 'number' ? newConfig.temperature : 0.7
}; };

View File

@ -58,7 +58,7 @@ const schema: Store.Schema<Schema> = {
additionalProperties: false, additionalProperties: false,
default: { default: {
provider: 'ollama', provider: 'ollama',
model: 'phi4', model: 'llama2:7b',
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
temperature: 0.7 temperature: 0.7
} }

View File

@ -17,6 +17,7 @@
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"lucide-react": "^0.474.0",
"ollama": "^0.5.12", "ollama": "^0.5.12",
"openai": "^4.82.0", "openai": "^4.82.0",
"openrouter-client": "^1.2.0", "openrouter-client": "^1.2.0",
@ -6546,6 +6547,15 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lucide-react": {
"version": "0.474.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.474.0.tgz",
"integrity": "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/matcher": { "node_modules/matcher": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",

View File

@ -20,6 +20,7 @@
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"lucide-react": "^0.474.0",
"ollama": "^0.5.12", "ollama": "^0.5.12",
"openai": "^4.82.0", "openai": "^4.82.0",
"openrouter-client": "^1.2.0", "openrouter-client": "^1.2.0",

View File

@ -149,6 +149,18 @@ function AppContent() {
}, },
}} }}
> >
<Box
sx={{
display: 'flex',
flex: 1,
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
}}
>
Data Hound V2
</Box>
<div className="window-controls"> <div className="window-controls">
<button <button
className="control-button" className="control-button"
@ -247,15 +259,48 @@ function AppContent() {
boxSizing: 'border-box' boxSizing: 'border-box'
}} }}
> >
<TextField <TextField
fullWidth fullWidth
variant="outlined" multiline
placeholder="Ask a question..." variant="outlined"
value={input} placeholder="Ask a question..."
onChange={(e) => setInput(e.target.value)} value={input}
disabled={isLoading} onChange={(e) => setInput(e.target.value)}
size="small" disabled={isLoading}
/> size="small"
minRows={1}
maxRows={4}
onKeyDown={(event) => {
if (event.key === 'Enter') {
if (event.shiftKey) {
return; // Allow new line with Shift+Enter
}
event.preventDefault();
handleSubmit(event);
}
}}
InputProps={{
style: {
resize: 'vertical'
},
sx: {
'&::-webkit-scrollbar': {
width: '8px',
background: 'transparent',
},
'&::-webkit-scrollbar-thumb': {
background: (theme) => theme.palette.primary.main, // Using your theme's primary blue
borderRadius: '2px', // Made more square
'&:hover': {
background: (theme) => theme.palette.primary.light, // Lighter blue on hover
},
},
'&::-webkit-scrollbar-track': {
background: 'transparent',
},
}
}}
/>
<IconButton <IconButton
type="submit" type="submit"
disabled={isLoading || !input.trim()} disabled={isLoading || !input.trim()}

View File

@ -1,9 +1,10 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Box, Typography, Paper, Avatar } from '@mui/material'; import { Box, Typography, Paper, Avatar, Collapse, IconButton } from '@mui/material';
import type { DocumentMetadata } from '../../../electron/types'; import { Loader2, ChevronDown, ChevronUp } from 'lucide-react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight'; import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github.css'; import 'highlight.js/styles/github.css';
import type { DocumentMetadata } from '../../../electron/types';
interface ChatMessage { interface ChatMessage {
id: string; id: string;
@ -18,8 +19,220 @@ interface MessageListProps {
messages: ChatMessage[]; messages: ChatMessage[];
} }
export default function MessageList({ messages }: MessageListProps) { interface ThinkingContentProps {
const messageListRef = useRef<HTMLDivElement>(null); content: string;
isThinking: boolean;
}
interface MessageSegment {
type: 'text' | 'thinking';
content: string;
inProgress?: boolean;
}
interface MessageContentProps {
text: string;
}
const ThinkingContent: React.FC<ThinkingContentProps> = ({ content, isThinking }) => {
const [isExpanded, setIsExpanded] = useState<boolean>(false);
return (
<Box sx={{ mt: 1 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
cursor: 'pointer',
color: 'text.secondary'
}}
onClick={() => setIsExpanded(!isExpanded)}
>
<IconButton size="small">
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</IconButton>
<Typography variant="caption" sx={{ fontWeight: 500 }}>
Thinking Process
</Typography>
{isThinking && (
<Box
component={Loader2}
size={16}
className="animate-spin"
sx={{
animation: 'spin 1s linear infinite',
'@keyframes spin': {
'0%': {
transform: 'rotate(0deg)',
},
'100%': {
transform: 'rotate(360deg)',
},
},
}}
/>
)}
</Box>
<Collapse in={isExpanded}>
<Box sx={{
pl: 4,
pr: 2,
py: 1,
mt: 1,
borderLeft: '2px solid',
borderColor: 'divider',
color: 'text.secondary'
}}>
<ReactMarkdown
className="markdown-body"
rehypePlugins={[rehypeHighlight]}
>
{content}
</ReactMarkdown>
</Box>
</Collapse>
</Box>
);
};
const MessageContent: React.FC<MessageContentProps> = ({ text }) => {
const [segments, setSegments] = useState<MessageSegment[]>([]);
const [isThinking, setIsThinking] = useState<boolean>(false);
useEffect(() => {
const parseContent = (): void => {
const parts: MessageSegment[] = [];
let remainingText = text;
// Handle <think> tags
const thinkRegex = /<think>([\s\S]*?)(?:<\/think>|$)/g;
let lastIndex = 0;
let match;
while ((match = thinkRegex.exec(text)) !== null) {
// Add text before the think tag if there is any
const beforeText = text.slice(lastIndex, match.index).trim();
if (beforeText) {
parts.push({
type: 'text',
content: beforeText
});
}
// Add the thinking content
parts.push({
type: 'thinking',
content: match[1],
inProgress: !match[0].endsWith('</think>')
});
lastIndex = match.index + match[0].length;
}
// Add any remaining text after the last think tag
const afterText = text.slice(lastIndex).trim();
if (afterText) {
parts.push({
type: 'text',
content: afterText
});
}
setSegments(parts);
setIsThinking(parts.some(part => part.type === 'thinking' && part.inProgress));
};
parseContent();
}, [text]);
return (
<Box>
{segments.map((segment, index) => {
if (segment.type === 'text' && segment.content.trim()) {
return (
<Box key={index}>
<Box sx={{
'& .markdown-body': {
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
minHeight: '1.5em',
lineHeight: 1.6,
'& pre': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
padding: 2,
borderRadius: 1,
overflow: 'auto'
},
'& code': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
padding: '0.2em 0.4em',
borderRadius: 1,
fontSize: '85%'
},
'& h1, & h2, & h3, & h4, & h5, & h6': {
marginTop: '24px',
marginBottom: '16px',
fontWeight: 600,
lineHeight: 1.25
},
'& p': {
marginTop: '0',
marginBottom: '16px'
},
'& a': {
color: (theme) => theme.palette.primary.main,
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline'
}
},
'& img': {
maxWidth: '100%',
height: 'auto'
},
'& blockquote': {
padding: '0 1em',
color: (theme) => theme.palette.text.secondary,
borderLeft: (theme) => `0.25em solid ${theme.palette.divider}`,
margin: '0 0 16px 0'
},
'& ul, & ol': {
paddingLeft: '2em',
marginBottom: '16px'
}
}
}}>
<ReactMarkdown
className="markdown-body"
rehypePlugins={[rehypeHighlight]}
>
{segment.content}
</ReactMarkdown>
</Box>
</Box>
);
}
if (segment.type === 'thinking') {
return (
<Box key={index}>
<ThinkingContent
content={segment.content}
isThinking={segment.inProgress || false}
/>
</Box>
);
}
return null;
})}
</Box>
);
};
const MessageList: React.FC<MessageListProps> = ({ messages }) => {
// Memoize messages to preserve their original timestamps
const memoizedMessages = React.useMemo(() => messages, [messages]);
const messageListRef = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
if (messageListRef.current) { if (messageListRef.current) {
@ -49,11 +262,9 @@ export default function MessageList({ messages }: MessageListProps) {
background: (theme) => theme.palette.action.hover, background: (theme) => theme.palette.action.hover,
}, },
}, },
'&::-webkit-scrollbar-track': { }}
background: 'transparent', >
}, {memoizedMessages.map((message) => (
}}>
{messages.map((message) => (
<Box <Box
key={message.id} key={message.id}
sx={{ sx={{
@ -65,142 +276,45 @@ export default function MessageList({ messages }: MessageListProps) {
alignSelf: message.isUser ? 'flex-end' : 'flex-start', alignSelf: message.isUser ? 'flex-end' : 'flex-start',
}} }}
> >
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}> <Avatar
<Avatar src={message.isUser ? '/profiles/user-profile.webp' : '/profiles/ai-profile.webp'}
src={message.isUser ? '/profiles/user-profile.webp' : '/profiles/ai-profile.webp'} alt={message.isUser ? 'User' : 'AI'}
alt={message.isUser ? 'User' : 'AI'} sx={{ width: 40, height: 40 }}
variant="square" />
sx={{ <Paper
width: 40,
height: 40,
boxShadow: 1
}}
/>
<Typography
variant="caption"
sx={{
fontSize: '0.75rem',
color: 'text.secondary',
fontWeight: 500
}}
>
{message.isUser ? 'User' : 'Data Hound'}
</Typography>
</Box>
<Box
component={Paper}
elevation={1} elevation={1}
sx={{ sx={{
p: 2, px: 2,
flex: 1, py: 1.5,
bgcolor: message.isUser ? 'primary.main' : 'background.paper', flex: 1,
color: message.isUser ? 'primary.contrastText' : 'text.primary', bgcolor: message.isUser ? 'primary.main' : 'background.paper',
transition: 'all 0.2s ease-in-out', color: message.isUser ? 'primary.contrastText' : 'text.primary',
boxShadow: 1, borderRadius: 2,
borderRadius: 2, }}
'&:hover': {
boxShadow: 2,
},
}}
>
<Box sx={{
'& .markdown-body': {
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
minHeight: '1.5em',
lineHeight: 1.6,
'& pre': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
padding: 2,
borderRadius: 1,
overflow: 'auto'
},
'& code': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
padding: '0.2em 0.4em',
borderRadius: 1,
fontSize: '85%'
},
'& h1, & h2, & h3, & h4, & h5, & h6': {
marginTop: '24px',
marginBottom: '16px',
fontWeight: 600,
lineHeight: 1.25
},
'& p': {
marginTop: '0',
marginBottom: '16px'
},
'& a': {
color: (theme) => theme.palette.primary.main,
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline'
}
},
'& img': {
maxWidth: '100%',
height: 'auto'
},
'& blockquote': {
padding: '0 1em',
color: (theme) => theme.palette.text.secondary,
borderLeft: (theme) => `0.25em solid ${theme.palette.divider}`,
margin: '0 0 16px 0'
},
'& ul, & ol': {
paddingLeft: '2em',
marginBottom: '16px'
}
}
}}>
<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' }}>
<Typography
variant="caption"
color="text.secondary"
sx={{
display: 'block',
mb: 0.5,
fontWeight: 'medium'
}}
>
Sources:
</Typography>
{message.sources.map((source, index) => (
<Typography
key={index}
variant="caption"
component="div"
color="text.secondary"
sx={{
pl: 1,
borderLeft: '2px solid',
borderColor: 'divider'
}}
>
{source.path}
</Typography>
))}
</Box>
)}
<Typography
variant="caption"
color={message.isUser ? '#ffffff' : 'text.secondary'}
sx={{ display: 'block', mt: 0.5 }}
> >
{new Date(message.timestamp).toLocaleTimeString()} <MessageContent text={message.text} />
</Typography> {message.sources && message.sources.length > 0 && (
</Box> <Box sx={{ mt: 2, pt: 1, borderTop: '1px solid', borderColor: 'divider' }}>
<Typography variant="caption" color="text.secondary">
Sources:
</Typography>
{message.sources.map((source, index) => (
<Typography
key={index}
variant="caption"
component="div"
color="text.secondary"
>
{source.path}
</Typography>
))}
</Box>
)}
</Paper>
</Box> </Box>
))} ))}
</Box> </Box>
); );
} };
export default MessageList;

View File

@ -13,6 +13,8 @@ import {
Alert, Alert,
Box, Box,
Typography, Typography,
TextField,
InputAdornment,
} from '@mui/material'; } from '@mui/material';
import { import {
Folder as FolderIcon, Folder as FolderIcon,
@ -21,7 +23,10 @@ import {
Cloud as DropboxIcon, Cloud as DropboxIcon,
Chat as DiscordIcon, Chat as DiscordIcon,
Computer as LocalIcon, Computer as LocalIcon,
Search,
Padding
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useElectron } from '../../hooks/useElectron'; import { useElectron } from '../../hooks/useElectron';
interface Directory { interface Directory {
@ -105,16 +110,46 @@ export default function DirectoryPicker({ onSelect }: DirectoryPickerProps) {
mr: 2, mr: 2,
mb: 2, mb: 2,
}}> }}>
<Typography sx={{ pl:2 ,flexGrow: 1 }} variant="h6">Folders</Typography>
<Button <TextField
fullWidth
variant="outlined"
size="small"
placeholder="Search..."
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search className="text-gray-400"/>
</InputAdornment>
),
sx: {ml:2,
'&::-webkit-scrollbar': {
width: '8px',
background: 'transparent',
},
'&::-webkit-scrollbar-thumb': {
background: '#2196f3',
borderRadius: '2px',
'&:hover': {
background: '#64b5f6',
},
},
'&::-webkit-scrollbar-track': {
background: 'transparent',
}
}
}}
/> <Button
variant="contained" variant="contained"
onClick={handleOpen} onClick={handleOpen}
size="small" size="small"
sx={{ sx={{
ml:3,
minWidth: 0, minWidth: 0,
width: '32px', width: '64px',
height: '32px', height: '38px',
borderRadius: '50%', borderRadius: '5%',
padding: 0, padding: 0,
overflow: 'hidden', overflow: 'hidden',
backgroundColor: 'primary.main', backgroundColor: 'primary.main',
@ -130,10 +165,10 @@ export default function DirectoryPicker({ onSelect }: DirectoryPickerProps) {
}, },
'&:hover': { '&:hover': {
backgroundColor: 'primary.dark', backgroundColor: 'primary.dark',
width: '180px', width: '400%',
borderRadius: '16px', borderRadius: '5%',
'& .buttonText': { '& .buttonText': {
width: '110px', width: '150px',
marginLeft: '8px', marginLeft: '8px',
}, },
}, },

View File

@ -22,7 +22,7 @@ export function useLLMConfig() {
}); });
setConfig(config || { setConfig(config || {
provider: 'ollama', provider: 'ollama',
model: 'phi4', model: 'llama2:7b',
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
temperature: 0.7, temperature: 0.7,
apiKey: undefined apiKey: undefined