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
*.tsbuildinfo
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':
const ollamaResponse = await ollamaService.chat({
model: this.#config.model || 'phi4:latest',
model: this.#config.model || 'llama2:7b',
messages: [{ role: 'user', content: question }],
temperature: this.#config.temperature,
onChunk,
@ -195,7 +195,7 @@ export class LLMService {
const cleanConfig = {
provider: newConfig.provider,
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),
temperature: typeof newConfig.temperature === 'number' ? newConfig.temperature : 0.7
};

View File

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

View File

@ -17,6 +17,7 @@
"chokidar": "^3.5.3",
"electron-store": "^8.1.0",
"highlight.js": "^11.11.1",
"lucide-react": "^0.474.0",
"ollama": "^0.5.12",
"openai": "^4.82.0",
"openrouter-client": "^1.2.0",
@ -6546,6 +6547,15 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",

View File

@ -20,6 +20,7 @@
"chokidar": "^3.5.3",
"electron-store": "^8.1.0",
"highlight.js": "^11.11.1",
"lucide-react": "^0.474.0",
"ollama": "^0.5.12",
"openai": "^4.82.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">
<button
className="control-button"
@ -247,15 +259,48 @@ function AppContent() {
boxSizing: 'border-box'
}}
>
<TextField
fullWidth
variant="outlined"
placeholder="Ask a question..."
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isLoading}
size="small"
/>
<TextField
fullWidth
multiline
variant="outlined"
placeholder="Ask a question..."
value={input}
onChange={(e) => setInput(e.target.value)}
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
type="submit"
disabled={isLoading || !input.trim()}

View File

@ -1,9 +1,10 @@
import React, { useEffect, useRef } from 'react';
import { Box, Typography, Paper, Avatar } from '@mui/material';
import type { DocumentMetadata } from '../../../electron/types';
import React, { useEffect, useRef, useState } from 'react';
import { Box, Typography, Paper, Avatar, Collapse, IconButton } from '@mui/material';
import { Loader2, ChevronDown, ChevronUp } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github.css';
import type { DocumentMetadata } from '../../../electron/types';
interface ChatMessage {
id: string;
@ -18,8 +19,220 @@ interface MessageListProps {
messages: ChatMessage[];
}
export default function MessageList({ messages }: MessageListProps) {
const messageListRef = useRef<HTMLDivElement>(null);
interface ThinkingContentProps {
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(() => {
if (messageListRef.current) {
@ -49,11 +262,9 @@ export default function MessageList({ messages }: MessageListProps) {
background: (theme) => theme.palette.action.hover,
},
},
'&::-webkit-scrollbar-track': {
background: 'transparent',
},
}}>
{messages.map((message) => (
}}
>
{memoizedMessages.map((message) => (
<Box
key={message.id}
sx={{
@ -65,142 +276,45 @@ export default function MessageList({ messages }: MessageListProps) {
alignSelf: message.isUser ? 'flex-end' : 'flex-start',
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
<Avatar
src={message.isUser ? '/profiles/user-profile.webp' : '/profiles/ai-profile.webp'}
alt={message.isUser ? 'User' : 'AI'}
variant="square"
sx={{
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}
<Avatar
src={message.isUser ? '/profiles/user-profile.webp' : '/profiles/ai-profile.webp'}
alt={message.isUser ? 'User' : 'AI'}
sx={{ width: 40, height: 40 }}
/>
<Paper
elevation={1}
sx={{
p: 2,
flex: 1,
bgcolor: message.isUser ? 'primary.main' : 'background.paper',
color: message.isUser ? 'primary.contrastText' : 'text.primary',
transition: 'all 0.2s ease-in-out',
boxShadow: 1,
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 }}
px: 2,
py: 1.5,
flex: 1,
bgcolor: message.isUser ? 'primary.main' : 'background.paper',
color: message.isUser ? 'primary.contrastText' : 'text.primary',
borderRadius: 2,
}}
>
{new Date(message.timestamp).toLocaleTimeString()}
</Typography>
</Box>
<MessageContent text={message.text} />
{message.sources && message.sources.length > 0 && (
<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>
);
}
};
export default MessageList;

View File

@ -13,6 +13,8 @@ import {
Alert,
Box,
Typography,
TextField,
InputAdornment,
} from '@mui/material';
import {
Folder as FolderIcon,
@ -21,7 +23,10 @@ import {
Cloud as DropboxIcon,
Chat as DiscordIcon,
Computer as LocalIcon,
Search,
Padding
} from '@mui/icons-material';
import { useElectron } from '../../hooks/useElectron';
interface Directory {
@ -105,16 +110,46 @@ export default function DirectoryPicker({ onSelect }: DirectoryPickerProps) {
mr: 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"
onClick={handleOpen}
size="small"
sx={{
ml:3,
minWidth: 0,
width: '32px',
height: '32px',
borderRadius: '50%',
width: '64px',
height: '38px',
borderRadius: '5%',
padding: 0,
overflow: 'hidden',
backgroundColor: 'primary.main',
@ -130,10 +165,10 @@ export default function DirectoryPicker({ onSelect }: DirectoryPickerProps) {
},
'&:hover': {
backgroundColor: 'primary.dark',
width: '180px',
borderRadius: '16px',
width: '400%',
borderRadius: '5%',
'& .buttonText': {
width: '110px',
width: '150px',
marginLeft: '8px',
},
},

View File

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