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