init
This commit is contained in:
commit
086e311aa4
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
261
DEVELOPMENT.md
Normal file
261
DEVELOPMENT.md
Normal file
@ -0,0 +1,261 @@
|
||||
# Data Hound Development Guide
|
||||
|
||||
This document provides detailed technical information for developers working on the Data Hound project. It covers architecture, development workflows, and best practices.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Main Process (Electron)
|
||||
|
||||
The main process (`electron/`) handles:
|
||||
|
||||
- File system operations (`services/fileSystem.ts`)
|
||||
- LLM service integration (`services/llmService.ts`, `services/ollamaService.ts`)
|
||||
- Vector store management (`services/vectorStore.ts`)
|
||||
- IPC communication (`ipc/handlers.ts`)
|
||||
- Application state persistence (`store.ts`)
|
||||
|
||||
#### Key Services
|
||||
|
||||
1. **File System Service** (`fileSystem.ts`)
|
||||
- Handles file indexing and monitoring
|
||||
- Manages file metadata extraction
|
||||
- Implements file type detection and parsing
|
||||
|
||||
2. **LLM Service** (`llmService.ts`)
|
||||
- Manages LLM provider connections
|
||||
- Handles prompt engineering
|
||||
- Implements response streaming
|
||||
|
||||
3. **Vector Store** (`vectorStore.ts`)
|
||||
- Manages ChromaDB integration
|
||||
- Handles document embeddings
|
||||
- Implements semantic search functionality
|
||||
|
||||
### Renderer Process (React)
|
||||
|
||||
The renderer process (`src/`) is organized into:
|
||||
|
||||
- Components (`components/`)
|
||||
- Contexts (`contexts/`)
|
||||
- Custom hooks (`hooks/`)
|
||||
- Type definitions (`electron.d.ts`)
|
||||
|
||||
#### Key Components
|
||||
|
||||
1. **ChatPanel**
|
||||
- Handles user queries and LLM responses
|
||||
- Manages conversation history
|
||||
- Implements message rendering
|
||||
|
||||
2. **FileExplorer**
|
||||
- Directory selection and navigation
|
||||
- File list visualization
|
||||
- File metadata display
|
||||
|
||||
3. **ScanningPanel**
|
||||
- Progress visualization for file scanning
|
||||
- Status updates
|
||||
- Error handling
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### Adding a New LLM Provider
|
||||
|
||||
1. Create a new service in `electron/services/`
|
||||
2. Implement the provider interface:
|
||||
```typescript
|
||||
interface LLMProvider {
|
||||
initialize(): Promise<void>;
|
||||
query(prompt: string): Promise<string>;
|
||||
streamResponse(prompt: string): AsyncGenerator<string>;
|
||||
}
|
||||
```
|
||||
3. Add provider configuration to `store.ts`
|
||||
4. Update the settings UI in `SettingsPanel`
|
||||
5. Add the provider to the LLM service factory
|
||||
|
||||
### Adding File Type Support
|
||||
|
||||
1. Update `fileSystem.ts` with new file type detection
|
||||
2. Implement parsing logic in a new service
|
||||
3. Add metadata extraction
|
||||
4. Update the vector store schema if needed
|
||||
5. Add UI support in FileExplorer
|
||||
|
||||
### Adding a New Panel
|
||||
|
||||
1. Create component in `src/components/`
|
||||
2. Add routing in `App.tsx`
|
||||
3. Implement required hooks
|
||||
4. Add IPC handlers if needed
|
||||
5. Update navigation
|
||||
|
||||
## Development Workflows
|
||||
|
||||
### Local Development
|
||||
|
||||
1. Start Electron development:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
This runs:
|
||||
- Vite dev server for React
|
||||
- Electron with hot reload
|
||||
- TypeScript compilation in watch mode
|
||||
|
||||
2. Debug main process:
|
||||
- Use VSCode launch configuration
|
||||
- Console logs appear in terminal
|
||||
- Break points work in VSCode
|
||||
|
||||
3. Debug renderer process:
|
||||
- Use Chrome DevTools (Cmd/Ctrl+Shift+I)
|
||||
- React DevTools available
|
||||
- Network tab shows IPC calls
|
||||
|
||||
### Testing
|
||||
|
||||
1. Unit Tests:
|
||||
- Located in `__tests__` directories
|
||||
- Run with `npm test`
|
||||
- Focus on service logic
|
||||
|
||||
2. Integration Tests:
|
||||
- Test IPC communication
|
||||
- Verify file system operations
|
||||
- Check LLM integration
|
||||
|
||||
3. E2E Tests:
|
||||
- Use Playwright
|
||||
- Test full user workflows
|
||||
- Verify cross-platform behavior
|
||||
|
||||
## Best Practices
|
||||
|
||||
### TypeScript
|
||||
|
||||
1. Use strict type checking
|
||||
2. Define interfaces for all IPC messages
|
||||
3. Avoid `any` - use proper types
|
||||
4. Use discriminated unions for state
|
||||
|
||||
### React Components
|
||||
|
||||
1. Use functional components
|
||||
2. Implement proper error boundaries
|
||||
3. Memoize expensive computations
|
||||
4. Use proper prop types
|
||||
|
||||
### Electron
|
||||
|
||||
1. Validate IPC messages
|
||||
2. Handle window state properly
|
||||
3. Implement proper error handling
|
||||
4. Use proper security practices
|
||||
|
||||
### State Management
|
||||
|
||||
1. Use contexts for shared state
|
||||
2. Implement proper loading states
|
||||
3. Handle errors gracefully
|
||||
4. Use proper TypeScript types
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding an IPC Handler
|
||||
|
||||
1. Define types in `preload-types.ts`:
|
||||
```typescript
|
||||
interface IPCHandlers {
|
||||
newHandler: (arg: ArgType) => Promise<ReturnType>;
|
||||
}
|
||||
```
|
||||
|
||||
2. Implement handler in `ipc/handlers.ts`:
|
||||
```typescript
|
||||
ipcMain.handle('newHandler', async (event, arg: ArgType) => {
|
||||
// Implementation
|
||||
});
|
||||
```
|
||||
|
||||
3. Add to preload script:
|
||||
```typescript
|
||||
newHandler: (arg: ArgType) => ipcRenderer.invoke('newHandler', arg)
|
||||
```
|
||||
|
||||
4. Use in renderer:
|
||||
```typescript
|
||||
const result = await window.electron.newHandler(arg);
|
||||
```
|
||||
|
||||
### Updating the Database Schema
|
||||
|
||||
1. Create migration in `electron/services/vectorStore.ts`
|
||||
2. Update type definitions
|
||||
3. Implement data migration
|
||||
4. Update queries
|
||||
5. Test migration
|
||||
|
||||
### Adding Settings
|
||||
|
||||
1. Add to store schema in `store.ts`
|
||||
2. Update settings component
|
||||
3. Implement validation
|
||||
4. Add migration if needed
|
||||
5. Update relevant services
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **IPC Communication Failures**
|
||||
- Check handler registration
|
||||
- Verify type definitions
|
||||
- Check error handling
|
||||
|
||||
2. **File System Issues**
|
||||
- Verify permissions
|
||||
- Check path handling
|
||||
- Validate file operations
|
||||
|
||||
3. **LLM Integration**
|
||||
- Verify API keys
|
||||
- Check network connectivity
|
||||
- Validate response handling
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
1. **Main Process**
|
||||
- Profile file system operations
|
||||
- Optimize database queries
|
||||
- Implement proper caching
|
||||
|
||||
2. **Renderer Process**
|
||||
- Use React.memo for expensive components
|
||||
- Implement virtual scrolling
|
||||
- Optimize re-renders
|
||||
|
||||
## Release Process
|
||||
|
||||
1. Update version in `package.json`
|
||||
2. Run full test suite
|
||||
3. Build production version
|
||||
4. Test packaged application
|
||||
5. Create release notes
|
||||
6. Tag release in git
|
||||
7. Build installers
|
||||
8. Publish release
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create feature branch
|
||||
3. Follow code style
|
||||
4. Add tests
|
||||
5. Submit pull request
|
||||
|
||||
Remember to:
|
||||
- Follow TypeScript best practices
|
||||
- Add proper documentation
|
||||
- Include tests
|
||||
- Update this guide as needed
|
36
README.md
Normal file
36
README.md
Normal file
@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
BIN
ai-profile.webp
Normal file
BIN
ai-profile.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 398 KiB |
30
electron-file-search/.eslintrc.json
Normal file
30
electron-file-search/.eslintrc.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"ignorePatterns": ["dist", "dist-electron", ".eslintrc.json"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["react-refresh"],
|
||||
"rules": {
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ "allowConstantExport": true }
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
43
electron-file-search/.gitignore
vendored
Normal file
43
electron-file-search/.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
dist
|
||||
dist-electron
|
||||
release
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Electron store files
|
||||
config.json
|
||||
|
||||
# ChromaDB files
|
||||
vectordb/
|
||||
chroma.sqlite3
|
||||
|
||||
# Tesseract temp files
|
||||
*.traineddata
|
113
electron-file-search/README.md
Normal file
113
electron-file-search/README.md
Normal file
@ -0,0 +1,113 @@
|
||||
# Data Hound
|
||||
|
||||
An Electron-based desktop application that uses AI to help you search and understand your files through natural language queries.
|
||||
|
||||
## Features
|
||||
|
||||
- Natural language search across your files
|
||||
- Support for multiple file types (text, PDF, images via OCR)
|
||||
- Multiple LLM provider support (OpenAI, OpenRouter, Ollama)
|
||||
- Real-time file monitoring and indexing
|
||||
- Vector-based semantic search
|
||||
- Dark mode interface
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Electron
|
||||
- React
|
||||
- TypeScript
|
||||
- Material-UI
|
||||
- LangChain
|
||||
- ChromaDB
|
||||
- Tesseract.js (OCR)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- npm 8+
|
||||
- For Ollama support: Ollama running locally
|
||||
- For OCR: Tesseract installed on your system
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone [repository-url]
|
||||
cd electron-file-search
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### LLM Providers
|
||||
|
||||
The application supports three LLM providers:
|
||||
|
||||
1. **OpenAI**
|
||||
- Requires an API key
|
||||
- Default model: gpt-3.5-turbo
|
||||
|
||||
2. **OpenRouter**
|
||||
- Requires an API key
|
||||
- Supports various models
|
||||
- Custom base URL configuration
|
||||
|
||||
3. **Ollama**
|
||||
- Runs locally
|
||||
- No API key required
|
||||
- Default URL: http://localhost:11434
|
||||
- Default model: llama2
|
||||
|
||||
Configure your preferred provider through the settings panel in the application.
|
||||
|
||||
## Building
|
||||
|
||||
To create a production build:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
To package the application:
|
||||
|
||||
```bash
|
||||
npm run package
|
||||
```
|
||||
|
||||
This will create platform-specific installers in the `release` directory.
|
||||
|
||||
## Development
|
||||
|
||||
- `npm run dev` - Start the development server
|
||||
- `npm run lint` - Run ESLint
|
||||
- `npm run build` - Create a production build
|
||||
- `npm run package` - Package the application
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
electron-file-search/
|
||||
├── electron/ # Electron main process code
|
||||
│ ├── main.ts # Main entry point
|
||||
│ ├── preload.ts # Preload script
|
||||
│ └── services/ # Backend services
|
||||
├── src/ # React renderer code
|
||||
│ ├── components/ # React components
|
||||
│ ├── contexts/ # React contexts
|
||||
│ ├── hooks/ # Custom hooks
|
||||
│ └── main.tsx # Renderer entry point
|
||||
└── scripts/ # Build and development scripts
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
187
electron-file-search/electron/ipc/handlers.ts
Normal file
187
electron-file-search/electron/ipc/handlers.ts
Normal file
@ -0,0 +1,187 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { DocumentMetadata, LLMConfig, ServiceError } from '../types';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
interface IpcHandlerParams {
|
||||
_: unknown;
|
||||
dirPath?: string;
|
||||
path?: string;
|
||||
question?: string;
|
||||
config?: LLMConfig;
|
||||
content?: string;
|
||||
metadata?: DocumentMetadata;
|
||||
}
|
||||
|
||||
export function setupIpcHandlers() {
|
||||
// File System Handlers
|
||||
ipcMain.handle('start-watching', async (_: unknown, dirPath: string) => {
|
||||
try {
|
||||
await (await import('../services/fileSystem')).fileSystemService.startWatching(dirPath);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error starting watch:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('stop-watching', async (_: unknown, dirPath: string) => {
|
||||
try {
|
||||
await (await import('../services/fileSystem')).fileSystemService.stopWatching(dirPath);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error stopping watch:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('add-excluded-path', async (_: unknown, path: string) => {
|
||||
try {
|
||||
(await import('../services/fileSystem')).fileSystemService.addExcludedPath(path);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error adding excluded path:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
// LLM Handlers
|
||||
ipcMain.handle('query-llm', async (event, question: string) => {
|
||||
try {
|
||||
const webContents = event.sender;
|
||||
const response = await (await import('../services/llmService')).llmService.query(
|
||||
question,
|
||||
(chunk: string) => {
|
||||
webContents.send('llm-response-chunk', chunk);
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error querying LLM:', err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('update-llm-config', async (_: unknown, config: LLMConfig) => {
|
||||
try {
|
||||
await (await import('../services/llmService')).llmService.updateConfig(config);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error updating LLM config:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-llm-config', async () => {
|
||||
try {
|
||||
const config = (await import('../services/llmService')).llmService.getConfig();
|
||||
return { success: true, data: config };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error getting LLM config:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-ollama-models', async () => {
|
||||
try {
|
||||
const models = await (await import('../services/llmService')).llmService.getOllamaModels();
|
||||
return { success: true, data: models };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error getting Ollama models:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-documents', async () => {
|
||||
try {
|
||||
const documents = (await import('../services/vectorStore')).vectorStoreService.getDocuments();
|
||||
return { success: true, data: documents };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error getting documents:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Vector Store Handlers
|
||||
ipcMain.handle('add-document', async (_: unknown, content: string, metadata: DocumentMetadata) => {
|
||||
try {
|
||||
await (await import('../services/vectorStore')).vectorStoreService.addDocument(content, metadata);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error adding document:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-document', async (_: unknown, path: string) => {
|
||||
try {
|
||||
await (await import('../services/vectorStore')).vectorStoreService.deleteDocument(path);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error deleting document:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('update-document', async (_: unknown, content: string, metadata: DocumentMetadata) => {
|
||||
try {
|
||||
await (await import('../services/vectorStore')).vectorStoreService.updateDocument(content, metadata);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error updating document:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-user-home', async () => {
|
||||
try {
|
||||
const homeDir = os.homedir();
|
||||
return { success: true, data: homeDir };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error getting user home:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('list-directories', async (_: unknown, dirPath: string) => {
|
||||
try {
|
||||
const directories = await new Promise<Array<{name: string; path: string}>>((resolve, reject) => {
|
||||
fs.readdir(dirPath, { withFileTypes: true }, (err, items) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const folders = items
|
||||
.filter(item => item.isDirectory())
|
||||
.map(item => ({
|
||||
name: item.name,
|
||||
path: path.join(dirPath, item.name)
|
||||
}));
|
||||
resolve(folders);
|
||||
});
|
||||
});
|
||||
|
||||
return { success: true, data: directories };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('Error listing directories:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default setupIpcHandlers;
|
118
electron-file-search/electron/main.ts
Normal file
118
electron-file-search/electron/main.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { store as electronStore } from './store';
|
||||
import { setupIpcHandlers } from './ipc/handlers';
|
||||
|
||||
// Initialize IPC handlers immediately
|
||||
setupIpcHandlers();
|
||||
|
||||
function createWindow() {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
frame: false,
|
||||
titleBarStyle: 'hidden',
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
sandbox: false,
|
||||
webSecurity: true,
|
||||
},
|
||||
show: false,
|
||||
});
|
||||
|
||||
// In development, use the Vite dev server
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
// Wait for dev server to be ready
|
||||
const pollDevServer = async () => {
|
||||
try {
|
||||
const response = await fetch(process.env.VITE_DEV_SERVER_URL);
|
||||
if (response.ok) {
|
||||
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
setTimeout(pollDevServer, 500);
|
||||
}
|
||||
} catch {
|
||||
setTimeout(pollDevServer, 500);
|
||||
}
|
||||
};
|
||||
pollDevServer();
|
||||
} else {
|
||||
// In production, load the built files
|
||||
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
|
||||
}
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
// Handle window state
|
||||
const windowState = electronStore.get('windowState', {
|
||||
width: 1200,
|
||||
height: 800
|
||||
});
|
||||
mainWindow.setSize(windowState.width, windowState.height);
|
||||
|
||||
mainWindow.on('close', () => {
|
||||
const { width, height } = mainWindow.getBounds();
|
||||
electronStore.set('windowState', { width, height });
|
||||
});
|
||||
|
||||
// Set up security headers
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': [
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// IPC handlers for file system operations
|
||||
ipcMain.handle('get-app-path', () => {
|
||||
return app.getPath('userData');
|
||||
});
|
||||
|
||||
// Window control handlers
|
||||
ipcMain.handle('window-minimize', (event) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
window?.minimize();
|
||||
});
|
||||
|
||||
ipcMain.handle('window-maximize', (event) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
if (window?.isMaximized()) {
|
||||
window.unmaximize();
|
||||
} else {
|
||||
window?.maximize();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('window-close', (event) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
window?.close();
|
||||
});
|
||||
|
||||
export default app;
|
48
electron-file-search/electron/preload-types.ts
Normal file
48
electron-file-search/electron/preload-types.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import type { LLMConfig, DocumentMetadata } from './types';
|
||||
|
||||
interface Directory {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
// File System Operations
|
||||
startWatching: (dirPath: string) => Promise<void>;
|
||||
stopWatching: (dirPath: string) => Promise<void>;
|
||||
addExcludedPath: (path: string) => Promise<void>;
|
||||
|
||||
// LLM Operations
|
||||
queryLLM: (question: string) => Promise<{
|
||||
answer: string;
|
||||
sources: DocumentMetadata[];
|
||||
}>;
|
||||
updateLLMConfig: (config: LLMConfig) => Promise<void>;
|
||||
getLLMConfig: () => Promise<LLMConfig>;
|
||||
getOllamaModels: () => Promise<{ success: boolean; data?: string[]; error?: string }>;
|
||||
|
||||
// Vector Store Operations
|
||||
getDocuments: () => Promise<{ success: boolean; data?: DocumentMetadata[]; error?: string }>;
|
||||
addDocument: (content: string, metadata: DocumentMetadata) => Promise<void>;
|
||||
deleteDocument: (path: string) => Promise<void>;
|
||||
updateDocument: (content: string, metadata: DocumentMetadata) => Promise<void>;
|
||||
|
||||
// File Processing
|
||||
processFile: (filePath: string) => Promise<void>;
|
||||
|
||||
// System Paths
|
||||
getUserHome: () => Promise<{ success: boolean; data?: string; error?: string }>;
|
||||
getAppPath: () => Promise<string>;
|
||||
|
||||
// Directory Operations
|
||||
listDirectories: (dirPath: string) => Promise<{ success: boolean; data?: Directory[]; error?: string }>;
|
||||
|
||||
// Event Handling
|
||||
on: (channel: string, callback: (event: unknown, ...args: any[]) => void) => void;
|
||||
off: (channel: string, callback: (event: unknown, ...args: any[]) => void) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
76
electron-file-search/electron/preload.ts
Normal file
76
electron-file-search/electron/preload.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import type { LLMConfig, DocumentMetadata } from './types';
|
||||
|
||||
interface Directory {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface IpcResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Expose protected methods that allow the renderer process to use
|
||||
// the ipcRenderer without exposing the entire object
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
// File System Operations
|
||||
startWatching: async (dirPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke('start-watching', dirPath),
|
||||
stopWatching: async (dirPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke('stop-watching', dirPath),
|
||||
addExcludedPath: async (path: string): Promise<void> =>
|
||||
ipcRenderer.invoke('add-excluded-path', path),
|
||||
|
||||
// LLM Operations
|
||||
queryLLM: async (question: string): Promise<{
|
||||
answer: string;
|
||||
sources: DocumentMetadata[];
|
||||
}> => ipcRenderer.invoke('query-llm', question),
|
||||
updateLLMConfig: async (config: LLMConfig): Promise<void> =>
|
||||
ipcRenderer.invoke('update-llm-config', config),
|
||||
getLLMConfig: async (): Promise<LLMConfig> =>
|
||||
ipcRenderer.invoke('get-llm-config'),
|
||||
getOllamaModels: async (): Promise<IpcResponse<string[]>> =>
|
||||
ipcRenderer.invoke('get-ollama-models'),
|
||||
|
||||
// Vector Store Operations
|
||||
getDocuments: async (): Promise<IpcResponse<DocumentMetadata[]>> =>
|
||||
ipcRenderer.invoke('get-documents'),
|
||||
addDocument: async (content: string, metadata: DocumentMetadata): Promise<void> =>
|
||||
ipcRenderer.invoke('add-document', content, metadata),
|
||||
deleteDocument: async (path: string): Promise<void> =>
|
||||
ipcRenderer.invoke('delete-document', path),
|
||||
updateDocument: async (content: string, metadata: DocumentMetadata): Promise<void> =>
|
||||
ipcRenderer.invoke('update-document', content, metadata),
|
||||
|
||||
// File Processing
|
||||
processFile: async (filePath: string): Promise<void> =>
|
||||
ipcRenderer.invoke('process-file', filePath),
|
||||
|
||||
// System Paths
|
||||
getUserHome: async (): Promise<IpcResponse<string>> =>
|
||||
ipcRenderer.invoke('get-user-home'),
|
||||
getAppPath: async (): Promise<string> =>
|
||||
ipcRenderer.invoke('get-app-path'),
|
||||
|
||||
// Directory Operations
|
||||
listDirectories: async (dirPath: string): Promise<IpcResponse<Directory[]>> =>
|
||||
ipcRenderer.invoke('list-directories', dirPath),
|
||||
|
||||
// Event Handling
|
||||
on: (channel: string, callback: (event: unknown, ...args: any[]) => void) => {
|
||||
ipcRenderer.on(channel, callback);
|
||||
},
|
||||
off: (channel: string, callback: (event: unknown, ...args: any[]) => void) => {
|
||||
ipcRenderer.removeListener(channel, callback);
|
||||
},
|
||||
|
||||
// Window Controls
|
||||
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
|
||||
maximizeWindow: () => ipcRenderer.invoke('window-maximize'),
|
||||
closeWindow: () => ipcRenderer.invoke('window-close'),
|
||||
});
|
||||
|
||||
export type { Directory, IpcResponse };
|
104
electron-file-search/electron/services/fileSystem.ts
Normal file
104
electron-file-search/electron/services/fileSystem.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { FSWatcher } from 'chokidar';
|
||||
import * as chokidar from 'chokidar';
|
||||
import Store from 'electron-store';
|
||||
import { ServiceError } from '../types';
|
||||
|
||||
const store = new Store<{
|
||||
watchedPaths: string[];
|
||||
excludedPaths: string[];
|
||||
}>();
|
||||
|
||||
class FileSystemService {
|
||||
private watchers: Map<string, FSWatcher>;
|
||||
private excludedPaths: Set<string>;
|
||||
|
||||
constructor() {
|
||||
this.watchers = new Map();
|
||||
this.excludedPaths = new Set(store.get('excludedPaths', []));
|
||||
|
||||
// Add example paths
|
||||
const examplePaths = [
|
||||
'C:/example/llm-only',
|
||||
'C:/example/ocr-only',
|
||||
'C:/example/both-enabled'
|
||||
];
|
||||
|
||||
// Set initial watched paths
|
||||
store.set('watchedPaths', examplePaths);
|
||||
|
||||
// Start watching example paths
|
||||
examplePaths.forEach(path => {
|
||||
this.watchers.set(path, null); // Add to watchers without actual watcher since paths don't exist
|
||||
});
|
||||
}
|
||||
|
||||
public async startWatching(dirPath: string): Promise<void> {
|
||||
if (this.watchers.has(dirPath)) {
|
||||
throw new ServiceError(`Already watching directory: ${dirPath}`);
|
||||
}
|
||||
|
||||
const watcher = chokidar.watch(dirPath, {
|
||||
ignored: [
|
||||
/(^|[\/\\])\../, // Ignore dotfiles
|
||||
'**/node_modules/**',
|
||||
...Array.from(this.excludedPaths),
|
||||
],
|
||||
persistent: true,
|
||||
ignoreInitial: false,
|
||||
});
|
||||
|
||||
watcher.on('add', path => {
|
||||
console.log(`File ${path} has been added`);
|
||||
// TODO: Process file
|
||||
});
|
||||
|
||||
watcher.on('change', path => {
|
||||
console.log(`File ${path} has been changed`);
|
||||
// TODO: Process file changes
|
||||
});
|
||||
|
||||
watcher.on('unlink', path => {
|
||||
console.log(`File ${path} has been removed`);
|
||||
// TODO: Remove from vector store
|
||||
});
|
||||
|
||||
this.watchers.set(dirPath, watcher);
|
||||
const watchedPaths = store.get('watchedPaths', []);
|
||||
if (!watchedPaths.includes(dirPath)) {
|
||||
store.set('watchedPaths', [...watchedPaths, dirPath]);
|
||||
}
|
||||
}
|
||||
|
||||
public async stopWatching(dirPath: string): Promise<void> {
|
||||
const watcher = this.watchers.get(dirPath);
|
||||
if (!watcher) {
|
||||
throw new ServiceError(`Not watching directory: ${dirPath}`);
|
||||
}
|
||||
|
||||
await watcher.close();
|
||||
this.watchers.delete(dirPath);
|
||||
|
||||
const watchedPaths = store.get('watchedPaths', []);
|
||||
store.set('watchedPaths', watchedPaths.filter(p => p !== dirPath));
|
||||
}
|
||||
|
||||
public addExcludedPath(path: string): void {
|
||||
this.excludedPaths.add(path);
|
||||
store.set('excludedPaths', Array.from(this.excludedPaths));
|
||||
|
||||
// Update all watchers with new excluded path
|
||||
this.watchers.forEach(watcher => {
|
||||
watcher.unwatch(path);
|
||||
});
|
||||
}
|
||||
|
||||
public getWatchedPaths(): string[] {
|
||||
return Array.from(this.watchers.keys());
|
||||
}
|
||||
|
||||
public getExcludedPaths(): string[] {
|
||||
return Array.from(this.excludedPaths);
|
||||
}
|
||||
}
|
||||
|
||||
export const fileSystemService = new FileSystemService();
|
236
electron-file-search/electron/services/llmService.ts
Normal file
236
electron-file-search/electron/services/llmService.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import { ServiceError, LLMConfig, DocumentMetadata } from '../types';
|
||||
const { store } = require('../store');
|
||||
import OpenAI from 'openai';
|
||||
import { OpenRouter } from 'openrouter-client';
|
||||
import { ollamaService } from './ollamaService';
|
||||
|
||||
type Message = { role: 'system' | 'user' | 'assistant'; content: string };
|
||||
|
||||
interface OpenAIClient {
|
||||
chat: {
|
||||
completions: {
|
||||
create: Function;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface OpenRouterStreamResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
choices: Array<{ delta?: { content?: string }; message?: { content: string } }>;
|
||||
};
|
||||
errorCode?: number;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
type OpenRouterConfig = {
|
||||
temperature?: number;
|
||||
model?: string;
|
||||
stream?: boolean;
|
||||
};
|
||||
|
||||
interface OpenRouterClient {
|
||||
chat: (messages: Message[], config?: OpenRouterConfig) => Promise<OpenRouterStreamResponse>;
|
||||
}
|
||||
|
||||
|
||||
export class LLMService {
|
||||
#config: LLMConfig;
|
||||
#openaiClient: OpenAIClient | null;
|
||||
#openrouterClient: OpenRouterClient | null;
|
||||
|
||||
constructor() {
|
||||
const storedConfig = store.get('llm_config');
|
||||
|
||||
this.#config = storedConfig || {
|
||||
provider: 'ollama',
|
||||
model: 'jimscard/blackhat-hacker:v2',
|
||||
baseUrl: 'http://localhost:11434',
|
||||
temperature: 0.7,
|
||||
apiKey: null
|
||||
};
|
||||
|
||||
// Ensure config is saved with defaults
|
||||
store.set('llm_config', this.#config);
|
||||
this.#openaiClient = null;
|
||||
this.#openrouterClient = null;
|
||||
this.#initializeClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#initializeClient() {
|
||||
switch (this.#config?.provider) {
|
||||
case 'openai':
|
||||
if (!this.#config.apiKey) {
|
||||
throw new ServiceError('OpenAI API key is required');
|
||||
}
|
||||
this.#openaiClient = new OpenAI({
|
||||
apiKey: this.#config.apiKey,
|
||||
baseURL: this.#config.baseUrl,
|
||||
});
|
||||
break;
|
||||
case 'openrouter':
|
||||
if (!this.#config.apiKey) {
|
||||
throw new ServiceError('OpenRouter API key is required');
|
||||
}
|
||||
this.#openrouterClient = new OpenRouter(this.#config.apiKey);
|
||||
break;
|
||||
case 'ollama':
|
||||
if (this.#config.baseUrl) {
|
||||
ollamaService.updateBaseUrl(this.#config.baseUrl);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async query(
|
||||
question: string,
|
||||
onChunk?: (chunk: string) => void
|
||||
): Promise<{ answer: string, sources: DocumentMetadata[] }> {
|
||||
if (!this.#config?.provider) {
|
||||
throw new ServiceError('LLM provider not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
switch (this.#config.provider) {
|
||||
case 'openai':
|
||||
if (!this.#openaiClient) {
|
||||
throw new ServiceError('OpenAI client not initialized');
|
||||
}
|
||||
const openaiResponse = await this.#openaiClient.chat.completions.create({
|
||||
model: this.#config.model || 'gpt-3.5-turbo',
|
||||
messages: [{ role: 'user', content: question }],
|
||||
temperature: this.#config.temperature || 0.7,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
let openaiText = '';
|
||||
for await (const chunk of openaiResponse) {
|
||||
const content = chunk.choices[0]?.delta?.content || '';
|
||||
if (content) {
|
||||
openaiText += content;
|
||||
onChunk?.(content);
|
||||
}
|
||||
}
|
||||
response = openaiText;
|
||||
break;
|
||||
|
||||
case 'openrouter':
|
||||
if (!this.#openrouterClient) {
|
||||
throw new ServiceError('OpenRouter client not initialized');
|
||||
}
|
||||
const openrouterResponse = await this.#openrouterClient.chat(
|
||||
[{ role: 'user', content: question }],
|
||||
{
|
||||
model: this.#config.model || 'openai/gpt-3.5-turbo',
|
||||
temperature: this.#config.temperature || 0.7,
|
||||
stream: true,
|
||||
}
|
||||
);
|
||||
if (!openrouterResponse.success) {
|
||||
throw new ServiceError(openrouterResponse.errorMessage || 'OpenRouter request failed');
|
||||
}
|
||||
|
||||
let routerText = '';
|
||||
for await (const chunk of openrouterResponse.data?.choices || []) {
|
||||
const content = chunk.delta?.content || chunk.message?.content || '';
|
||||
if (content) {
|
||||
routerText += content;
|
||||
onChunk?.(content);
|
||||
}
|
||||
}
|
||||
response = routerText;
|
||||
break;
|
||||
|
||||
case 'ollama':
|
||||
const ollamaResponse = await ollamaService.chat({
|
||||
model: this.#config.model || 'phi4:latest',
|
||||
messages: [{ role: 'user', content: question }],
|
||||
temperature: this.#config.temperature,
|
||||
onChunk,
|
||||
});
|
||||
response = ollamaResponse.message.content;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ServiceError(`Unsupported provider: ${this.#config.provider}`);
|
||||
}
|
||||
|
||||
/** @type {DocumentMetadata[]} */
|
||||
const sources = []; // TODO: Implement source retrieval from vector store
|
||||
|
||||
return {
|
||||
answer: response,
|
||||
sources,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error querying LLM:', error);
|
||||
throw new ServiceError(
|
||||
error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {LLMConfig}
|
||||
*/
|
||||
getConfig() {
|
||||
return this.#config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LLMConfig} newConfig - The new LLM configuration
|
||||
*/
|
||||
async updateConfig(newConfig) {
|
||||
// Validate required fields from schema
|
||||
if (!newConfig.provider) {
|
||||
throw new ServiceError('Provider is required');
|
||||
}
|
||||
|
||||
// Clean config to only include allowed properties from schema
|
||||
const cleanConfig = {
|
||||
provider: newConfig.provider,
|
||||
apiKey: newConfig.apiKey ?? null,
|
||||
model: newConfig.model ?? (newConfig.provider === 'ollama' ? 'phi4' : null),
|
||||
baseUrl: newConfig.provider === 'ollama' ? (newConfig.baseUrl ?? 'http://localhost:11434') : (newConfig.baseUrl ?? null),
|
||||
temperature: typeof newConfig.temperature === 'number' ? newConfig.temperature : 0.7
|
||||
};
|
||||
|
||||
// Validate provider-specific requirements
|
||||
if (cleanConfig.provider !== 'ollama' && !cleanConfig.apiKey) {
|
||||
throw new ServiceError(`${cleanConfig.provider} requires an API key`);
|
||||
}
|
||||
|
||||
try {
|
||||
store.set('llm_config', cleanConfig);
|
||||
this.#config = cleanConfig;
|
||||
this.#initializeClient();
|
||||
} catch (error) {
|
||||
throw new ServiceError(
|
||||
error instanceof Error ? error.message : 'Failed to update config'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available models from Ollama server
|
||||
* @returns {Promise<string[]>} List of model names
|
||||
*/
|
||||
async getOllamaModels() {
|
||||
try {
|
||||
return await ollamaService.getModels();
|
||||
} catch (error) {
|
||||
console.error('Error fetching Ollama models:', error);
|
||||
throw new ServiceError(
|
||||
error instanceof Error ? error.message : 'Failed to fetch Ollama models'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const llmService = new LLMService();
|
||||
export { llmService };
|
222
electron-file-search/electron/services/ollamaService.ts
Normal file
222
electron-file-search/electron/services/ollamaService.ts
Normal file
@ -0,0 +1,222 @@
|
||||
import { ServiceError } from '../types';
|
||||
import { net } from 'electron';
|
||||
|
||||
interface OllamaModel {
|
||||
name: string;
|
||||
modified_at: string;
|
||||
size: number;
|
||||
digest: string;
|
||||
}
|
||||
|
||||
interface OllamaListResponse {
|
||||
models: Array<{
|
||||
name: string;
|
||||
model: string;
|
||||
modified_at: string;
|
||||
size: number;
|
||||
digest: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface OllamaChatResponse {
|
||||
message: {
|
||||
content: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OllamaChatParams {
|
||||
model: string;
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
temperature?: number;
|
||||
onChunk?: (chunk: string) => void;
|
||||
}
|
||||
|
||||
class OllamaService {
|
||||
private baseUrl: string = 'http://127.0.0.1:11434';
|
||||
|
||||
private async makeRequest<T>(
|
||||
path: string,
|
||||
method: string = 'GET',
|
||||
body?: any,
|
||||
onChunk?: (chunk: string) => void
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const url = new URL(path, this.baseUrl);
|
||||
const request = net.request({
|
||||
url: url.toString(),
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
let responseData = '';
|
||||
let streamBuffer = '';
|
||||
|
||||
request.on('response', (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
const error = new Error(`HTTP error! status: ${response.statusCode}`);
|
||||
console.error('Ollama request failed:', {
|
||||
path,
|
||||
statusCode: response.statusCode,
|
||||
error
|
||||
});
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
try {
|
||||
const chunkStr = chunk.toString();
|
||||
|
||||
if (path === '/api/chat') {
|
||||
// Handle streaming chat response
|
||||
streamBuffer += chunkStr;
|
||||
const lines = streamBuffer.split('\n');
|
||||
|
||||
// Process all complete lines except the last one
|
||||
for (let i = 0; i < lines.length - 1; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
if (parsed.message?.content && onChunk) {
|
||||
onChunk(parsed.message.content);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse chat chunk:', { line, error: e });
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the last potentially incomplete line
|
||||
streamBuffer = lines[lines.length - 1];
|
||||
} else {
|
||||
// For non-streaming endpoints
|
||||
responseData += chunkStr;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error processing chunk:', e);
|
||||
}
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
try {
|
||||
if (path === '/api/chat') {
|
||||
// Handle any remaining data in the buffer
|
||||
if (streamBuffer.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(streamBuffer);
|
||||
if (parsed.message?.content && onChunk) {
|
||||
onChunk(parsed.message.content);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse final chat chunk:', { buffer: streamBuffer, error: e });
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
message: {
|
||||
content: ''
|
||||
}
|
||||
} as T);
|
||||
} else {
|
||||
// For non-streaming endpoints
|
||||
const trimmedResponse = responseData.trim();
|
||||
if (!trimmedResponse) {
|
||||
throw new Error('Empty response received');
|
||||
}
|
||||
resolve(JSON.parse(trimmedResponse) as T);
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to process response: ${e.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', (error) => {
|
||||
console.error('Request error:', {
|
||||
path,
|
||||
error: error.message
|
||||
});
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (body) {
|
||||
const bodyStr = JSON.stringify(body);
|
||||
console.log('Sending request:', {
|
||||
path,
|
||||
method,
|
||||
body: bodyStr
|
||||
});
|
||||
request.write(bodyStr);
|
||||
}
|
||||
|
||||
request.end();
|
||||
} catch (e) {
|
||||
console.error('Failed to make request:', e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getModels(): Promise<string[]> {
|
||||
try {
|
||||
const response = await this.makeRequest<OllamaListResponse>('/api/tags');
|
||||
return response.models.map(model => model.name);
|
||||
} catch (error) {
|
||||
console.error('Error fetching Ollama models:', error);
|
||||
throw new ServiceError(
|
||||
error instanceof Error ? error.message : 'Failed to fetch Ollama models'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async chat(params: OllamaChatParams): Promise<OllamaChatResponse> {
|
||||
if (!params?.model || !params?.messages?.length) {
|
||||
throw new ServiceError('Invalid chat parameters: model and messages are required');
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Starting chat request:', {
|
||||
model: params.model,
|
||||
messageCount: params.messages.length
|
||||
});
|
||||
|
||||
const { onChunk, temperature, ...requestParams } = params;
|
||||
const response = await this.makeRequest<OllamaChatResponse>(
|
||||
'/api/chat',
|
||||
'POST',
|
||||
{
|
||||
...requestParams,
|
||||
stream: true,
|
||||
temperature: temperature ?? 0.7
|
||||
},
|
||||
onChunk
|
||||
);
|
||||
|
||||
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:', {
|
||||
error: errorMessage,
|
||||
params: {
|
||||
model: params.model,
|
||||
messageCount: params.messages.length
|
||||
}
|
||||
});
|
||||
throw new ServiceError(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
updateBaseUrl(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export const ollamaService = new OllamaService();
|
70
electron-file-search/electron/services/vectorStore.ts
Normal file
70
electron-file-search/electron/services/vectorStore.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import Store from 'electron-store';
|
||||
import { DocumentMetadata } from '../types';
|
||||
|
||||
const store = new Store<{
|
||||
documents: Record<string, DocumentMetadata>;
|
||||
}>();
|
||||
|
||||
class VectorStoreService {
|
||||
private documents: Map<string, DocumentMetadata>;
|
||||
|
||||
constructor() {
|
||||
this.documents = new Map(Object.entries(store.get('documents', {})));
|
||||
|
||||
// Add example documents if none exist
|
||||
if (this.documents.size === 0) {
|
||||
const examples: DocumentMetadata[] = [
|
||||
{
|
||||
path: 'C:/example/llm-only',
|
||||
type: 'directory',
|
||||
lastModified: Date.now(),
|
||||
size: 0,
|
||||
hasEmbeddings: true,
|
||||
hasOcr: false
|
||||
},
|
||||
{
|
||||
path: 'C:/example/ocr-only',
|
||||
type: 'directory',
|
||||
lastModified: Date.now(),
|
||||
size: 0,
|
||||
hasEmbeddings: false,
|
||||
hasOcr: true
|
||||
},
|
||||
{
|
||||
path: 'C:/example/both-enabled',
|
||||
type: 'directory',
|
||||
lastModified: Date.now(),
|
||||
size: 0,
|
||||
hasEmbeddings: true,
|
||||
hasOcr: true
|
||||
}
|
||||
];
|
||||
|
||||
examples.forEach(doc => {
|
||||
this.documents.set(doc.path, doc);
|
||||
});
|
||||
store.set('documents', Object.fromEntries(this.documents));
|
||||
}
|
||||
}
|
||||
|
||||
public async addDocument(content: string, metadata: DocumentMetadata): Promise<void> {
|
||||
// TODO: Implement vector storage
|
||||
this.documents.set(metadata.path, metadata);
|
||||
store.set('documents', Object.fromEntries(this.documents));
|
||||
}
|
||||
|
||||
public async deleteDocument(path: string): Promise<void> {
|
||||
this.documents.delete(path);
|
||||
store.set('documents', Object.fromEntries(this.documents));
|
||||
}
|
||||
|
||||
public async updateDocument(content: string, metadata: DocumentMetadata): Promise<void> {
|
||||
await this.addDocument(content, metadata);
|
||||
}
|
||||
|
||||
public getDocuments(): DocumentMetadata[] {
|
||||
return Array.from(this.documents.values());
|
||||
}
|
||||
}
|
||||
|
||||
export const vectorStoreService = new VectorStoreService();
|
14
electron-file-search/electron/store.d.ts
vendored
Normal file
14
electron-file-search/electron/store.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
import { ElectronStore } from 'electron-store';
|
||||
|
||||
declare module 'electron-store' {
|
||||
interface ElectronStore<T> {
|
||||
get<K extends keyof T>(key: K): T[K];
|
||||
set<K extends keyof T>(key: K, value: T[K]): void;
|
||||
// Add other methods as needed
|
||||
has<K extends keyof T>(key: K): boolean;
|
||||
delete<K extends keyof T>(key: K): void;
|
||||
clear(): void;
|
||||
}
|
||||
}
|
||||
|
||||
export = ElectronStore;
|
111
electron-file-search/electron/store.ts
Normal file
111
electron-file-search/electron/store.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import Store from 'electron-store';
|
||||
|
||||
interface WindowState {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface LLMConfig {
|
||||
provider: 'openai' | 'openrouter' | 'ollama';
|
||||
apiKey?: string | null;
|
||||
model?: string;
|
||||
baseUrl?: string;
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
path: string;
|
||||
type: string;
|
||||
lastModified: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface Schema {
|
||||
windowState: WindowState;
|
||||
llm_config: LLMConfig;
|
||||
watchedPaths: string[];
|
||||
excludedPaths: string[];
|
||||
documents: Record<string, Document>;
|
||||
}
|
||||
|
||||
const schema: Store.Schema<Schema> = {
|
||||
windowState: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
width: { type: 'number', minimum: 400 },
|
||||
height: { type: 'number', minimum: 300 }
|
||||
},
|
||||
required: ['width', 'height'],
|
||||
additionalProperties: false,
|
||||
default: {
|
||||
width: 1200,
|
||||
height: 800
|
||||
}
|
||||
},
|
||||
llm_config: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
provider: {
|
||||
type: 'string',
|
||||
enum: ['openai', 'openrouter', 'ollama']
|
||||
},
|
||||
apiKey: { type: ['string', 'null'] },
|
||||
model: { type: 'string' },
|
||||
baseUrl: { type: 'string' },
|
||||
temperature: { type: 'number', minimum: 0, maximum: 1 }
|
||||
},
|
||||
required: ['provider'],
|
||||
additionalProperties: false,
|
||||
default: {
|
||||
provider: 'ollama',
|
||||
model: 'phi4',
|
||||
baseUrl: 'http://localhost:11434',
|
||||
temperature: 0.7
|
||||
}
|
||||
},
|
||||
watchedPaths: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
default: []
|
||||
},
|
||||
excludedPaths: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
default: []
|
||||
},
|
||||
documents: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string' },
|
||||
type: { type: 'string' },
|
||||
lastModified: { type: 'number' },
|
||||
size: { type: 'number' }
|
||||
},
|
||||
required: ['path', 'type', 'lastModified', 'size']
|
||||
},
|
||||
default: {}
|
||||
}
|
||||
};
|
||||
|
||||
export const store = new Store<Schema>({
|
||||
schema,
|
||||
name: 'config',
|
||||
clearInvalidConfig: true,
|
||||
migrations: {
|
||||
'>=0.0.1': (store) => {
|
||||
const currentConfig = store.get('llm_config');
|
||||
if (currentConfig) {
|
||||
const cleanConfig = {
|
||||
provider: currentConfig.provider || 'ollama',
|
||||
apiKey: currentConfig.apiKey,
|
||||
model: currentConfig.model,
|
||||
baseUrl: currentConfig.baseUrl,
|
||||
temperature: currentConfig.temperature
|
||||
};
|
||||
store.set('llm_config', cleanConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
23
electron-file-search/electron/tsconfig.json
Normal file
23
electron-file-search/electron/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"outDir": "../dist-electron",
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"*": ["../node_modules/*"]
|
||||
},
|
||||
"types": ["node", "electron"]
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.cts"],
|
||||
"exclude": ["../src"]
|
||||
}
|
22
electron-file-search/electron/tsconfig.preload.json
Normal file
22
electron-file-search/electron/tsconfig.preload.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"target": "ES2020",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"outDir": "../dist-electron",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"*": ["../node_modules/*"]
|
||||
}
|
||||
},
|
||||
"files": ["preload.ts"],
|
||||
"include": ["preload-types.ts", "types.ts"]
|
||||
}
|
33
electron-file-search/electron/types.ts
Normal file
33
electron-file-search/electron/types.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export interface LLMConfig {
|
||||
provider: 'openai' | 'openrouter' | 'ollama';
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
baseUrl?: string;
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export interface DocumentMetadata {
|
||||
path: string;
|
||||
type: string;
|
||||
lastModified: number;
|
||||
size: number;
|
||||
hasEmbeddings?: boolean;
|
||||
hasOcr?: boolean;
|
||||
}
|
||||
|
||||
export class ServiceError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ServiceError';
|
||||
}
|
||||
}
|
||||
|
||||
// For CommonJS compatibility
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
ServiceError,
|
||||
// Export interface types for TypeScript
|
||||
LLMConfig: Symbol.for('LLMConfig'),
|
||||
DocumentMetadata: Symbol.for('DocumentMetadata')
|
||||
};
|
||||
}
|
8
electron-file-search/electron/types/pdf.d.ts
vendored
Normal file
8
electron-file-search/electron/types/pdf.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
declare module 'pdfjs-dist/build/pdf.mjs' {
|
||||
export * from 'pdfjs-dist';
|
||||
}
|
||||
|
||||
declare module 'pdfjs-dist/build/pdf.worker.mjs' {
|
||||
const workerSrc: string;
|
||||
export default workerSrc;
|
||||
}
|
38
electron-file-search/eslint.config.js
Normal file
38
electron-file-search/eslint.config.js
Normal file
@ -0,0 +1,38 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import react from 'eslint-plugin-react'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
react,
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
13
electron-file-search/index.html
Normal file
13
electron-file-search/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:;" />
|
||||
<title>Data Hound</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
9955
electron-file-search/package-lock.json
generated
Normal file
9955
electron-file-search/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
electron-file-search/package.json
Normal file
49
electron-file-search/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "electron-file-search",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"main": "dist-electron/main.js",
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.cjs",
|
||||
"build": "node scripts/build.js",
|
||||
"build:vite": "vite build",
|
||||
"clean": "rimraf dist dist-electron .vite",
|
||||
"preview": "vite preview",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@mui/icons-material": "^5.15.7",
|
||||
"@mui/material": "^5.15.7",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron-store": "^8.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"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",
|
||||
"rehype-highlight": "^7.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.16",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"electron": "^28.1.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"rimraf": "^5.0.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
6
electron-file-search/postcss.config.mjs
Normal file
6
electron-file-search/postcss.config.mjs
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
1
electron-file-search/public/vite.svg
Normal file
1
electron-file-search/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
50
electron-file-search/scripts/build.js
Normal file
50
electron-file-search/scripts/build.js
Normal file
@ -0,0 +1,50 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, '..');
|
||||
|
||||
async function build() {
|
||||
try {
|
||||
// Build Vite/React
|
||||
console.log('Building Vite/React...');
|
||||
await spawnAsync('npm', ['run', 'build:vite']);
|
||||
|
||||
// Build main process
|
||||
console.log('Building main process...');
|
||||
await spawnAsync('tsc', ['-p', 'electron/tsconfig.json']);
|
||||
|
||||
// Build preload script
|
||||
console.log('Building preload script...');
|
||||
await spawnAsync('tsc', ['-p', 'electron/tsconfig.preload.json']);
|
||||
|
||||
console.log('Build complete!');
|
||||
} catch (error) {
|
||||
console.error('Build failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function spawnAsync(command, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const childProcess = spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
childProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
build();
|
150
electron-file-search/scripts/dev.cjs
Normal file
150
electron-file-search/scripts/dev.cjs
Normal file
@ -0,0 +1,150 @@
|
||||
const { spawn } = require('child_process');
|
||||
const { createServer, build } = require('vite');
|
||||
const electron = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* @type {import('vite').LogLevel}
|
||||
*/
|
||||
const LOG_LEVEL = 'info';
|
||||
|
||||
/** @type {import('vite').InlineConfig} */
|
||||
const sharedConfig = {
|
||||
mode: 'development',
|
||||
build: {
|
||||
watch: {},
|
||||
},
|
||||
logLevel: LOG_LEVEL,
|
||||
};
|
||||
|
||||
/** @type {import('vite').InlineConfig} */
|
||||
const preloadConfig = {
|
||||
...sharedConfig,
|
||||
configFile: false,
|
||||
root: path.join(__dirname, '..'),
|
||||
build: {
|
||||
...sharedConfig.build,
|
||||
outDir: 'dist-electron',
|
||||
lib: {
|
||||
entry: path.join(__dirname, '../electron/preload.ts'),
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron'],
|
||||
output: {
|
||||
entryFileNames: '[name].js',
|
||||
},
|
||||
},
|
||||
emptyOutDir: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Find all TypeScript files in the electron directory
|
||||
const electronDir = path.join(__dirname, '../electron');
|
||||
const entries = {};
|
||||
function addEntry(file) {
|
||||
const relativePath = path.relative(electronDir, file);
|
||||
const name = relativePath.replace(/\.ts$/, '');
|
||||
entries[name] = file;
|
||||
}
|
||||
|
||||
function scanDirectory(dir) {
|
||||
const files = fs.readdirSync(dir);
|
||||
files.forEach(file => {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
scanDirectory(fullPath);
|
||||
} else if (file.endsWith('.ts')) {
|
||||
addEntry(fullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scanDirectory(electronDir);
|
||||
|
||||
/** @type {import('vite').InlineConfig} */
|
||||
const mainConfig = {
|
||||
...sharedConfig,
|
||||
configFile: false,
|
||||
root: path.join(__dirname, '..'),
|
||||
build: {
|
||||
...sharedConfig.build,
|
||||
outDir: 'dist-electron',
|
||||
lib: {
|
||||
entry: entries,
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron', 'electron-store', 'path', 'os', 'chokidar', 'openai', 'openrouter-client', 'ollama'],
|
||||
output: {
|
||||
entryFileNames: '[name].js',
|
||||
format: 'cjs',
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: path.join(__dirname, '../electron'),
|
||||
},
|
||||
},
|
||||
emptyOutDir: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {(server: import('vite').ViteDevServer) => Promise<import('rollup').RollupWatcher>}
|
||||
*/
|
||||
function watchMain(server) {
|
||||
/**
|
||||
* @type {import('child_process').ChildProcessWithoutNullStreams | null}
|
||||
*/
|
||||
let electronProcess = null;
|
||||
|
||||
return build({
|
||||
...mainConfig,
|
||||
plugins: [{
|
||||
name: 'electron-main-watcher',
|
||||
writeBundle() {
|
||||
if (electronProcess) {
|
||||
electronProcess.kill();
|
||||
electronProcess = null;
|
||||
}
|
||||
|
||||
electronProcess = spawn(electron, ['.'], {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
VITE_DEV_SERVER_URL: `http://localhost:${server.config.server.port}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {() => Promise<import('rollup').RollupWatcher>}
|
||||
*/
|
||||
function watchPreload() {
|
||||
return build(preloadConfig);
|
||||
}
|
||||
|
||||
// bootstrap
|
||||
async function start() {
|
||||
try {
|
||||
console.log('Starting Vite dev server...');
|
||||
const server = await createServer({ ...sharedConfig });
|
||||
await server.listen();
|
||||
console.log(`Dev server running at: ${server.config.server.port}`);
|
||||
|
||||
console.log('Building preload script...');
|
||||
await watchPreload();
|
||||
|
||||
console.log('Starting Electron...');
|
||||
await watchMain(server);
|
||||
console.log('Development environment ready!');
|
||||
} catch (error) {
|
||||
console.error('Error starting dev server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
start();
|
155
electron-file-search/scripts/dev.js
Normal file
155
electron-file-search/scripts/dev.js
Normal file
@ -0,0 +1,155 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { createServer, build } from 'vite';
|
||||
import electron from 'electron/index.js';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
/**
|
||||
* @type {import('vite').LogLevel}
|
||||
*/
|
||||
const LOG_LEVEL = 'info';
|
||||
|
||||
/** @type {import('vite').InlineConfig} */
|
||||
const sharedConfig = {
|
||||
mode: 'development',
|
||||
build: {
|
||||
watch: {},
|
||||
},
|
||||
logLevel: LOG_LEVEL,
|
||||
};
|
||||
|
||||
/** @type {import('vite').InlineConfig} */
|
||||
const preloadConfig = {
|
||||
...sharedConfig,
|
||||
configFile: false,
|
||||
root: path.join(__dirname, '..'),
|
||||
build: {
|
||||
...sharedConfig.build,
|
||||
outDir: 'dist-electron',
|
||||
lib: {
|
||||
entry: path.join(__dirname, '../electron/preload.ts'),
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron'],
|
||||
output: {
|
||||
entryFileNames: '[name].js',
|
||||
},
|
||||
},
|
||||
emptyOutDir: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Find all TypeScript files in the electron directory
|
||||
const electronDir = path.join(__dirname, '../electron');
|
||||
const entries = {};
|
||||
function addEntry(file) {
|
||||
const relativePath = path.relative(electronDir, file);
|
||||
const name = relativePath.replace(/\.ts$/, '');
|
||||
entries[name] = file;
|
||||
}
|
||||
|
||||
function scanDirectory(dir) {
|
||||
const files = fs.readdirSync(dir);
|
||||
files.forEach(file => {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
scanDirectory(fullPath);
|
||||
} else if (file.endsWith('.ts')) {
|
||||
addEntry(fullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scanDirectory(electronDir);
|
||||
|
||||
/** @type {import('vite').InlineConfig} */
|
||||
const mainConfig = {
|
||||
...sharedConfig,
|
||||
configFile: false,
|
||||
root: path.join(__dirname, '..'),
|
||||
build: {
|
||||
...sharedConfig.build,
|
||||
outDir: 'dist-electron',
|
||||
lib: {
|
||||
entry: entries,
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron', 'electron-store', 'path', 'os', 'chokidar', 'openai', 'openrouter-client', 'ollama'],
|
||||
output: {
|
||||
entryFileNames: '[name].js',
|
||||
format: 'cjs',
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: path.join(__dirname, '../electron'),
|
||||
},
|
||||
},
|
||||
emptyOutDir: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {(server: import('vite').ViteDevServer) => Promise<import('rollup').RollupWatcher>}
|
||||
*/
|
||||
function watchMain(server) {
|
||||
/**
|
||||
* @type {import('child_process').ChildProcessWithoutNullStreams | null}
|
||||
*/
|
||||
let electronProcess = null;
|
||||
|
||||
return build({
|
||||
...mainConfig,
|
||||
plugins: [{
|
||||
name: 'electron-main-watcher',
|
||||
writeBundle() {
|
||||
if (electronProcess) {
|
||||
electronProcess.kill();
|
||||
electronProcess = null;
|
||||
}
|
||||
|
||||
electronProcess = spawn(electron, ['.'], {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
VITE_DEV_SERVER_URL: `http://localhost:${server.config.server.port}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {() => Promise<import('rollup').RollupWatcher>}
|
||||
*/
|
||||
function watchPreload() {
|
||||
return build(preloadConfig);
|
||||
}
|
||||
|
||||
// bootstrap
|
||||
async function start() {
|
||||
try {
|
||||
console.log('Starting Vite dev server...');
|
||||
const server = await createServer({ ...sharedConfig });
|
||||
await server.listen();
|
||||
console.log(`Dev server running at: ${server.config.server.port}`);
|
||||
|
||||
console.log('Building preload script...');
|
||||
await watchPreload();
|
||||
|
||||
console.log('Starting Electron...');
|
||||
await watchMain(server);
|
||||
console.log('Development environment ready!');
|
||||
} catch (error) {
|
||||
console.error('Error starting dev server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
start();
|
67
electron-file-search/scripts/watch-electron.js
Normal file
67
electron-file-search/scripts/watch-electron.js
Normal file
@ -0,0 +1,67 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { watch } from 'fs';
|
||||
import { exec } from 'child_process';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
let electronProcess = null;
|
||||
let isElectronRunning = false;
|
||||
|
||||
function runElectron() {
|
||||
if (isElectronRunning) {
|
||||
electronProcess.kill();
|
||||
isElectronRunning = false;
|
||||
}
|
||||
|
||||
// Set environment variable for development
|
||||
const env = { ...process.env, NODE_ENV: 'development' };
|
||||
|
||||
electronProcess = spawn('electron', ['.'], {
|
||||
stdio: 'inherit',
|
||||
env
|
||||
});
|
||||
|
||||
isElectronRunning = true;
|
||||
|
||||
electronProcess.on('close', () => {
|
||||
isElectronRunning = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Initial build
|
||||
exec('tsc -p electron/tsconfig.json', (error) => {
|
||||
if (error) {
|
||||
console.error('Error building Electron files:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Initial Electron build complete');
|
||||
runElectron();
|
||||
});
|
||||
|
||||
// Watch for changes
|
||||
watch('electron', { recursive: true }, (eventType, filename) => {
|
||||
if (filename && filename.endsWith('.ts')) {
|
||||
console.log(`File ${filename} changed, rebuilding...`);
|
||||
|
||||
exec('tsc -p electron/tsconfig.json', (error) => {
|
||||
if (error) {
|
||||
console.error('Error rebuilding Electron files:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Rebuild complete, restarting Electron...');
|
||||
runElectron();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle process termination
|
||||
process.on('SIGINT', () => {
|
||||
if (electronProcess) {
|
||||
electronProcess.kill();
|
||||
}
|
||||
process.exit();
|
||||
});
|
287
electron-file-search/src/App.tsx
Normal file
287
electron-file-search/src/App.tsx
Normal file
@ -0,0 +1,287 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, CssBaseline, ThemeProvider, createTheme, Tabs, Tab, TextField, IconButton } from '@mui/material';
|
||||
import { Send as SendIcon, DeleteOutline as ClearIcon, Close as CloseIcon, Remove as MinimizeIcon, Fullscreen as MaximizeIcon } from '@mui/icons-material';
|
||||
import { useChat } from './hooks/useChat';
|
||||
import ChatPanel from './components/ChatPanel';
|
||||
import FileExplorer from './components/FileExplorer';
|
||||
import SettingsPanel from './components/SettingsPanel';
|
||||
import HomePanel from './components/HomePanel';
|
||||
import ScanningPanel from './components/ScanningPanel';
|
||||
import ReportingPanel from './components/ReportingPanel';
|
||||
import { ElectronProvider } from './contexts/ElectronContext';
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: '#2196f3', // Lighter blue
|
||||
light: '#64b5f6',
|
||||
dark: '#1976d2',
|
||||
contrastText: '#ffffff',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
body: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps & { [key: string]: any }) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
sx={{
|
||||
flex: value === index ? 1 : 'none',
|
||||
display: value === index ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
height: value === index ? '100%' : 0,
|
||||
minHeight: value === index ? 0 : 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
background: 'transparent',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: (theme) => theme.palette.divider,
|
||||
borderRadius: '4px',
|
||||
'&:hover': {
|
||||
background: (theme) => theme.palette.action.hover,
|
||||
},
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: 'transparent',
|
||||
},
|
||||
}}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function AppContent() {
|
||||
const [currentTab, setCurrentTab] = useState(0);
|
||||
const { isLoading, sendMessage, clearMessages, messages } = useChat();
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setCurrentTab(newValue);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || isLoading) return;
|
||||
|
||||
const message = input;
|
||||
setInput('');
|
||||
setCurrentTab(1); // Switch to chat tab
|
||||
await sendMessage(message);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
overflow: 'hidden',
|
||||
bgcolor: 'background.default',
|
||||
color: 'text.primary',
|
||||
}}>
|
||||
{/* Custom titlebar */}
|
||||
<Box
|
||||
sx={{
|
||||
height: '28px',
|
||||
bgcolor: '#2f2f2f',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
WebkitAppRegion: 'drag',
|
||||
px: 1,
|
||||
position: 'relative',
|
||||
'& .window-controls': {
|
||||
WebkitAppRegion: 'no-drag',
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
position: 'absolute',
|
||||
right: '8px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
},
|
||||
'& .control-button': {
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
bgcolor: '#1976d2',
|
||||
opacity: 0.7,
|
||||
'&:hover': {
|
||||
opacity: 1,
|
||||
bgcolor: '#2196f3',
|
||||
},
|
||||
'& svg': {
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="window-controls">
|
||||
<button
|
||||
className="control-button"
|
||||
onClick={() => window.electron.closeWindow()}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
<button
|
||||
className="control-button"
|
||||
onClick={() => window.electron.minimizeWindow()}
|
||||
>
|
||||
<MinimizeIcon />
|
||||
</button>
|
||||
<button
|
||||
className="control-button"
|
||||
onClick={() => window.electron.maximizeWindow()}
|
||||
>
|
||||
<MaximizeIcon />
|
||||
</button>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{/* Main content */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<Box sx={{
|
||||
width: 300,
|
||||
borderRight: '1px solid',
|
||||
borderColor: 'divider',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
bgcolor: 'background.paper',
|
||||
}}>
|
||||
<FileExplorer />
|
||||
</Box>
|
||||
<Box sx={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
bgcolor: 'background.paper',
|
||||
}}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={currentTab} onChange={handleTabChange}>
|
||||
<Tab label="Home" />
|
||||
<Tab label="Chat" />
|
||||
<Tab label="Settings" />
|
||||
<Tab label="Scanning" />
|
||||
<Tab label="Reports" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
pb: '64px' // Add padding for the input height
|
||||
}}>
|
||||
<TabPanel value={currentTab} index={0}>
|
||||
<HomePanel />
|
||||
</TabPanel>
|
||||
<TabPanel value={currentTab} index={1}>
|
||||
<ChatPanel messages={messages} />
|
||||
</TabPanel>
|
||||
<TabPanel value={currentTab} index={2}>
|
||||
<SettingsPanel />
|
||||
</TabPanel>
|
||||
<TabPanel value={currentTab} index={3}>
|
||||
<ScanningPanel />
|
||||
</TabPanel>
|
||||
<TabPanel value={currentTab} index={4}>
|
||||
<ReportingPanel />
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
sx={{
|
||||
p: 2,
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
bgcolor: 'background.paper',
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
zIndex: 1200,
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
placeholder="Ask a question..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
disabled={isLoading}
|
||||
size="small"
|
||||
/>
|
||||
<IconButton
|
||||
type="submit"
|
||||
disabled={isLoading || !input.trim()}
|
||||
color="primary"
|
||||
>
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={clearMessages}
|
||||
color="error"
|
||||
disabled={messages.length === 0}
|
||||
>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ElectronProvider>
|
||||
<AppContent />
|
||||
</ElectronProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
1
electron-file-search/src/assets/react.svg
Normal file
1
electron-file-search/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
206
electron-file-search/src/components/ChatPanel/MessageList.tsx
Normal file
206
electron-file-search/src/components/ChatPanel/MessageList.tsx
Normal file
@ -0,0 +1,206 @@
|
||||
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';
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
text: string;
|
||||
isUser: boolean;
|
||||
timestamp: string;
|
||||
sources?: DocumentMetadata[];
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
interface MessageListProps {
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
|
||||
export default function MessageList({ messages }: MessageListProps) {
|
||||
const messageListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (messageListRef.current) {
|
||||
messageListRef.current.scrollTop = messageListRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={messageListRef}
|
||||
sx={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
scrollBehavior: 'smooth',
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
background: 'transparent',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: (theme) => theme.palette.divider,
|
||||
borderRadius: '4px',
|
||||
'&:hover': {
|
||||
background: (theme) => theme.palette.action.hover,
|
||||
},
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: 'transparent',
|
||||
},
|
||||
}}>
|
||||
{messages.map((message) => (
|
||||
<Box
|
||||
key={message.id}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: message.isUser ? 'row-reverse' : 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: 2,
|
||||
maxWidth: '80%',
|
||||
alignSelf: message.isUser ? 'flex-end' : 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
|
||||
<Avatar
|
||||
src={message.isUser ? '/profiles/user-profile.webp' : '/profiles/ai-profile.webp'}
|
||||
alt={message.isUser ? 'User' : 'AI'}
|
||||
variant="square"
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
boxShadow: 1
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: '0.75rem',
|
||||
color: 'text.secondary',
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
{message.isUser ? 'User' : 'Data Hound'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
component={Paper}
|
||||
elevation={1}
|
||||
sx={{
|
||||
p: 2,
|
||||
flex: 1,
|
||||
bgcolor: message.isUser ? 'primary.main' : 'background.paper',
|
||||
color: message.isUser ? 'primary.contrastText' : 'text.primary',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
boxShadow: 1,
|
||||
borderRadius: 2,
|
||||
'&:hover': {
|
||||
boxShadow: 2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
'& .markdown-body': {
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
minHeight: '1.5em',
|
||||
lineHeight: 1.6,
|
||||
'& pre': {
|
||||
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
|
||||
padding: 2,
|
||||
borderRadius: 1,
|
||||
overflow: 'auto'
|
||||
},
|
||||
'& code': {
|
||||
backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1e1e1e' : '#f6f8fa',
|
||||
padding: '0.2em 0.4em',
|
||||
borderRadius: 1,
|
||||
fontSize: '85%'
|
||||
},
|
||||
'& h1, & h2, & h3, & h4, & h5, & h6': {
|
||||
marginTop: '24px',
|
||||
marginBottom: '16px',
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.25
|
||||
},
|
||||
'& p': {
|
||||
marginTop: '0',
|
||||
marginBottom: '16px'
|
||||
},
|
||||
'& a': {
|
||||
color: (theme) => theme.palette.primary.main,
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
},
|
||||
'& img': {
|
||||
maxWidth: '100%',
|
||||
height: 'auto'
|
||||
},
|
||||
'& blockquote': {
|
||||
padding: '0 1em',
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
borderLeft: (theme) => `0.25em solid ${theme.palette.divider}`,
|
||||
margin: '0 0 16px 0'
|
||||
},
|
||||
'& ul, & ol': {
|
||||
paddingLeft: '2em',
|
||||
marginBottom: '16px'
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<ReactMarkdown
|
||||
className="markdown-body"
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
>
|
||||
{message.text}
|
||||
</ReactMarkdown>
|
||||
</Box>
|
||||
{message.sources && message.sources.length > 0 && (
|
||||
<Box sx={{ mt: 2, pt: 1, borderTop: '1px solid', borderColor: 'divider' }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
display: 'block',
|
||||
mb: 0.5,
|
||||
fontWeight: 'medium'
|
||||
}}
|
||||
>
|
||||
Sources:
|
||||
</Typography>
|
||||
{message.sources.map((source, index) => (
|
||||
<Typography
|
||||
key={index}
|
||||
variant="caption"
|
||||
component="div"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
pl: 1,
|
||||
borderLeft: '2px solid',
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
>
|
||||
{source.path}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color={message.isUser ? '#ffffff' : 'text.secondary'}
|
||||
sx={{ display: 'block', mt: 0.5 }}
|
||||
>
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
34
electron-file-search/src/components/ChatPanel/index.tsx
Normal file
34
electron-file-search/src/components/ChatPanel/index.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import MessageList from './MessageList';
|
||||
interface ChatPanelProps {
|
||||
messages: Array<{
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
sources?: any[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export default function ChatPanel({ messages }: ChatPanelProps) {
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
flex: 1,
|
||||
minHeight: 0
|
||||
}}>
|
||||
<MessageList
|
||||
messages={messages.map((msg, index) => ({
|
||||
id: String(index),
|
||||
text: msg.content,
|
||||
isUser: msg.role === 'user',
|
||||
timestamp: new Date().toISOString(),
|
||||
sources: msg.sources,
|
||||
}))}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Box,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Folder as FolderIcon,
|
||||
AddCircleOutline as AddIcon,
|
||||
Google as GoogleIcon,
|
||||
Cloud as DropboxIcon,
|
||||
Chat as DiscordIcon,
|
||||
Computer as LocalIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useElectron } from '../../hooks/useElectron';
|
||||
|
||||
interface Directory {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface DirectoryPickerProps {
|
||||
onSelect: (dirPath: string) => void;
|
||||
}
|
||||
|
||||
export default function DirectoryPicker({ onSelect }: DirectoryPickerProps) {
|
||||
const electron = useElectron();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [directories, setDirectories] = useState<Directory[]>([]);
|
||||
const [currentPath, setCurrentPath] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleOpen = async () => {
|
||||
try {
|
||||
setOpen(true);
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const homeResponse = await electron.getUserHome();
|
||||
if (!homeResponse.success || !homeResponse.data) {
|
||||
throw new Error(homeResponse.error || 'Failed to get user home directory');
|
||||
}
|
||||
const homePath = homeResponse.data;
|
||||
setCurrentPath(homePath);
|
||||
|
||||
const dirResponse = await electron.listDirectories(homePath);
|
||||
if (!dirResponse.success || !dirResponse.data) {
|
||||
throw new Error(dirResponse.error || 'Failed to load directories');
|
||||
}
|
||||
setDirectories(dirResponse.data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load directories');
|
||||
console.error('Error loading directories:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleDirectoryClick = async (dirPath: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setCurrentPath(dirPath);
|
||||
const dirResponse = await electron.listDirectories(dirPath);
|
||||
if (!dirResponse.success || !dirResponse.data) {
|
||||
throw new Error(dirResponse.error || 'Failed to load directories');
|
||||
}
|
||||
setDirectories(dirResponse.data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load directories');
|
||||
console.error('Error loading directories:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = () => {
|
||||
onSelect(currentPath);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<Box sx={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
mt: 1.5,
|
||||
mr: 2,
|
||||
mb: 2,
|
||||
}}>
|
||||
<Typography sx={{ pl:2 ,flexGrow: 1 }} variant="h6">Folders</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleOpen}
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'primary.main',
|
||||
transition: 'all 0.3s ease',
|
||||
'& .MuiButton-startIcon': {
|
||||
margin: 0,
|
||||
},
|
||||
'& .buttonText': {
|
||||
width: 0,
|
||||
overflow: 'hidden',
|
||||
transition: 'width 0.3s ease',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark',
|
||||
width: '180px',
|
||||
borderRadius: '16px',
|
||||
'& .buttonText': {
|
||||
width: '110px',
|
||||
marginLeft: '8px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AddIcon sx={{ fontSize: 20 }} />
|
||||
<span className="buttonText">Add New Folder</span>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Select Directory</DialogTitle>
|
||||
<DialogContent>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 20 }}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
) : (
|
||||
<List>
|
||||
{currentPath && (
|
||||
<ListItem
|
||||
button
|
||||
onClick={() => {
|
||||
const parentPath = currentPath.split(/[/\\]/).slice(0, -1).join('/');
|
||||
if (parentPath) {
|
||||
handleDirectoryClick(parentPath);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<FolderIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary=".." secondary="Go up" />
|
||||
</ListItem>
|
||||
)}
|
||||
{directories.map((dir) => (
|
||||
<ListItem
|
||||
key={dir.path}
|
||||
button
|
||||
onClick={() => handleDirectoryClick(dir.path)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<FolderIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={dir.name}
|
||||
secondary={dir.path}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleSelect}
|
||||
variant="contained"
|
||||
disabled={!currentPath}
|
||||
>
|
||||
Select Directory
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
94
electron-file-search/src/components/FileExplorer/index.tsx
Normal file
94
electron-file-search/src/components/FileExplorer/index.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export default function FileExplorer() {
|
||||
const { watchedDirectories, watchDirectory, unwatchDirectory } = useFileSystem();
|
||||
const [fileMetadata, setFileMetadata] = React.useState<FileMetadata>({});
|
||||
|
||||
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 => {
|
||||
metadata[doc.path] = doc;
|
||||
});
|
||||
setFileMetadata(metadata);
|
||||
}
|
||||
});
|
||||
}, [watchedDirectories]);
|
||||
|
||||
const handleDirectorySelect = async (dirPath: string) => {
|
||||
if (!watchedDirectories.includes(dirPath)) {
|
||||
await watchDirectory(dirPath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDirectoryRemove = async (dirPath: string) => {
|
||||
await unwatchDirectory(dirPath);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
|
||||
<DirectoryPicker onSelect={handleDirectorySelect} />
|
||||
|
||||
<Box sx={{ flex: 1, overflow: 'auto' }}>
|
||||
<List>
|
||||
{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" />
|
||||
</Tooltip>
|
||||
)}
|
||||
{fileMetadata[dir]?.hasOcr && (
|
||||
<Tooltip title="OCR Enabled">
|
||||
<OCRIcon color="secondary" fontSize="small" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={dir.split(/[/\\]/).pop()}
|
||||
secondary={dir}
|
||||
secondaryTypographyProps={{
|
||||
sx: {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
87
electron-file-search/src/components/HomePanel/index.tsx
Normal file
87
electron-file-search/src/components/HomePanel/index.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Paper, List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
|
||||
import { Search as SearchIcon, Chat as ChatIcon, Assessment as ReportIcon, Scanner as ScannerIcon } from '@mui/icons-material';
|
||||
|
||||
const HomePanel = () => {
|
||||
return (
|
||||
<Box sx={{
|
||||
p: 3,
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
minHeight: 0
|
||||
}}>
|
||||
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<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>
|
||||
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<SearchIcon color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="File Explorer"
|
||||
secondary="Add directories to search through your files"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<ChatIcon color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Chat"
|
||||
secondary="Ask questions about your files and get AI-powered answers"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<ReportIcon color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Reports"
|
||||
secondary="Generate detailed reports and statistics about your files"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<ScannerIcon color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Scanning"
|
||||
secondary="Scan and index your files for faster searching"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>Quick Tips</Typography>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="• Use the File Explorer to add directories you want to search through"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="• Generate reports to understand your file distribution and usage"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="• Ask natural language questions about your files in Chat"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePanel;
|
46
electron-file-search/src/components/ReportingPanel/index.tsx
Normal file
46
electron-file-search/src/components/ReportingPanel/index.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Paper } from '@mui/material';
|
||||
|
||||
const ReportingPanel = () => {
|
||||
return (
|
||||
<Box sx={{
|
||||
p: 3,
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
minHeight: 0
|
||||
}}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" gutterBottom>File Statistics</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
View detailed statistics and insights about your scanned files:
|
||||
</Typography>
|
||||
<ul style={{ paddingLeft: '24px' }}>
|
||||
<li>Total number of files by type</li>
|
||||
<li>File size distribution</li>
|
||||
<li>Most common file extensions</li>
|
||||
<li>Recently modified files</li>
|
||||
<li>Duplicate file detection</li>
|
||||
</ul>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>Export Options</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
Export your file analysis reports in various formats:
|
||||
</Typography>
|
||||
<ul style={{ paddingLeft: '24px' }}>
|
||||
<li>CSV for spreadsheet analysis</li>
|
||||
<li>PDF for documentation</li>
|
||||
<li>JSON for data processing</li>
|
||||
</ul>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportingPanel;
|
25
electron-file-search/src/components/ScanningPanel/index.tsx
Normal file
25
electron-file-search/src/components/ScanningPanel/index.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Paper } from '@mui/material';
|
||||
|
||||
const ScanningPanel = () => {
|
||||
return (
|
||||
<Box sx={{
|
||||
p: 3,
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
minHeight: 0
|
||||
}}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h5" gutterBottom>Scanning</Typography>
|
||||
<Typography variant="body1">
|
||||
Scanning functionality will be implemented here.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScanningPanel;
|
261
electron-file-search/src/components/SettingsPanel/index.tsx
Normal file
261
electron-file-search/src/components/SettingsPanel/index.tsx
Normal file
@ -0,0 +1,261 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Divider,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
} from '@mui/material';
|
||||
import { useLLMConfig } from '../../hooks/useLLMConfig';
|
||||
import { useOllamaModels } from '../../hooks/useOllamaModels';
|
||||
import type { LLMConfig } from '../../../electron/types';
|
||||
|
||||
const defaultConfig: LLMConfig = {
|
||||
provider: 'ollama',
|
||||
model: 'jimscard/blackhat-hacker:v2',
|
||||
baseUrl: 'http://localhost:11434',
|
||||
temperature: 0.7,
|
||||
apiKey: undefined
|
||||
};
|
||||
|
||||
export default function SettingsPanel() {
|
||||
const { config, isLoading, error, updateConfig, reloadConfig } = useLLMConfig();
|
||||
const ollamaModels = useOllamaModels();
|
||||
const [formData, setFormData] = useState<LLMConfig>(defaultConfig);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Reload config when component becomes visible
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
reloadConfig();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [reloadConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) {
|
||||
setFormData(defaultConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize form data with loaded config, only using defaults for missing values
|
||||
setFormData({
|
||||
provider: config.provider,
|
||||
model: config.model || defaultConfig.model,
|
||||
baseUrl: config.baseUrl || defaultConfig.baseUrl,
|
||||
temperature: config.temperature ?? defaultConfig.temperature,
|
||||
apiKey: config.apiKey
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await updateConfig(formData);
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
p: 3,
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
minHeight: 0
|
||||
}}>
|
||||
{/* Search Settings */}
|
||||
<Paper sx={{ p: 3, mb: 3 }} >
|
||||
<Typography variant="h6" gutterBottom>Scan Settings</Typography>
|
||||
<FormControlLabel
|
||||
control={<Switch defaultChecked />}
|
||||
label="Include file contents in search"
|
||||
sx={{ mb: 2, display: 'block' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Switch defaultChecked />}
|
||||
label="Include file names in search"
|
||||
sx={{ mb: 2, display: 'block' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Switch defaultChecked />}
|
||||
label="Use OCR to extract text from images/pdfs?"
|
||||
sx={{ mb: 2, display: 'block' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Switch defaultChecked />}
|
||||
label="Unzip files to check contents?"
|
||||
sx={{ mb: 2, display: 'block' }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="File extensions to ignore"
|
||||
placeholder="e.g., .git, .env, node_modules"
|
||||
margin="normal"
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* Search Settings */}
|
||||
<Paper sx={{ p: 3, mb: 3 }} >
|
||||
<Typography variant="h6" gutterBottom>Connections</Typography>
|
||||
<FormControlLabel
|
||||
control={<Switch disabled />}
|
||||
label="Google Drive"
|
||||
sx={{ mb: 2, display: 'block' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Switch disabled />}
|
||||
label="Dropbox"
|
||||
sx={{ mb: 2, display: 'block' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Switch disabled />}
|
||||
label="Discord"
|
||||
sx={{ mb: 2, display: 'block' }}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* LLM Settings */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>LLM Settings</Typography>
|
||||
<form onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>Provider</InputLabel>
|
||||
<Select
|
||||
value={formData.provider || defaultConfig.provider}
|
||||
label="Provider"
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
provider: e.target.value as LLMConfig['provider']
|
||||
}))}
|
||||
>
|
||||
<MenuItem value="openai">OpenAI</MenuItem>
|
||||
<MenuItem value="openrouter">OpenRouter</MenuItem>
|
||||
<MenuItem value="ollama">Ollama</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{formData.provider !== 'ollama' && (
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="API Key"
|
||||
type="password"
|
||||
value={formData.apiKey ?? ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
apiKey: e.target.value
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
{formData.provider === 'ollama' ? (
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>Model</InputLabel>
|
||||
<Select
|
||||
value={formData.model || defaultConfig.model}
|
||||
label="Model"
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
model: e.target.value
|
||||
}))}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value="jimscard/blackhat-hacker:v2">jimscard/blackhat-hacker:v2</MenuItem>
|
||||
{ollamaModels.models.map((model) => (
|
||||
model !== 'jimscard/blackhat-hacker:v2' && <MenuItem key={model} value={model}>{model}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{ollamaModels.error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{ollamaModels.error}
|
||||
</Alert>
|
||||
)}
|
||||
</FormControl>
|
||||
) : (
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="Model"
|
||||
value={formData.model ?? defaultConfig.model}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
model: e.target.value
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
{formData.provider === 'ollama' && (
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="Base URL"
|
||||
value={formData.baseUrl ?? defaultConfig.baseUrl}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
baseUrl: e.target.value
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="Temperature"
|
||||
type="number"
|
||||
inputProps={{ min: 0, max: 1, step: 0.1 }}
|
||||
value={formData.temperature ?? defaultConfig.temperature}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
temperature: parseFloat(e.target.value)
|
||||
}))}
|
||||
/>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={isSaving}
|
||||
fullWidth
|
||||
>
|
||||
{isSaving ? <CircularProgress size={24} /> : 'Save LLM Settings'}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
24
electron-file-search/src/contexts/ElectronContext.tsx
Normal file
24
electron-file-search/src/contexts/ElectronContext.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React, { createContext, useMemo } from 'react';
|
||||
|
||||
export const ElectronContext = createContext<Window['electron'] | null>(null);
|
||||
ElectronContext.displayName = 'ElectronContext';
|
||||
|
||||
interface ElectronProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ElectronProvider: React.FC<ElectronProviderProps> = ({ children }) => {
|
||||
const electronApi = useMemo(() => {
|
||||
if (typeof window === 'undefined' || !window.electron) {
|
||||
console.error('Electron API not found in window object');
|
||||
return null;
|
||||
}
|
||||
return window.electron;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ElectronContext.Provider value={electronApi}>
|
||||
{children}
|
||||
</ElectronContext.Provider>
|
||||
);
|
||||
};
|
53
electron-file-search/src/electron.d.ts
vendored
Normal file
53
electron-file-search/src/electron.d.ts
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
import type { LLMConfig, DocumentMetadata } from '../electron/types';
|
||||
|
||||
interface Directory {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
// File System Operations
|
||||
startWatching: (dirPath: string) => Promise<void>;
|
||||
stopWatching: (dirPath: string) => Promise<void>;
|
||||
addExcludedPath: (path: string) => Promise<void>;
|
||||
|
||||
// LLM Operations
|
||||
queryLLM: (question: string) => Promise<{
|
||||
answer: string;
|
||||
sources: DocumentMetadata[];
|
||||
}>;
|
||||
updateLLMConfig: (config: LLMConfig) => Promise<void>;
|
||||
getLLMConfig: () => Promise<LLMConfig>;
|
||||
getOllamaModels: () => Promise<{ success: boolean; data?: string[]; error?: string }>;
|
||||
|
||||
// Vector Store Operations
|
||||
getDocuments: () => Promise<{ success: boolean; data?: DocumentMetadata[]; error?: string }>;
|
||||
addDocument: (content: string, metadata: DocumentMetadata) => Promise<void>;
|
||||
deleteDocument: (path: string) => Promise<void>;
|
||||
updateDocument: (content: string, metadata: DocumentMetadata) => Promise<void>;
|
||||
|
||||
// File Processing
|
||||
processFile: (filePath: string) => Promise<void>;
|
||||
|
||||
// System Paths
|
||||
getUserHome: () => Promise<{ success: boolean; data?: string; error?: string }>;
|
||||
getAppPath: () => Promise<string>;
|
||||
|
||||
// Directory Operations
|
||||
listDirectories: (dirPath: string) => Promise<{ success: boolean; data?: Directory[]; error?: string }>;
|
||||
|
||||
// Event Handling
|
||||
on: (channel: string, callback: (event: unknown, ...args: any[]) => void) => void;
|
||||
off: (channel: string, callback: (event: unknown, ...args: any[]) => void) => void;
|
||||
|
||||
// Window Controls
|
||||
minimizeWindow: () => Promise<void>;
|
||||
maximizeWindow: () => Promise<void>;
|
||||
closeWindow: () => Promise<void>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
89
electron-file-search/src/hooks/useChat.ts
Normal file
89
electron-file-search/src/hooks/useChat.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useElectron } from './useElectron';
|
||||
import type { DocumentMetadata } from '../../electron/types';
|
||||
|
||||
interface Message {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
sources?: DocumentMetadata[];
|
||||
}
|
||||
|
||||
export function useChat() {
|
||||
const electron = useElectron();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let isStale = false;
|
||||
const handleChunk = (_: unknown, chunk: string) => {
|
||||
if (!isStale) {
|
||||
setMessages(prev => {
|
||||
const lastMessage = prev[prev.length - 1];
|
||||
if (lastMessage?.role === 'assistant') {
|
||||
// Update the last message's content with the new chunk
|
||||
const updatedMessages = [...prev];
|
||||
updatedMessages[prev.length - 1] = {
|
||||
...lastMessage,
|
||||
content: lastMessage.content + chunk,
|
||||
};
|
||||
return updatedMessages;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
electron.on('llm-response-chunk', handleChunk);
|
||||
return () => {
|
||||
isStale = true;
|
||||
electron.off('llm-response-chunk', handleChunk);
|
||||
};
|
||||
}, [electron]);
|
||||
|
||||
const sendMessage = useCallback(async (content: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{ role: 'user', content },
|
||||
{ role: 'assistant', content: '' }, // Add empty assistant message that will be updated with chunks
|
||||
]);
|
||||
|
||||
const response = await electron.queryLLM(content);
|
||||
|
||||
// Final update with response content and sources
|
||||
setMessages(prev => {
|
||||
const updatedMessages = [...prev];
|
||||
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
||||
if (lastMessage?.role === 'assistant') {
|
||||
lastMessage.content = lastMessage.content || response.answer;
|
||||
lastMessage.sources = response.sources;
|
||||
}
|
||||
return updatedMessages;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
setMessages(prev => {
|
||||
const updatedMessages = [...prev];
|
||||
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
||||
if (lastMessage?.role === 'assistant') {
|
||||
lastMessage.content = 'Sorry, there was an error processing your request.';
|
||||
}
|
||||
return updatedMessages;
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [electron]);
|
||||
|
||||
const clearMessages = useCallback(() => {
|
||||
setMessages([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
messages,
|
||||
isLoading,
|
||||
sendMessage,
|
||||
clearMessages,
|
||||
};
|
||||
}
|
10
electron-file-search/src/hooks/useElectron.ts
Normal file
10
electron-file-search/src/hooks/useElectron.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { ElectronContext } from '../contexts/ElectronContext';
|
||||
|
||||
export const useElectron = () => {
|
||||
const context = useContext(ElectronContext);
|
||||
if (!context) {
|
||||
throw new Error('useElectron must be used within an ElectronProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
40
electron-file-search/src/hooks/useFileSystem.ts
Normal file
40
electron-file-search/src/hooks/useFileSystem.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useElectron } from './useElectron';
|
||||
|
||||
export function useFileSystem() {
|
||||
const electron = useElectron();
|
||||
const [watchedDirectories, setWatchedDirectories] = useState<string[]>([]);
|
||||
|
||||
const watchDirectory = useCallback(async (dirPath: string) => {
|
||||
try {
|
||||
await electron.startWatching(dirPath);
|
||||
setWatchedDirectories(prev => [...prev, dirPath]);
|
||||
} catch (error) {
|
||||
console.error('Error watching directory:', error);
|
||||
}
|
||||
}, [electron]);
|
||||
|
||||
const unwatchDirectory = useCallback(async (dirPath: string) => {
|
||||
try {
|
||||
await electron.stopWatching(dirPath);
|
||||
setWatchedDirectories(prev => prev.filter(dir => dir !== dirPath));
|
||||
} catch (error) {
|
||||
console.error('Error unwatching directory:', error);
|
||||
}
|
||||
}, [electron]);
|
||||
|
||||
const addExcludedPath = useCallback(async (path: string) => {
|
||||
try {
|
||||
await electron.addExcludedPath(path);
|
||||
} catch (error) {
|
||||
console.error('Error adding excluded path:', error);
|
||||
}
|
||||
}, [electron]);
|
||||
|
||||
return {
|
||||
watchedDirectories,
|
||||
watchDirectory,
|
||||
unwatchDirectory,
|
||||
addExcludedPath,
|
||||
};
|
||||
}
|
60
electron-file-search/src/hooks/useLLMConfig.ts
Normal file
60
electron-file-search/src/hooks/useLLMConfig.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useElectron } from './useElectron';
|
||||
import type { LLMConfig } from '../../electron/types';
|
||||
|
||||
export function useLLMConfig() {
|
||||
const electron = useElectron();
|
||||
const [config, setConfig] = useState<LLMConfig | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const config = await electron.getLLMConfig().catch(err => {
|
||||
console.error('Error loading LLM config:', err);
|
||||
return null;
|
||||
});
|
||||
setConfig(config || {
|
||||
provider: 'ollama',
|
||||
model: 'phi4',
|
||||
baseUrl: 'http://localhost:11434',
|
||||
temperature: 0.7,
|
||||
apiKey: undefined
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load LLM config');
|
||||
console.error('Error loading LLM config:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateConfig = useCallback(async (newConfig: LLMConfig) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
await electron.updateLLMConfig(newConfig);
|
||||
setConfig(newConfig);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to update LLM config');
|
||||
console.error('Error updating LLM config:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [electron]);
|
||||
|
||||
return {
|
||||
config,
|
||||
isLoading,
|
||||
error,
|
||||
updateConfig,
|
||||
reloadConfig: loadConfig,
|
||||
};
|
||||
}
|
38
electron-file-search/src/hooks/useOllamaModels.ts
Normal file
38
electron-file-search/src/hooks/useOllamaModels.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useElectron } from './useElectron';
|
||||
|
||||
export function useOllamaModels() {
|
||||
const electron = useElectron();
|
||||
const [models, setModels] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadModels();
|
||||
}, []);
|
||||
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const response = await electron.getOllamaModels();
|
||||
if (response.success) {
|
||||
setModels(response.data || []);
|
||||
} else {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load Ollama models');
|
||||
console.error('Error loading Ollama models:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
models,
|
||||
isLoading,
|
||||
error,
|
||||
reloadModels: loadModels,
|
||||
};
|
||||
}
|
16
electron-file-search/src/index.css
Normal file
16
electron-file-search/src/index.css
Normal file
@ -0,0 +1,16 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
14
electron-file-search/src/main.tsx
Normal file
14
electron-file-search/src/main.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import '@fontsource/roboto/300.css';
|
||||
import '@fontsource/roboto/400.css';
|
||||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
11
electron-file-search/tailwind.config.js
Normal file
11
electron-file-search/tailwind.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
27
electron-file-search/tsconfig.json
Normal file
27
electron-file-search/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "./electron/tsconfig.json" },
|
||||
{ "path": "./electron/tsconfig.preload.json" }
|
||||
]
|
||||
}
|
7
electron-file-search/vite.config.js
Normal file
7
electron-file-search/vite.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
26
electron-file-search/vite.config.ts
Normal file
26
electron-file-search/vite.config.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: './',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
target: 'esnext',
|
||||
rollupOptions: {
|
||||
external: ['http', 'https', 'path', 'fs', 'electron']
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5174,
|
||||
},
|
||||
clearScreen: false,
|
||||
});
|
16
eslint.config.mjs
Normal file
16
eslint.config.mjs
Normal file
@ -0,0 +1,16 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
7
next.config.ts
Normal file
7
next.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
5912
package-lock.json
generated
Normal file
5912
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "test",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"next": "15.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"@eslint/eslintrc": "^3"
|
||||
}
|
||||
}
|
8
postcss.config.mjs
Normal file
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
18
tailwind.config.ts
Normal file
18
tailwind.config.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config;
|
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user