Compare commits

..

No commits in common. "master" and "temp" have entirely different histories.
master ... temp

35 changed files with 238 additions and 2357 deletions

2
.gitignore vendored
View File

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

View File

@ -1,45 +1,3 @@
FROM deepseek-r1:8b
PARAMETER temperature 1.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."""
SYSTEM You are a helpful AI assistant.
PARAMETER num_gpu 0

View File

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

View File

@ -1 +0,0 @@
1.10.2

View File

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

View File

@ -78,18 +78,6 @@ export function setupIpcHandlers() {
}
});
// Ollama Operations
ipcMain.handle('check-ollama', async () => {
try {
const status = await (await import('../services/ollamaService')).ollamaService.checkOllamaInstallation();
return status;
} catch (error) {
const err = error as Error;
console.error('Error checking Ollama:', err);
throw err;
}
});
// Model Operations
ipcMain.handle('check-model', async (_: unknown, modelName: string) => {
try {
@ -221,84 +209,6 @@ export function setupIpcHandlers() {
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;

View File

@ -4,20 +4,6 @@ const os = require('os');
const spawn = require('child_process').spawn;
const { store: electronStore } = require('./store');
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
setupIpcHandlers();
@ -26,8 +12,6 @@ function createWindow() {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 1200, // Minimum width
minHeight: 800, // Minimum height
frame: false,
titleBarStyle: 'hidden',
webPreferences: {
@ -56,7 +40,7 @@ function createWindow() {
const response = await fetch(process.env.VITE_DEV_SERVER_URL);
if (response.ok) {
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
//mainWindow.webContents.openDevTools();
mainWindow.webContents.openDevTools();
} else {
setTimeout(pollDevServer, 500);
}
@ -137,7 +121,6 @@ function createWindow() {
}
app.whenReady().then(() => {
meilisearchService.startServer();
createWindow();
app.on('activate', () => {
@ -178,27 +161,46 @@ ipcMain.handle('window-close', (event) => {
window?.close();
});
ipcMain.handle('check-ollama', async () => {
const checkInstalled = () => {
return new Promise((resolve) => {
const check = spawn('ollama', ['--version']);
check.on('close', (code) => {
resolve(code === 0);
});
});
};
const checkRunning = async () => {
try {
const response = await fetch('http://localhost:11434/api/version');
return response.ok;
} catch (error) {
return false;
}
};
const startOllama = () => {
return new Promise<void>((resolve) => {
const start = spawn('ollama', ['serve']);
// Wait a bit for the server to start
setTimeout(resolve, 2000);
});
};
const installed = await checkInstalled();
let running = await checkRunning();
if (installed && !running) {
await startOllama();
running = await checkRunning();
}
return { installed, running };
});
ipcMain.handle('open-external', (_, url) => {
return require('electron').shell.openExternal(url);
});
// 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;

View File

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

View File

@ -2,11 +2,6 @@ import { FSWatcher } from 'chokidar';
import * as chokidar from 'chokidar';
import Store from 'electron-store';
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<{
watchedPaths: string[];
@ -16,13 +11,10 @@ const store = new Store<{
class FileSystemService {
private watchers: Map<string, FSWatcher>;
private excludedPaths: Set<string>;
private meilisearchService: MeilisearchService;
private readonly indexName = 'files';
constructor() {
this.watchers = new Map();
this.excludedPaths = new Set(store.get('excludedPaths', []));
this.meilisearchService = new MeilisearchService();
// Add example paths
const examplePaths = [
@ -40,49 +32,6 @@ 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> {
if (this.watchers.has(dirPath)) {
throw new ServiceError(`Already watching directory: ${dirPath}`);
@ -90,7 +39,7 @@ class FileSystemService {
const watcher = chokidar.watch(dirPath, {
ignored: [
/(^|[\\/])\../, // Ignore dotfiles
/(^|[\/\\])\../, // Ignore dotfiles
'**/node_modules/**',
...Array.from(this.excludedPaths),
],
@ -98,91 +47,9 @@ class FileSystemService {
ignoreInitial: false,
});
const indexName = this.slugify(dirPath);
// 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('add', path => {
console.log(`File ${path} has been added`);
// TODO: Process file
});
watcher.on('change', path => {

View File

@ -11,26 +11,9 @@ export class LLMService {
onChunk?: (chunk: string) => void
): Promise<{ answer: string, sources: DocumentMetadata[] }> {
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({
model: 'hf.co/Damien113/data_identification_manager_test:Q4_K_M',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }],
model: 'damien113/datahound:latest',
messages: [{ role: 'user', content: question }],
temperature: 0.7,
onChunk,
});
@ -53,7 +36,7 @@ By adhering to this framework, Data Hound maintains focus on DIM while addressin
getConfig() {
return {
provider: 'ollama',
model: 'hf.co/Damien113/data_identification_manager_test:Q4_K_M',
model: 'damien113/datahound:latest',
baseUrl: 'http://localhost:11434',
temperature: 0.7
};

View File

@ -1,176 +0,0 @@
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,15 +1,5 @@
import { ServiceError } from '../types';
import { net } from 'electron';
import { platform } from 'os';
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
interface OllamaStatus {
installed: boolean;
running: boolean;
}
interface OllamaModel {
name: string;
@ -50,60 +40,12 @@ class OllamaService {
private baseUrl: string = 'http://127.0.0.1:11434';
private _lastProgress: number | null = null;
async checkOllamaInstallation(): Promise<OllamaStatus> {
try {
// Check if ollama binary exists
const cmd = platform() === 'win32' ? 'where ollama' : 'which ollama';
await execAsync(cmd);
// Check if Ollama server is running by attempting to connect to the API
try {
await this.makeRequest<any>('/api/tags');
return { installed: true, running: true };
} catch (error) {
// 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>(
path: string,
method: string = 'GET',
body?: any,
onChunk?: (chunk: string) => void
): Promise<T> {
let accumulatedContent = ''; // Add accumulator for chat content
return new Promise((resolve, reject) => {
try {
const url = new URL(path, this.baseUrl);
@ -146,11 +88,8 @@ class OllamaService {
try {
const parsed = JSON.parse(line);
if (path === '/api/chat' && parsed.message?.content) {
accumulatedContent += parsed.message.content;
if (onChunk) {
if (path === '/api/chat' && parsed.message?.content && onChunk) {
onChunk(parsed.message.content);
}
} else if (path === '/api/pull' && onChunk) {
if (parsed.status === 'success') {
onChunk('downloading: 100% complete');
@ -179,26 +118,28 @@ class OllamaService {
response.on('end', () => {
try {
if (path === '/api/chat') {
// Process any remaining data in the buffer for chat
if (path === '/api/chat' || path === '/api/pull') {
// Handle any remaining data in the streaming buffer
if (streamBuffer.trim()) {
try {
const parsed = JSON.parse(streamBuffer);
if (parsed.message?.content) {
accumulatedContent += parsed.message.content;
if (path === '/api/chat' && parsed.message?.content && onChunk) {
onChunk(parsed.message.content);
} else if (path === '/api/pull' && onChunk) {
if (parsed.status === 'success') {
onChunk('downloading: 100% complete');
} else if (parsed.total && parsed.completed !== undefined) {
const percentage = ((parsed.completed / parsed.total) * 100).toFixed(1);
onChunk(`downloading: ${percentage}% complete`);
} else if (parsed.status) {
onChunk(parsed.status);
}
}
} catch (e) {
console.warn('Failed to parse final chat chunk:', { buffer: streamBuffer, error: e });
console.warn('Failed to parse final chunk:', { buffer: streamBuffer, error: e });
}
}
// Resolve with the complete accumulated content
resolve({
message: {
content: accumulatedContent
}
} as T);
} else if (path === '/api/pull') {
// For pull, resolve with success
// Resolve streaming endpoints with success response
resolve({ success: true } as T);
} else {
// For non-streaming endpoints, parse the accumulated response
@ -275,7 +216,11 @@ class OllamaService {
onChunk
);
return response as OllamaChatResponse;
if (!response?.message) {
throw new Error('Invalid response format from Ollama');
}
return response;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to chat with Ollama';
console.error('Chat error:', {

View File

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

View File

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

View File

@ -14,20 +14,16 @@
"@fontsource/roboto": "^5.0.8",
"@mui/icons-material": "^5.15.7",
"@mui/material": "^5.15.7",
"@mui/x-data-grid": "^7.26.0",
"chokidar": "^3.5.3",
"electron-store": "^8.1.0",
"highlight.js": "^11.11.1",
"meilisearch": "^0.48.2",
"ollama": "^0.5.12",
"openai": "^4.82.0",
"openrouter-client": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.3",
"react-virtuoso": "^4.12.5",
"rehype-highlight": "^7.0.2",
"slugify": "^1.6.6"
"rehype-highlight": "^7.0.2"
},
"devDependencies": {
"@types/node": "^20.11.16",
@ -1852,64 +1848,6 @@
}
}
},
"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": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -6775,12 +6713,6 @@
"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": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -8343,16 +8275,6 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -8509,12 +8431,6 @@
"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": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -8911,15 +8827,6 @@
"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": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@ -9687,15 +9594,6 @@
"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": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",

View File

@ -7,10 +7,8 @@
"dev": "node scripts/dev.cjs",
"build": "node scripts/build.js",
"build:vite": "vite build",
"clean": "rimraf dist dist-electron .vite release",
"clean": "rimraf dist dist-electron .vite",
"preview": "vite preview",
"package": "npm run build && electron-builder",
"pack:all": "electron-builder --win --linux --mac",
"postinstall": "electron-builder install-app-deps"
},
"dependencies": {
@ -19,41 +17,16 @@
"@fontsource/roboto": "^5.0.8",
"@mui/icons-material": "^5.15.7",
"@mui/material": "^5.15.7",
"@mui/x-data-grid": "^7.26.0",
"chokidar": "^3.5.3",
"electron-store": "^8.1.0",
"highlight.js": "^11.11.1",
"meilisearch": "^0.48.2",
"ollama": "^0.5.12",
"openai": "^4.82.0",
"openrouter-client": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.3",
"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
"rehype-highlight": "^7.0.2"
},
"devDependencies": {
"@types/node": "^20.11.16",

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Box, CssBaseline, ThemeProvider, createTheme, Tabs, Tab, TextField, IconButton, Typography, Grow, Tooltip, Button, Chip } from '@mui/material';
import { Send as SendIcon, DeleteOutline as ClearIcon, Close as CloseIcon, Remove as MinimizeIcon, Fullscreen as MaximizeIcon, Download, UpgradeTwoTone, OpenInBrowser } from '@mui/icons-material';
import { Box, CssBaseline, ThemeProvider, createTheme, Tabs, Tab, TextField, IconButton } from '@mui/material';
import { Send as SendIcon, DeleteOutline as ClearIcon, Close as CloseIcon, Remove as MinimizeIcon, Fullscreen as MaximizeIcon } from '@mui/icons-material';
import { useChat } from './hooks/useChat';
import ChatPanel from './components/ChatPanel';
import FileExplorer from './components/FileExplorer';
@ -97,9 +97,6 @@ function AppContent() {
await sendMessage(message);
};
const handleClick = () => {
window.open('https://data443.com', '_blank');
};
return (
<ThemeProvider theme={theme}>
<CssBaseline />
@ -114,7 +111,7 @@ function AppContent() {
{/* Custom titlebar */}
<Box
sx={{
height: '40px',
height: '28px',
bgcolor: '#2f2f2f',
display: 'flex',
alignItems: 'center',
@ -153,120 +150,7 @@ function AppContent() {
},
}}
>
<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
className="control-button"
onClick={() => window.electron.closeWindow()}
@ -315,9 +199,9 @@ function AppContent() {
<Tabs value={currentTab} onChange={handleTabChange}>
<Tab label="Home" />
<Tab label="Chat" />
<Tab disabled label="Reports" />
<Tab disabled label="Cleanup" />
<Tab label="Settings" />
<Tab label="Scanning" />
<Tab label="Reports" />
</Tabs>
</Box>
<Box sx={{
@ -335,10 +219,13 @@ function AppContent() {
<ChatPanel messages={messages} />
</TabPanel>
<TabPanel value={currentTab} index={2}>
<ReportingPanel />
<SettingsPanel />
</TabPanel>
<TabPanel value={currentTab} index={3}>
<ScanningPanel />
</TabPanel>
<TabPanel value={currentTab} index={4}>
<SettingsPanel />
<ReportingPanel />
</TabPanel>
</Box>
</Box>
@ -377,34 +264,13 @@ function AppContent() {
>
<SendIcon />
</IconButton>
<Tooltip title="Clear all messages"
arrow
placement="top">
<IconButton
onClick={clearMessages}
color="error"
disabled={messages.length === 0}
sx={{
'@keyframes glow': {
'0%': {
boxShadow: '0 0 0 0 rgba(211, 47, 47, 0.4)' // error color with opacity
},
'50%': {
boxShadow: '0 0 0 8px rgba(211, 47, 47, 0)'
},
'100%': {
boxShadow: '0 0 0 0 rgba(211, 47, 47, 0)'
}
},
animation: messages.length > 0 ? 'glow 2s infinite' : 'none',
'&:hover': {
animation: 'none' // Stop animation on hover
}
}}
>
<ClearIcon /> {/* Added back the icon */}
<ClearIcon />
</IconButton>
</Tooltip>
</Box>
</Box>
</ThemeProvider>
@ -417,7 +283,7 @@ function App() {
return (
<ElectronProvider>
{!ollamaInstalled ? (
<OllamaCheck onInstalled={(arg: boolean) => setOllamaInstalled(arg)} />
<OllamaCheck onInstalled={() => setOllamaInstalled(true)} />
) : (
<AppContent />
)}

View File

@ -1,12 +1,9 @@
import React, { useEffect, useRef, useState, useMemo } from 'react';
import { Box, Typography, Paper, Avatar, IconButton, List, ListItem, ListItemIcon, ListItemText, useTheme } from '@mui/material';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import React, { useEffect, useRef } from 'react';
import { Box, Typography, Paper, Avatar } from '@mui/material';
import type { DocumentMetadata } from '../../../electron/types';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github.css';
import { Keyboard, Waves, Shield, DataObject, Folder, Lock } from '@mui/icons-material';
import ThinkingIndicator from './ThinkingIndicator';
interface ChatMessage {
id: string;
@ -56,10 +53,6 @@ export default function MessageList({ messages }: MessageListProps) {
background: 'transparent',
},
}}>
{messages.length>0 ? (
<>
{messages.map((message) => (
<Box
key={message.id}
@ -68,8 +61,8 @@ export default function MessageList({ messages }: MessageListProps) {
flexDirection: message.isUser ? 'row-reverse' : 'row',
alignItems: 'flex-start',
gap: 2,
maxWidth: message.isUser ? '80%' : '100%',
alignSelf: message.isUser ? 'flex-end' : 'stretch',
maxWidth: '80%',
alignSelf: message.isUser ? 'flex-end' : 'flex-start',
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
@ -91,16 +84,14 @@ export default function MessageList({ messages }: MessageListProps) {
fontWeight: 500
}}
>
{message.isUser ? 'User' : 'Data Identification Manager'}
{message.isUser ? 'User' : 'Data Hound'}
</Typography>
</Box>
<Box
component={Paper}
elevation={1}
sx={{
px: 2.5, // Keep horizontal padding
py: message.text.includes('<think>') ? 0 : 1.5, // Reduce vertical padding
pb: 1.5,
p: 2,
flex: 1,
bgcolor: message.isUser ? 'primary.main' : 'background.paper',
color: message.isUser ? 'primary.contrastText' : 'text.primary',
@ -114,29 +105,31 @@ export default function MessageList({ messages }: MessageListProps) {
>
<Box sx={{
'& .markdown-body': {
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
minHeight: '1.5em',
lineHeight: 1.6,
'& pre': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
padding: 0,
padding: 2,
borderRadius: 1,
overflow: 'auto'
},
'& code': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
padding: '0.2em 0.4em',
borderRadius: 1,
fontSize: '85%'
},
'& h1, & h2, & h3, & h4, & h5, & h6': {
marginTop: '0px', // Reduced from 24px
marginBottom: '0px', // Added margin bottom for headers
marginTop: '24px',
marginBottom: '16px',
fontWeight: 600,
lineHeight: 1.25
},
'& p': {
marginTop: '0',
marginBottom: '0px' // Added margin bottom for paragraphs
marginBottom: '16px'
},
'& a': {
color: (theme) => theme.palette.primary.main,
@ -153,101 +146,23 @@ export default function MessageList({ messages }: MessageListProps) {
padding: '0 1em',
color: (theme) => theme.palette.text.secondary,
borderLeft: (theme) => `0.25em solid ${theme.palette.divider}`,
margin: '0 0 0px 0'
margin: '0 0 16px 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'
marginBottom: '16px'
}
}
}}>
{message.text.includes('<think>') ? (
message.text.split(/<think>|<\/think>/).map((segment, index) => {
if (index % 2 === 1) { // This is a thinking section
// Calculate thinking time - assume 1 character = 0.1s
const thinkingTime = (segment.length * 0.1).toFixed(1);
return (
<div key={index} className="thinking-section">
<div
className="thinking-header"
onClick={() => {
const content = document.getElementById(`thinking-content-${message.id}-${index}`);
const indicator = document.getElementById(`thinking-indicator-${message.id}-${index}`);
if (content && indicator) {
content.classList.toggle('open');
indicator.classList.toggle('open');
}
}}
>
<div className="thinking-header-left">
<ChevronRightIcon
id={`thinking-indicator-${message.id}-${index}`}
className="indicator"
/>
<span>Reasoning</span>
</div>
<span className="timestamp">(thought for {thinkingTime}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>
{message.sources && message.sources.length > 0 && (
<Box sx={{ mt: 1, pt: 0, borderTop: '1px solid', borderColor: 'divider' }}>
<Box sx={{ mt: 2, pt: 1, borderTop: '1px solid', borderColor: 'divider' }}>
<Typography
variant="caption"
color="text.secondary"
@ -276,178 +191,16 @@ export default function MessageList({ messages }: MessageListProps) {
))}
</Box>
)}
<Typography
variant="caption"
color={message.isUser ? '#ffffff' : 'text.secondary'}
sx={{ display: 'block', mt: 0.5 }}
>
{new Date(message.timestamp).toLocaleTimeString()}
</Typography>
</Box>
</Box>
))}
</>
):(
<>
<WelcomeMessage/>
</>
)}
</Box>
);
}
const WelcomeMessage = () => {
const [text, setText] = useState('');
const fullText = 'Send a message...';
const [showCursor, setShowCursor] = useState(true);
const theme = useTheme();
useEffect(() => {
if (text.length < fullText.length) {
const timeout = setTimeout(() => {
setText(fullText.slice(0, text.length + 1));
}, 100);
return () => clearTimeout(timeout);
}
}, [text]);
useEffect(() => {
const cursorInterval = setInterval(() => {
setShowCursor(prev => !prev);
}, 530);
return () => clearInterval(cursorInterval);
}, []);
return (
<Paper
elevation={0}
sx={{
width: '100%',
maxWidth: '4xl',
mx: 'auto',
p: 4,
background: 'rgb(0,0,0,0)',
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3 }}>
{/* Animated Keyboard and Hands */}
<Box sx={{ position: 'relative' }}>
<Keyboard
style={{
width: 64,
height: 64,
color: theme.palette.primary.main,
animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -16,
left: '50%',
transform: 'translateX(-50%)'
}}
>
<Waves
style={{
width: 32,
height: 32,
color: theme.palette.primary.light,
animation: 'bounce 1s infinite'
}}
/>
</Box>
</Box>
{/* Typing Animation */}
<Typography
variant="h4"
sx={{
fontWeight: 600,
color: theme.palette.text.primary,
mt: 2
}}
>
{text}
<Box
component="span"
sx={{
ml: 0.5,
opacity: showCursor ? 1 : 0,
transition: 'opacity 0.3s'
}}
>
|
</Box>
</Typography>
{/* Example Questions */}
<List sx={{ width: '100%', mt: 4 }}>
<ListItem
component={Paper}
elevation={1}
sx={{
mb: 2,
borderRadius: 1,
transition: 'box-shadow 0.3s',
'&:hover': { boxShadow: 3 }
}}
>
<ListItemIcon>
<Folder style={{ color: theme.palette.primary.main }} />
</ListItemIcon>
<ListItemText
primary="Can you analyze my CSV file and create a visualization of the trends?"
sx={{ color: theme.palette.text.primary }}
/>
</ListItem>
<ListItem
component={Paper}
elevation={1}
sx={{
mb: 2,
borderRadius: 1,
transition: 'box-shadow 0.3s',
'&:hover': { boxShadow: 3 }
}}
>
<ListItemIcon>
<Shield style={{ color: theme.palette.success.main }} />
</ListItemIcon>
<ListItemText
primary="What security measures does Data443 implement for data protection?"
sx={{ color: theme.palette.text.primary }}
/>
</ListItem>
<ListItem
component={Paper}
elevation={1}
sx={{
mb: 2,
borderRadius: 1,
transition: 'box-shadow 0.3s',
'&:hover': { boxShadow: 3 }
}}
>
<ListItemIcon>
<Lock style={{ color: theme.palette.secondary.main }} />
</ListItemIcon>
<ListItemText
primary="How can Data443's products help with GDPR compliance?"
sx={{ color: theme.palette.text.primary }}
/>
</ListItem>
</List>
{/* Footer Note */}
<Typography
variant="body2"
sx={{
color: theme.palette.text.secondary,
textAlign: 'center',
mt: 3
}}
>
Ask me anything about your data, security needs, or Data443 products
</Typography>
</Box>
</Paper>
);
};

View File

@ -1,72 +0,0 @@
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

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

View File

@ -1,192 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,96 +14,3 @@ html, body {
height: 100%;
width: 100%;
}
.error-message {
width: 100%;
background-color: rgba(211, 47, 47, 0.1);
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 8px;
color: #d32f2f;
border: 1px solid rgba(211, 47, 47, 0.2);
}
[data-theme="dark"] .error-message {
background-color: rgba(211, 47, 47, 0.15);
color: #ef5350;
border-color: rgba(211, 47, 47, 0.3);
}
.thinking-section {
border-radius: 8px;
margin: 0 0 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,23 +5,17 @@ import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: './',
base: process.env.ELECTRON_VITE_DEV_SERVER_URL ? '/' : './',
build: {
outDir: 'dist',
emptyOutDir: true,
target: 'esnext',
rollupOptions: {
external: ['electron', 'electron-store', ...Object.keys(require('./package.json').dependencies)],
output: {
format: 'es',
entryFileNames: '[name].js',
chunkFileNames: '[name].js',
assetFileNames: '[name][extname]'
}
external: ['http', 'https', 'path', 'fs', 'electron']
},
assetsInlineLimit: 0,
minify: 'esbuild',
sourcemap: true
assetsDir: '.',
minify: true,
sourcemap: false
},
resolve: {
alias: {

567
package-lock.json generated
View File

@ -1,567 +0,0 @@
{
"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"
}
}
}
}

View File

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