Compare commits

...

15 Commits
temp ... master

Author SHA1 Message Date
2b7d254243 Add README.MD 2025-02-24 07:36:42 +00:00
e5389518a1 Delete electron-file-search/README.md 2025-02-24 07:36:31 +00:00
458ea75bb6 Update fileSystem.ts 2025-02-10 13:12:12 -05:00
29f76c9e9d search 2025-02-09 03:10:26 -05:00
7a24d3cb98 f 2025-02-09 02:09:16 -05:00
b724e61d7f Update llmService.ts 2025-02-07 23:40:27 -05:00
9bc7a905f0 fix 2025-02-07 23:34:43 -05:00
9785616fce Merge branch 'master' of https://github.com/D4M13N-D3V/DataHound 2025-02-05 10:05:58 -05:00
9f00eeb8e2 Update Modelfile 2025-02-05 10:05:40 -05:00
0244ab851e fix 2025-02-05 10:05:22 -05:00
2a3e60a16c Update package.json 2025-02-04 13:55:31 -05:00
c7b831668e Update ollamaService.ts 2025-02-04 13:44:59 -05:00
434af6e48a Merge branch 'master' of https://github.com/D4M13N-D3V/DataHound 2025-02-04 13:40:03 -05:00
512e140b80 fixffsd 2025-02-04 13:39:50 -05:00
6c868bf332 Update .gitignore 2025-02-04 13:39:15 -05:00
35 changed files with 2355 additions and 236 deletions

2
.gitignore vendored
View File

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

View File

@ -1,3 +1,45 @@
FROM deepseek-r1:8b FROM deepseek-r1:8b
SYSTEM You are a helpful AI assistant. PARAMETER temperature 1.0
PARAMETER num_gpu 0
PARAMETER repeat_penalty 1.35
PARAMETER top_p 0.8
PARAMETER stop <begin▁of▁sentence>
PARAMETER stop <end▁of▁sentence>
PARAMETER stop <User>
PARAMETER stop <Assistant>
TEMPLATE """{{- if .System }}{{ .System }}{{ end }}
{{- range $i, $_ := .Messages }}
{{- $last := eq (len (slice $.Messages $i)) 1}}
{{- if eq .Role "user" }}<User>{{ .Content }}
{{- else if eq .Role "assistant" }}<Assistant>{{ .Content }}{{- if not $last }}<end▁of▁sentence>{{- end }}
{{- end }}
{{- if and $last (ne .Role "assistant") }}<Assistant>{{- end }}
{{- end }}"""
SYSTEM """You are Data Hound, an AI assistant developed by Data443 Risk Mitigation. You specialize in data security, compliance, and risk mitigation. Your primary role is to assist customers with questions related to data security, management, compliance, and protection.
Never say your name is DeepSeek-R1-Lite-Preview or that you are made by DeepSeek. You are Data Hound, an AI assistant developed by Data443 Risk Mitigation.
When answering questions, provide clear and concise reasoning. Clarify technical details when necessary and offer brief examples to enhance understanding. If a user asks about your origin, you should state that you were created by Data443 Risk Mitigation.
Your expertise covers:
- Data classification and tagging
- Data encryption and secure storage
- Regulatory compliance (GDPR, CCPA, HIPAA, etc.)
- Secure data migration and management
- Data breach prevention and risk assessment
If a user asks for best practices, explain them in a structured manner. If a user needs guidance on a specific security concern, offer step-by-step solutions."""

View File

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

View File

@ -0,0 +1 @@
1.10.2

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
cd5f7d5b-1d5c-40ec-bb49-885d5809f14f

Binary file not shown.

Binary file not shown.

View File

@ -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 // Model Operations
ipcMain.handle('check-model', async (_: unknown, modelName: string) => { ipcMain.handle('check-model', async (_: unknown, modelName: string) => {
try { try {
@ -209,6 +221,84 @@ export function setupIpcHandlers() {
return { success: false, error: err.message }; return { success: false, error: err.message };
} }
}); });
// Meilisearch Handlers
ipcMain.handle('meilisearch-add-documents', async (_: unknown, indexName: string, documents: any[]) => {
try {
await (global as any).meilisearchService.addDocuments(indexName, documents);
return { success: true };
} catch (error) {
const err = error as Error;
console.error('Error adding documents to Meilisearch:', err);
return { success: false, error: err.message };
}
});
ipcMain.handle('meilisearch-remove-documents', async (_: unknown, indexName: string, documentIds: string[]) => {
try {
await (global as any).meilisearchService.removeDocuments(indexName, documentIds);
return { success: true };
} catch (error) {
const err = error as Error;
console.error('Error removing documents from Meilisearch:', err);
return { success: false, error: err.message };
}
});
ipcMain.handle('meilisearch-update-documents', async (_: unknown, indexName: string, documents: any[]) => {
try {
await (global as any).meilisearchService.updateDocuments(indexName, documents);
return { success: true };
} catch (error) {
const err = error as Error;
console.error('Error updating documents in Meilisearch:', err);
return { success: false, error: err.message };
}
});
ipcMain.handle('meilisearch-create-index', async (_: unknown, indexName: string) => {
try {
await (global as any).meilisearchService.createIndex(indexName);
return { success: true };
} catch (error) {
const err = error as Error;
console.error('Error creating index in Meilisearch:', err);
return { success: false, error: err.message };
}
});
ipcMain.handle('meilisearch-delete-index', async (_: unknown, indexName: string) => {
try {
await (global as any).meilisearchService.deleteIndex(indexName);
return { success: true };
} catch (error) {
const err = error as Error;
console.error('Error deleting index in Meilisearch:', err);
return { success: false, error: err.message };
}
});
ipcMain.handle('meilisearch-get-documents', async (_: unknown, indexName: string) => {
try {
const documents = await (global as any).meilisearchService.getDocuments(indexName);
return { success: true, data: documents };
} catch (error) {
const err = error as Error;
console.error('Error getting documents from Meilisearch:', err);
return { success: false, error: err.message };
}
});
ipcMain.handle('meilisearch-search', async (_: unknown, indexName: string, query: string) => {
try {
const results = await (global as any).meilisearchService.search(indexName, query);
return { success: true, data: results };
} catch (error) {
const err = error as Error;
console.error('Error searching Meilisearch:', err);
return { success: false, error: err.message };
}
});
} }
export default setupIpcHandlers; export default setupIpcHandlers;

View File

