Initial commit: Foxus - Local-First AI Coding Assistant with FastAPI backend, Tauri frontend, and Ollama integration
This commit is contained in:
commit
d9d5b41041
164
.gitignore
vendored
Normal file
164
.gitignore
vendored
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# Foxus Project .gitignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.npm
|
||||||
|
.eslintcache
|
||||||
|
.node_repl_history
|
||||||
|
*.tgz
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# Frontend build outputs
|
||||||
|
frontend/dist/
|
||||||
|
frontend/dist-ssr/
|
||||||
|
frontend/build/
|
||||||
|
|
||||||
|
# Tauri
|
||||||
|
frontend/src-tauri/target/
|
||||||
|
frontend/src-tauri/Cargo.lock
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
target/
|
||||||
|
Cargo.lock
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# IDE and editors
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids/
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
public
|
||||||
|
|
||||||
|
# Storybook build outputs
|
||||||
|
.out
|
||||||
|
.storybook-out
|
||||||
|
|
||||||
|
# Temporary folders
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# AI model files (if downloaded locally)
|
||||||
|
*.bin
|
||||||
|
*.gguf
|
||||||
|
models/
|
||||||
|
|
||||||
|
# Database files
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.backup
|
243
PROJECT_SUMMARY.md
Normal file
243
PROJECT_SUMMARY.md
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
# Foxus - Local-First AI Coding Assistant
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Foxus** is a privacy-focused, fully offline coding assistant that provides AI-powered code completion, refactoring, bug fixing, and code explanation using locally running language models. Built with modern technologies, it offers a seamless development experience without sending your code to external servers.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### 🔒 **Privacy & Security**
|
||||||
|
- **Fully Local**: All processing happens on your machine
|
||||||
|
- **No Internet Required**: Works completely offline
|
||||||
|
- **Zero Data Collection**: Your code never leaves your computer
|
||||||
|
|
||||||
|
### 🧠 **AI-Powered Assistance**
|
||||||
|
- **Code Explanation**: Understand complex code snippets
|
||||||
|
- **Smart Refactoring**: Improve code quality and maintainability
|
||||||
|
- **Bug Detection & Fixing**: Identify and resolve issues
|
||||||
|
- **Code Completion**: Intelligent autocomplete suggestions
|
||||||
|
- **Documentation Generation**: Auto-generate comments and docs
|
||||||
|
|
||||||
|
### 💻 **Developer Experience**
|
||||||
|
- **Multi-Language Support**: Python, JavaScript, TypeScript, Go, Java, Rust, and more
|
||||||
|
- **Keyboard Shortcuts**: Quick access to AI commands (`/explain`, `/refactor`, `/fix`)
|
||||||
|
- **Modern UI**: Clean, responsive interface with dark theme
|
||||||
|
- **Project Context**: Multi-file analysis and understanding
|
||||||
|
|
||||||
|
### ⚡ **Performance & Compatibility**
|
||||||
|
- **Cross-Platform**: Windows, Linux, and macOS support
|
||||||
|
- **Lightweight**: Built with Tauri for minimal resource usage
|
||||||
|
- **Fast Response**: Local models provide quick feedback
|
||||||
|
- **Extensible**: Easy to add new AI models and commands
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Framework**: Tauri + React + TypeScript
|
||||||
|
- **Editor**: Monaco Editor (VS Code engine)
|
||||||
|
- **Styling**: Tailwind CSS
|
||||||
|
- **State Management**: Zustand
|
||||||
|
- **Build Tool**: Vite
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **API**: FastAPI (Python)
|
||||||
|
- **LLM Integration**: Ollama
|
||||||
|
- **Models**: CodeLlama, Deepseek-Coder, StarCoder
|
||||||
|
- **File Handling**: Python file system APIs
|
||||||
|
|
||||||
|
### Desktop Application
|
||||||
|
- **Framework**: Tauri (Rust + Web Technologies)
|
||||||
|
- **Bundle Size**: ~15MB (significantly smaller than Electron)
|
||||||
|
- **Performance**: Native-level performance
|
||||||
|
|
||||||
|
## Supported AI Models
|
||||||
|
|
||||||
|
### Code-Specialized Models
|
||||||
|
1. **CodeLlama 7B/13B** - Meta's code generation model
|
||||||
|
2. **Deepseek-Coder 6.7B** - Advanced code understanding
|
||||||
|
3. **StarCoder 7B** - Multi-language code completion
|
||||||
|
4. **CodeGemma 7B** - Google's code model
|
||||||
|
|
||||||
|
### Model Capabilities
|
||||||
|
- Code generation and completion
|
||||||
|
- Bug detection and fixing
|
||||||
|
- Code explanation and documentation
|
||||||
|
- Refactoring suggestions
|
||||||
|
- Multi-language understanding
|
||||||
|
|
||||||
|
## Architecture Benefits
|
||||||
|
|
||||||
|
### Local-First Approach
|
||||||
|
- **Privacy**: Code never leaves your machine
|
||||||
|
- **Speed**: No network latency
|
||||||
|
- **Reliability**: Works without internet
|
||||||
|
- **Cost**: No API fees or usage limits
|
||||||
|
|
||||||
|
### Modular Design
|
||||||
|
- **Extensible**: Easy to add new features
|
||||||
|
- **Maintainable**: Clear separation of concerns
|
||||||
|
- **Testable**: Well-structured codebase
|
||||||
|
- **Scalable**: Can handle large projects
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### Individual Developers
|
||||||
|
- **Learning**: Understand unfamiliar code
|
||||||
|
- **Productivity**: Speed up coding with AI assistance
|
||||||
|
- **Quality**: Improve code through AI suggestions
|
||||||
|
- **Debugging**: Get help fixing complex issues
|
||||||
|
|
||||||
|
### Teams & Organizations
|
||||||
|
- **Code Reviews**: AI-assisted code analysis
|
||||||
|
- **Standards**: Consistent code quality
|
||||||
|
- **Documentation**: Auto-generated code docs
|
||||||
|
- **Training**: Help junior developers learn
|
||||||
|
|
||||||
|
### Enterprise
|
||||||
|
- **Security**: Keep sensitive code private
|
||||||
|
- **Compliance**: Meet data residency requirements
|
||||||
|
- **Customization**: Add domain-specific models
|
||||||
|
- **Integration**: Embed in existing workflows
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Quick Setup (5 minutes)
|
||||||
|
1. **Install Prerequisites**: Node.js, Python, Rust, Ollama
|
||||||
|
2. **Install Dependencies**: `npm install` & `pip install -r requirements.txt`
|
||||||
|
3. **Download AI Model**: `ollama pull codellama:7b-code`
|
||||||
|
4. **Start Services**: Backend API & Frontend app
|
||||||
|
5. **Start Coding**: Open files and use AI commands
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
1. **Open Foxus**: Launch the desktop application
|
||||||
|
2. **Load Project**: Open your code files
|
||||||
|
3. **Select Code**: Highlight code you want help with
|
||||||
|
4. **Use AI Commands**:
|
||||||
|
- `Ctrl+Shift+E` - Explain code
|
||||||
|
- `Ctrl+Shift+R` - Refactor code
|
||||||
|
- `Ctrl+Shift+F` - Fix bugs
|
||||||
|
- `Ctrl+K` - Command palette
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
foxus/
|
||||||
|
├── backend/ # FastAPI Python backend
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── api/ # API routes (ai, files, models)
|
||||||
|
│ │ ├── core/ # Configuration and settings
|
||||||
|
│ │ ├── models/ # Pydantic data models
|
||||||
|
│ │ └── services/ # Ollama integration
|
||||||
|
│ ├── main.py # FastAPI entry point
|
||||||
|
│ └── requirements.txt # Python dependencies
|
||||||
|
├── frontend/ # Tauri + React frontend
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # React UI components
|
||||||
|
│ │ ├── stores/ # Zustand state management
|
||||||
|
│ │ ├── hooks/ # Custom React hooks
|
||||||
|
│ │ └── App.tsx # Main application
|
||||||
|
│ ├── src-tauri/ # Tauri Rust backend
|
||||||
|
│ │ ├── src/main.rs # Rust entry point
|
||||||
|
│ │ └── tauri.conf.json # Tauri configuration
|
||||||
|
│ └── package.json # Node.js dependencies
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── README.md # Project overview
|
||||||
|
├── SETUP.md # Installation guide
|
||||||
|
└── PROJECT_SUMMARY.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Roadmap
|
||||||
|
|
||||||
|
### Phase 1: MVP ✅
|
||||||
|
- [x] Basic code editor interface
|
||||||
|
- [x] Local AI model integration
|
||||||
|
- [x] Core AI commands (explain, refactor, fix)
|
||||||
|
- [x] Desktop application framework
|
||||||
|
|
||||||
|
### Phase 2: Enhanced Features
|
||||||
|
- [ ] Advanced code completion
|
||||||
|
- [ ] Project-wide context awareness
|
||||||
|
- [ ] Custom AI prompts
|
||||||
|
- [ ] File tree and project management
|
||||||
|
- [ ] Settings and preferences
|
||||||
|
|
||||||
|
### Phase 3: Advanced Capabilities
|
||||||
|
- [ ] Plugin system
|
||||||
|
- [ ] Custom model training
|
||||||
|
- [ ] Team collaboration features
|
||||||
|
- [ ] Integration with version control
|
||||||
|
- [ ] Advanced debugging assistance
|
||||||
|
|
||||||
|
## Comparison with Alternatives
|
||||||
|
|
||||||
|
| Feature | Foxus | GitHub Copilot | Cursor | Windsurf |
|
||||||
|
|---------|-------|----------------|--------|----------|
|
||||||
|
| **Privacy** | ✅ Fully Local | ❌ Cloud-based | ❌ Cloud-based | ❌ Cloud-based |
|
||||||
|
| **Offline** | ✅ Yes | ❌ No | ❌ No | ❌ No |
|
||||||
|
| **Cost** | ✅ Free | 💰 $10/month | 💰 $20/month | 💰 $15/month |
|
||||||
|
| **Customization** | ✅ Full control | ❌ Limited | ❌ Limited | ❌ Limited |
|
||||||
|
| **Multi-language** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
|
||||||
|
| **Speed** | ⚡ Local | 🌐 Network | 🌐 Network | 🌐 Network |
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### How to Contribute
|
||||||
|
1. **Fork** the repository
|
||||||
|
2. **Create** a feature branch
|
||||||
|
3. **Implement** your changes
|
||||||
|
4. **Add** tests if applicable
|
||||||
|
5. **Submit** a pull request
|
||||||
|
|
||||||
|
### Areas for Contribution
|
||||||
|
- **UI/UX Improvements**: Better design and user experience
|
||||||
|
- **AI Model Integration**: Support for new models
|
||||||
|
- **Language Support**: Additional programming languages
|
||||||
|
- **Performance**: Optimization and speed improvements
|
||||||
|
- **Documentation**: Guides and examples
|
||||||
|
|
||||||
|
## Technical Highlights
|
||||||
|
|
||||||
|
### Performance Optimizations
|
||||||
|
- **Tauri vs Electron**: 10x smaller bundle size
|
||||||
|
- **Local Processing**: Zero network latency
|
||||||
|
- **Efficient State Management**: Zustand for minimal re-renders
|
||||||
|
- **Code Splitting**: Lazy loading for faster startup
|
||||||
|
|
||||||
|
### Security Features
|
||||||
|
- **No External Calls**: All processing happens locally
|
||||||
|
- **File System Sandboxing**: Tauri security model
|
||||||
|
- **Input Validation**: Comprehensive API validation
|
||||||
|
- **Error Handling**: Graceful failure recovery
|
||||||
|
|
||||||
|
## Future Vision
|
||||||
|
|
||||||
|
Foxus aims to become the **de facto standard** for privacy-conscious developers who want AI assistance without compromising their code security. The goal is to create an ecosystem where:
|
||||||
|
|
||||||
|
- **Developers** have full control over their AI assistant
|
||||||
|
- **Organizations** can maintain code privacy and compliance
|
||||||
|
- **AI Models** can be easily customized for specific domains
|
||||||
|
- **Innovation** happens locally without external dependencies
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- **README.md**: Quick project overview
|
||||||
|
- **SETUP.md**: Detailed installation guide
|
||||||
|
- **API Documentation**: Available at `http://localhost:8000/docs`
|
||||||
|
|
||||||
|
### Community
|
||||||
|
- **Issues**: Report bugs and request features
|
||||||
|
- **Discussions**: Ask questions and share ideas
|
||||||
|
- **Contributing**: Help improve Foxus
|
||||||
|
|
||||||
|
### Support
|
||||||
|
- Check troubleshooting in SETUP.md
|
||||||
|
- Review API logs for debugging
|
||||||
|
- Ensure Ollama is running properly
|
||||||
|
- Verify model availability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Foxus** represents the future of AI-assisted development: **powerful, private, and fully under your control**. Join us in building the next generation of coding tools that respect developer privacy while maximizing productivity.
|
||||||
|
|
||||||
|
🦊 **Start coding smarter, not harder, with Foxus!**
|
151
README.md
Normal file
151
README.md
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# Foxus - Local-First AI Coding Assistant
|
||||||
|
|
||||||
|
A privacy-focused, fully offline coding assistant that provides AI-powered code completion, refactoring, bug fixing, and code explanation using locally running language models.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔒 **Fully Local & Private**: No internet connection required, all data stays on your machine
|
||||||
|
- 🧠 **AI-Powered Assistance**: Code completion, refactoring, bug fixing, and explanation
|
||||||
|
- 🌐 **Multi-Language Support**: Python, JavaScript, TypeScript, Go, Java, Rust, and more
|
||||||
|
- 💻 **Cross-Platform**: Windows, Linux, and macOS support
|
||||||
|
- ⌨️ **Keyboard Shortcuts**: Quick AI commands (`/explain`, `/refactor`, `/fix`, etc.)
|
||||||
|
- 📁 **Project Context**: Multi-file analysis and understanding
|
||||||
|
- 🎨 **Modern UI**: Clean, responsive interface built with React and Tauri
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Tauri + React │◄──►│ FastAPI Server │◄──►│ Ollama/LLM │
|
||||||
|
│ (Frontend) │ │ (Backend) │ │ (Local Models) │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
|
||||||
|
- **Frontend**: Tauri + React + TypeScript + Monaco Editor
|
||||||
|
- **Backend**: FastAPI (Python) + uvicorn
|
||||||
|
- **LLM Runtime**: Ollama
|
||||||
|
- **Models**: CodeLlama, Deepseek-Coder, StarCoder
|
||||||
|
- **Styling**: Tailwind CSS
|
||||||
|
- **State Management**: Zustand
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js (v18+)
|
||||||
|
- Python (3.9+)
|
||||||
|
- Rust (for Tauri)
|
||||||
|
- Ollama
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone and setup the project**:
|
||||||
|
```bash
|
||||||
|
git clone <repository>
|
||||||
|
cd foxus
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**:
|
||||||
|
```bash
|
||||||
|
# Install frontend dependencies
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Install backend dependencies
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install and start Ollama**:
|
||||||
|
```bash
|
||||||
|
# Install Ollama (Linux/macOS)
|
||||||
|
curl -fsSL https://ollama.ai/install.sh | sh
|
||||||
|
|
||||||
|
# Pull a coding model
|
||||||
|
ollama pull codellama:7b-code
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Start the application**:
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Start backend
|
||||||
|
cd backend
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# Terminal 2: Start frontend
|
||||||
|
cd frontend
|
||||||
|
npm run tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
foxus/
|
||||||
|
├── frontend/ # Tauri + React application
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # React components
|
||||||
|
│ │ ├── services/ # API services
|
||||||
|
│ │ ├── hooks/ # Custom React hooks
|
||||||
|
│ │ ├── stores/ # State management
|
||||||
|
│ │ └── utils/ # Utility functions
|
||||||
|
│ ├── src-tauri/ # Tauri backend (Rust)
|
||||||
|
│ └── package.json
|
||||||
|
├── backend/ # FastAPI server
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── api/ # API routes
|
||||||
|
│ │ ├── core/ # Core functionality
|
||||||
|
│ │ ├── models/ # Pydantic models
|
||||||
|
│ │ └── services/ # Business logic
|
||||||
|
│ ├── main.py
|
||||||
|
│ └── requirements.txt
|
||||||
|
├── docs/ # Documentation
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### AI Commands
|
||||||
|
|
||||||
|
- `/explain` - Explain selected code
|
||||||
|
- `/refactor` - Suggest refactoring improvements
|
||||||
|
- `/fix` - Fix bugs in selected code
|
||||||
|
- `/complete` - Auto-complete code
|
||||||
|
- `/comment` - Add comments to code
|
||||||
|
- `/test` - Generate unit tests
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
|
- `Ctrl+K` (or `Cmd+K`) - Open AI command palette
|
||||||
|
- `Ctrl+Shift+E` - Explain code
|
||||||
|
- `Ctrl+Shift+R` - Refactor code
|
||||||
|
- `Ctrl+Shift+F` - Fix code
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Adding New AI Commands
|
||||||
|
|
||||||
|
1. Add command to `backend/app/api/ai.py`
|
||||||
|
2. Update frontend command palette in `frontend/src/components/CommandPalette.tsx`
|
||||||
|
3. Add keyboard shortcut in `frontend/src/hooks/useKeyboardShortcuts.ts`
|
||||||
|
|
||||||
|
### Supported Models
|
||||||
|
|
||||||
|
- CodeLlama (7B, 13B, 34B)
|
||||||
|
- Deepseek-Coder (1.3B, 6.7B, 33B)
|
||||||
|
- StarCoder (1B, 3B, 7B, 15B)
|
||||||
|
- CodeT5+ (220M, 770M, 2B, 6B)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Add tests
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see LICENSE file for details.
|
269
SETUP.md
Normal file
269
SETUP.md
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
# Foxus Setup Guide
|
||||||
|
|
||||||
|
This guide will walk you through setting up **Foxus**, a local-first AI coding assistant.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before starting, ensure you have the following installed:
|
||||||
|
|
||||||
|
### Required Software
|
||||||
|
|
||||||
|
1. **Node.js** (v18 or higher)
|
||||||
|
```bash
|
||||||
|
# Check version
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Python** (3.9 or higher)
|
||||||
|
```bash
|
||||||
|
# Check version
|
||||||
|
python --version
|
||||||
|
pip --version
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Rust** (for Tauri)
|
||||||
|
```bash
|
||||||
|
# Install Rust
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
source ~/.cargo/env
|
||||||
|
|
||||||
|
# Check version
|
||||||
|
rustc --version
|
||||||
|
cargo --version
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Ollama** (for local AI models)
|
||||||
|
```bash
|
||||||
|
# Linux/macOS
|
||||||
|
curl -fsSL https://ollama.ai/install.sh | sh
|
||||||
|
|
||||||
|
# Windows: Download from https://ollama.ai/download
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
### 1. Install Dependencies
|
||||||
|
|
||||||
|
#### Backend Dependencies
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Frontend Dependencies
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Setup Ollama and AI Models
|
||||||
|
|
||||||
|
1. **Start Ollama service**:
|
||||||
|
```bash
|
||||||
|
ollama serve
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Pull a coding model** (choose one):
|
||||||
|
```bash
|
||||||
|
# Recommended for most users (lighter model)
|
||||||
|
ollama pull codellama:7b-code
|
||||||
|
|
||||||
|
# For better performance (larger model)
|
||||||
|
ollama pull codellama:13b-code
|
||||||
|
|
||||||
|
# Alternative models
|
||||||
|
ollama pull deepseek-coder:6.7b
|
||||||
|
ollama pull starcoder:7b
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify model installation**:
|
||||||
|
```bash
|
||||||
|
ollama list
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure Environment
|
||||||
|
|
||||||
|
1. **Create backend environment file**:
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
cp .env.example .env # If example exists
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Edit `.env` file** (optional):
|
||||||
|
```env
|
||||||
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
|
DEFAULT_MODEL=codellama:7b-code
|
||||||
|
DEBUG=true
|
||||||
|
HOST=127.0.0.1
|
||||||
|
PORT=8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Install Tauri CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Tauri CLI globally
|
||||||
|
npm install -g @tauri-apps/cli
|
||||||
|
|
||||||
|
# Or use with npx (no global install needed)
|
||||||
|
npx @tauri-apps/cli --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
1. **Start the backend API server**:
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
The backend will start on `http://localhost:8000`
|
||||||
|
|
||||||
|
2. **Start the frontend application** (in a new terminal):
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run tauri:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The Foxus application window should open automatically.
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run tauri:build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a distributable application in `frontend/src-tauri/target/release/bundle/`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Test Backend API
|
||||||
|
```bash
|
||||||
|
# Check API health
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
# Check AI service
|
||||||
|
curl http://localhost:8000/api/ai/health
|
||||||
|
|
||||||
|
# List available models
|
||||||
|
curl http://localhost:8000/api/models/list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Frontend
|
||||||
|
- Open the Foxus application
|
||||||
|
- Check for "AI Service Connected" status
|
||||||
|
- Try opening a file
|
||||||
|
- Test AI commands using Ctrl+K (or Cmd+K)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Ollama not running**:
|
||||||
|
```bash
|
||||||
|
# Start Ollama service
|
||||||
|
ollama serve
|
||||||
|
|
||||||
|
# Check if running
|
||||||
|
curl http://localhost:11434/api/tags
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Port conflicts**:
|
||||||
|
- Backend: Change `PORT` in backend `.env` file
|
||||||
|
- Frontend: Change port in `frontend/vite.config.ts`
|
||||||
|
|
||||||
|
3. **Model not found**:
|
||||||
|
```bash
|
||||||
|
# Pull the default model
|
||||||
|
ollama pull codellama:7b-code
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
ollama list
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Rust compilation errors**:
|
||||||
|
```bash
|
||||||
|
# Update Rust
|
||||||
|
rustup update
|
||||||
|
|
||||||
|
# Clear Tauri cache
|
||||||
|
cd frontend
|
||||||
|
npm run tauri clean
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Node.js/NPM issues**:
|
||||||
|
```bash
|
||||||
|
# Clear npm cache
|
||||||
|
npm cache clean --force
|
||||||
|
|
||||||
|
# Delete node_modules and reinstall
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
foxus/
|
||||||
|
├── backend/ # FastAPI Python backend
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── api/ # API routes
|
||||||
|
│ │ ├── core/ # Configuration
|
||||||
|
│ │ ├── models/ # Data models
|
||||||
|
│ │ └── services/ # Business logic
|
||||||
|
│ ├── main.py # Entry point
|
||||||
|
│ └── requirements.txt # Dependencies
|
||||||
|
├── frontend/ # Tauri + React frontend
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # React components
|
||||||
|
│ │ ├── stores/ # State management
|
||||||
|
│ │ ├── hooks/ # Custom hooks
|
||||||
|
│ │ └── App.tsx # Main app
|
||||||
|
│ ├── src-tauri/ # Tauri Rust backend
|
||||||
|
│ └── package.json # Dependencies
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Features
|
||||||
|
|
||||||
|
1. **Backend API endpoints**: Add to `backend/app/api/`
|
||||||
|
2. **Frontend components**: Add to `frontend/src/components/`
|
||||||
|
3. **State management**: Use Zustand stores in `frontend/src/stores/`
|
||||||
|
4. **AI commands**: Extend `backend/app/services/ollama_service.py`
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
|
- `Ctrl+K` / `Cmd+K`: Open command palette
|
||||||
|
- `Ctrl+S` / `Cmd+S`: Save current file
|
||||||
|
- `Ctrl+Shift+E`: Explain selected code
|
||||||
|
- `Ctrl+Shift+R`: Refactor selected code
|
||||||
|
- `Ctrl+Shift+F`: Fix selected code
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Customize AI models**: Download and test different models
|
||||||
|
2. **Configure file associations**: Add support for new languages
|
||||||
|
3. **Extend AI commands**: Add custom prompts and commands
|
||||||
|
4. **UI customization**: Modify themes and layouts
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
1. Check the troubleshooting section above
|
||||||
|
2. Review logs in the terminal
|
||||||
|
3. Ensure all prerequisites are installed
|
||||||
|
4. Verify Ollama is running and models are available
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Add tests if applicable
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
Happy coding with Foxus! 🦊
|
1
backend/app/__init__.py
Normal file
1
backend/app/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Foxus Backend App Package
|
1
backend/app/api/__init__.py
Normal file
1
backend/app/api/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# API routes and endpoints
|
327
backend/app/api/ai.py
Normal file
327
backend/app/api/ai.py
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
"""
|
||||||
|
AI API routes for code assistance
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import List
|
||||||
|
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from app.models.ai import (
|
||||||
|
AIRequest, AIResponse, CodeCompletionRequest, CodeCompletionResponse,
|
||||||
|
ExplainRequest, RefactorRequest, FixRequest, MultiFileRequest
|
||||||
|
)
|
||||||
|
from app.services.ollama_service import ollama_service
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/process", response_model=AIResponse)
|
||||||
|
async def process_ai_request(request: AIRequest):
|
||||||
|
"""Process general AI request for code assistance"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if Ollama is available
|
||||||
|
if not await ollama_service.is_available():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="Ollama service is not available. Please ensure Ollama is running."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build prompt based on command
|
||||||
|
prompt = ollama_service.build_prompt(
|
||||||
|
command=request.command,
|
||||||
|
code=request.code,
|
||||||
|
language=request.language,
|
||||||
|
context=request.context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate completion
|
||||||
|
result = await ollama_service.generate_completion(
|
||||||
|
prompt=prompt,
|
||||||
|
model=request.model
|
||||||
|
)
|
||||||
|
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
|
||||||
|
return AIResponse(
|
||||||
|
success=True,
|
||||||
|
result=result,
|
||||||
|
execution_time=execution_time,
|
||||||
|
model_used=request.model or ollama_service.default_model
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
return AIResponse(
|
||||||
|
success=False,
|
||||||
|
result=f"Error processing request: {str(e)}",
|
||||||
|
execution_time=execution_time
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/explain", response_model=AIResponse)
|
||||||
|
async def explain_code(request: ExplainRequest):
|
||||||
|
"""Explain code functionality"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not await ollama_service.is_available():
|
||||||
|
raise HTTPException(status_code=503, detail="Ollama service unavailable")
|
||||||
|
|
||||||
|
# Build explanation prompt
|
||||||
|
prompt = f"""
|
||||||
|
Explain the following {request.language.value if request.language else 'code'} code in {request.detail_level} detail:
|
||||||
|
|
||||||
|
```{request.language.value if request.language else 'code'}
|
||||||
|
{request.code}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please provide a clear explanation that covers:
|
||||||
|
1. What this code does
|
||||||
|
2. How it works
|
||||||
|
3. Key concepts used
|
||||||
|
4. Any potential issues or improvements
|
||||||
|
|
||||||
|
Explanation:"""
|
||||||
|
|
||||||
|
result = await ollama_service.generate_completion(prompt=prompt)
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
|
||||||
|
return AIResponse(
|
||||||
|
success=True,
|
||||||
|
result=result,
|
||||||
|
execution_time=execution_time,
|
||||||
|
model_used=ollama_service.default_model
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
return AIResponse(
|
||||||
|
success=False,
|
||||||
|
result=f"Error explaining code: {str(e)}",
|
||||||
|
execution_time=execution_time
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/refactor", response_model=AIResponse)
|
||||||
|
async def refactor_code(request: RefactorRequest):
|
||||||
|
"""Refactor code for better quality"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not await ollama_service.is_available():
|
||||||
|
raise HTTPException(status_code=503, detail="Ollama service unavailable")
|
||||||
|
|
||||||
|
prompt = f"""
|
||||||
|
Refactor the following {request.language.value if request.language else 'code'} code to improve readability, performance, and maintainability:
|
||||||
|
|
||||||
|
```{request.language.value if request.language else 'code'}
|
||||||
|
{request.code}
|
||||||
|
```
|
||||||
|
|
||||||
|
Focus on {request.refactor_type} improvements. Please provide:
|
||||||
|
1. The refactored code
|
||||||
|
2. Explanation of changes made
|
||||||
|
3. Benefits of the refactoring
|
||||||
|
|
||||||
|
Refactored code:"""
|
||||||
|
|
||||||
|
result = await ollama_service.generate_completion(prompt=prompt)
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
|
||||||
|
return AIResponse(
|
||||||
|
success=True,
|
||||||
|
result=result,
|
||||||
|
execution_time=execution_time,
|
||||||
|
model_used=ollama_service.default_model
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
return AIResponse(
|
||||||
|
success=False,
|
||||||
|
result=f"Error refactoring code: {str(e)}",
|
||||||
|
execution_time=execution_time
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/fix", response_model=AIResponse)
|
||||||
|
async def fix_code(request: FixRequest):
|
||||||
|
"""Fix bugs in code"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not await ollama_service.is_available():
|
||||||
|
raise HTTPException(status_code=503, detail="Ollama service unavailable")
|
||||||
|
|
||||||
|
prompt = ollama_service.build_prompt(
|
||||||
|
command="fix",
|
||||||
|
code=request.code,
|
||||||
|
language=request.language,
|
||||||
|
error_message=request.error_message
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await ollama_service.generate_completion(prompt=prompt)
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
|
||||||
|
return AIResponse(
|
||||||
|
success=True,
|
||||||
|
result=result,
|
||||||
|
execution_time=execution_time,
|
||||||
|
model_used=ollama_service.default_model
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
return AIResponse(
|
||||||
|
success=False,
|
||||||
|
result=f"Error fixing code: {str(e)}",
|
||||||
|
execution_time=execution_time
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/complete", response_model=CodeCompletionResponse)
|
||||||
|
async def complete_code(request: CodeCompletionRequest):
|
||||||
|
"""Generate code completion"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not await ollama_service.is_available():
|
||||||
|
raise HTTPException(status_code=503, detail="Ollama service unavailable")
|
||||||
|
|
||||||
|
# Extract context around cursor position
|
||||||
|
code_before = request.code[:request.cursor_position]
|
||||||
|
code_after = request.code[request.cursor_position:]
|
||||||
|
|
||||||
|
prompt = f"""
|
||||||
|
Complete the following {request.language.value if request.language else 'code'} code at the cursor position:
|
||||||
|
|
||||||
|
```{request.language.value if request.language else 'code'}
|
||||||
|
{code_before}<CURSOR>{code_after}
|
||||||
|
```
|
||||||
|
|
||||||
|
Provide only the code that should be inserted at the cursor position. Keep it concise and contextually appropriate.
|
||||||
|
|
||||||
|
Completion:"""
|
||||||
|
|
||||||
|
result = await ollama_service.generate_completion(
|
||||||
|
prompt=prompt,
|
||||||
|
max_tokens=request.max_tokens
|
||||||
|
)
|
||||||
|
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
|
||||||
|
return CodeCompletionResponse(
|
||||||
|
success=True,
|
||||||
|
completions=[result.strip()],
|
||||||
|
cursor_position=request.cursor_position,
|
||||||
|
execution_time=execution_time
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
return CodeCompletionResponse(
|
||||||
|
success=False,
|
||||||
|
completions=[],
|
||||||
|
cursor_position=request.cursor_position,
|
||||||
|
execution_time=execution_time
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/stream")
|
||||||
|
async def stream_ai_response(request: AIRequest):
|
||||||
|
"""Stream AI response for real-time feedback"""
|
||||||
|
try:
|
||||||
|
if not await ollama_service.is_available():
|
||||||
|
raise HTTPException(status_code=503, detail="Ollama service unavailable")
|
||||||
|
|
||||||
|
prompt = ollama_service.build_prompt(
|
||||||
|
command=request.command,
|
||||||
|
code=request.code,
|
||||||
|
language=request.language,
|
||||||
|
context=request.context
|
||||||
|
)
|
||||||
|
|
||||||
|
async def generate():
|
||||||
|
try:
|
||||||
|
async for chunk in ollama_service.generate_streaming(
|
||||||
|
prompt=prompt,
|
||||||
|
model=request.model
|
||||||
|
):
|
||||||
|
yield f"data: {chunk}\n\n"
|
||||||
|
yield "data: [DONE]\n\n"
|
||||||
|
except Exception as e:
|
||||||
|
yield f"data: ERROR: {str(e)}\n\n"
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
generate(),
|
||||||
|
media_type="text/plain",
|
||||||
|
headers={"Cache-Control": "no-cache"}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post("/multifile", response_model=AIResponse)
|
||||||
|
async def process_multifile_request(request: MultiFileRequest):
|
||||||
|
"""Process multi-file analysis request"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not await ollama_service.is_available():
|
||||||
|
raise HTTPException(status_code=503, detail="Ollama service unavailable")
|
||||||
|
|
||||||
|
# Build context from multiple files
|
||||||
|
file_context = "\n\n".join([
|
||||||
|
f"File: {file_info['path']}\n```\n{file_info['content']}\n```"
|
||||||
|
for file_info in request.files
|
||||||
|
])
|
||||||
|
|
||||||
|
# Focus on specific file if provided
|
||||||
|
focus_context = ""
|
||||||
|
if request.focus_file:
|
||||||
|
focus_file = next(
|
||||||
|
(f for f in request.files if f['path'] == request.focus_file),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if focus_file:
|
||||||
|
focus_context = f"\n\nFocus on this file: {focus_file['path']}\n```\n{focus_file['content']}\n```"
|
||||||
|
|
||||||
|
prompt = f"""
|
||||||
|
Analyze the following multi-file codebase and {request.command.value}:
|
||||||
|
|
||||||
|
{file_context}
|
||||||
|
{focus_context}
|
||||||
|
|
||||||
|
{f"Additional context: {request.context}" if request.context else ""}
|
||||||
|
|
||||||
|
Please provide analysis considering the relationships between files and overall code structure.
|
||||||
|
|
||||||
|
Response:"""
|
||||||
|
|
||||||
|
result = await ollama_service.generate_completion(prompt=prompt)
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
|
||||||
|
return AIResponse(
|
||||||
|
success=True,
|
||||||
|
result=result,
|
||||||
|
execution_time=execution_time,
|
||||||
|
model_used=ollama_service.default_model,
|
||||||
|
metadata={"files_analyzed": len(request.files)}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
return AIResponse(
|
||||||
|
success=False,
|
||||||
|
result=f"Error processing multifile request: {str(e)}",
|
||||||
|
execution_time=execution_time
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def ai_health():
|
||||||
|
"""Check AI service health"""
|
||||||
|
is_available = await ollama_service.is_available()
|
||||||
|
models = await ollama_service.list_models() if is_available else []
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ollama_available": is_available,
|
||||||
|
"models_count": len(models),
|
||||||
|
"default_model": ollama_service.default_model,
|
||||||
|
"base_url": ollama_service.base_url
|
||||||
|
}
|
317
backend/app/api/files.py
Normal file
317
backend/app/api/files.py
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
"""
|
||||||
|
File API routes for file operations and analysis
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import aiofiles
|
||||||
|
from typing import List, Dict
|
||||||
|
from fastapi import APIRouter, HTTPException, UploadFile, File
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
class FileInfo(BaseModel):
|
||||||
|
"""File information model"""
|
||||||
|
path: str
|
||||||
|
name: str
|
||||||
|
size: int
|
||||||
|
extension: str
|
||||||
|
is_supported: bool
|
||||||
|
language: str = None
|
||||||
|
|
||||||
|
class FileContent(BaseModel):
|
||||||
|
"""File content model"""
|
||||||
|
path: str
|
||||||
|
content: str
|
||||||
|
language: str = None
|
||||||
|
size: int
|
||||||
|
|
||||||
|
class DirectoryStructure(BaseModel):
|
||||||
|
"""Directory structure model"""
|
||||||
|
name: str
|
||||||
|
path: str
|
||||||
|
is_file: bool
|
||||||
|
children: List['DirectoryStructure'] = []
|
||||||
|
file_info: FileInfo = None
|
||||||
|
|
||||||
|
@router.get("/supported-extensions")
|
||||||
|
async def get_supported_extensions():
|
||||||
|
"""Get list of supported file extensions"""
|
||||||
|
return {
|
||||||
|
"extensions": settings.SUPPORTED_EXTENSIONS,
|
||||||
|
"max_file_size": settings.MAX_FILE_SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.post("/analyze", response_model=List[FileInfo])
|
||||||
|
async def analyze_files(file_paths: List[str]):
|
||||||
|
"""Analyze multiple files and return their information"""
|
||||||
|
file_infos = []
|
||||||
|
|
||||||
|
for file_path in file_paths:
|
||||||
|
try:
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
stat = os.stat(file_path)
|
||||||
|
if stat.st_size > settings.MAX_FILE_SIZE:
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = os.path.basename(file_path)
|
||||||
|
extension = os.path.splitext(name)[1].lower()
|
||||||
|
is_supported = extension in settings.SUPPORTED_EXTENSIONS
|
||||||
|
|
||||||
|
# Determine language from extension
|
||||||
|
language = get_language_from_extension(extension)
|
||||||
|
|
||||||
|
file_info = FileInfo(
|
||||||
|
path=file_path,
|
||||||
|
name=name,
|
||||||
|
size=stat.st_size,
|
||||||
|
extension=extension,
|
||||||
|
is_supported=is_supported,
|
||||||
|
language=language
|
||||||
|
)
|
||||||
|
|
||||||
|
file_infos.append(file_info)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error analyzing file {file_path}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return file_infos
|
||||||
|
|
||||||
|
@router.post("/read", response_model=FileContent)
|
||||||
|
async def read_file_content(file_path: str):
|
||||||
|
"""Read and return file content"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
|
stat = os.stat(file_path)
|
||||||
|
if stat.st_size > settings.MAX_FILE_SIZE:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=413,
|
||||||
|
detail=f"File too large. Maximum size is {settings.MAX_FILE_SIZE} bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
extension = os.path.splitext(file_path)[1].lower()
|
||||||
|
if extension not in settings.SUPPORTED_EXTENSIONS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=415,
|
||||||
|
detail=f"Unsupported file type: {extension}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = await f.read()
|
||||||
|
|
||||||
|
language = get_language_from_extension(extension)
|
||||||
|
|
||||||
|
return FileContent(
|
||||||
|
path=file_path,
|
||||||
|
content=content,
|
||||||
|
language=language,
|
||||||
|
size=len(content)
|
||||||
|
)
|
||||||
|
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=415,
|
||||||
|
detail="File contains non-text content or unsupported encoding"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post("/read-multiple", response_model=List[FileContent])
|
||||||
|
async def read_multiple_files(file_paths: List[str]):
|
||||||
|
"""Read multiple files and return their contents"""
|
||||||
|
file_contents = []
|
||||||
|
|
||||||
|
for file_path in file_paths:
|
||||||
|
try:
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
stat = os.stat(file_path)
|
||||||
|
if stat.st_size > settings.MAX_FILE_SIZE:
|
||||||
|
continue
|
||||||
|
|
||||||
|
extension = os.path.splitext(file_path)[1].lower()
|
||||||
|
if extension not in settings.SUPPORTED_EXTENSIONS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = await f.read()
|
||||||
|
|
||||||
|
language = get_language_from_extension(extension)
|
||||||
|
|
||||||
|
file_content = FileContent(
|
||||||
|
path=file_path,
|
||||||
|
content=content,
|
||||||
|
language=language,
|
||||||
|
size=len(content)
|
||||||
|
)
|
||||||
|
|
||||||
|
file_contents.append(file_content)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading file {file_path}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return file_contents
|
||||||
|
|
||||||
|
@router.get("/directory-structure")
|
||||||
|
async def get_directory_structure(path: str, max_depth: int = 3):
|
||||||
|
"""Get directory structure for file explorer"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise HTTPException(status_code=404, detail="Directory not found")
|
||||||
|
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
raise HTTPException(status_code=400, detail="Path is not a directory")
|
||||||
|
|
||||||
|
structure = await build_directory_structure(path, max_depth)
|
||||||
|
return structure
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post("/upload")
|
||||||
|
async def upload_file(file: UploadFile = File(...)):
|
||||||
|
"""Upload a file for analysis"""
|
||||||
|
try:
|
||||||
|
if file.size > settings.MAX_FILE_SIZE:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=413,
|
||||||
|
detail=f"File too large. Maximum size is {settings.MAX_FILE_SIZE} bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
extension = os.path.splitext(file.filename)[1].lower()
|
||||||
|
if extension not in settings.SUPPORTED_EXTENSIONS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=415,
|
||||||
|
detail=f"Unsupported file type: {extension}"
|
||||||
|
)
|
||||||
|
|
||||||
|
content = await file.read()
|
||||||
|
content_str = content.decode('utf-8')
|
||||||
|
|
||||||
|
language = get_language_from_extension(extension)
|
||||||
|
|
||||||
|
return FileContent(
|
||||||
|
path=file.filename,
|
||||||
|
content=content_str,
|
||||||
|
language=language,
|
||||||
|
size=len(content_str)
|
||||||
|
)
|
||||||
|
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=415,
|
||||||
|
detail="File contains non-text content or unsupported encoding"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
async def build_directory_structure(path: str, max_depth: int, current_depth: int = 0) -> DirectoryStructure:
|
||||||
|
"""Recursively build directory structure"""
|
||||||
|
name = os.path.basename(path)
|
||||||
|
if not name:
|
||||||
|
name = path
|
||||||
|
|
||||||
|
structure = DirectoryStructure(
|
||||||
|
name=name,
|
||||||
|
path=path,
|
||||||
|
is_file=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_depth >= max_depth:
|
||||||
|
return structure
|
||||||
|
|
||||||
|
try:
|
||||||
|
entries = os.listdir(path)
|
||||||
|
entries.sort()
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
if entry.startswith('.'): # Skip hidden files
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry_path = os.path.join(path, entry)
|
||||||
|
|
||||||
|
if os.path.isfile(entry_path):
|
||||||
|
# File entry
|
||||||
|
stat = os.stat(entry_path)
|
||||||
|
extension = os.path.splitext(entry)[1].lower()
|
||||||
|
is_supported = extension in settings.SUPPORTED_EXTENSIONS
|
||||||
|
language = get_language_from_extension(extension)
|
||||||
|
|
||||||
|
file_info = FileInfo(
|
||||||
|
path=entry_path,
|
||||||
|
name=entry,
|
||||||
|
size=stat.st_size,
|
||||||
|
extension=extension,
|
||||||
|
is_supported=is_supported,
|
||||||
|
language=language
|
||||||
|
)
|
||||||
|
|
||||||
|
file_structure = DirectoryStructure(
|
||||||
|
name=entry,
|
||||||
|
path=entry_path,
|
||||||
|
is_file=True,
|
||||||
|
file_info=file_info
|
||||||
|
)
|
||||||
|
|
||||||
|
structure.children.append(file_structure)
|
||||||
|
|
||||||
|
elif os.path.isdir(entry_path):
|
||||||
|
# Directory entry
|
||||||
|
dir_structure = await build_directory_structure(
|
||||||
|
entry_path, max_depth, current_depth + 1
|
||||||
|
)
|
||||||
|
structure.children.append(dir_structure)
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
pass # Skip directories we can't read
|
||||||
|
|
||||||
|
return structure
|
||||||
|
|
||||||
|
def get_language_from_extension(extension: str) -> str:
|
||||||
|
"""Map file extension to programming language"""
|
||||||
|
extension_map = {
|
||||||
|
'.py': 'python',
|
||||||
|
'.js': 'javascript',
|
||||||
|
'.ts': 'typescript',
|
||||||
|
'.jsx': 'javascript',
|
||||||
|
'.tsx': 'typescript',
|
||||||
|
'.java': 'java',
|
||||||
|
'.go': 'go',
|
||||||
|
'.rs': 'rust',
|
||||||
|
'.cpp': 'cpp',
|
||||||
|
'.cc': 'cpp',
|
||||||
|
'.cxx': 'cpp',
|
||||||
|
'.c': 'c',
|
||||||
|
'.h': 'c',
|
||||||
|
'.hpp': 'cpp',
|
||||||
|
'.cs': 'csharp',
|
||||||
|
'.php': 'php',
|
||||||
|
'.rb': 'ruby',
|
||||||
|
'.swift': 'swift',
|
||||||
|
'.kt': 'kotlin',
|
||||||
|
'.scala': 'scala',
|
||||||
|
'.sh': 'shell',
|
||||||
|
'.bash': 'shell',
|
||||||
|
'.zsh': 'shell',
|
||||||
|
'.html': 'html',
|
||||||
|
'.htm': 'html',
|
||||||
|
'.css': 'css',
|
||||||
|
'.scss': 'scss',
|
||||||
|
'.sass': 'sass',
|
||||||
|
'.sql': 'sql',
|
||||||
|
'.md': 'markdown',
|
||||||
|
'.yaml': 'yaml',
|
||||||
|
'.yml': 'yaml',
|
||||||
|
'.json': 'json',
|
||||||
|
'.xml': 'xml'
|
||||||
|
}
|
||||||
|
|
||||||
|
return extension_map.get(extension.lower(), 'text')
|
242
backend/app/api/models.py
Normal file
242
backend/app/api/models.py
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
"""
|
||||||
|
Models API routes for managing LLM models
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from app.models.ai import ModelInfo, ModelListResponse
|
||||||
|
from app.services.ollama_service import ollama_service
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/list", response_model=ModelListResponse)
|
||||||
|
async def list_models():
|
||||||
|
"""List all available models from Ollama"""
|
||||||
|
try:
|
||||||
|
if not await ollama_service.is_available():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="Ollama service is not available. Please ensure Ollama is running."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get models from Ollama
|
||||||
|
ollama_models = await ollama_service.list_models()
|
||||||
|
|
||||||
|
model_infos = []
|
||||||
|
for model in ollama_models:
|
||||||
|
model_name = model.get("name", "unknown")
|
||||||
|
model_size = model.get("size", 0)
|
||||||
|
|
||||||
|
# Format size for display
|
||||||
|
size_str = format_bytes(model_size)
|
||||||
|
|
||||||
|
# Get model capabilities based on name
|
||||||
|
capabilities = get_model_capabilities(model_name)
|
||||||
|
|
||||||
|
# Get model description
|
||||||
|
description = get_model_description(model_name)
|
||||||
|
|
||||||
|
model_info = ModelInfo(
|
||||||
|
name=model_name,
|
||||||
|
size=size_str,
|
||||||
|
description=description,
|
||||||
|
capabilities=capabilities,
|
||||||
|
is_available=True
|
||||||
|
)
|
||||||
|
|
||||||
|
model_infos.append(model_info)
|
||||||
|
|
||||||
|
# Add supported models that aren't installed
|
||||||
|
installed_model_names = [m.name for m in model_infos]
|
||||||
|
for supported_model in settings.SUPPORTED_MODELS:
|
||||||
|
if supported_model not in installed_model_names:
|
||||||
|
model_info = ModelInfo(
|
||||||
|
name=supported_model,
|
||||||
|
size="Not installed",
|
||||||
|
description=get_model_description(supported_model),
|
||||||
|
capabilities=get_model_capabilities(supported_model),
|
||||||
|
is_available=False
|
||||||
|
)
|
||||||
|
model_infos.append(model_info)
|
||||||
|
|
||||||
|
return ModelListResponse(
|
||||||
|
models=model_infos,
|
||||||
|
default_model=settings.DEFAULT_MODEL,
|
||||||
|
current_model=settings.DEFAULT_MODEL
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post("/pull/{model_name}")
|
||||||
|
async def pull_model(model_name: str):
|
||||||
|
"""Pull/download a model from Ollama"""
|
||||||
|
try:
|
||||||
|
if not await ollama_service.is_available():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="Ollama service is not available"
|
||||||
|
)
|
||||||
|
|
||||||
|
if model_name not in settings.SUPPORTED_MODELS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Model {model_name} is not in the supported models list"
|
||||||
|
)
|
||||||
|
|
||||||
|
success = await ollama_service.pull_model(model_name)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return {"message": f"Model {model_name} pulled successfully"}
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Failed to pull model {model_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get("/info/{model_name}", response_model=ModelInfo)
|
||||||
|
async def get_model_info(model_name: str):
|
||||||
|
"""Get detailed information about a specific model"""
|
||||||
|
try:
|
||||||
|
if not await ollama_service.is_available():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="Ollama service is not available"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all models
|
||||||
|
ollama_models = await ollama_service.list_models()
|
||||||
|
|
||||||
|
# Find the specific model
|
||||||
|
target_model = None
|
||||||
|
for model in ollama_models:
|
||||||
|
if model.get("name") == model_name:
|
||||||
|
target_model = model
|
||||||
|
break
|
||||||
|
|
||||||
|
if not target_model:
|
||||||
|
# Check if it's a supported model that's not installed
|
||||||
|
if model_name in settings.SUPPORTED_MODELS:
|
||||||
|
return ModelInfo(
|
||||||
|
name=model_name,
|
||||||
|
size="Not installed",
|
||||||
|
description=get_model_description(model_name),
|
||||||
|
capabilities=get_model_capabilities(model_name),
|
||||||
|
is_available=False
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Model {model_name} not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
size_str = format_bytes(target_model.get("size", 0))
|
||||||
|
|
||||||
|
return ModelInfo(
|
||||||
|
name=model_name,
|
||||||
|
size=size_str,
|
||||||
|
description=get_model_description(model_name),
|
||||||
|
capabilities=get_model_capabilities(model_name),
|
||||||
|
is_available=True
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get("/supported")
|
||||||
|
async def get_supported_models():
|
||||||
|
"""Get list of supported models"""
|
||||||
|
return {
|
||||||
|
"supported_models": settings.SUPPORTED_MODELS,
|
||||||
|
"default_model": settings.DEFAULT_MODEL,
|
||||||
|
"model_descriptions": {
|
||||||
|
model: get_model_description(model)
|
||||||
|
for model in settings.SUPPORTED_MODELS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/current")
|
||||||
|
async def get_current_model():
|
||||||
|
"""Get currently selected model"""
|
||||||
|
return {
|
||||||
|
"current_model": settings.DEFAULT_MODEL,
|
||||||
|
"is_available": await ollama_service.is_available()
|
||||||
|
}
|
||||||
|
|
||||||
|
def format_bytes(size_bytes: int) -> str:
|
||||||
|
"""Format bytes into human readable format"""
|
||||||
|
if size_bytes == 0:
|
||||||
|
return "0 B"
|
||||||
|
|
||||||
|
size_names = ["B", "KB", "MB", "GB", "TB"]
|
||||||
|
import math
|
||||||
|
i = int(math.floor(math.log(size_bytes, 1024)))
|
||||||
|
p = math.pow(1024, i)
|
||||||
|
s = round(size_bytes / p, 2)
|
||||||
|
return f"{s} {size_names[i]}"
|
||||||
|
|
||||||
|
def get_model_capabilities(model_name: str) -> List[str]:
|
||||||
|
"""Get capabilities for a specific model"""
|
||||||
|
capabilities_map = {
|
||||||
|
"codellama": [
|
||||||
|
"Code generation",
|
||||||
|
"Code completion",
|
||||||
|
"Bug fixing",
|
||||||
|
"Code explanation",
|
||||||
|
"Refactoring",
|
||||||
|
"Multi-language support"
|
||||||
|
],
|
||||||
|
"deepseek-coder": [
|
||||||
|
"Advanced code generation",
|
||||||
|
"Code understanding",
|
||||||
|
"Bug detection",
|
||||||
|
"Code optimization",
|
||||||
|
"Documentation generation",
|
||||||
|
"Multi-language support"
|
||||||
|
],
|
||||||
|
"starcoder": [
|
||||||
|
"Code completion",
|
||||||
|
"Code generation",
|
||||||
|
"Cross-language understanding",
|
||||||
|
"Documentation",
|
||||||
|
"Code translation"
|
||||||
|
],
|
||||||
|
"codegemma": [
|
||||||
|
"Code generation",
|
||||||
|
"Code explanation",
|
||||||
|
"Bug fixing",
|
||||||
|
"Refactoring",
|
||||||
|
"Test generation"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find matching capabilities
|
||||||
|
for key, capabilities in capabilities_map.items():
|
||||||
|
if key in model_name.lower():
|
||||||
|
return capabilities
|
||||||
|
|
||||||
|
# Default capabilities
|
||||||
|
return [
|
||||||
|
"Code assistance",
|
||||||
|
"Text generation",
|
||||||
|
"Code completion"
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_model_description(model_name: str) -> str:
|
||||||
|
"""Get description for a specific model"""
|
||||||
|
descriptions = {
|
||||||
|
"codellama:7b-code": "Meta's CodeLlama 7B optimized for code generation and understanding",
|
||||||
|
"codellama:13b-code": "Meta's CodeLlama 13B with enhanced code capabilities",
|
||||||
|
"deepseek-coder:6.7b": "DeepSeek's code-specialized model with strong programming abilities",
|
||||||
|
"starcoder:7b": "BigCode's StarCoder model for code generation and completion",
|
||||||
|
"codegemma:7b": "Google's CodeGemma model for code understanding and generation"
|
||||||
|
}
|
||||||
|
|
||||||
|
return descriptions.get(
|
||||||
|
model_name,
|
||||||
|
f"Code-specialized language model: {model_name}"
|
||||||
|
)
|
1
backend/app/core/__init__.py
Normal file
1
backend/app/core/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Core utilities and configuration
|
52
backend/app/core/config.py
Normal file
52
backend/app/core/config.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
Configuration settings for Foxus Backend
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import List
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""Application settings with environment variable support"""
|
||||||
|
|
||||||
|
# App settings
|
||||||
|
APP_NAME: str = "Foxus API"
|
||||||
|
VERSION: str = "1.0.0"
|
||||||
|
DEBUG: bool = True
|
||||||
|
|
||||||
|
# Server settings
|
||||||
|
HOST: str = "127.0.0.1"
|
||||||
|
PORT: int = 8000
|
||||||
|
|
||||||
|
# Ollama settings
|
||||||
|
OLLAMA_BASE_URL: str = "http://localhost:11434"
|
||||||
|
DEFAULT_MODEL: str = "codellama:7b-code"
|
||||||
|
|
||||||
|
# Supported models
|
||||||
|
SUPPORTED_MODELS: List[str] = [
|
||||||
|
"codellama:7b-code",
|
||||||
|
"codellama:13b-code",
|
||||||
|
"deepseek-coder:6.7b",
|
||||||
|
"starcoder:7b",
|
||||||
|
"codegemma:7b"
|
||||||
|
]
|
||||||
|
|
||||||
|
# File processing
|
||||||
|
MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 10MB
|
||||||
|
SUPPORTED_EXTENSIONS: List[str] = [
|
||||||
|
".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".java", ".cpp", ".c", ".h",
|
||||||
|
".rs", ".php", ".rb", ".swift", ".kt", ".cs", ".scala", ".sh", ".bash",
|
||||||
|
".yaml", ".yml", ".json", ".xml", ".html", ".css", ".sql", ".md"
|
||||||
|
]
|
||||||
|
|
||||||
|
# AI settings
|
||||||
|
MAX_TOKENS: int = 4096
|
||||||
|
TEMPERATURE: float = 0.1
|
||||||
|
TOP_P: float = 0.9
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
case_sensitive = True
|
||||||
|
|
||||||
|
# Create global settings instance
|
||||||
|
settings = Settings()
|
1
backend/app/services/__init__.py
Normal file
1
backend/app/services/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Business logic and services
|
288
backend/app/services/ollama_service.py
Normal file
288
backend/app/services/ollama_service.py
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
"""
|
||||||
|
Ollama service for local LLM integration
|
||||||
|
"""
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from typing import Dict, List, Optional, AsyncGenerator
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.models.ai import LanguageType, AICommand
|
||||||
|
|
||||||
|
class OllamaService:
|
||||||
|
"""Service for interacting with Ollama API"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.base_url = settings.OLLAMA_BASE_URL
|
||||||
|
self.default_model = settings.DEFAULT_MODEL
|
||||||
|
self.client = httpx.AsyncClient(timeout=60.0)
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self.client.aclose()
|
||||||
|
|
||||||
|
async def is_available(self) -> bool:
|
||||||
|
"""Check if Ollama is running and available"""
|
||||||
|
try:
|
||||||
|
response = await self.client.get(f"{self.base_url}/api/tags")
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def list_models(self) -> List[Dict]:
|
||||||
|
"""List available models from Ollama"""
|
||||||
|
try:
|
||||||
|
response = await self.client.get(f"{self.base_url}/api/tags")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
return data.get("models", [])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error listing models: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def pull_model(self, model_name: str) -> bool:
|
||||||
|
"""Pull/download a model if not available"""
|
||||||
|
try:
|
||||||
|
payload = {"name": model_name}
|
||||||
|
response = await self.client.post(
|
||||||
|
f"{self.base_url}/api/pull",
|
||||||
|
json=payload,
|
||||||
|
timeout=300.0 # 5 minutes for model download
|
||||||
|
)
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error pulling model {model_name}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def generate_completion(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
model: Optional[str] = None,
|
||||||
|
temperature: float = settings.TEMPERATURE,
|
||||||
|
max_tokens: int = settings.MAX_TOKENS,
|
||||||
|
stream: bool = False
|
||||||
|
) -> str:
|
||||||
|
"""Generate text completion from Ollama"""
|
||||||
|
model_name = model or self.default_model
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model_name,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": stream,
|
||||||
|
"options": {
|
||||||
|
"temperature": temperature,
|
||||||
|
"num_predict": max_tokens,
|
||||||
|
"top_p": settings.TOP_P
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self.client.post(
|
||||||
|
f"{self.base_url}/api/generate",
|
||||||
|
json=payload,
|
||||||
|
timeout=120.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
if stream:
|
||||||
|
# Handle streaming response
|
||||||
|
full_response = ""
|
||||||
|
for line in response.iter_lines():
|
||||||
|
if line:
|
||||||
|
data = json.loads(line)
|
||||||
|
if "response" in data:
|
||||||
|
full_response += data["response"]
|
||||||
|
if data.get("done", False):
|
||||||
|
break
|
||||||
|
return full_response
|
||||||
|
else:
|
||||||
|
# Handle single response
|
||||||
|
data = response.json()
|
||||||
|
return data.get("response", "")
|
||||||
|
else:
|
||||||
|
raise Exception(f"Ollama API error: {response.status_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating completion: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def generate_streaming(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
model: Optional[str] = None,
|
||||||
|
temperature: float = settings.TEMPERATURE,
|
||||||
|
max_tokens: int = settings.MAX_TOKENS
|
||||||
|
) -> AsyncGenerator[str, None]:
|
||||||
|
"""Generate streaming completion from Ollama"""
|
||||||
|
model_name = model or self.default_model
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model_name,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": True,
|
||||||
|
"options": {
|
||||||
|
"temperature": temperature,
|
||||||
|
"num_predict": max_tokens,
|
||||||
|
"top_p": settings.TOP_P
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with self.client.stream(
|
||||||
|
"POST",
|
||||||
|
f"{self.base_url}/api/generate",
|
||||||
|
json=payload,
|
||||||
|
timeout=120.0
|
||||||
|
) as response:
|
||||||
|
if response.status_code == 200:
|
||||||
|
async for line in response.aiter_lines():
|
||||||
|
if line:
|
||||||
|
try:
|
||||||
|
data = json.loads(line)
|
||||||
|
if "response" in data:
|
||||||
|
yield data["response"]
|
||||||
|
if data.get("done", False):
|
||||||
|
break
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise Exception(f"Ollama API error: {response.status_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in streaming generation: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def build_prompt(
|
||||||
|
self,
|
||||||
|
command: AICommand,
|
||||||
|
code: str,
|
||||||
|
language: Optional[LanguageType] = None,
|
||||||
|
context: Optional[str] = None,
|
||||||
|
error_message: Optional[str] = None
|
||||||
|
) -> str:
|
||||||
|
"""Build appropriate prompt based on command and context"""
|
||||||
|
|
||||||
|
lang_name = language.value if language else "code"
|
||||||
|
|
||||||
|
prompts = {
|
||||||
|
AICommand.EXPLAIN: f"""
|
||||||
|
Explain the following {lang_name} code in clear, concise terms:
|
||||||
|
|
||||||
|
```{lang_name}
|
||||||
|
{code}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please provide:
|
||||||
|
1. What this code does
|
||||||
|
2. Key concepts and algorithms used
|
||||||
|
3. Any potential issues or improvements
|
||||||
|
|
||||||
|
Response:""",
|
||||||
|
|
||||||
|
AICommand.REFACTOR: f"""
|
||||||
|
Refactor the following {lang_name} code to improve readability, performance, and maintainability:
|
||||||
|
|
||||||
|
```{lang_name}
|
||||||
|
{code}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please provide:
|
||||||
|
1. Refactored code
|
||||||
|
2. Explanation of changes made
|
||||||
|
3. Benefits of the refactoring
|
||||||
|
|
||||||
|
Refactored code:""",
|
||||||
|
|
||||||
|
AICommand.FIX: f"""
|
||||||
|
Fix the bugs or issues in the following {lang_name} code:
|
||||||
|
|
||||||
|
```{lang_name}
|
||||||
|
{code}
|
||||||
|
```
|
||||||
|
|
||||||
|
{f"Error message: {error_message}" if error_message else ""}
|
||||||
|
|
||||||
|
Please provide:
|
||||||
|
1. Fixed code
|
||||||
|
2. Explanation of what was wrong
|
||||||
|
3. How the fix addresses the issue
|
||||||
|
|
||||||
|
Fixed code:""",
|
||||||
|
|
||||||
|
AICommand.COMPLETE: f"""
|
||||||
|
Complete the following {lang_name} code based on the context:
|
||||||
|
|
||||||
|
```{lang_name}
|
||||||
|
{code}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please provide the most likely completion that follows naturally from the existing code.
|
||||||
|
|
||||||
|
Completion:""",
|
||||||
|
|
||||||
|
AICommand.COMMENT: f"""
|
||||||
|
Add clear, helpful comments to the following {lang_name} code:
|
||||||
|
|
||||||
|
```{lang_name}
|
||||||
|
{code}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please provide the same code with appropriate comments explaining the functionality.
|
||||||
|
|
||||||
|
Commented code:""",
|
||||||
|
|
||||||
|
AICommand.TEST: f"""
|
||||||
|
Generate comprehensive unit tests for the following {lang_name} code:
|
||||||
|
|
||||||
|
```{lang_name}
|
||||||
|
{code}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please provide:
|
||||||
|
1. Complete test cases covering different scenarios
|
||||||
|
2. Test setup and teardown if needed
|
||||||
|
3. Comments explaining what each test validates
|
||||||
|
|
||||||
|
Test code:""",
|
||||||
|
|
||||||
|
AICommand.OPTIMIZE: f"""
|
||||||
|
Optimize the following {lang_name} code for better performance:
|
||||||
|
|
||||||
|
```{lang_name}
|
||||||
|
{code}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please provide:
|
||||||
|
1. Optimized code
|
||||||
|
2. Explanation of optimizations made
|
||||||
|
3. Expected performance improvements
|
||||||
|
|
||||||
|
Optimized code:""",
|
||||||
|
|
||||||
|
AICommand.DOCUMENT: f"""
|
||||||
|
Generate comprehensive documentation for the following {lang_name} code:
|
||||||
|
|
||||||
|
```{lang_name}
|
||||||
|
{code}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please provide:
|
||||||
|
1. Function/class documentation
|
||||||
|
2. Parameter descriptions
|
||||||
|
3. Return value descriptions
|
||||||
|
4. Usage examples
|
||||||
|
|
||||||
|
Documentation:"""
|
||||||
|
}
|
||||||
|
|
||||||
|
base_prompt = prompts.get(command, f"Analyze this {lang_name} code:\n\n```{lang_name}\n{code}\n```\n\nResponse:")
|
||||||
|
|
||||||
|
if context:
|
||||||
|
base_prompt = f"Context: {context}\n\n{base_prompt}"
|
||||||
|
|
||||||
|
return base_prompt
|
||||||
|
|
||||||
|
# Create singleton instance
|
||||||
|
ollama_service = OllamaService()
|
52
backend/main.py
Normal file
52
backend/main.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
Foxus Backend - Local AI Coding Assistant
|
||||||
|
Main FastAPI application entry point
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.api import ai, files, models
|
||||||
|
|
||||||
|
# Create FastAPI app
|
||||||
|
app = FastAPI(
|
||||||
|
title="Foxus API",
|
||||||
|
description="Local AI Coding Assistant Backend",
|
||||||
|
version="1.0.0",
|
||||||
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add CORS middleware for frontend communication
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["http://localhost:1420", "https://tauri.localhost"], # Tauri default origins
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include API routes
|
||||||
|
app.include_router(ai.router, prefix="/api/ai", tags=["AI"])
|
||||||
|
app.include_router(files.router, prefix="/api/files", tags=["Files"])
|
||||||
|
app.include_router(models.router, prefix="/api/models", tags=["Models"])
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return {"message": "Foxus API is running", "version": "1.0.0"}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
"""Health check for monitoring"""
|
||||||
|
return {"status": "healthy", "service": "foxus-api"}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(
|
||||||
|
"main:app",
|
||||||
|
host=settings.HOST,
|
||||||
|
port=settings.PORT,
|
||||||
|
reload=settings.DEBUG,
|
||||||
|
log_level="info"
|
||||||
|
)
|
12
backend/requirements.txt
Normal file
12
backend/requirements.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
pydantic==2.5.0
|
||||||
|
pydantic-settings==2.1.0
|
||||||
|
python-multipart==0.0.6
|
||||||
|
aiofiles==23.2.1
|
||||||
|
httpx==0.25.2
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
typing-extensions==4.9.0
|
||||||
|
langchain==0.1.0
|
||||||
|
openai==1.6.1
|
14
frontend/index.html
Normal file
14
frontend/index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Foxus - Local AI Code Assistant</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
44
frontend/package.json
Normal file
44
frontend/package.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "foxus-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri",
|
||||||
|
"tauri:dev": "tauri dev",
|
||||||
|
"tauri:build": "tauri build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"lint:fix": "eslint . --ext ts,tsx --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@tauri-apps/api": "^1.5.1",
|
||||||
|
"@tauri-apps/plugin-shell": "^1.0.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"zustand": "^4.4.7",
|
||||||
|
"lucide-react": "^0.294.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"react-hotkeys-hook": "^4.4.1",
|
||||||
|
"react-resizable-panels": "^0.0.61"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^1.5.8",
|
||||||
|
"@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.16",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"tailwindcss": "^3.3.6",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.0.8"
|
||||||
|
}
|
||||||
|
}
|
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
22
frontend/src-tauri/Cargo.toml
Normal file
22
frontend/src-tauri/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "foxus"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "A local-first AI coding assistant"
|
||||||
|
authors = ["Foxus Team"]
|
||||||
|
license = "MIT"
|
||||||
|
repository = ""
|
||||||
|
default-run = "foxus"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.60"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "1.5.0", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
tauri = { version = "1.5.0", features = [ "api-all", "shell-open", "dialog-open", "dialog-save"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = [ "custom-protocol" ]
|
||||||
|
custom-protocol = [ "tauri/custom-protocol" ]
|
3
frontend/src-tauri/build.rs
Normal file
3
frontend/src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
54
frontend/src-tauri/src/main.rs
Normal file
54
frontend/src-tauri/src/main.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||||
|
#[tauri::command]
|
||||||
|
fn greet(name: &str) -> String {
|
||||||
|
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn read_file_content(path: String) -> Result<String, String> {
|
||||||
|
match std::fs::read_to_string(&path) {
|
||||||
|
Ok(content) => Ok(content),
|
||||||
|
Err(e) => Err(format!("Failed to read file: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn write_file_content(path: String, content: String) -> Result<(), String> {
|
||||||
|
match std::fs::write(&path, content) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(format!("Failed to write file: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn check_file_exists(path: String) -> bool {
|
||||||
|
std::path::Path::new(&path).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.setup(|app| {
|
||||||
|
// Setup app window
|
||||||
|
let window = app.get_window("main").unwrap();
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
window.open_devtools();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
greet,
|
||||||
|
read_file_content,
|
||||||
|
write_file_content,
|
||||||
|
check_file_exists
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
76
frontend/src-tauri/tauri.conf.json
Normal file
76
frontend/src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"devPath": "http://localhost:1420",
|
||||||
|
"distDir": "../dist",
|
||||||
|
"withGlobalTauri": false
|
||||||
|
},
|
||||||
|
"package": {
|
||||||
|
"productName": "Foxus",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"tauri": {
|
||||||
|
"allowlist": {
|
||||||
|
"all": false,
|
||||||
|
"shell": {
|
||||||
|
"all": false,
|
||||||
|
"open": true
|
||||||
|
},
|
||||||
|
"dialog": {
|
||||||
|
"all": false,
|
||||||
|
"open": true,
|
||||||
|
"save": true
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"all": false,
|
||||||
|
"readFile": true,
|
||||||
|
"writeFile": true,
|
||||||
|
"readDir": true,
|
||||||
|
"createDir": true,
|
||||||
|
"removeFile": true,
|
||||||
|
"exists": true,
|
||||||
|
"scope": ["$HOME/**", "$DESKTOP/**", "$DOCUMENT/**", "$DOWNLOAD/**"]
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"all": false,
|
||||||
|
"request": true,
|
||||||
|
"scope": ["http://localhost:8000/**", "http://127.0.0.1:8000/**"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"identifier": "com.foxus.app",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"fullscreen": false,
|
||||||
|
"resizable": true,
|
||||||
|
"title": "Foxus - Local AI Code Assistant",
|
||||||
|
"width": 1400,
|
||||||
|
"height": 900,
|
||||||
|
"minWidth": 800,
|
||||||
|
"minHeight": 600,
|
||||||
|
"center": true,
|
||||||
|
"decorations": true,
|
||||||
|
"transparent": false,
|
||||||
|
"alwaysOnTop": false,
|
||||||
|
"skipTaskbar": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
100
frontend/src/App.tsx
Normal file
100
frontend/src/App.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||||
|
import Editor from './components/Editor';
|
||||||
|
import Sidebar from './components/Sidebar';
|
||||||
|
import AIPanel from './components/AIPanel';
|
||||||
|
import StatusBar from './components/StatusBar';
|
||||||
|
import CommandPalette from './components/CommandPalette';
|
||||||
|
import { useEditorStore } from './stores/editorStore';
|
||||||
|
import { useAIStore } from './stores/aiStore';
|
||||||
|
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [showCommandPalette, setShowCommandPalette] = useState(false);
|
||||||
|
const { isConnected, checkConnection } = useAIStore();
|
||||||
|
const { currentFile, openFiles } = useEditorStore();
|
||||||
|
|
||||||
|
// Setup keyboard shortcuts
|
||||||
|
useKeyboardShortcuts({
|
||||||
|
onToggleCommandPalette: () => setShowCommandPalette(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check AI service connection on startup
|
||||||
|
useEffect(() => {
|
||||||
|
checkConnection();
|
||||||
|
}, [checkConnection]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen bg-dark-900 text-dark-100 flex flex-col">
|
||||||
|
{/* Main application layout */}
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<PanelGroup direction="horizontal">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<Panel defaultSize={20} minSize={15} maxSize={30}>
|
||||||
|
<Sidebar />
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<PanelResizeHandle className="w-1 bg-dark-700 hover:bg-primary-600 transition-colors" />
|
||||||
|
|
||||||
|
{/* Main editor area */}
|
||||||
|
<Panel defaultSize={60} minSize={40}>
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
{/* Editor tabs */}
|
||||||
|
{openFiles.length > 0 && (
|
||||||
|
<div className="flex bg-dark-800 border-b border-dark-700">
|
||||||
|
{openFiles.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.path}
|
||||||
|
className={`px-4 py-2 border-r border-dark-700 cursor-pointer transition-colors ${
|
||||||
|
currentFile?.path === file.path
|
||||||
|
? 'bg-dark-700 text-primary-400'
|
||||||
|
: 'hover:bg-dark-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-sm">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Editor */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<Editor />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<PanelResizeHandle className="w-1 bg-dark-700 hover:bg-primary-600 transition-colors" />
|
||||||
|
|
||||||
|
{/* AI Panel */}
|
||||||
|
<Panel defaultSize={20} minSize={15} maxSize={40}>
|
||||||
|
<AIPanel />
|
||||||
|
</Panel>
|
||||||
|
</PanelGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status bar */}
|
||||||
|
<StatusBar />
|
||||||
|
|
||||||
|
{/* Command palette */}
|
||||||
|
{showCommandPalette && (
|
||||||
|
<CommandPalette
|
||||||
|
isOpen={showCommandPalette}
|
||||||
|
onClose={() => setShowCommandPalette(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Connection status indicator */}
|
||||||
|
{!isConnected && (
|
||||||
|
<div className="fixed top-4 right-4 bg-red-600 text-white px-4 py-2 rounded-lg shadow-lg animate-fade-in">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-2 h-2 bg-red-300 rounded-full animate-pulse" />
|
||||||
|
<span className="text-sm font-medium">AI Service Disconnected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
16
frontend/src/components/AIPanel.tsx
Normal file
16
frontend/src/components/AIPanel.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const AIPanel: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="h-full bg-dark-800 border-l border-dark-700 flex flex-col">
|
||||||
|
<div className="p-4 border-b border-dark-700">
|
||||||
|
<h3 className="text-lg font-semibold">AI Assistant</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
<div className="text-sm text-dark-400">AI chat interface will be here</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AIPanel;
|
27
frontend/src/components/CommandPalette.tsx
Normal file
27
frontend/src/components/CommandPalette.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface CommandPaletteProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommandPalette: React.FC<CommandPaletteProps> = ({ isOpen, onClose }) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-dark-800 rounded-lg p-4 w-96">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Command Palette</h3>
|
||||||
|
<div className="text-sm text-dark-400">AI commands will be here</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="mt-4 px-4 py-2 bg-primary-600 text-white rounded hover:bg-primary-700"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommandPalette;
|
14
frontend/src/components/Editor.tsx
Normal file
14
frontend/src/components/Editor.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Editor: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="h-full bg-dark-800 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-xl font-semibold text-dark-300 mb-2">Monaco Editor</h2>
|
||||||
|
<p className="text-dark-400">Code editor will be integrated here</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Editor;
|
14
frontend/src/components/Sidebar.tsx
Normal file
14
frontend/src/components/Sidebar.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Sidebar: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="h-full bg-dark-800 border-r border-dark-700 p-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Explorer</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm text-dark-400">File explorer will be here</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
12
frontend/src/components/StatusBar.tsx
Normal file
12
frontend/src/components/StatusBar.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const StatusBar: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="h-6 bg-dark-700 border-t border-dark-600 px-4 flex items-center justify-between text-xs text-dark-300">
|
||||||
|
<div>Ready</div>
|
||||||
|
<div>Status bar content</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusBar;
|
77
frontend/src/hooks/useKeyboardShortcuts.ts
Normal file
77
frontend/src/hooks/useKeyboardShortcuts.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useEditorStore } from '../stores/editorStore';
|
||||||
|
import { useAIStore } from '../stores/aiStore';
|
||||||
|
|
||||||
|
interface KeyboardShortcutsProps {
|
||||||
|
onToggleCommandPalette: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useKeyboardShortcuts = ({ onToggleCommandPalette }: KeyboardShortcutsProps) => {
|
||||||
|
const { saveCurrentFile, currentFile } = useEditorStore();
|
||||||
|
const { explainCode, refactorCode, fixCode } = useAIStore();
|
||||||
|
|
||||||
|
// Command palette
|
||||||
|
useHotkeys('ctrl+k, cmd+k', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onToggleCommandPalette();
|
||||||
|
});
|
||||||
|
|
||||||
|
// File operations
|
||||||
|
useHotkeys('ctrl+s, cmd+s', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
saveCurrentFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
// AI commands with selected text
|
||||||
|
useHotkeys('ctrl+shift+e, cmd+shift+e', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentFile) {
|
||||||
|
const selectedText = getSelectedText();
|
||||||
|
if (selectedText) {
|
||||||
|
await explainCode(selectedText, currentFile.language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useHotkeys('ctrl+shift+r, cmd+shift+r', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentFile) {
|
||||||
|
const selectedText = getSelectedText();
|
||||||
|
if (selectedText) {
|
||||||
|
await refactorCode(selectedText, currentFile.language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useHotkeys('ctrl+shift+f, cmd+shift+f', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentFile) {
|
||||||
|
const selectedText = getSelectedText();
|
||||||
|
if (selectedText) {
|
||||||
|
await fixCode(selectedText, currentFile.language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quick AI commands
|
||||||
|
useHotkeys('alt+e', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onToggleCommandPalette();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Developer tools (only in development)
|
||||||
|
useHotkeys('f12', (e) => {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
e.preventDefault();
|
||||||
|
// Toggle developer tools if available
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get selected text from Monaco editor
|
||||||
|
const getSelectedText = (): string => {
|
||||||
|
// This would integrate with Monaco editor's selection API
|
||||||
|
// For now, return empty string as placeholder
|
||||||
|
const selection = window.getSelection();
|
||||||
|
return selection ? selection.toString() : '';
|
||||||
|
};
|
79
frontend/src/index.css
Normal file
79
frontend/src/index.css
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Custom scrollbar styles */
|
||||||
|
@layer utilities {
|
||||||
|
.scrollbar-thin {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgb(100 116 139) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgb(100 116 139);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgb(71 85 105);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Monaco Editor custom styling */
|
||||||
|
.monaco-editor {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AI response styling */
|
||||||
|
.ai-response {
|
||||||
|
@apply prose prose-sm max-w-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-response pre {
|
||||||
|
@apply bg-dark-800 text-dark-100 rounded-lg p-4 overflow-x-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-response code {
|
||||||
|
@apply bg-dark-700 text-primary-400 px-1 py-0.5 rounded text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom focus styles */
|
||||||
|
.focus-ring {
|
||||||
|
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-dark-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading animations */
|
||||||
|
.loading-dots {
|
||||||
|
@apply inline-flex space-x-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dots > div {
|
||||||
|
@apply w-2 h-2 bg-primary-500 rounded-full animate-pulse;
|
||||||
|
animation-delay: calc(var(--i) * 0.2s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File tree styling */
|
||||||
|
.file-tree {
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree-item {
|
||||||
|
@apply flex items-center py-1 px-2 rounded cursor-pointer hover:bg-dark-700 transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree-item.selected {
|
||||||
|
@apply bg-primary-600 text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree-item.folder {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
10
frontend/src/main.tsx
Normal file
10
frontend/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
316
frontend/src/stores/aiStore.ts
Normal file
316
frontend/src/stores/aiStore.ts
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export interface AIMessage {
|
||||||
|
id: string;
|
||||||
|
type: 'user' | 'assistant' | 'system';
|
||||||
|
content: string;
|
||||||
|
timestamp: Date;
|
||||||
|
command?: string;
|
||||||
|
metadata?: {
|
||||||
|
executionTime?: number;
|
||||||
|
model?: string;
|
||||||
|
language?: string;
|
||||||
|
filePath?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIModel {
|
||||||
|
name: string;
|
||||||
|
size: string;
|
||||||
|
description?: string;
|
||||||
|
capabilities: string[];
|
||||||
|
isAvailable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AIState {
|
||||||
|
// Connection state
|
||||||
|
isConnected: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Chat and messages
|
||||||
|
messages: AIMessage[];
|
||||||
|
currentModel: string;
|
||||||
|
availableModels: AIModel[];
|
||||||
|
|
||||||
|
// AI request state
|
||||||
|
isProcessing: boolean;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
sendMessage: (content: string, command?: string) => Promise<void>;
|
||||||
|
clearMessages: () => void;
|
||||||
|
checkConnection: () => Promise<void>;
|
||||||
|
loadModels: () => Promise<void>;
|
||||||
|
setCurrentModel: (model: string) => void;
|
||||||
|
explainCode: (code: string, language?: string) => Promise<string>;
|
||||||
|
refactorCode: (code: string, language?: string) => Promise<string>;
|
||||||
|
fixCode: (code: string, language?: string, error?: string) => Promise<string>;
|
||||||
|
completeCode: (code: string, cursorPosition: number, language?: string) => Promise<string>;
|
||||||
|
setError: (error: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_BASE_URL = 'http://localhost:8000/api';
|
||||||
|
|
||||||
|
export const useAIStore = create<AIState>((set, get) => ({
|
||||||
|
isConnected: false,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
messages: [],
|
||||||
|
currentModel: 'codellama:7b-code',
|
||||||
|
availableModels: [],
|
||||||
|
isProcessing: false,
|
||||||
|
|
||||||
|
sendMessage: async (content, command) => {
|
||||||
|
const messageId = Date.now().toString();
|
||||||
|
const userMessage: AIMessage = {
|
||||||
|
id: messageId,
|
||||||
|
type: 'user',
|
||||||
|
content,
|
||||||
|
timestamp: new Date(),
|
||||||
|
command,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add user message immediately
|
||||||
|
set(state => ({
|
||||||
|
messages: [...state.messages, userMessage],
|
||||||
|
isProcessing: true,
|
||||||
|
error: null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/ai/process`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
command: command || 'explain',
|
||||||
|
code: content,
|
||||||
|
model: get().currentModel,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const assistantMessage: AIMessage = {
|
||||||
|
id: `${messageId}_response`,
|
||||||
|
type: 'assistant',
|
||||||
|
content: data.result,
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
executionTime: data.execution_time,
|
||||||
|
model: data.model_used,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
set(state => ({
|
||||||
|
messages: [...state.messages, assistantMessage],
|
||||||
|
isProcessing: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
throw new Error(data.result || 'Unknown error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage: AIMessage = {
|
||||||
|
id: `${messageId}_error`,
|
||||||
|
type: 'system',
|
||||||
|
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
set(state => ({
|
||||||
|
messages: [...state.messages, errorMessage],
|
||||||
|
isProcessing: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearMessages: () => set({ messages: [] }),
|
||||||
|
|
||||||
|
checkConnection: async () => {
|
||||||
|
try {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/ai/health`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
set({
|
||||||
|
isConnected: response.ok && data.ollama_available,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isConnected: false,
|
||||||
|
isLoading: false,
|
||||||
|
error: 'Failed to connect to AI service',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadModels: async () => {
|
||||||
|
try {
|
||||||
|
set({ isLoading: true });
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/models/list`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load models');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
set({
|
||||||
|
availableModels: data.models,
|
||||||
|
currentModel: data.current_model || get().currentModel,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: 'Failed to load models',
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setCurrentModel: (model) => set({ currentModel: model }),
|
||||||
|
|
||||||
|
explainCode: async (code, language) => {
|
||||||
|
try {
|
||||||
|
set({ isProcessing: true, error: null });
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/ai/explain`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code,
|
||||||
|
language,
|
||||||
|
detail_level: 'medium',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
set({ isProcessing: false });
|
||||||
|
return data.result;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.result);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
isProcessing: false,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refactorCode: async (code, language) => {
|
||||||
|
try {
|
||||||
|
set({ isProcessing: true, error: null });
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/ai/refactor`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code,
|
||||||
|
language,
|
||||||
|
refactor_type: 'general',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
set({ isProcessing: false });
|
||||||
|
return data.result;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.result);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
isProcessing: false,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fixCode: async (code, language, errorMessage) => {
|
||||||
|
try {
|
||||||
|
set({ isProcessing: true, error: null });
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/ai/fix`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code,
|
||||||
|
language,
|
||||||
|
error_message: errorMessage,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
set({ isProcessing: false });
|
||||||
|
return data.result;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.result);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
isProcessing: false,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
completeCode: async (code, cursorPosition, language) => {
|
||||||
|
try {
|
||||||
|
set({ isProcessing: true, error: null });
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/ai/complete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code,
|
||||||
|
cursor_position: cursorPosition,
|
||||||
|
language,
|
||||||
|
max_tokens: 50,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.completions.length > 0) {
|
||||||
|
set({ isProcessing: false });
|
||||||
|
return data.completions[0];
|
||||||
|
} else {
|
||||||
|
throw new Error('No completions available');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
isProcessing: false,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setError: (error) => set({ error }),
|
||||||
|
}));
|
178
frontend/src/stores/editorStore.ts
Normal file
178
frontend/src/stores/editorStore.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
language: string;
|
||||||
|
isDirty: boolean;
|
||||||
|
cursorPosition?: {
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditorState {
|
||||||
|
// File management
|
||||||
|
openFiles: FileInfo[];
|
||||||
|
currentFile: FileInfo | null;
|
||||||
|
|
||||||
|
// Editor state
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
openFile: (file: Omit<FileInfo, 'isDirty'>) => void;
|
||||||
|
closeFile: (path: string) => void;
|
||||||
|
setCurrentFile: (path: string) => void;
|
||||||
|
updateFileContent: (path: string, content: string) => void;
|
||||||
|
saveFile: (path: string) => Promise<void>;
|
||||||
|
saveCurrentFile: () => Promise<void>;
|
||||||
|
setCursorPosition: (path: string, line: number, column: number) => void;
|
||||||
|
setError: (error: string | null) => void;
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEditorStore = create<EditorState>((set, get) => ({
|
||||||
|
openFiles: [],
|
||||||
|
currentFile: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
openFile: (file) => {
|
||||||
|
const { openFiles } = get();
|
||||||
|
|
||||||
|
// Check if file is already open
|
||||||
|
const existingFile = openFiles.find(f => f.path === file.path);
|
||||||
|
if (existingFile) {
|
||||||
|
set({ currentFile: existingFile });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new file
|
||||||
|
const newFile: FileInfo = {
|
||||||
|
...file,
|
||||||
|
isDirty: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
set({
|
||||||
|
openFiles: [...openFiles, newFile],
|
||||||
|
currentFile: newFile,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
closeFile: (path) => {
|
||||||
|
const { openFiles, currentFile } = get();
|
||||||
|
const updatedFiles = openFiles.filter(f => f.path !== path);
|
||||||
|
|
||||||
|
// If closing current file, select another one
|
||||||
|
let newCurrentFile = currentFile;
|
||||||
|
if (currentFile?.path === path) {
|
||||||
|
newCurrentFile = updatedFiles.length > 0 ? updatedFiles[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
openFiles: updatedFiles,
|
||||||
|
currentFile: newCurrentFile,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setCurrentFile: (path) => {
|
||||||
|
const { openFiles } = get();
|
||||||
|
const file = openFiles.find(f => f.path === path);
|
||||||
|
if (file) {
|
||||||
|
set({ currentFile: file });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateFileContent: (path, content) => {
|
||||||
|
const { openFiles, currentFile } = get();
|
||||||
|
|
||||||
|
const updatedFiles = openFiles.map(file => {
|
||||||
|
if (file.path === path) {
|
||||||
|
const updatedFile = {
|
||||||
|
...file,
|
||||||
|
content,
|
||||||
|
isDirty: content !== file.content,
|
||||||
|
};
|
||||||
|
return updatedFile;
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update current file if it matches
|
||||||
|
const updatedCurrentFile = currentFile?.path === path
|
||||||
|
? updatedFiles.find(f => f.path === path)
|
||||||
|
: currentFile;
|
||||||
|
|
||||||
|
set({
|
||||||
|
openFiles: updatedFiles,
|
||||||
|
currentFile: updatedCurrentFile || null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
saveFile: async (path) => {
|
||||||
|
const { openFiles } = get();
|
||||||
|
const file = openFiles.find(f => f.path === path);
|
||||||
|
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
|
||||||
|
// Use Tauri's file writing capability
|
||||||
|
const { writeFile } = await import('@tauri-apps/api/fs');
|
||||||
|
await writeFile(path, file.content);
|
||||||
|
|
||||||
|
// Mark file as saved
|
||||||
|
const updatedFiles = openFiles.map(f =>
|
||||||
|
f.path === path ? { ...f, isDirty: false } : f
|
||||||
|
);
|
||||||
|
|
||||||
|
set({
|
||||||
|
openFiles: updatedFiles,
|
||||||
|
currentFile: updatedFiles.find(f => f.path === path) || null,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: `Failed to save file: ${error}`,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveCurrentFile: async () => {
|
||||||
|
const { currentFile, saveFile } = get();
|
||||||
|
if (currentFile) {
|
||||||
|
await saveFile(currentFile.path);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setCursorPosition: (path, line, column) => {
|
||||||
|
const { openFiles, currentFile } = get();
|
||||||
|
|
||||||
|
const updatedFiles = openFiles.map(file => {
|
||||||
|
if (file.path === path) {
|
||||||
|
return {
|
||||||
|
...file,
|
||||||
|
cursorPosition: { line, column },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedCurrentFile = currentFile?.path === path
|
||||||
|
? updatedFiles.find(f => f.path === path)
|
||||||
|
: currentFile;
|
||||||
|
|
||||||
|
set({
|
||||||
|
openFiles: updatedFiles,
|
||||||
|
currentFile: updatedCurrentFile || null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setError: (error) => set({ error }),
|
||||||
|
setLoading: (loading) => set({ isLoading: loading }),
|
||||||
|
}));
|
61
frontend/tailwind.config.js
Normal file
61
frontend/tailwind.config.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', 'monospace'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f0f9ff',
|
||||||
|
100: '#e0f2fe',
|
||||||
|
200: '#bae6fd',
|
||||||
|
300: '#7dd3fc',
|
||||||
|
400: '#38bdf8',
|
||||||
|
500: '#0ea5e9',
|
||||||
|
600: '#0284c7',
|
||||||
|
700: '#0369a1',
|
||||||
|
800: '#075985',
|
||||||
|
900: '#0c4a6e',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
50: '#f8fafc',
|
||||||
|
100: '#f1f5f9',
|
||||||
|
200: '#e2e8f0',
|
||||||
|
300: '#cbd5e1',
|
||||||
|
400: '#94a3b8',
|
||||||
|
500: '#64748b',
|
||||||
|
600: '#475569',
|
||||||
|
700: '#334155',
|
||||||
|
800: '#1e293b',
|
||||||
|
900: '#0f172a',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
'fade-in': {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
'slide-up': {
|
||||||
|
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
},
|
||||||
|
'pulse-ring': {
|
||||||
|
'0%': { transform: 'scale(0.33)', opacity: '1' },
|
||||||
|
'80%': { transform: 'scale(1)', opacity: '0' },
|
||||||
|
'100%': { transform: 'scale(1)', opacity: '0' },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fade-in 0.2s ease-out',
|
||||||
|
'slide-up': 'slide-up 0.2s ease-out',
|
||||||
|
'pulse-ring': 'pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
31
frontend/tsconfig.json
Normal file
31
frontend/tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Path mapping */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
21
frontend/vite.config.ts
Normal file
21
frontend/vite.config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig(async () => ({
|
||||||
|
plugins: [react()],
|
||||||
|
|
||||||
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
|
//
|
||||||
|
// 1. prevent vite from obscuring rust errors
|
||||||
|
clearScreen: false,
|
||||||
|
// 2. tauri expects a fixed port, fail if that port is not available
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
watch: {
|
||||||
|
// 3. tell vite to ignore watching `src-tauri`
|
||||||
|
ignored: ["**/src-tauri/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
Loading…
x
Reference in New Issue
Block a user