This commit is contained in:
Damien 2025-02-05 10:05:22 -05:00
parent 2a3e60a16c
commit 0244ab851e
14 changed files with 504 additions and 112 deletions

View File

@ -1,3 +1,3 @@
FROM deepseek-r1:8b
FROM deepseek-r1:1.5b
SYSTEM You are a helpful AI assistant.
PARAMETER num_gpu 0

View File

@ -1,4 +1,4 @@
# Data Hound
# Data Identification Manager : Personal Edition
An Electron-based desktop application that uses AI to help you search and understand your files through natural language queries.

View File

@ -12,6 +12,8 @@ function createWindow() {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 1200, // Minimum width
minHeight: 800, // Minimum height
frame: false,
titleBarStyle: 'hidden',
webPreferences: {
@ -40,7 +42,7 @@ function createWindow() {
const response = await fetch(process.env.VITE_DEV_SERVER_URL);
if (response.ok) {
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
mainWindow.webContents.openDevTools();
//mainWindow.webContents.openDevTools();
} else {
setTimeout(pollDevServer, 500);
}

View File

@ -1,6 +1,7 @@
const { contextBridge, ipcRenderer } = require('electron');
// Import types
import type { IpcRendererEvent } from 'electron';
import type { LLMConfig, DocumentMetadata } from './types';
interface Directory {
@ -73,7 +74,7 @@ contextBridge.exposeInMainWorld('electron', {
checkModel: (modelName: string) => ipcRenderer.invoke('check-model', modelName),
pullModel: (modelName: string, onProgress: (status: string) => void) => {
const channel = `pull-model-progress-${modelName}`;
ipcRenderer.on(channel, (_event, status) => onProgress(status));
ipcRenderer.on(channel, (_event: IpcRendererEvent, status: string) => onProgress(status));
return ipcRenderer.invoke('pull-model', modelName).finally(() => {
ipcRenderer.removeListener(channel, onProgress);
});

View File

@ -12,7 +12,7 @@ export class LLMService {
): Promise<{ answer: string, sources: DocumentMetadata[] }> {
try {
const ollamaResponse = await ollamaService.chat({
model: 'damien113/datahound:latest',
model: 'damien113/datahound-gpu:8b',
messages: [{ role: 'user', content: question }],
temperature: 0.7,
onChunk,
@ -36,7 +36,7 @@ export class LLMService {
getConfig() {
return {
provider: 'ollama',
model: 'damien113/datahound:latest',
model: 'damien113/datahound-gpu:8b',
baseUrl: 'http://localhost:11434',
temperature: 0.7
};

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:;" />
<title>Data Hound</title>
<title>Data Identification Manager : Personal Edition</title>
</head>
<body>
<div id="root"></div>

View File

@ -41,14 +41,15 @@
"dist-electron/**/*"
],
"win": {
"target": "nsis"
"target": "portable"
},
"mac": {
"target": "dmg"
},
"linux": {
"target": "AppImage"
}
},
"forceCodeSigning": false
},
"devDependencies": {
"@types/node": "^20.11.16",

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Box, CssBaseline, ThemeProvider, createTheme, Tabs, Tab, TextField, IconButton } from '@mui/material';
import { Send as SendIcon, DeleteOutline as ClearIcon, Close as CloseIcon, Remove as MinimizeIcon, Fullscreen as MaximizeIcon } from '@mui/icons-material';
import { Box, CssBaseline, ThemeProvider, createTheme, Tabs, Tab, TextField, IconButton, Typography, Grow, Tooltip, Button, Chip } from '@mui/material';
import { Send as SendIcon, DeleteOutline as ClearIcon, Close as CloseIcon, Remove as MinimizeIcon, Fullscreen as MaximizeIcon, Download, UpgradeTwoTone, OpenInBrowser } from '@mui/icons-material';
import { useChat } from './hooks/useChat';
import ChatPanel from './components/ChatPanel';
import FileExplorer from './components/FileExplorer';
@ -97,6 +97,9 @@ function AppContent() {
await sendMessage(message);
};
const handleClick = () => {
window.open('https://data443.com', '_blank');
};
return (
<ThemeProvider theme={theme}>
<CssBaseline />
@ -111,7 +114,7 @@ function AppContent() {
{/* Custom titlebar */}
<Box
sx={{
height: '28px',
height: '40px',
bgcolor: '#2f2f2f',
display: 'flex',
alignItems: 'center',
@ -150,7 +153,133 @@ function AppContent() {
},
}}
>
<div className="window-controls">
<Button
variant="outlined"
size="small"
startIcon={<UpgradeTwoTone />}
onClick={(handleClick)}
sx={{
z:100,
'@keyframes flash': {
'0%': {
borderColor: 'primary.main',
color: 'primary.main',
boxShadow: '0 0 0 0 rgba(25, 118, 210, 0.4)'
},
'50%': {
borderColor: 'white',
color: 'white',
boxShadow: '0 0 0 4px rgba(25, 118, 210, 0)'
},
'100%': {
borderColor: 'primary.main',
color: 'primary.main',
boxShadow: '0 0 0 0 rgba(25, 118, 210, 0)'
}
},
width: '135px',
minHeight: '24px',
px: 2,
py: 0.5,
borderColor: 'white',
color: 'white',
textTransform: 'none',
fontWeight: 500,
fontSize: '0.8125rem',
lineHeight: 1.2,
animation: 'flash 2s infinite',
cursor: 'pointer',
'& .MuiSvgIcon-root': {
animation: 'flash 2s infinite'
},
'&:hover': {
borderColor: 'white',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
cursor: 'pointer'
}
}}
>
Upgrade
</Button>
<Button
variant="outlined"
size="small"
startIcon={<OpenInBrowser />}
onClick={(handleClick)}
sx={{
ml:2,
z:100,
'@keyframes flash': {
'0%': {
borderColor: 'primary.main',
color: 'primary.main',
boxShadow: '0 0 0 0 rgba(25, 118, 210, 0.4)'
},
'50%': {
borderColor: 'white',
color: 'white',
boxShadow: '0 0 0 4px rgba(25, 118, 210, 0)'
},
'100%': {
borderColor: 'primary.main',
color: 'primary.main',
boxShadow: '0 0 0 0 rgba(25, 118, 210, 0)'
}
},
width: '135px',
minHeight: '24px',
px: 2,
py: 0.5,
borderColor: 'white',
color: 'white',
textTransform: 'none',
fontWeight: 500,
fontSize: '0.8125rem',
lineHeight: 1.2,
animation: 'flash 2s infinite',
cursor: 'pointer',
'& .MuiSvgIcon-root': {
animation: 'flash 2s infinite'
},
'&:hover': {
borderColor: 'white',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
cursor: 'pointer'
}
}}
>
Enterprise
</Button>
<Typography
sx={{
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
width: 'auto',
textAlign: 'center',
display: 'flex',
alignItems: 'center',
gap: 1
}}
>
Data Identification Manager
</Typography>
<div className="window-controls">
<Chip
label="Personal Edition (Free)"
variant="outlined"
size="small"
sx={{
bgcolor: 'primary.main', // or 'secondary.main', 'error.main', etc.
color: 'primary.contrastText',
borderRadius: 1,
height: 'auto',
'& .MuiChip-label': {
padding: '4px 8px',
}
}}
/>
<button
className="control-button"
onClick={() => window.electron.closeWindow()}
@ -200,8 +329,9 @@ function AppContent() {
<Tab label="Home" />
<Tab label="Chat" />
<Tab label="Settings" />
<Tab label="Scanning" />
<Tab label="Reports" />
<Tab disabled label="Scanning" />
<Tab disabled label="Reports" />
<Tab disabled label="Cleanup" />
</Tabs>
</Box>
<Box sx={{
@ -264,13 +394,34 @@ function AppContent() {
>
<SendIcon />
</IconButton>
<IconButton
onClick={clearMessages}
color="error"
disabled={messages.length === 0}
>
<ClearIcon />
</IconButton>
<Tooltip title="Clear all messages"
arrow
placement="top">
<IconButton
onClick={clearMessages}
color="error"
disabled={messages.length === 0}
sx={{
'@keyframes glow': {
'0%': {
boxShadow: '0 0 0 0 rgba(211, 47, 47, 0.4)' // error color with opacity
},
'50%': {
boxShadow: '0 0 0 8px rgba(211, 47, 47, 0)'
},
'100%': {
boxShadow: '0 0 0 0 rgba(211, 47, 47, 0)'
}
},
animation: messages.length > 0 ? 'glow 2s infinite' : 'none',
'&:hover': {
animation: 'none' // Stop animation on hover
}
}}
>
<ClearIcon /> {/* Added back the icon */}
</IconButton>
</Tooltip>
</Box>
</Box>
</ThemeProvider>
@ -283,7 +434,7 @@ function App() {
return (
<ElectronProvider>
{!ollamaInstalled ? (
<OllamaCheck onInstalled={() => setOllamaInstalled(true)} />
<OllamaCheck onInstalled={(arg: boolean) => setOllamaInstalled(arg)} />
) : (
<AppContent />
)}

View File

@ -1,10 +1,11 @@
import React, { useEffect, useRef, useState, useMemo } from 'react';
import { Box, Typography, Paper, Avatar, IconButton } from '@mui/material';
import { Box, Typography, Paper, Avatar, IconButton, List, ListItem, ListItemIcon, ListItemText, useTheme } 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';
import 'highlight.js/styles/github.css';
import { Keyboard, Waves, Shield, DataObject, Folder, Lock } from '@mui/icons-material';
interface ChatMessage {
id: string;
@ -54,7 +55,11 @@ export default function MessageList({ messages }: MessageListProps) {
background: 'transparent',
},
}}>
{messages.map((message) => (
{messages.length>0 ? (
<>
{messages.map((message) => (
<Box
key={message.id}
sx={{
@ -85,14 +90,16 @@ export default function MessageList({ messages }: MessageListProps) {
fontWeight: 500
}}
>
{message.isUser ? 'User' : 'Data Hound'}
{message.isUser ? 'User' : 'Data Identification Manager'}
</Typography>
</Box>
<Box
component={Paper}
elevation={1}
sx={{
p: 2.5,
px: 2.5, // Keep horizontal padding
py: message.text.includes('<think>') ? 0 : 1.5, // Reduce vertical padding
pb: 1.5,
flex: 1,
bgcolor: message.isUser ? 'primary.main' : 'background.paper',
color: message.isUser ? 'primary.contrastText' : 'text.primary',
@ -106,56 +113,67 @@ export default function MessageList({ messages }: MessageListProps) {
>
<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'
wordBreak: 'break-word',
minHeight: '1.5em',
lineHeight: 1.6,
'& pre': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
padding: 0,
borderRadius: 1,
overflow: 'auto'
},
'& code': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
borderRadius: 1,
fontSize: '85%'
},
'& h1, & h2, & h3, & h4, & h5, & h6': {
marginTop: '0px', // Reduced from 24px
marginBottom: '0px', // Added margin bottom for headers
fontWeight: 600,
lineHeight: 1.25
},
'& p': {
marginTop: '0',
marginBottom: '0px' // Added margin bottom for paragraphs
},
'& 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 0px 0'
},
'& ul, & ol': {
paddingLeft: '2em',
marginTop: '0px', // Added margin top
marginBottom: '0px'
},
'& li': { // Added specific list item styling
marginBottom: '0px' // Add space between list items
},
'& li:last-child': { // Remove bottom margin from last list item
marginBottom: '0'
},
'& li > p': { // Adjust paragraph spacing within list items
marginBottom: '0px'
},
'& li > p:last-child': {
marginBottom: '0'
}
}
}}>
{message.text.includes('<think>') ? (
{message.text.includes('<think>') || message.text.length==0 ? (
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
@ -181,13 +199,19 @@ export default function MessageList({ messages }: MessageListProps) {
/>
<span>Reasoning</span>
</div>
<span className="timestamp">(thought for {thinkingTime}s)</span>
<span className="timestamp">(thought for {thinkingTime}ms)</span>
</div>
<div
id={`thinking-content-${message.id}-${index}`}
className="thinking-content"
>
<ReactMarkdown
key={index}
className="markdown-body"
rehypePlugins={[rehypeHighlight]}
>
{segment}
</ReactMarkdown>
</div>
</div>
);
@ -247,16 +271,178 @@ export default function MessageList({ messages }: MessageListProps) {
))}
</Box>
)}
<Typography
variant="caption"
color={message.isUser ? '#ffffff' : 'text.secondary'}
sx={{ display: 'block', mt: 0.5 }}
>
{new Date(message.timestamp).toLocaleTimeString()}
</Typography>
</Box>
</Box>
))}
</>
):(
<>
<WelcomeMessage/>
</>
)}
</Box>
);
}
const WelcomeMessage = () => {
const [text, setText] = useState('');
const fullText = 'Send a message...';
const [showCursor, setShowCursor] = useState(true);
const theme = useTheme();
useEffect(() => {
if (text.length < fullText.length) {
const timeout = setTimeout(() => {
setText(fullText.slice(0, text.length + 1));
}, 100);
return () => clearTimeout(timeout);
}
}, [text]);
useEffect(() => {
const cursorInterval = setInterval(() => {
setShowCursor(prev => !prev);
}, 530);
return () => clearInterval(cursorInterval);
}, []);
return (
<Paper
elevation={0}
sx={{
width: '100%',
maxWidth: '4xl',
mx: 'auto',
p: 4,
background: 'rgb(0,0,0,0)',
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3 }}>
{/* Animated Keyboard and Hands */}
<Box sx={{ position: 'relative' }}>
<Keyboard
style={{
width: 64,
height: 64,
color: theme.palette.primary.main,
animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -16,
left: '50%',
transform: 'translateX(-50%)'
}}
>
<Waves
style={{
width: 32,
height: 32,
color: theme.palette.primary.light,
animation: 'bounce 1s infinite'
}}
/>
</Box>
</Box>
{/* Typing Animation */}
<Typography
variant="h4"
sx={{
fontWeight: 600,
color: theme.palette.text.primary,
mt: 2
}}
>
{text}
<Box
component="span"
sx={{
ml: 0.5,
opacity: showCursor ? 1 : 0,
transition: 'opacity 0.3s'
}}
>
|
</Box>
</Typography>
{/* Example Questions */}
<List sx={{ width: '100%', mt: 4 }}>
<ListItem
component={Paper}
elevation={1}
sx={{
mb: 2,
borderRadius: 1,
transition: 'box-shadow 0.3s',
'&:hover': { boxShadow: 3 }
}}
>
<ListItemIcon>
<Folder style={{ color: theme.palette.primary.main }} />
</ListItemIcon>
<ListItemText
primary="Can you analyze my CSV file and create a visualization of the trends?"
sx={{ color: theme.palette.text.primary }}
/>
</ListItem>
<ListItem
component={Paper}
elevation={1}
sx={{
mb: 2,
borderRadius: 1,
transition: 'box-shadow 0.3s',
'&:hover': { boxShadow: 3 }
}}
>
<ListItemIcon>
<Shield style={{ color: theme.palette.success.main }} />
</ListItemIcon>
<ListItemText
primary="What security measures does Data443 implement for data protection?"
sx={{ color: theme.palette.text.primary }}
/>
</ListItem>
<ListItem
component={Paper}
elevation={1}
sx={{
mb: 2,
borderRadius: 1,
transition: 'box-shadow 0.3s',
'&:hover': { boxShadow: 3 }
}}
>
<ListItemIcon>
<Lock style={{ color: theme.palette.secondary.main }} />
</ListItemIcon>
<ListItemText
primary="How can Data443's products help with GDPR compliance?"
sx={{ color: theme.palette.text.primary }}
/>
</ListItem>
</List>
{/* Footer Note */}
<Typography
variant="body2"
sx={{
color: theme.palette.text.secondary,
textAlign: 'center',
mt: 3
}}
>
Ask me anything about your data, security needs, or Data443 products
</Typography>
</Box>
</Paper>
);
};

View File

@ -13,6 +13,9 @@ import {
Alert,
Box,
Typography,
InputAdornment,
IconButton,
TextField,
} from '@mui/material';
import {
Folder as FolderIcon,
@ -21,6 +24,8 @@ import {
Cloud as DropboxIcon,
Chat as DiscordIcon,
Computer as LocalIcon,
X,
Search,
} from '@mui/icons-material';
import { useElectron } from '../../hooks/useElectron';
@ -105,16 +110,48 @@ export default function DirectoryPicker({ onSelect }: DirectoryPickerProps) {
mr: 2,
mb: 2,
}}>
<Typography sx={{ pl:2 ,flexGrow: 1 }} variant="h6">Folders</Typography>
<Button
<TextField
fullWidth
size="small"
placeholder="Search..."
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search color="action" />
</InputAdornment>
),
// endAdornment: && (
// <InputAdornment position="end">
// <IconButton
// aria-label="clear search"
// edge="end"
// size="small"
// >
// <CloseIcon />
// </IconButton>
// </InputAdornment>
// )
}}
sx={{
pl:2,
pr:2,
maxWidth: 'md',
'& .MuiOutlinedInput-root': {
'&:hover fieldset': {
borderColor: 'primary.main',
},
},
}}
/> <Button
variant="contained"
onClick={handleOpen}
size="small"
sx={{
minWidth: 0,
width: '32px',
height: '32px',
borderRadius: '50%',
width: '40px',
height: '40px',
padding: 0,
overflow: 'hidden',
backgroundColor: 'primary.main',
@ -130,16 +167,15 @@ export default function DirectoryPicker({ onSelect }: DirectoryPickerProps) {
},
'&:hover': {
backgroundColor: 'primary.dark',
width: '180px',
borderRadius: '16px',
width: '400px',
'& .buttonText': {
width: '110px',
width: '300px',
marginLeft: '8px',
},
},
}}
>
<AddIcon sx={{ fontSize: 20 }} />
<AddIcon sx={{ ml:0.25, fontSize: 20 }} />
<span className="buttonText">Add New Folder</span>
</Button>
</Box>

View File

@ -15,7 +15,7 @@ const HomePanel = () => {
}}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h5" gutterBottom>Welcome to Data Hound</Typography>
<Typography variant="h5" gutterBottom>Welcome to Data Identification Manager</Typography>
<Typography variant="body1" paragraph>
This application helps you search through your files and interact with their contents using AI assistance.
</Typography>

View File

@ -19,7 +19,7 @@ const theme = createTheme({
});
interface OllamaCheckProps {
onInstalled: () => void;
onInstalled: (arg:boolean) => void;
}
interface OllamaStatus {
@ -37,7 +37,7 @@ export function OllamaCheck({ onInstalled }: OllamaCheckProps) {
const [modelStatus, setModelStatus] = useState<ModelStatus | null>(null);
const [isChecking, setIsChecking] = useState(true);
const [downloadProgress, setDownloadProgress] = useState<number>(0);
const MODEL_NAME = 'damien113/datahound:latest';
const MODEL_NAME = 'damien113/datahound-gpu:8b';
const checkOllama = async () => {
try {
@ -53,16 +53,20 @@ export function OllamaCheck({ onInstalled }: OllamaCheckProps) {
}
};
const checkModel = async () => {
const checkModel = async (retries = 3) => {
try {
const status = await window.electron.checkModel(MODEL_NAME);
setModelStatus(status);
if (status.installed) {
onInstalled();
onInstalled(true);
}
} catch (error) {
console.error('Error checking model:', error);
setModelStatus({ installed: false, installing: false });
if (retries > 0) {
setTimeout(() => checkModel(retries - 1), 2000);
} else {
setModelStatus({ installed: false, installing: false });
}
}
};
@ -72,6 +76,7 @@ export function OllamaCheck({ onInstalled }: OllamaCheckProps) {
try {
// Set initial installation state once at the start
setModelStatus({ installed: false, installing: true });
onInstalled(false);
setInstallError(null);
setDownloadProgress(0);
let downloadComplete = false;
@ -96,7 +101,7 @@ export function OllamaCheck({ onInstalled }: OllamaCheckProps) {
// Show verification screen for a moment before transitioning
await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second delay
setModelStatus({ installed: true, installing: false });
onInstalled();
onInstalled(true);
return;
}
@ -108,6 +113,8 @@ export function OllamaCheck({ onInstalled }: OllamaCheckProps) {
} catch (error) {
console.error('Error installing model:', error);
setModelStatus({ installed: false, installing: false });
onInstalled(false);
setInstallError(error instanceof Error ? error.message : 'Unknown error occurred');
setDownloadProgress(0);
}
@ -126,7 +133,7 @@ export function OllamaCheck({ onInstalled }: OllamaCheckProps) {
if (!ollamaStatus?.installed || !ollamaStatus?.running) {
checkOllama();
}
}, 5000);
}, 2000);
// Cleanup interval on unmount
return () => clearInterval(interval);
@ -236,7 +243,7 @@ export function OllamaCheck({ onInstalled }: OllamaCheckProps) {
color: 'text.primary',
}}>
<Typography variant="h5" sx={{ color: 'text.primary', mb: 1 }}>
{modelStatus?.installing ? 'Installing Data Hound AI Model' : 'AI Model Required'}
{modelStatus?.installing ? 'Installing Data Identification Manager AI Model' : 'AI Model Required'}
</Typography>
{installError && (
<Typography sx={{ color: 'error.main', mb: 2, textAlign: 'center' }}>
@ -284,7 +291,7 @@ export function OllamaCheck({ onInstalled }: OllamaCheckProps) {
fontSize: '1.1rem',
}}
>
Download Data Hound AI Model
Download Data Identification Manager AI Model
</Button>
)}
</Box>

View File

@ -33,7 +33,7 @@ html, body {
.thinking-section {
border-radius: 8px;
margin: 0 0 8px 0;
margin: 0 0 28px 0;
overflow: hidden;
background-color: #1976d2;
color: white;
@ -42,11 +42,10 @@ html, body {
[data-theme="dark"] .thinking-section {
background-color: #1565c0;
}
.thinking-header {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: space-between; /* Ensures left content stays left, timestamp stays right */
padding: 12px 16px;
cursor: pointer;
user-select: none;
@ -68,13 +67,17 @@ html, body {
[data-theme="dark"] .thinking-header:hover {
background-color: rgba(0, 0, 0, 0.3);
}
.markdown-body {
padding-top: ;
}
.thinking-header-left {
display: flex;
align-items: center;
gap: 8px;
gap: 8px; /* Controls spacing between chevron and "Reasoning" */
}
.thinking-header .indicator {
width: 20px;
height: 20px;
@ -87,8 +90,7 @@ html, body {
}
.thinking-header .timestamp {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.7);
margin-left: auto; /* Pushes it to the right */
}
.thinking-content {

View File

@ -5,17 +5,23 @@ import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: process.env.ELECTRON_VITE_DEV_SERVER_URL ? '/' : './',
base: './',
build: {
outDir: 'dist',
emptyOutDir: true,
target: 'esnext',
rollupOptions: {
external: ['http', 'https', 'path', 'fs', 'electron']
external: ['electron', 'electron-store', ...Object.keys(require('./package.json').dependencies)],
output: {
format: 'es',
entryFileNames: '[name].js',
chunkFileNames: '[name].js',
assetFileNames: '[name][extname]'
}
},
assetsDir: '.',
minify: true,
sourcemap: false
assetsInlineLimit: 0,
minify: 'esbuild',
sourcemap: true
},
resolve: {
alias: {