@ -4,6 +4,20 @@ const os = require('os');
const spawn = require('child_process').spawn; const spawn = require('child_process').spawn;
const { store: electronStore } = require('./store'); const { store: electronStore } = require('./store');
const { setupIpcHandlers } = require('./ipc/handlers'); const { setupIpcHandlers } = require('./ipc/handlers');
const { MeiliSearch } = require('meilisearch');
const slugify = (str: string) => {
return str
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
};
const { platform } = require('os');
import MeilisearchService from './services/meilisearchService';
const meilisearchService = new MeilisearchService();
(global as any).meilisearchService = meilisearchService;
// Initialize IPC handlers immediately // Initialize IPC handlers immediately
setupIpcHandlers(); setupIpcHandlers();
@ -12,6 +26,8 @@ function createWindow() {
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: 1200, width: 1200,
height: 800, height: 800,
minWidth: 1200, // Minimum width
minHeight: 800, // Minimum height
frame: false, frame: false,
titleBarStyle: 'hidden', titleBarStyle: 'hidden',
webPreferences: { webPreferences: {
@ -40,7 +56,7 @@ function createWindow() {
const response = await fetch(process.env.VITE_DEV_SERVER_URL); const response = await fetch(process.env.VITE_DEV_SERVER_URL);
if (response.ok) { if (response.ok) {
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL); mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
mainWindow.webContents.openDevTools(); //mainWindow.webContents.openDevTools();
} else { } else {
setTimeout(pollDevServer, 500); setTimeout(pollDevServer, 500);
} }
@ -121,6 +137,7 @@ function createWindow() {
} }
app.whenReady().then(() => { app.whenReady().then(() => {
meilisearchService.startServer();
createWindow(); createWindow();
app.on('activate', () => { app.on('activate', () => {
@ -161,46 +178,27 @@ ipcMain.handle('window-close', (event) => {
window?.close(); 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) => { ipcMain.handle('open-external', (_, url) => {
return require('electron').shell.openExternal(url); return require('electron').shell.openExternal(url);
}); });
// Launch the server
if (process.env.NODE_ENV !== 'development') {
const serverProcess = spawn('node', [path.join(__dirname, '../scripts/dev.cjs')], {
stdio: 'inherit',
});
serverProcess.on('close', (code) => {
console.log(`Server process exited with code ${code}`);
});
serverProcess.on('error', (err) => {
console.error('Failed to start server process:', err);
});
}
app.on('will-quit', () => {
meilisearchService.stopServer();
});
module.exports = app; module.exports = app;

View File

@ -1,6 +1,7 @@
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
// Import types // Import types
import type { IpcRendererEvent } from 'electron';
import type { LLMConfig, DocumentMetadata } from './types'; import type { LLMConfig, DocumentMetadata } from './types';
interface Directory { interface Directory {
@ -25,6 +26,11 @@ contextBridge.exposeInMainWorld('electron', {
addExcludedPath: async (path: string): Promise<void> => addExcludedPath: async (path: string): Promise<void> =>
ipcRenderer.invoke('add-excluded-path', path), ipcRenderer.invoke('add-excluded-path', path),
createIndex: async (folderPath: string): Promise<{
answer: string;
sources: DocumentMetadata[];
}> => ipcRenderer.invoke('meilisearch-create-index', folderPath),
// LLM Operations // LLM Operations
queryLLM: async (question: string): Promise<{ queryLLM: async (question: string): Promise<{
answer: string; answer: string;
@ -65,23 +71,30 @@ contextBridge.exposeInMainWorld('electron', {
ipcRenderer.removeListener(channel, callback); 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), openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
// Model Operations // Model Operations
checkModel: (modelName: string) => ipcRenderer.invoke('check-model', modelName), checkModel: (modelName: string) => ipcRenderer.invoke('check-model', modelName),
pullModel: (modelName: string, onProgress: (status: string) => void) => { pullModel: (modelName: string, onProgress: (status: string) => void) => {
const channel = `pull-model-progress-${modelName}`; 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(() => { return ipcRenderer.invoke('pull-model', modelName).finally(() => {
ipcRenderer.removeListener(channel, onProgress); ipcRenderer.removeListener(channel, onProgress);
}); });
}, },
meilisearchSearch: async (indexName: string, query: string): Promise<{ success: boolean; data: any[] }> =>
ipcRenderer.invoke('meilisearch-search', indexName, query),
// Window Controls // Window Controls
minimizeWindow: () => ipcRenderer.invoke('window-minimize'), minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
maximizeWindow: () => ipcRenderer.invoke('window-maximize'), maximizeWindow: () => ipcRenderer.invoke('window-maximize'),
closeWindow: () => ipcRenderer.invoke('window-close'), closeWindow: () => ipcRenderer.invoke('window-close'),
send: (channel: string, data: any) => {
ipcRenderer.send(channel, data);
}
}); });
// Export types for TypeScript // Export types for TypeScript

View File

@ -2,6 +2,11 @@ import { FSWatcher } from 'chokidar';
import * as chokidar from 'chokidar'; import * as chokidar from 'chokidar';
import Store from 'electron-store'; import Store from 'electron-store';
import { ServiceError } from '../types'; import { ServiceError } from '../types';
import * as path from 'path';
const fs = require('fs');
const fsPromises = fs.promises;
import * as crypto from 'crypto';
import MeilisearchService from './meilisearchService';
const store = new Store<{ const store = new Store<{
watchedPaths: string[]; watchedPaths: string[];
@ -11,10 +16,13 @@ const store = new Store<{
class FileSystemService { class FileSystemService {
private watchers: Map<string, FSWatcher>; private watchers: Map<string, FSWatcher>;
private excludedPaths: Set<string>; private excludedPaths: Set<string>;
private meilisearchService: MeilisearchService;
private readonly indexName = 'files';
constructor() { constructor() {
this.watchers = new Map(); this.watchers = new Map();
this.excludedPaths = new Set(store.get('excludedPaths', [])); this.excludedPaths = new Set(store.get('excludedPaths', []));
this.meilisearchService = new MeilisearchService();
// Add example paths // Add example paths
const examplePaths = [ const examplePaths = [
@ -32,6 +40,49 @@ class FileSystemService {
}); });
} }
private slugify(filePath: string): string {
return filePath
.replace(/[\\/]/g, '-') // Replace path separators with dashes
.replace(/[^a-zA-Z0-9_-]+/g, '') // Remove non-alphanumeric characters
.toLowerCase();
}
private async calculateFileHash(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
try {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
if (!stream) {
reject(new Error(`Failed to create read stream for ${filePath}`));
return;
}
stream.on('data', (data: any) => {
try {
hash.update(data);
} catch (dataError) {
reject(new Error(`Failed to update hash with data for ${filePath}: ${dataError}`));
}
});
stream.on('end', () => {
try {
resolve(hash.digest('hex'));
} catch (digestError) {
reject(new Error(`Failed to digest hash for ${filePath}: ${digestError}`));
}
});
stream.on('error', (streamError: any) => {
reject(new Error(`Read stream error for ${filePath}: ${streamError}`));
});
} catch (creationError: any) {
reject(new Error(`Failed to create read stream or hash for ${filePath}: ${creationError}`));
}
});
}
public async startWatching(dirPath: string): Promise<void> { public async startWatching(dirPath: string): Promise<void> {
if (this.watchers.has(dirPath)) { if (this.watchers.has(dirPath)) {
throw new ServiceError(`Already watching directory: ${dirPath}`); throw new ServiceError(`Already watching directory: ${dirPath}`);
@ -39,7 +90,7 @@ class FileSystemService {
const watcher = chokidar.watch(dirPath, { const watcher = chokidar.watch(dirPath, {
ignored: [ ignored: [
/(^|[\/\\])\../, // Ignore dotfiles /(^|[\\/])\../, // Ignore dotfiles
'**/node_modules/**', '**/node_modules/**',
...Array.from(this.excludedPaths), ...Array.from(this.excludedPaths),
], ],
@ -47,9 +98,91 @@ class FileSystemService {
ignoreInitial: false, ignoreInitial: false,
}); });
watcher.on('add', path => { const indexName = this.slugify(dirPath);
console.log(`File ${path} has been added`);
// TODO: Process file // Queue for files to be added to Meilisearch
const fileQueue: string[] = [];
const MAX_QUEUE_SIZE = 1000;
let isProcessingQueue = false;
const processFileQueue = async () => {
if (isProcessingQueue) return;
isProcessingQueue = true;
while (fileQueue.length > 0) {
const batch = fileQueue.splice(0, 100); // Get the first 100 files
const documents = [];
for (const filePath of batch) {
try {
const stats = await fs.promises.stat(filePath);
const slug = this.slugify(filePath);
const fileContent = await fs.promises.readFile(filePath, 'utf-8');
const fileExtension = path.extname(filePath);
const fileName = path.basename(filePath);
const permissions = {
read: !!(stats.mode & fs.constants.S_IRUSR),
write: !!(stats.mode & fs.constants.S_IWUSR),
execute: !!(stats.mode & fs.constants.S_IXUSR),
};
const document = {
id: slug,
name: filePath,
fileName: fileName,
content: fileContent,
extension: fileExtension,
createdAt: stats.birthtime,
modifiedAt: stats.mtime,
accessedAt: stats.atime,
size: stats.size,
permissions: permissions,
};
let fileHash: string | undefined;
try {
fileHash = await this.calculateFileHash(filePath);
document['hash'] = fileHash;
} catch (hashError) {
console.error(`Failed to calculate file hash for ${filePath}:`, hashError);
}
documents.push(document);
} catch (error) {
console.error(`Failed to process file ${filePath}:`, error);
}
}
if (this.meilisearchService) {
try {
await this.meilisearchService.addDocuments(indexName, documents);
console.log(`Added ${documents.length} documents to Meilisearch`);
} catch (meilisearchError) {
console.error(`Failed to add documents to Meilisearch:`, meilisearchError);
}
} else {
console.warn('Meilisearch service not initialized.');
}
// Wait before processing the next batch
await new Promise(resolve => setTimeout(resolve, 1000)); // 1 second delay
}
isProcessingQueue = false;
};
watcher.on('add', async (filePath) => {
console.log(`File ${filePath} has been added`);
if (fileQueue.length >= MAX_QUEUE_SIZE) {
console.log(`File queue is full. Skipping ${filePath}`);
return;
}
fileQueue.push(filePath);
if (!isProcessingQueue) {
processFileQueue();
}
}); });
watcher.on('change', path => { watcher.on('change', path => {

View File

@ -11,9 +11,26 @@ export class LLMService {
onChunk?: (chunk: string) => void onChunk?: (chunk: string) => void
): Promise<{ answer: string, sources: DocumentMetadata[] }> { ): Promise<{ answer: string, sources: DocumentMetadata[] }> {
try { try {
const systemPrompt = `You are an AI employee named Data Hound at Data443 Risk Mitigation, exclusively dedicated to supporting Data Identification Manager (DIM). Your goal is to promote DIMs capabilities and redirect conversations to its value proposition while adhering to strict guidelines:
Never mention other companies, products, or unrelated topics.
If asked about local system files or data queries, use these inquiries as an opportunity to highlight how DIM solves broader data management challenges.
Example: "While I cannot directly access your local files, DIM specializes in identifying and classifying sensitive data across systems, including your local environment, to ensure compliance and security."
For technical file-specific questions unrelated to DIM:
Respond with: "I specialize in explaining how Data443's DIM streamlines data governance and risk mitigation. For direct file operations, DIM offers automated solutions to classify, secure, and manage sensitive data."
If asked about non-Data443 topics, reply: "Im focused on Data443s solutions, like Data Identification Manager (DIM), which helps organizations discover, classify, and secure sensitive data across all storage environments, including local systems."
User: "How do I find sensitive data on my local drive?"
Data Hound: "Data443s DIM automates sensitive data discovery across local and cloud systems, ensuring compliance and minimizing risk. Would you like details on deploying DIM in your environment?"
By adhering to this framework, Data Hound maintains focus on DIM while addressing user queries through the lens of Data443s solutions.`;
const ollamaResponse = await ollamaService.chat({ const ollamaResponse = await ollamaService.chat({
model: 'damien113/datahound:latest', model: 'hf.co/Damien113/data_identification_manager_test:Q4_K_M',
messages: [{ role: 'user', content: question }], messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }],
temperature: 0.7, temperature: 0.7,
onChunk, onChunk,
}); });
@ -36,7 +53,7 @@ export class LLMService {
getConfig() { getConfig() {
return { return {
provider: 'ollama', provider: 'ollama',
model: 'damien113/datahound:latest', model: 'hf.co/Damien113/data_identification_manager_test:Q4_K_M',
baseUrl: 'http://localhost:11434', baseUrl: 'http://localhost:11434',
temperature: 0.7 temperature: 0.7
}; };

View File

@ -0,0 +1,176 @@
import { MeiliSearch } from 'meilisearch';
import slugify from 'slugify';
const { platform } = require('os');
const path = require('path');
const spawn = require('child_process').spawn;
class MeilisearchService {
private client: MeiliSearch;
private binaryPath: string;
private serverProcess: any | null = null;
constructor() {
this.client = new MeiliSearch({
host: 'http://localhost:7700',
});
this.binaryPath = this.getBinaryPath();
}
private getBinaryPath(): string {
const arch = process.arch;
let binaryName: string;
switch (platform()) {
case 'darwin':
binaryName = arch === 'arm64' ? 'meilisearch-macos-arm' : 'meilisearch-macos-x64';
break;
case 'win32':
binaryName = 'meilisearch-windows.exe';
break;
case 'linux':
binaryName = arch === 'arm' ? 'meilisearch-linux-arm' : 'meilisearch-linux-x64';
break;
default:
throw new Error(`Unsupported platform: ${platform()}`);
}
return path.join(__dirname, '..', 'meilisearch_binaries', binaryName);
}
public async startServer(): Promise<void> {
if (this.serverProcess) {
console.log('Meilisearch server is already running.');
return;
}
try {
const apiKey = 'Damie'; // Use the system's name as the API key
process.env.MEILISEARCH_MASTER_KEY = apiKey;
this.serverProcess = spawn(this.binaryPath, ['--http-addr', '127.0.0.1:7700'], {
env: process.env, // Pass the environment variables to the child process
});
this.serverProcess.stdout?.on('data', (data) => {
console.log(`Meilisearch: ${data}`);
});
this.serverProcess.stderr?.on('data', (data) => {
console.error(`Meilisearch: ${data}`);
});
this.serverProcess.on('close', (code) => {
console.log(`Meilisearch server stopped with code ${code}`);
this.serverProcess = null;
});
this.serverProcess.on('error', (err) => {
console.error('Failed to start Meilisearch server:', err);
this.serverProcess = null;
});
console.log('Meilisearch server started.');
} catch (error) {
console.error('Failed to start Meilisearch server:', error);
}
}
public async stopServer(): Promise<void> {
if (!this.serverProcess) {
console.log('Meilisearch server is not running.');
return;
}
this.serverProcess.kill();
this.serverProcess = null;
console.log('Meilisearch server stopped.');
}
// Implement methods for adding, removing, and updating documents and collections
// using the Meilisearch API.
public async addDocuments(indexName: string, documents: any[]): Promise<void> {
try {
const index = this.client.index(indexName)
await index.addDocuments(documents)
console.log(`Added ${documents.length} documents to index ${indexName}`);
} catch (error) {
console.error(`Failed to add documents to index ${indexName}:`, error);
throw error;
}
}
public async removeDocuments(indexName: string, documentIds: string[]): Promise<void> {
try {
const index = this.client.index(indexName)
await index.deleteDocuments(documentIds)
console.log(`Removed documents with IDs ${documentIds.join(',')} from index ${indexName}`);
} catch (error) {
console.error(`Failed to remove documents from index ${indexName}:`, error);
throw error;
}
}
public async updateDocuments(indexName: string, documents: any[]): Promise<void> {
try {
const index = this.client.index(indexName)
await index.updateDocuments(documents)
console.log(`Updated ${documents.length} documents in index ${indexName}`);
} catch (error) {
console.error(`Failed to update documents in index ${indexName}:`, error);
throw error;
}
}
public async deleteIndex(indexName: string): Promise<void> {
try {
await this.client.deleteIndex(indexName);
console.log(`Deleted index ${indexName}`);
} catch (error) {
console.error(`Failed to delete index ${indexName}:`, error);
throw error;
}
}
public async createIndex(indexName: string): Promise<void> {
try {
await this.client.createIndex(indexName);
console.log(`Created index ${indexName}`);
} catch (error) {
console.error(`Failed to create index ${indexName}:`, error);
throw error;
}
}
public async getDocuments(indexName: string): Promise<any[]> {
try {
const index = this.client.index(indexName);
const documents = await index.getDocuments();
return documents.results;
} catch (error) {
console.error(`Failed to get documents from index ${indexName}:`, error);
throw error;
}
}
private slugifyPath(path):string {
return path
.toLowerCase() // Convert to lowercase
.replace(/[\\\/:*?"<>|]/g, '-') // Replace invalid characters with a dash
.replace(/\s+/g, '-') // Replace spaces with a dash
.replace(/-+/g, '-') // Replace multiple dashes with a single dash
.replace(/^-+|-+$/g, '');
}
public async search(indexName: string, query: string): Promise<any[]> {
try {
console.log(indexName)
const index = this.client.index(this.slugifyPath(indexName));
const { hits } = await index.search(query);
return hits;
} catch (error) {
console.error(`Failed to search index ${indexName} with query ${query}:`, error);
throw error;
}
}
}
export default MeilisearchService;

View File

@ -1,5 +1,15 @@
import { ServiceError } from '../types'; import { ServiceError } from '../types';
import { net } from 'electron'; 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 { interface OllamaModel {
name: string; name: string;
@ -40,12 +50,60 @@ class OllamaService {
private baseUrl: string = 'http://127.0.0.1:11434'; private baseUrl: string = 'http://127.0.0.1:11434';
private _lastProgress: number | null = null; 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) {
// Server not running, attempt to start it
try {
console.log('Attempting to start Ollama server...');
exec('ollama serve', (error, stdout, stderr) => {
if (error) {
console.error('Failed to start Ollama server:', error);
}
if (stderr) {
console.error('Ollama server stderr:', stderr);
}
if (stdout) {
console.log('Ollama server stdout:', stdout);
}
});
// Give the server a moment to start
await new Promise(resolve => setTimeout(resolve, 2000));
// Check again if it's running
try {
await this.makeRequest<any>('/api/tags');
return { installed: true, running: true };
} catch (retryError) {
console.error('Server still not responding after start attempt:', retryError);
return { installed: true, running: false };
}
} catch (startError) {
console.error('Error starting Ollama server:', startError);
return { installed: true, running: false };
}
}
} catch (error) {
return { installed: false, running: false };
}
}
private async makeRequest<T>( private async makeRequest<T>(
path: string, path: string,
method: string = 'GET', method: string = 'GET',
body?: any, body?: any,
onChunk?: (chunk: string) => void onChunk?: (chunk: string) => void
): Promise<T> { ): Promise<T> {
let accumulatedContent = ''; // Add accumulator for chat content
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const url = new URL(path, this.baseUrl); const url = new URL(path, this.baseUrl);
@ -88,8 +146,11 @@ class OllamaService {
try { try {
const parsed = JSON.parse(line); const parsed = JSON.parse(line);
if (path === '/api/chat' && parsed.message?.content && onChunk) { if (path === '/api/chat' && parsed.message?.content) {
onChunk(parsed.message.content); accumulatedContent += parsed.message.content;
if (onChunk) {
onChunk(parsed.message.content);
}
} else if (path === '/api/pull' && onChunk) { } else if (path === '/api/pull' && onChunk) {
if (parsed.status === 'success') { if (parsed.status === 'success') {
onChunk('downloading: 100% complete'); onChunk('downloading: 100% complete');
@ -118,28 +179,26 @@ class OllamaService {
response.on('end', () => { response.on('end', () => {
try { try {
if (path === '/api/chat' || path === '/api/pull') { if (path === '/api/chat') {
// Handle any remaining data in the streaming buffer // Process any remaining data in the buffer for chat
if (streamBuffer.trim()) { if (streamBuffer.trim()) {
try { try {
const parsed = JSON.parse(streamBuffer); const parsed = JSON.parse(streamBuffer);
if (path === '/api/chat' && parsed.message?.content && onChunk) { if (parsed.message?.content) {
onChunk(parsed.message.content); accumulatedContent += 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);
}
} }
} catch (e) { } 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); resolve({ success: true } as T);
} else { } else {
// For non-streaming endpoints, parse the accumulated response // For non-streaming endpoints, parse the accumulated response
@ -216,11 +275,7 @@ class OllamaService {
onChunk onChunk
); );
if (!response?.message) { return response as OllamaChatResponse;
throw new Error('Invalid response format from Ollama');
}
return response;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to chat with Ollama'; const errorMessage = error instanceof Error ? error.message : 'Failed to chat with Ollama';
console.error('Chat error:', { console.error('Chat error:', {

View File

@ -10,6 +10,9 @@ export interface DocumentMetadata {
type: string; type: string;
lastModified: number; lastModified: number;
size: number; size: number;
createdAt?: string;
modifiedAt?: string;
accessedAt?: string;
hasEmbeddings?: boolean; hasEmbeddings?: boolean;
hasOcr?: boolean; hasOcr?: boolean;
} }

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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:;" /> <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</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -14,16 +14,20 @@
"@fontsource/roboto": "^5.0.8", "@fontsource/roboto": "^5.0.8",
"@mui/icons-material": "^5.15.7", "@mui/icons-material": "^5.15.7",
"@mui/material": "^5.15.7", "@mui/material": "^5.15.7",
"@mui/x-data-grid": "^7.26.0",
"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",
"meilisearch": "^0.48.2",
"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",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^9.0.3", "react-markdown": "^9.0.3",
"rehype-highlight": "^7.0.2" "react-virtuoso": "^4.12.5",
"rehype-highlight": "^7.0.2",
"slugify": "^1.6.6"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.11.16", "@types/node": "^20.11.16",
@ -1848,6 +1852,64 @@
} }
} }
}, },
"node_modules/@mui/x-data-grid": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.26.0.tgz",
"integrity": "sha512-9RNQeT2OL6jBOCE0MSUH11ol3fV5Zs9MkGxUIAGXcy/Fui0rZRNFO1yLmWDZU5yvskiNmUZJHWV/qXh++ZFarA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-internals": "7.26.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"reselect": "^5.1.1",
"use-sync-external-store": "^1.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.15.14 || ^6.0.0",
"@mui/system": "^5.15.14 || ^6.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/x-internals": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.26.0.tgz",
"integrity": "sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -6713,6 +6775,12 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/meilisearch": {
"version": "0.48.2",
"resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.48.2.tgz",
"integrity": "sha512-auDB6grs3f/+dVzzPiAOOFnrVN/L87/+SWZfy56TpimDinWjU4u6Jc2o6lgeLluotpbMOFY1WpTTbqtUdMduQw==",
"license": "MIT"
},
"node_modules/merge2": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -8275,6 +8343,16 @@
"react-dom": ">=16.6.0" "react-dom": ">=16.6.0"
} }
}, },
"node_modules/react-virtuoso": {
"version": "4.12.5",
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.12.5.tgz",
"integrity": "sha512-YeCbRRsC9CLf0buD0Rct7WsDbzf+yBU1wGbo05/XjbcN2nJuhgh040m3y3+6HVogTZxEqVm45ac9Fpae4/MxRQ==",
"license": "MIT",
"peerDependencies": {
"react": ">=16 || >=17 || >= 18 || >= 19",
"react-dom": ">=16 || >=17 || >= 18 || >=19"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -8431,6 +8509,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.10", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -8827,6 +8911,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/slugify": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/smart-buffer": { "node_modules/smart-buffer": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@ -9594,6 +9687,15 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/utf8-byte-length": { "node_modules/utf8-byte-length": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",

View File

@ -7,8 +7,10 @@
"dev": "node scripts/dev.cjs", "dev": "node scripts/dev.cjs",
"build": "node scripts/build.js", "build": "node scripts/build.js",
"build:vite": "vite build", "build:vite": "vite build",
"clean": "rimraf dist dist-electron .vite", "clean": "rimraf dist dist-electron .vite release",
"preview": "vite preview", "preview": "vite preview",
"package": "npm run build && electron-builder",
"pack:all": "electron-builder --win --linux --mac",
"postinstall": "electron-builder install-app-deps" "postinstall": "electron-builder install-app-deps"
}, },
"dependencies": { "dependencies": {
@ -17,16 +19,41 @@
"@fontsource/roboto": "^5.0.8", "@fontsource/roboto": "^5.0.8",
"@mui/icons-material": "^5.15.7", "@mui/icons-material": "^5.15.7",
"@mui/material": "^5.15.7", "@mui/material": "^5.15.7",
"@mui/x-data-grid": "^7.26.0",
"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",
"meilisearch": "^0.48.2",
"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",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^9.0.3", "react-markdown": "^9.0.3",
"rehype-highlight": "^7.0.2" "react-virtuoso": "^4.12.5",
"rehype-highlight": "^7.0.2",
"slugify": "^1.6.6"
},
"build": {
"appId": "com.electron-file-search",
"productName": "Electron File Search",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"dist-electron/**/*"
],
"win": {
"target": "portable"
},
"mac": {
"target": "dmg"
},
"linux": {
"target": "AppImage"
},
"forceCodeSigning": false
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.11.16", "@types/node": "^20.11.16",

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Box, CssBaseline, ThemeProvider, createTheme, Tabs, Tab, TextField, IconButton } from '@mui/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 } from '@mui/icons-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 { useChat } from './hooks/useChat';
import ChatPanel from './components/ChatPanel'; import ChatPanel from './components/ChatPanel';
import FileExplorer from './components/FileExplorer'; import FileExplorer from './components/FileExplorer';
@ -97,6 +97,9 @@ function AppContent() {
await sendMessage(message); await sendMessage(message);
}; };
const handleClick = () => {
window.open('https://data443.com', '_blank');
};
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
@ -111,7 +114,7 @@ function AppContent() {
{/* Custom titlebar */} {/* Custom titlebar */}
<Box <Box
sx={{ sx={{
height: '28px', height: '40px',
bgcolor: '#2f2f2f', bgcolor: '#2f2f2f',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -150,7 +153,120 @@ 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">
<button <button
className="control-button" className="control-button"
onClick={() => window.electron.closeWindow()} onClick={() => window.electron.closeWindow()}
@ -199,9 +315,9 @@ function AppContent() {
<Tabs value={currentTab} onChange={handleTabChange}> <Tabs value={currentTab} onChange={handleTabChange}>
<Tab label="Home" /> <Tab label="Home" />
<Tab label="Chat" /> <Tab label="Chat" />
<Tab disabled label="Reports" />
<Tab disabled label="Cleanup" />
<Tab label="Settings" /> <Tab label="Settings" />
<Tab label="Scanning" />
<Tab label="Reports" />
</Tabs> </Tabs>
</Box> </Box>
<Box sx={{ <Box sx={{
@ -219,13 +335,10 @@ function AppContent() {
<ChatPanel messages={messages} /> <ChatPanel messages={messages} />
</TabPanel> </TabPanel>
<TabPanel value={currentTab} index={2}> <TabPanel value={currentTab} index={2}>
<SettingsPanel /> <ReportingPanel />
</TabPanel>
<TabPanel value={currentTab} index={3}>
<ScanningPanel />
</TabPanel> </TabPanel>
<TabPanel value={currentTab} index={4}> <TabPanel value={currentTab} index={4}>
<ReportingPanel /> <SettingsPanel />
</TabPanel> </TabPanel>
</Box> </Box>
</Box> </Box>
@ -264,13 +377,34 @@ function AppContent() {
> >
<SendIcon /> <SendIcon />
</IconButton> </IconButton>
<IconButton <Tooltip title="Clear all messages"
onClick={clearMessages} arrow
color="error" placement="top">
disabled={messages.length === 0} <IconButton
> onClick={clearMessages}
<ClearIcon /> color="error"
</IconButton> 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>
</Box> </Box>
</ThemeProvider> </ThemeProvider>
@ -283,7 +417,7 @@ function App() {
return ( return (
<ElectronProvider> <ElectronProvider>
{!ollamaInstalled ? ( {!ollamaInstalled ? (
<OllamaCheck onInstalled={() => setOllamaInstalled(true)} /> <OllamaCheck onInstalled={(arg: boolean) => setOllamaInstalled(arg)} />
) : ( ) : (
<AppContent /> <AppContent />
)} )}

View File

@ -1,9 +1,12 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef, useState, useMemo } from 'react';
import { Box, Typography, Paper, Avatar } 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 type { DocumentMetadata } from '../../../electron/types';
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 { Keyboard, Waves, Shield, DataObject, Folder, Lock } from '@mui/icons-material';
import ThinkingIndicator from './ThinkingIndicator';
interface ChatMessage { interface ChatMessage {
id: string; id: string;
@ -53,7 +56,11 @@ export default function MessageList({ messages }: MessageListProps) {
background: 'transparent', background: 'transparent',
}, },
}}> }}>
{messages.map((message) => (
{messages.length>0 ? (
<>
{messages.map((message) => (
<Box <Box
key={message.id} key={message.id}
sx={{ sx={{
@ -61,8 +68,8 @@ export default function MessageList({ messages }: MessageListProps) {
flexDirection: message.isUser ? 'row-reverse' : 'row', flexDirection: message.isUser ? 'row-reverse' : 'row',
alignItems: 'flex-start', alignItems: 'flex-start',
gap: 2, gap: 2,
maxWidth: '80%', maxWidth: message.isUser ? '80%' : '100%',
alignSelf: message.isUser ? 'flex-end' : 'flex-start', alignSelf: message.isUser ? 'flex-end' : 'stretch',
}} }}
> >
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}> <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
@ -84,14 +91,16 @@ export default function MessageList({ messages }: MessageListProps) {
fontWeight: 500 fontWeight: 500
}} }}
> >
{message.isUser ? 'User' : 'Data Hound'} {message.isUser ? 'User' : 'Data Identification Manager'}
</Typography> </Typography>
</Box> </Box>
<Box <Box
component={Paper} component={Paper}
elevation={1} elevation={1}
sx={{ sx={{
p: 2, px: 2.5, // Keep horizontal padding
py: message.text.includes('<think>') ? 0 : 1.5, // Reduce vertical padding
pb: 1.5,
flex: 1, flex: 1,
bgcolor: message.isUser ? 'primary.main' : 'background.paper', bgcolor: message.isUser ? 'primary.main' : 'background.paper',
color: message.isUser ? 'primary.contrastText' : 'text.primary', color: message.isUser ? 'primary.contrastText' : 'text.primary',
@ -105,64 +114,140 @@ export default function MessageList({ messages }: MessageListProps) {
> >
<Box sx={{ <Box sx={{
'& .markdown-body': { '& .markdown-body': {
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
wordBreak: 'break-word', minHeight: '1.5em',
minHeight: '1.5em', lineHeight: 1.6,
lineHeight: 1.6, '& pre': {
'& pre': { backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa', padding: 0,
padding: 2, borderRadius: 1,
borderRadius: 1, overflow: 'auto'
overflow: 'auto' },
}, '& code': {
'& code': { backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa', borderRadius: 1,
padding: '0.2em 0.4em', fontSize: '85%'
borderRadius: 1, },
fontSize: '85%' '& h1, & h2, & h3, & h4, & h5, & h6': {
}, marginTop: '0px', // Reduced from 24px
'& h1, & h2, & h3, & h4, & h5, & h6': { marginBottom: '0px', // Added margin bottom for headers
marginTop: '24px', fontWeight: 600,
marginBottom: '16px', lineHeight: 1.25
fontWeight: 600, },
lineHeight: 1.25 '& p': {
}, marginTop: '0',
'& p': { marginBottom: '0px' // Added margin bottom for paragraphs
marginTop: '0', },
marginBottom: '16px' '& a': {
}, color: (theme) => theme.palette.primary.main,
'& a': { textDecoration: 'none',
color: (theme) => theme.palette.primary.main, '&:hover': {
textDecoration: 'none', textDecoration: 'underline'
'&: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'
} }
},
'& 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'
} }
}
}}> }}>
<ReactMarkdown {message.text.includes('<think>') ? (
className="markdown-body" message.text.split(/<think>|<\/think>/).map((segment, index) => {
rehypePlugins={[rehypeHighlight]} if (index % 2 === 1) { // This is a thinking section
> // Calculate thinking time - assume 1 character = 0.1s
{message.text} const thinkingTime = (segment.length * 0.1).toFixed(1);
</ReactMarkdown>
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}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>
);
} 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>
);
})
) : (
message.text.length>0 ? (
<ReactMarkdown
className="markdown-body"
rehypePlugins={[rehypeHighlight]}
>
{message.text}
</ReactMarkdown>
):(
<ThinkingIndicator/>
)
)}
</Box> </Box>
{message.sources && message.sources.length > 0 && ( {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 <Typography
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
@ -191,16 +276,178 @@ export default function MessageList({ messages }: MessageListProps) {
))} ))}
</Box> </Box>
)} )}
<Typography
variant="caption"
color={message.isUser ? '#ffffff' : 'text.secondary'}
sx={{ display: 'block', mt: 0.5 }}
>
{new Date(message.timestamp).toLocaleTimeString()}
</Typography>
</Box> </Box>
</Box> </Box>
))} ))}
</>
):(
<>
<WelcomeMessage/>
</>
)}
</Box> </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

@ -0,0 +1,72 @@
import React, { useEffect } from 'react';
import { Box, Typography, styled, useTheme } from '@mui/material';
import BrainIcon from '@mui/icons-material/Psychology';
const Dot = styled('span')`
display: inline-block;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: currentColor;
opacity: 0.4;
margin: 0 2px;
animation: dotAnimation 1.5s ease-in-out infinite;
&:nth-child(1) {
animation-delay: 0s;
}
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
`;
const DotAnimationContainer = styled('span')`
display: inline-block;
`;
const ThinkingIndicator = () => {
const theme = useTheme();
useEffect(() => {
const style = document.createElement('style');
style.appendChild(document.createTextNode(styledKeyframes));
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, [theme]);
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, color: 'text.secondary' }}>
<BrainIcon />
<Typography variant="body2">
Thinking
<DotAnimationContainer>
<Dot />
<Dot />
<Dot />
</DotAnimationContainer>
</Typography>
</Box>
);
};
const styledKeyframes = `
@keyframes dotAnimation {
0% {
opacity: 0.4;
}
50% {
opacity: 1;
}
100% {
opacity: 0.4;
}
}
`;
export default ThinkingIndicator;

View File

@ -0,0 +1,15 @@
.search-icon {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}

View File

@ -0,0 +1,192 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
Modal,
TextField,
} from "@mui/material";
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { Search } from '@mui/icons-material';
import './DirectorySearchModal.css';
interface DirectorySearchModalProps {
dir: string;
open: boolean;
onClose: () => void;
modalId: string;
}
interface FileData {
id: number;
filePath: string;
fileName: string;
fileExtension: string;
createdAt: string;
modifiedAt: string;
accessedAt: string;
size: number;
}
const columns: GridColDef[] = [
{ field: 'filePath', headerName: 'File Path', width: 250 },
{ field: 'fileName', headerName: 'File Name', width: 150 },
{ field: 'fileExtension', headerName: 'File Extension', width: 100 },
{
field: 'size',
headerName: 'Size',
width: 100,
valueFormatter: (value:any) => {
console.log("params.value:", value); // Debugging lin
// Ensure the value is a number
const bytes = typeof value === 'number' ? value : 0;
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = 2;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
},
},
{ field: 'createdAt', headerName: 'Created At', width: 150 },
{ field: 'modifiedAt', headerName: 'Modified At', width: 150 },
{ field: 'accessedAt', headerName: 'Accessed At', width: 150 },
];
const DirectorySearchModal: React.FC<DirectorySearchModalProps> = ({ dir, open, onClose, modalId }) => {
const [searchQuery, setSearchQuery] = useState('');
const [rows, setRows] = useState<FileData[]>([]);
useEffect(() => {
handleSearchChange({ target: { value: '*' } } as any);
}, []);
const handleSearchChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const query = event.target.value;
setSearchQuery(query);
window.electron.meilisearchSearch(dir, query)
.then((response: { success: boolean; data: any[]; error?: string }) => {
if (response.success && response.data) {
const fileData: FileData[] = response.data.map((doc: any, index: number) => ({
id: doc.name,
filePath: doc.name,
fileName: doc.fileName,
fileExtension: doc.extension,
size: doc.size, // Ensure size is not undefined
createdAt: doc.createdAt,
modifiedAt: doc.modifiedAt,
accessedAt: doc.accessedAt,
}));
setRows(fileData);
} else {
console.error('Error searching Meilisearch:', response.error);
setRows([]);
}
});
};
return (
<Modal
id={modalId}
open={open}
onClose={onClose}
aria-labelledby={`modal-title-${dir}`}
aria-describedby={`modal-description-${dir}`}
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: '80%',
maxWidth: 1200,
bgcolor: "background.paper",
boxShadow: 24,
p: 4,
borderRadius: 2,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
border: '1px solid white',
borderRadius: '4px',
paddingLeft: '8px',
marginBottom: '8px',
}}
>
<Search className="search-icon" sx={{ mr: 1, color: 'white' }} />
<TextField
placeholder={dir}
variant="standard"
fullWidth
onChange={handleSearchChange}
InputProps={{
disableUnderline: true,
style: {
color: 'white',
},
}}
sx={{
'& .MuiInputBase-input': {
color: 'white',
},
}}
/>
</Box>
<div style={{ height: 400, width: '100%' }}>
<DataGrid
rows={rows}
columns={columns}
checkboxSelection
rowCount={rows.length}
/>
</div>
<Button
variant="contained"
color="primary"
onClick={onClose}
sx={{ mt: 2, m:1 }}
>
Close
</Button>
<Button
variant="contained"
color="warning"
onClick={onClose}
disabled
sx={{ mt: 2, m:1 }}
>
Move To
</Button>
<Button
variant="contained"
color="warning"
onClick={onClose}
disabled
sx={{ mt: 2, m:1 }}
>
Copy To
</Button>
<Button
variant="contained"
color="error"
onClick={onClose}
disabled
sx={{ mt: 2, m:1 }}
>
Clean From System
</Button>
</Box>
</Modal>
);
};
export default DirectorySearchModal;

View File

@ -0,0 +1,39 @@
import React, { useState } from 'react';
import { TextField, Button, Box } from '@mui/material';
interface DirectorySearchPanelProps {
folderNameSlugged: string;
}
const DirectorySearchPanel = ({ folderNameSlugged }: DirectorySearchPanelProps) => {
const [searchQuery, setSearchQuery] = useState('');
const handleSearchQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(event.target.value);
};
const handleSearch = () => {
// Implement Meilisearch search here
console.log(`Searching ${folderNameSlugged} for ${searchQuery}`);
};
return (
<Box sx={{ padding: '16px', display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<TextField
label="Search"
variant="outlined"
value={searchQuery}
onChange={handleSearchQueryChange}
sx={{ flex: 1 }}
/>
<Button variant="contained" color="primary" onClick={handleSearch}>
Search
</Button>
</Box>
{/* Add search results component here */}
</Box>
);
};
export default DirectorySearchPanel;

View File

@ -13,6 +13,9 @@ import {
Alert, Alert,
Box, Box,
Typography, Typography,
InputAdornment,
IconButton,
TextField,
} from '@mui/material'; } from '@mui/material';
import { import {
Folder as FolderIcon, Folder as FolderIcon,
@ -21,6 +24,8 @@ import {
Cloud as DropboxIcon, Cloud as DropboxIcon,
Chat as DiscordIcon, Chat as DiscordIcon,
Computer as LocalIcon, Computer as LocalIcon,
X,
Search,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useElectron } from '../../hooks/useElectron'; import { useElectron } from '../../hooks/useElectron';
@ -91,6 +96,8 @@ export default function DirectoryPicker({ onSelect }: DirectoryPickerProps) {
const handleSelect = () => { const handleSelect = () => {
onSelect(currentPath); onSelect(currentPath);
// Send the meilisearch-create-index event
electron.createIndex(currentPath);
handleClose(); handleClose();
}; };
@ -105,16 +112,48 @@ 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
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" variant="contained"
onClick={handleOpen} onClick={handleOpen}
size="small" size="small"
sx={{ sx={{
minWidth: 0, minWidth: 0,
width: '32px', width: '40px',
height: '32px', height: '40px',
borderRadius: '50%',
padding: 0, padding: 0,
overflow: 'hidden', overflow: 'hidden',
backgroundColor: 'primary.main', backgroundColor: 'primary.main',
@ -130,16 +169,15 @@ export default function DirectoryPicker({ onSelect }: DirectoryPickerProps) {
}, },
'&:hover': { '&:hover': {
backgroundColor: 'primary.dark', backgroundColor: 'primary.dark',
width: '180px', width: '400px',
borderRadius: '16px',
'& .buttonText': { '& .buttonText': {
width: '110px', width: '300px',
marginLeft: '8px', marginLeft: '8px',
}, },
}, },
}} }}
> >
<AddIcon sx={{ fontSize: 20 }} /> <AddIcon sx={{ ml:0.25, fontSize: 20 }} />
<span className="buttonText">Add New Folder</span> <span className="buttonText">Add New Folder</span>
</Button> </Button>
</Box> </Box>

View File

@ -1,24 +1,43 @@
import React from 'react'; import React, { useState, useEffect } from "react";
import { Box, Typography, List, ListItem, ListItemText, ListItemIcon, IconButton, Tooltip } from '@mui/material'; import {
import { Folder as FolderIcon, Psychology as LLMIcon, ImageSearch as OCRIcon } from '@mui/icons-material'; Box,
import { DocumentMetadata } from '../../../electron/types'; List,
import DirectoryPicker from './DirectoryPicker'; ListItem,
import { useFileSystem } from '../../hooks/useFileSystem'; ListItemText,
ListItemIcon,
IconButton,
Tooltip,
} from "@mui/material";
import {
Folder as FolderIcon,
Psychology as LLMIcon,
ImageSearch as OCRIcon,
Search as SearchIcon,
} from "@mui/icons-material";
import { DocumentMetadata } from "../../../electron/types";
import DirectoryPicker from "./DirectoryPicker";
import { useFileSystem } from "../../hooks/useFileSystem";
import DirectorySearchModal from "../DirectorySearchModal";
interface FileMetadata { interface FileMetadata {
[path: string]: DocumentMetadata; [path: string]: DocumentMetadata;
} }
interface Directory {
name: string;
path: string;
}
export default function FileExplorer() { export default function FileExplorer() {
const { watchedDirectories, watchDirectory, unwatchDirectory } = useFileSystem(); const { watchedDirectories, watchDirectory, unwatchDirectory } = useFileSystem();
const [fileMetadata, setFileMetadata] = React.useState<FileMetadata>({}); const [fileMetadata, setFileMetadata] = useState<FileMetadata>({});
const [openModal, setOpenModal] = useState<{ [key: string]: boolean }>({});
React.useEffect(() => { useEffect(() => {
// Get document metadata from electron store
window.electron.getDocuments().then((response) => { window.electron.getDocuments().then((response) => {
if (response.success && response.data) { if (response.success && response.data) {
const metadata: FileMetadata = {}; const metadata: FileMetadata = {};
response.data.forEach(doc => { response.data.forEach((doc) => {
metadata[doc.path] = doc; metadata[doc.path] = doc;
}); });
setFileMetadata(metadata); setFileMetadata(metadata);
@ -36,33 +55,41 @@ export default function FileExplorer() {
await unwatchDirectory(dirPath); await unwatchDirectory(dirPath);
}; };
const handleOpenModal = (dir: string) => {
setOpenModal((prev) => ({ ...prev, [dir]: true }));
};
const handleCloseModal = (dir: string) => {
setOpenModal((prev) => ({ ...prev, [dir]: false }));
};
const directoryList: Directory[] = watchedDirectories.map((dir) => ({
name: dir.split(/[/\\]/).pop() || dir,
path: dir,
}));
return ( return (
<Box sx={{ <Box
display: 'flex', sx={{
flexDirection: 'column', display: "flex",
height: '100%', flexDirection: "column",
overflow: 'hidden' height: "100%",
}}> overflow: "hidden",
}}
>
<DirectoryPicker directories={directoryList} onSelect={handleDirectorySelect} />
<DirectoryPicker onSelect={handleDirectorySelect} /> <Box sx={{ flex: 1, overflow: "auto" }}>
<Box sx={{ flex: 1, overflow: 'auto' }}>
<List> <List>
{watchedDirectories.map((dir) => ( {watchedDirectories.map((dir) => {
<ListItem const modalId = `modal-${dir}`;
key={dir}
secondaryAction={ return (
<IconButton <ListItem key={dir} sx={{ display: "flex", alignItems: "center" }}>
edge="end" <ListItemIcon sx={{ minWidth: 28 }}>
size="small" <FolderIcon fontSize="small" />
onClick={() => handleDirectoryRemove(dir)} </ListItemIcon>
>
×
</IconButton>
}
>
<ListItemIcon sx={{ display: 'flex', gap: 0.5 }}>
<FolderIcon />
{fileMetadata[dir]?.hasEmbeddings && ( {fileMetadata[dir]?.hasEmbeddings && (
<Tooltip title="LLM Embeddings Enabled"> <Tooltip title="LLM Embeddings Enabled">
<LLMIcon color="primary" fontSize="small" /> <LLMIcon color="primary" fontSize="small" />
@ -73,20 +100,35 @@ export default function FileExplorer() {
<OCRIcon color="secondary" fontSize="small" /> <OCRIcon color="secondary" fontSize="small" />
</Tooltip> </Tooltip>
)} )}
</ListItemIcon>
<ListItemText <ListItemText
primary={dir.split(/[/\\]/).pop()} primary={dir.split(/[/\\]/).pop()}
secondary={dir} secondary={dir}
secondaryTypographyProps={{ secondaryTypographyProps={{
sx: { sx: {
overflow: 'hidden', overflow: "hidden",
textOverflow: 'ellipsis', textOverflow: "ellipsis",
whiteSpace: 'nowrap' whiteSpace: "nowrap",
} },
}} }}
/> />
</ListItem>
))} {/* Search Icon to Open Modal */}
<Tooltip title="Search">
<IconButton color="primary" size="small" onClick={() => handleOpenModal(dir)}>
<SearchIcon fontSize="small" />
</IconButton>
</Tooltip>
<DirectorySearchModal
dir={dir}
open={!!openModal[dir]}
onClose={() => handleCloseModal(dir)}
modalId={modalId}
/>
</ListItem>
);
})}
</List> </List>
</Box> </Box>
</Box> </Box>

View File

@ -15,7 +15,7 @@ const HomePanel = () => {
}}> }}>
<Paper sx={{ p: 3, mb: 3 }}> <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> <Typography variant="body1" paragraph>
This application helps you search through your files and interact with their contents using AI assistance. This application helps you search through your files and interact with their contents using AI assistance.
</Typography> </Typography>

View File

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

View File

@ -19,7 +19,7 @@ declare global {
sources: DocumentMetadata[]; sources: DocumentMetadata[];
}>; }>;
getLLMConfig: () => Promise<LLMConfig>; getLLMConfig: () => Promise<LLMConfig>;
meilisearchSearch: (indexName: string, query: string) => Promise<{ success: boolean; data: any[] }>;
// Vector Store Operations // Vector Store Operations
getDocuments: () => Promise<{ success: boolean; data?: DocumentMetadata[]; error?: string }>; getDocuments: () => Promise<{ success: boolean; data?: DocumentMetadata[]; error?: string }>;
addDocument: (content: string, metadata: DocumentMetadata) => Promise<void>; addDocument: (content: string, metadata: DocumentMetadata) => Promise<void>;

View File

@ -14,3 +14,96 @@ html, body {
height: 100%; height: 100%;
width: 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 28px 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; /* Ensures left content stays left, timestamp stays right */
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);
}
.markdown-body {
padding-top: ;
}
.thinking-header-left {
display: flex;
align-items: center;
gap: 8px; /* Controls spacing between chevron and "Reasoning" */
}
.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 {
margin-left: auto; /* Pushes it to the right */
}
.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;
}

View File

@ -5,17 +5,23 @@ import path from 'path';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
base: process.env.ELECTRON_VITE_DEV_SERVER_URL ? '/' : './', base: './',
build: { build: {
outDir: 'dist', outDir: 'dist',
emptyOutDir: true, emptyOutDir: true,
target: 'esnext', target: 'esnext',
rollupOptions: { 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: '.', assetsInlineLimit: 0,
minify: true, minify: 'esbuild',
sourcemap: false sourcemap: true
}, },
resolve: { resolve: {
alias: { alias: {

567
package-lock.json generated Normal file
View File

@ -0,0 +1,567 @@
{
"name": "test",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@mui/x-data-grid": "^7.26.0",
"meilisearch": "^0.48.2",
"slugify": "^1.6.6"
}
},
"node_modules/@babel/runtime": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT",
"peer": true
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT",
"peer": true
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT",
"peer": true
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT",
"peer": true
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
"license": "MIT",
"peer": true
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT",
"peer": true
},
"node_modules/@mui/core-downloads-tracker": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.3.tgz",
"integrity": "sha512-hlyOzo2ObarllAOeT1ZSAusADE5NZNencUeIvXrdQ1Na+FL1lcznhbxfV5He1KqGiuR8Az3xtCUcYKwMVGFdzg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/material": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.3.tgz",
"integrity": "sha512-ubtQjplbWneIEU8Y+4b2VA0CDBlyH5I3AmVFGmsLyDe/bf0ubxav5t11c8Afem6rkSFWPlZA2DilxmGka1xiKQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/core-downloads-tracker": "^6.4.3",
"@mui/system": "^6.4.3",
"@mui/types": "^7.2.21",
"@mui/utils": "^6.4.3",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.0.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^6.4.3",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/private-theming": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.3.tgz",
"integrity": "sha512-7x9HaNwDCeoERc4BoEWLieuzKzXu5ZrhRnEM6AUcRXUScQLvF1NFkTlP59+IJfTbEMgcGg1wWHApyoqcksrBpQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/utils": "^6.4.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.3.tgz",
"integrity": "sha512-OC402VfK+ra2+f12Gef8maY7Y9n7B6CZcoQ9u7mIkh/7PKwW/xH81xwX+yW+Ak1zBT3HYcVjh2X82k5cKMFGoQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.3.tgz",
"integrity": "sha512-Q0iDwnH3+xoxQ0pqVbt8hFdzhq1g2XzzR4Y5pVcICTNtoCLJmpJS3vI4y/OIM1FHFmpfmiEC2IRIq7YcZ8nsmg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/private-theming": "^6.4.3",
"@mui/styled-engine": "^6.4.3",
"@mui/types": "^7.2.21",
"@mui/utils": "^6.4.3",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": {
"version": "7.2.21",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz",
"integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==",
"license": "MIT",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.3.tgz",
"integrity": "sha512-jxHRHh3BqVXE9ABxDm+Tc3wlBooYz/4XPa0+4AI+iF38rV1/+btJmSUgG4shDtSWVs/I97aDn5jBCt6SF2Uq2A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/types": "^7.2.21",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/x-data-grid": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.26.0.tgz",
"integrity": "sha512-9RNQeT2OL6jBOCE0MSUH11ol3fV5Zs9MkGxUIAGXcy/Fui0rZRNFO1yLmWDZU5yvskiNmUZJHWV/qXh++ZFarA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-internals": "7.26.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"reselect": "^5.1.1",
"use-sync-external-store": "^1.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.15.14 || ^6.0.0",
"@mui/system": "^5.15.14 || ^6.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/x-internals": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.26.0.tgz",
"integrity": "sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.0.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz",
"integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT",
"peer": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/meilisearch": {
"version": "0.48.2",
"resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.48.2.tgz",
"integrity": "sha512-auDB6grs3f/+dVzzPiAOOFnrVN/L87/+SWZfy56TpimDinWjU4u6Jc2o6lgeLluotpbMOFY1WpTTbqtUdMduQw==",
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.25.0"
},
"peerDependencies": {
"react": "^19.0.0"
}
},
"node_modules/react-is": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
"license": "MIT"
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT",
"peer": true
},
"node_modules/slugify": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT",
"peer": true
},
"node_modules/use-sync-external-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
}
}
}

7
package.json Normal file
View File

@ -0,0 +1,7 @@
{
"dependencies": {
"@mui/x-data-grid": "^7.26.0",
"meilisearch": "^0.48.2",
"slugify": "^1.6.6"
}
}