Commit
·
6a50e97
1
Parent(s):
d8e039b
add
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +0 -35
- .gitignore +36 -21
- CONTRIBUTING.md +0 -230
- Dockerfile +1 -1
- LICENSE +0 -21
- README.md +137 -10
- app.py +172 -269
- backend/__init__.py +0 -0
- backend/api/__init__.py +0 -0
- backend/api/endpoints/__init__.py +0 -0
- backend/api/routes.py +17 -3
- backend/app.py +0 -243
- backend/config.py +44 -10
- backend/core/__init__.py +0 -0
- backend/main.py +1 -1
- backend/models.py +6 -0
- backend/services/__init__.py +0 -1
- backend/services/chat_service.py +115 -13
- backend/services/model_service.py +64 -30
- backend/utils/__init__.py +0 -0
- frontend/components.json +21 -0
- frontend/index.html +1 -1
- frontend/package-lock.json +109 -5
- frontend/package.json +3 -1
- frontend/src/App.tsx +0 -1
- frontend/src/components/Layout.tsx +18 -0
- frontend/src/components/Sidebar.tsx +11 -19
- frontend/src/components/chat/ChatContainer.tsx +148 -76
- frontend/src/components/chat/ChatInput.tsx +0 -138
- frontend/src/components/chat/ChatMessage.tsx +0 -192
- frontend/src/components/chat/ChatSessions.tsx +120 -161
- frontend/src/components/chat/index.ts +0 -4
- frontend/src/components/ui/alert-dialog.tsx +138 -0
- frontend/src/components/ui/badge.tsx +35 -0
- frontend/src/components/ui/button.tsx +9 -11
- frontend/src/components/ui/card.tsx +13 -19
- frontend/src/components/ui/chat.tsx +123 -0
- frontend/src/components/ui/collapsible.tsx +9 -0
- frontend/src/components/ui/label.tsx +23 -0
- frontend/src/components/ui/select.tsx +156 -0
- frontend/src/components/ui/slider.tsx +25 -0
- frontend/src/components/ui/switch.tsx +26 -0
- frontend/src/components/ui/textarea.tsx +17 -16
- frontend/src/hooks/useChat.ts +49 -14
- frontend/src/index.css +35 -114
- frontend/src/lib/chat-storage.ts +77 -104
- frontend/src/lib/utils.ts +1 -1
- frontend/src/pages/Home.tsx +97 -102
- frontend/src/pages/Models.tsx +339 -0
- frontend/src/pages/Playground.tsx +346 -225
.gitattributes
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
CHANGED
|
@@ -1,28 +1,38 @@
|
|
| 1 |
-
#
|
| 2 |
-
node_modules/
|
| 3 |
-
frontend/node_modules/
|
| 4 |
__pycache__/
|
| 5 |
*.py[cod]
|
| 6 |
*$py.class
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
# Build outputs
|
| 9 |
frontend/dist/
|
| 10 |
frontend/build/
|
| 11 |
|
| 12 |
-
# Environment
|
| 13 |
-
.env
|
| 14 |
-
.env.local
|
| 15 |
-
.env.development.local
|
| 16 |
-
.env.test.local
|
| 17 |
-
.env.production.local
|
| 18 |
-
.venv/
|
| 19 |
-
venv/
|
| 20 |
-
|
| 21 |
# IDE
|
| 22 |
.vscode/
|
| 23 |
.idea/
|
| 24 |
*.swp
|
| 25 |
*.swo
|
|
|
|
| 26 |
|
| 27 |
# OS
|
| 28 |
.DS_Store
|
|
@@ -32,15 +42,20 @@ Thumbs.db
|
|
| 32 |
*.log
|
| 33 |
logs/
|
| 34 |
|
| 35 |
-
#
|
| 36 |
-
.
|
| 37 |
-
.
|
| 38 |
-
.
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
# models/
|
| 42 |
-
# .cache/huggingface/
|
| 43 |
|
| 44 |
# Temporary files
|
| 45 |
*.tmp
|
| 46 |
-
*.temp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
|
|
|
|
|
|
| 2 |
__pycache__/
|
| 3 |
*.py[cod]
|
| 4 |
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
env/
|
| 8 |
+
venv/
|
| 9 |
+
.venv/
|
| 10 |
+
ENV/
|
| 11 |
+
env.bak/
|
| 12 |
+
venv.bak/
|
| 13 |
+
.pytest_cache/
|
| 14 |
+
*.egg-info/
|
| 15 |
+
dist/
|
| 16 |
+
build/
|
| 17 |
+
|
| 18 |
+
# Node.js
|
| 19 |
+
node_modules/
|
| 20 |
+
npm-debug.log*
|
| 21 |
+
yarn-debug.log*
|
| 22 |
+
yarn-error.log*
|
| 23 |
+
.npm
|
| 24 |
+
.eslintcache
|
| 25 |
|
| 26 |
# Build outputs
|
| 27 |
frontend/dist/
|
| 28 |
frontend/build/
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# IDE
|
| 31 |
.vscode/
|
| 32 |
.idea/
|
| 33 |
*.swp
|
| 34 |
*.swo
|
| 35 |
+
*~
|
| 36 |
|
| 37 |
# OS
|
| 38 |
.DS_Store
|
|
|
|
| 42 |
*.log
|
| 43 |
logs/
|
| 44 |
|
| 45 |
+
# Environment variables
|
| 46 |
+
.env
|
| 47 |
+
.env.local
|
| 48 |
+
.env.development.local
|
| 49 |
+
.env.test.local
|
| 50 |
+
.env.production.local
|
|
|
|
|
|
|
| 51 |
|
| 52 |
# Temporary files
|
| 53 |
*.tmp
|
| 54 |
+
*.temp
|
| 55 |
+
test_*.py
|
| 56 |
+
debug_*.py
|
| 57 |
+
quick_*.py
|
| 58 |
+
|
| 59 |
+
# Model cache (optional - uncomment if you don't want to track downloaded models)
|
| 60 |
+
# .cache/
|
| 61 |
+
# models/
|
CONTRIBUTING.md
DELETED
|
@@ -1,230 +0,0 @@
|
|
| 1 |
-
# Contributing to Edge LLM 🤝
|
| 2 |
-
|
| 3 |
-
Thank you for your interest in contributing to Edge LLM! This guide will help you get started with development and contributions.
|
| 4 |
-
|
| 5 |
-
## 🚀 Quick Setup for Contributors
|
| 6 |
-
|
| 7 |
-
### 1. Fork and Clone
|
| 8 |
-
```bash
|
| 9 |
-
# Fork the repository on Hugging Face Spaces
|
| 10 |
-
# Then clone your fork
|
| 11 |
-
git clone https://huggingface.co/spaces/[your-username]/EdgeLLM
|
| 12 |
-
cd EdgeLLM
|
| 13 |
-
```
|
| 14 |
-
|
| 15 |
-
### 2. Install Dependencies
|
| 16 |
-
```bash
|
| 17 |
-
# Install Python dependencies
|
| 18 |
-
pip install -r requirements.txt
|
| 19 |
-
|
| 20 |
-
# Install Node.js dependencies
|
| 21 |
-
cd frontend && npm install && cd ..
|
| 22 |
-
|
| 23 |
-
# Optional: Install root package for scripts
|
| 24 |
-
npm install
|
| 25 |
-
```
|
| 26 |
-
|
| 27 |
-
### 3. Start Development
|
| 28 |
-
```bash
|
| 29 |
-
# Option 1: Use npm scripts
|
| 30 |
-
npm run dev
|
| 31 |
-
|
| 32 |
-
# Option 2: Use Python script
|
| 33 |
-
python scripts/start_platform.py
|
| 34 |
-
|
| 35 |
-
# Option 3: Start manually
|
| 36 |
-
npm run backend # Terminal 1
|
| 37 |
-
npm run frontend # Terminal 2
|
| 38 |
-
```
|
| 39 |
-
|
| 40 |
-
## 📁 Project Structure
|
| 41 |
-
|
| 42 |
-
```
|
| 43 |
-
EdgeLLM/ # Main project directory
|
| 44 |
-
├── 🔧 Backend
|
| 45 |
-
│ ├── backend/
|
| 46 |
-
│ │ ├── api/ # API routes
|
| 47 |
-
│ │ ├── services/ # Business logic
|
| 48 |
-
│ │ ├── models.py # Data models
|
| 49 |
-
│ │ ├── config.py # Configuration
|
| 50 |
-
│ │ └── main.py # FastAPI app
|
| 51 |
-
│ ├── app.py # Entry point
|
| 52 |
-
│ └── requirements.txt # Python dependencies
|
| 53 |
-
├── 💻 Frontend
|
| 54 |
-
│ ├── frontend/
|
| 55 |
-
│ │ ├── src/
|
| 56 |
-
│ │ │ ├── components/ # React components
|
| 57 |
-
│ │ │ ├── pages/ # Page components
|
| 58 |
-
│ │ │ ├── hooks/ # Custom hooks
|
| 59 |
-
│ │ │ └── types/ # TypeScript types
|
| 60 |
-
│ │ ├── package.json # Frontend dependencies
|
| 61 |
-
│ │ └── vite.config.ts # Build configuration
|
| 62 |
-
│ └── static/ # Built assets (auto-generated)
|
| 63 |
-
├── 🔨 Development
|
| 64 |
-
│ ├── scripts/ # Development scripts
|
| 65 |
-
│ ├── package.json # Root scripts
|
| 66 |
-
│ └── .gitignore # Git ignore rules
|
| 67 |
-
└── 📚 Documentation
|
| 68 |
-
├── README.md # Main documentation
|
| 69 |
-
└── CONTRIBUTING.md # This file
|
| 70 |
-
```
|
| 71 |
-
|
| 72 |
-
## 🛠️ Development Workflow
|
| 73 |
-
|
| 74 |
-
### Frontend Development
|
| 75 |
-
```bash
|
| 76 |
-
cd frontend
|
| 77 |
-
npm run dev # Start dev server (hot reload)
|
| 78 |
-
npm run build # Build for production
|
| 79 |
-
npm run preview # Preview production build
|
| 80 |
-
```
|
| 81 |
-
|
| 82 |
-
### Backend Development
|
| 83 |
-
```bash
|
| 84 |
-
# Start with auto-reload
|
| 85 |
-
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
|
| 86 |
-
|
| 87 |
-
# Or use npm script
|
| 88 |
-
npm run backend
|
| 89 |
-
```
|
| 90 |
-
|
| 91 |
-
### Full Stack Development
|
| 92 |
-
```bash
|
| 93 |
-
# Start both frontend and backend
|
| 94 |
-
npm run dev
|
| 95 |
-
|
| 96 |
-
# Build everything
|
| 97 |
-
npm run build
|
| 98 |
-
```
|
| 99 |
-
|
| 100 |
-
## 🧪 Testing Your Changes
|
| 101 |
-
|
| 102 |
-
### 1. Frontend Testing
|
| 103 |
-
```bash
|
| 104 |
-
cd frontend
|
| 105 |
-
npm run test # Run tests
|
| 106 |
-
npm run build # Ensure build works
|
| 107 |
-
```
|
| 108 |
-
|
| 109 |
-
### 2. Backend Testing
|
| 110 |
-
```bash
|
| 111 |
-
# Start backend and test API endpoints
|
| 112 |
-
curl http://localhost:8000/health
|
| 113 |
-
curl http://localhost:8000/models
|
| 114 |
-
```
|
| 115 |
-
|
| 116 |
-
### 3. Integration Testing
|
| 117 |
-
```bash
|
| 118 |
-
# Build and test full application
|
| 119 |
-
npm run build
|
| 120 |
-
python app.py # Test production build
|
| 121 |
-
```
|
| 122 |
-
|
| 123 |
-
## 📝 Code Style Guidelines
|
| 124 |
-
|
| 125 |
-
### Frontend (TypeScript/React)
|
| 126 |
-
- Use TypeScript for type safety
|
| 127 |
-
- Follow React best practices
|
| 128 |
-
- Use ShadCN UI components when possible
|
| 129 |
-
- Keep components small and focused
|
| 130 |
-
- Use custom hooks for reusable logic
|
| 131 |
-
|
| 132 |
-
### Backend (Python/FastAPI)
|
| 133 |
-
- Use type hints everywhere
|
| 134 |
-
- Follow PEP 8 style guide
|
| 135 |
-
- Keep services modular
|
| 136 |
-
- Add docstrings to functions
|
| 137 |
-
- Use Pydantic models for data validation
|
| 138 |
-
|
| 139 |
-
### General
|
| 140 |
-
- Write descriptive commit messages
|
| 141 |
-
- Keep functions small and focused
|
| 142 |
-
- Add comments for complex logic
|
| 143 |
-
- Update documentation for new features
|
| 144 |
-
|
| 145 |
-
## 🔄 Contribution Process
|
| 146 |
-
|
| 147 |
-
### 1. Create a Feature Branch
|
| 148 |
-
```bash
|
| 149 |
-
git checkout -b feature/your-feature-name
|
| 150 |
-
```
|
| 151 |
-
|
| 152 |
-
### 2. Make Your Changes
|
| 153 |
-
- Follow the code style guidelines
|
| 154 |
-
- Add tests if applicable
|
| 155 |
-
- Update documentation
|
| 156 |
-
|
| 157 |
-
### 3. Test Your Changes
|
| 158 |
-
```bash
|
| 159 |
-
npm run build # Ensure everything builds
|
| 160 |
-
npm run dev # Test in development
|
| 161 |
-
```
|
| 162 |
-
|
| 163 |
-
### 4. Commit and Push
|
| 164 |
-
```bash
|
| 165 |
-
git add .
|
| 166 |
-
git commit -m "feat: add your feature description"
|
| 167 |
-
git push origin feature/your-feature-name
|
| 168 |
-
```
|
| 169 |
-
|
| 170 |
-
### 5. Create a Pull Request
|
| 171 |
-
- Describe your changes clearly
|
| 172 |
-
- Include screenshots if UI changes
|
| 173 |
-
- Reference any related issues
|
| 174 |
-
|
| 175 |
-
## 🎯 Areas for Contribution
|
| 176 |
-
|
| 177 |
-
### 🔧 Backend Improvements
|
| 178 |
-
- Add new model support
|
| 179 |
-
- Improve error handling
|
| 180 |
-
- Add model caching optimizations
|
| 181 |
-
- Create API tests
|
| 182 |
-
|
| 183 |
-
### 💻 Frontend Enhancements
|
| 184 |
-
- Add new UI components
|
| 185 |
-
- Improve chat interface
|
| 186 |
-
- Add dark mode support
|
| 187 |
-
- Enhance accessibility
|
| 188 |
-
|
| 189 |
-
### 📚 Documentation
|
| 190 |
-
- Improve README
|
| 191 |
-
- Add code comments
|
| 192 |
-
- Create tutorials
|
| 193 |
-
- Update API documentation
|
| 194 |
-
|
| 195 |
-
### 🚀 DevOps & Deployment
|
| 196 |
-
- Improve Docker configuration
|
| 197 |
-
- Add CI/CD workflows
|
| 198 |
-
- Optimize build process
|
| 199 |
-
- Add monitoring
|
| 200 |
-
|
| 201 |
-
## 🐛 Bug Reports
|
| 202 |
-
|
| 203 |
-
When reporting bugs, please include:
|
| 204 |
-
- Steps to reproduce
|
| 205 |
-
- Expected behavior
|
| 206 |
-
- Actual behavior
|
| 207 |
-
- Browser/OS information
|
| 208 |
-
- Console error messages
|
| 209 |
-
|
| 210 |
-
## 💡 Feature Requests
|
| 211 |
-
|
| 212 |
-
When requesting features, please include:
|
| 213 |
-
- Clear description of the feature
|
| 214 |
-
- Use case and motivation
|
| 215 |
-
- Proposed implementation approach
|
| 216 |
-
- Any relevant examples
|
| 217 |
-
|
| 218 |
-
## 📞 Getting Help
|
| 219 |
-
|
| 220 |
-
- **Issues**: Create a GitHub issue for bugs or questions
|
| 221 |
-
- **Discussions**: Use GitHub discussions for general questions
|
| 222 |
-
- **Documentation**: Check the README and API docs first
|
| 223 |
-
|
| 224 |
-
## 🙏 Thank You!
|
| 225 |
-
|
| 226 |
-
Every contribution, no matter how small, helps make Edge LLM better for everyone. We appreciate your time and effort!
|
| 227 |
-
|
| 228 |
-
---
|
| 229 |
-
|
| 230 |
-
**Happy coding!** 🚀
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -13,4 +13,4 @@ COPY --chown=user ./requirements.txt requirements.txt
|
|
| 13 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 14 |
|
| 15 |
COPY --chown=user . /app
|
| 16 |
-
CMD ["
|
|
|
|
| 13 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 14 |
|
| 15 |
COPY --chown=user . /app
|
| 16 |
+
CMD ["python", "app.py"]
|
LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
MIT License
|
| 2 |
-
|
| 3 |
-
Copyright (c) 2025 ZEKUN WU
|
| 4 |
-
|
| 5 |
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
-
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
-
in the Software without restriction, including without limitation the rights
|
| 8 |
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
-
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
-
furnished to do so, subject to the following conditions:
|
| 11 |
-
|
| 12 |
-
The above copyright notice and this permission notice shall be included in all
|
| 13 |
-
copies or substantial portions of the Software.
|
| 14 |
-
|
| 15 |
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
-
SOFTWARE.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,10 +1,137 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Edge LLM Platform
|
| 2 |
+
|
| 3 |
+
A lightweight, local LLM inference platform with a modern web interface.
|
| 4 |
+
|
| 5 |
+
> **Note**: All development now happens directly in this repository (EdgeLLM_HF). This is both the development environment and the production Hugging Face Space.
|
| 6 |
+
|
| 7 |
+
## ✨ Features
|
| 8 |
+
|
| 9 |
+
### 🤖 **Hybrid Model Support**
|
| 10 |
+
- **Local Models**: Run Qwen models locally for privacy
|
| 11 |
+
- **API Models**: Access powerful cloud models via [AiHubMix API](https://docs.aihubmix.com/en/api/Qwen)
|
| 12 |
+
- **Seamless Switching**: Switch between local and API models effortlessly
|
| 13 |
+
- **Thinking Models**: Support for models with visible reasoning process
|
| 14 |
+
|
| 15 |
+
### 🌐 **Available Models**
|
| 16 |
+
|
| 17 |
+
#### Local Models (Privacy-First)
|
| 18 |
+
- `Qwen/Qwen3-4B-Thinking-2507` - Local model with thinking process (~8GB)
|
| 19 |
+
- `Qwen/Qwen3-4B-Instruct-2507` - Local direct instruction model (~8GB)
|
| 20 |
+
|
| 21 |
+
#### API Models (Cloud-Powered)
|
| 22 |
+
- `Qwen/Qwen3-30B-A3B` - Advanced Qwen3 with dynamic thinking modes
|
| 23 |
+
- `qwen2.5-vl-72b-instruct` - Multimodal model with vision capabilities
|
| 24 |
+
- `Qwen/QVQ-72B-Preview` - Visual reasoning with thinking process
|
| 25 |
+
|
| 26 |
+
### 🎨 **Modern UI/UX**
|
| 27 |
+
- **Responsive Design**: Works on desktop and mobile
|
| 28 |
+
- **Chat Interface**: Beautiful conversation bubbles with session management
|
| 29 |
+
- **Model Management**: Easy switching between local and API models
|
| 30 |
+
- **Parameter Controls**: Temperature, max tokens, and system prompts
|
| 31 |
+
- **Session History**: Persistent conversations with localStorage
|
| 32 |
+
|
| 33 |
+
## 📁 Project Structure
|
| 34 |
+
|
| 35 |
+
```
|
| 36 |
+
EdgeLLM/
|
| 37 |
+
├── frontend/ # 🎨 React frontend with ShadCN UI
|
| 38 |
+
├── backend/ # 🔧 FastAPI backend
|
| 39 |
+
├── static/ # 📱 Built frontend assets
|
| 40 |
+
├── app.py # 🌐 Production entry point
|
| 41 |
+
├── requirements.txt # 🐍 Python dependencies
|
| 42 |
+
└── README.md # 📖 Documentation
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
## 🎯 Quick Start
|
| 46 |
+
|
| 47 |
+
1. **Clone the repository**
|
| 48 |
+
```bash
|
| 49 |
+
git clone https://huggingface.co/spaces/wu981526092/EdgeLLM
|
| 50 |
+
cd EdgeLLM
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
2. **Set up environment variables**
|
| 54 |
+
```bash
|
| 55 |
+
# Create .env file with your API credentials
|
| 56 |
+
echo 'api_key="your-aihubmix-api-key"' > .env
|
| 57 |
+
echo 'base_url="https://aihubmix.com/v1"' >> .env
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
3. **Install dependencies**
|
| 61 |
+
```bash
|
| 62 |
+
pip install -r requirements.txt
|
| 63 |
+
cd frontend && npm install && cd ..
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
4. **Run locally**
|
| 67 |
+
```bash
|
| 68 |
+
python app.py
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
5. **Deploy changes**
|
| 72 |
+
```bash
|
| 73 |
+
# Build frontend if needed
|
| 74 |
+
cd frontend && npm run build && cd ..
|
| 75 |
+
|
| 76 |
+
# Push to Hugging Face
|
| 77 |
+
git add .
|
| 78 |
+
git commit -m "Update: your changes"
|
| 79 |
+
git push
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
## 🌐 Live Demo
|
| 83 |
+
|
| 84 |
+
Visit the live demo at: [https://huggingface.co/spaces/wu981526092/EdgeLLM](https://huggingface.co/spaces/wu981526092/EdgeLLM)
|
| 85 |
+
|
| 86 |
+
## 🔧 Configuration
|
| 87 |
+
|
| 88 |
+
### Environment Variables
|
| 89 |
+
|
| 90 |
+
For local development, create a `.env` file:
|
| 91 |
+
```bash
|
| 92 |
+
api_key="your-aihubmix-api-key"
|
| 93 |
+
base_url="https://aihubmix.com/v1"
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
For production (Hugging Face Spaces), set these as secrets:
|
| 97 |
+
- `api_key`: Your AiHubMix API key
|
| 98 |
+
- `base_url`: API endpoint (https://aihubmix.com/v1)
|
| 99 |
+
|
| 100 |
+
### API Integration
|
| 101 |
+
|
| 102 |
+
This platform integrates with [AiHubMix API](https://docs.aihubmix.com/en/api/Qwen) for cloud-based model access. Features include:
|
| 103 |
+
|
| 104 |
+
- OpenAI-compatible API interface
|
| 105 |
+
- Support for Qwen 3 series models
|
| 106 |
+
- Multimodal capabilities (text + vision)
|
| 107 |
+
- Streaming and non-streaming responses
|
| 108 |
+
|
| 109 |
+
## 🛠️ Development Workflow
|
| 110 |
+
|
| 111 |
+
1. **Frontend development**: Work in `frontend/`
|
| 112 |
+
2. **Backend development**: Work in `backend/`
|
| 113 |
+
3. **Build frontend**: `cd frontend && npm run build`
|
| 114 |
+
4. **Deploy**: Standard git workflow
|
| 115 |
+
```bash
|
| 116 |
+
git add .
|
| 117 |
+
git commit -m "Your changes"
|
| 118 |
+
git push
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
## 🏗️ Architecture
|
| 122 |
+
|
| 123 |
+
### Backend (FastAPI)
|
| 124 |
+
- **Models Service**: Handles both local model loading and API client management
|
| 125 |
+
- **Chat Service**: Routes requests to appropriate generation method (local/API)
|
| 126 |
+
- **API Routes**: RESTful endpoints for model management and text generation
|
| 127 |
+
- **Configuration**: Environment-based settings for API credentials
|
| 128 |
+
|
| 129 |
+
### Frontend (React + TypeScript)
|
| 130 |
+
- **Modern UI**: Built with ShadCN components and Tailwind CSS
|
| 131 |
+
- **Chat Interface**: Real-time conversation with message bubbles
|
| 132 |
+
- **Model Management**: Easy switching between available models
|
| 133 |
+
- **Session Management**: Persistent chat history and settings
|
| 134 |
+
|
| 135 |
+
## 📄 License
|
| 136 |
+
|
| 137 |
+
MIT License - see `LICENSE` for details.
|
app.py
CHANGED
|
@@ -1,292 +1,195 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
import
|
| 8 |
-
|
|
|
|
|
|
|
| 9 |
import os
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
"
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
size_gb: str
|
| 64 |
-
is_loaded: bool
|
| 65 |
-
|
| 66 |
-
class ModelsResponse(BaseModel):
|
| 67 |
-
models: list[ModelInfo]
|
| 68 |
-
current_model: str
|
| 69 |
-
|
| 70 |
-
class ModelLoadRequest(BaseModel):
|
| 71 |
-
model_name: str
|
| 72 |
-
|
| 73 |
-
class ModelUnloadRequest(BaseModel):
|
| 74 |
-
model_name: str
|
| 75 |
-
|
| 76 |
-
def load_model_by_name(model_name: str):
|
| 77 |
-
"""Load a model into the cache"""
|
| 78 |
-
global models_cache
|
| 79 |
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
return True
|
| 82 |
|
| 83 |
-
if
|
| 84 |
-
return False
|
| 85 |
-
|
| 86 |
try:
|
| 87 |
-
|
| 88 |
-
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 89 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 90 |
-
model_name,
|
| 91 |
-
torch_dtype=torch.float16,
|
| 92 |
-
device_map="auto"
|
| 93 |
-
)
|
| 94 |
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
return False
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
global models_cache, current_model_name
|
| 108 |
-
|
| 109 |
-
if model_name in models_cache:
|
| 110 |
-
del models_cache[model_name]
|
| 111 |
-
if current_model_name == model_name:
|
| 112 |
-
current_model_name = None
|
| 113 |
-
print(f"Model {model_name} unloaded")
|
| 114 |
return True
|
| 115 |
-
return False
|
| 116 |
-
|
| 117 |
-
@app.on_event("startup")
|
| 118 |
-
async def startup_event():
|
| 119 |
-
"""Startup event - don't load models by default"""
|
| 120 |
-
print("🚀 Edge LLM API is starting up...")
|
| 121 |
-
print("💡 Models will be loaded on demand")
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
"
|
| 126 |
-
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
@app.get("/models", response_model=ModelsResponse)
|
| 133 |
-
async def get_models():
|
| 134 |
-
"""Get available models and their status"""
|
| 135 |
-
global current_model_name
|
| 136 |
-
|
| 137 |
-
models = []
|
| 138 |
-
for model_name, info in AVAILABLE_MODELS.items():
|
| 139 |
-
models.append(ModelInfo(
|
| 140 |
-
model_name=model_name,
|
| 141 |
-
name=info["name"],
|
| 142 |
-
supports_thinking=info["supports_thinking"],
|
| 143 |
-
description=info["description"],
|
| 144 |
-
size_gb=info["size_gb"],
|
| 145 |
-
is_loaded=model_name in models_cache
|
| 146 |
-
))
|
| 147 |
-
|
| 148 |
-
return ModelsResponse(
|
| 149 |
-
models=models,
|
| 150 |
-
current_model=current_model_name or ""
|
| 151 |
-
)
|
| 152 |
-
|
| 153 |
-
@app.post("/load-model")
|
| 154 |
-
async def load_model(request: ModelLoadRequest):
|
| 155 |
-
"""Load a specific model"""
|
| 156 |
-
global current_model_name
|
| 157 |
-
|
| 158 |
-
if request.model_name not in AVAILABLE_MODELS:
|
| 159 |
-
raise HTTPException(
|
| 160 |
-
status_code=400,
|
| 161 |
-
detail=f"Model {request.model_name} not available"
|
| 162 |
-
)
|
| 163 |
|
| 164 |
-
|
| 165 |
-
if success:
|
| 166 |
-
current_model_name = request.model_name
|
| 167 |
-
return {
|
| 168 |
-
"message": f"Model {request.model_name} loaded successfully",
|
| 169 |
-
"current_model": current_model_name
|
| 170 |
-
}
|
| 171 |
-
else:
|
| 172 |
-
raise HTTPException(
|
| 173 |
-
status_code=500,
|
| 174 |
-
detail=f"Failed to load model {request.model_name}"
|
| 175 |
-
)
|
| 176 |
-
|
| 177 |
-
@app.post("/unload-model")
|
| 178 |
-
async def unload_model(request: ModelUnloadRequest):
|
| 179 |
-
"""Unload a specific model"""
|
| 180 |
-
global current_model_name
|
| 181 |
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
|
|
|
| 188 |
else:
|
| 189 |
-
|
| 190 |
-
status_code=404,
|
| 191 |
-
detail=f"Model {request.model_name} not found in cache"
|
| 192 |
-
)
|
| 193 |
-
|
| 194 |
-
@app.post("/set-current-model")
|
| 195 |
-
async def set_current_model(request: ModelLoadRequest):
|
| 196 |
-
"""Set the current active model"""
|
| 197 |
-
global current_model_name
|
| 198 |
-
|
| 199 |
-
if request.model_name not in models_cache:
|
| 200 |
-
raise HTTPException(
|
| 201 |
-
status_code=400,
|
| 202 |
-
detail=f"Model {request.model_name} is not loaded. Please load it first."
|
| 203 |
-
)
|
| 204 |
-
|
| 205 |
-
current_model_name = request.model_name
|
| 206 |
-
return {
|
| 207 |
-
"message": f"Current model set to {current_model_name}",
|
| 208 |
-
"current_model": current_model_name
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
@app.post("/generate", response_model=PromptResponse)
|
| 212 |
-
async def generate_text(request: PromptRequest):
|
| 213 |
-
"""Generate text using the loaded model"""
|
| 214 |
-
global current_model_name
|
| 215 |
-
|
| 216 |
-
# Use the model specified in request, or fall back to current model
|
| 217 |
-
model_to_use = request.model_name if request.model_name else current_model_name
|
| 218 |
-
|
| 219 |
-
if not model_to_use:
|
| 220 |
-
raise HTTPException(
|
| 221 |
-
status_code=400,
|
| 222 |
-
detail="No model specified. Please load a model first."
|
| 223 |
-
)
|
| 224 |
-
|
| 225 |
-
if model_to_use not in models_cache:
|
| 226 |
-
raise HTTPException(
|
| 227 |
-
status_code=400,
|
| 228 |
-
detail=f"Model {model_to_use} is not loaded. Please load it first."
|
| 229 |
-
)
|
| 230 |
|
| 231 |
try:
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
model_info = AVAILABLE_MODELS[model_to_use]
|
| 235 |
-
|
| 236 |
-
# Build the prompt
|
| 237 |
-
messages = []
|
| 238 |
-
if request.system_prompt:
|
| 239 |
-
messages.append({"role": "system", "content": request.system_prompt})
|
| 240 |
-
messages.append({"role": "user", "content": request.prompt})
|
| 241 |
-
|
| 242 |
-
# Apply chat template
|
| 243 |
-
formatted_prompt = tokenizer.apply_chat_template(
|
| 244 |
-
messages,
|
| 245 |
-
tokenize=False,
|
| 246 |
-
add_generation_prompt=True
|
| 247 |
-
)
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
|
|
|
| 251 |
|
| 252 |
-
#
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
max_new_tokens=request.max_new_tokens,
|
| 257 |
-
temperature=request.temperature,
|
| 258 |
-
do_sample=True,
|
| 259 |
-
pad_token_id=tokenizer.eos_token_id
|
| 260 |
-
)
|
| 261 |
|
| 262 |
-
#
|
| 263 |
-
|
| 264 |
-
|
| 265 |
|
| 266 |
-
#
|
| 267 |
-
|
| 268 |
-
|
|
|
|
| 269 |
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
if "</thinking>" in thinking_part:
|
| 275 |
-
thinking_content = thinking_part.split("</thinking>")[0].strip()
|
| 276 |
-
remaining = thinking_part.split("</thinking>", 1)[1] if "</thinking>" in thinking_part else ""
|
| 277 |
-
final_content = remaining.strip()
|
| 278 |
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
content=final_content,
|
| 282 |
-
model_used=model_to_use,
|
| 283 |
-
supports_thinking=model_info["supports_thinking"]
|
| 284 |
-
)
|
| 285 |
|
| 286 |
except Exception as e:
|
| 287 |
-
print(f"
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
if __name__ == "__main__":
|
| 291 |
-
import uvicorn
|
| 292 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Edge LLM API - Main application entry point with integrated frontend
|
| 3 |
+
|
| 4 |
+
This entry point handles both backend API and frontend serving,
|
| 5 |
+
with automatic port detection and process management.
|
| 6 |
+
"""
|
| 7 |
+
import uvicorn
|
| 8 |
+
import socket
|
| 9 |
+
import subprocess
|
| 10 |
+
import sys
|
| 11 |
import os
|
| 12 |
+
import time
|
| 13 |
+
import signal
|
| 14 |
+
import webbrowser
|
| 15 |
+
from backend.main import app
|
| 16 |
+
|
| 17 |
+
def find_free_port(start_port=8000, max_attempts=50):
|
| 18 |
+
"""Find a free port starting from start_port"""
|
| 19 |
+
for port in range(start_port, start_port + max_attempts):
|
| 20 |
+
try:
|
| 21 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
| 22 |
+
s.bind(('localhost', port))
|
| 23 |
+
return port
|
| 24 |
+
except OSError:
|
| 25 |
+
continue
|
| 26 |
+
raise RuntimeError(f"Could not find a free port in range {start_port}-{start_port + max_attempts}")
|
| 27 |
+
|
| 28 |
+
def kill_processes_on_port(port):
|
| 29 |
+
"""Kill processes using the specified port"""
|
| 30 |
+
try:
|
| 31 |
+
if os.name == 'nt': # Windows
|
| 32 |
+
result = subprocess.run(['netstat', '-ano'], capture_output=True, text=True)
|
| 33 |
+
lines = result.stdout.split('\n')
|
| 34 |
+
for line in lines:
|
| 35 |
+
if f':{port}' in line and 'LISTENING' in line:
|
| 36 |
+
parts = line.split()
|
| 37 |
+
if len(parts) >= 5:
|
| 38 |
+
pid = parts[-1]
|
| 39 |
+
try:
|
| 40 |
+
subprocess.run(['taskkill', '/pid', pid, '/f'],
|
| 41 |
+
capture_output=True, check=True)
|
| 42 |
+
print(f"✅ Killed process {pid} on port {port}")
|
| 43 |
+
except subprocess.CalledProcessError:
|
| 44 |
+
pass
|
| 45 |
+
else: # Unix/Linux/macOS
|
| 46 |
+
try:
|
| 47 |
+
result = subprocess.run(['lsof', '-ti', f':{port}'],
|
| 48 |
+
capture_output=True, text=True)
|
| 49 |
+
pids = result.stdout.strip().split('\n')
|
| 50 |
+
for pid in pids:
|
| 51 |
+
if pid:
|
| 52 |
+
subprocess.run(['kill', '-9', pid], capture_output=True)
|
| 53 |
+
print(f"✅ Killed process {pid} on port {port}")
|
| 54 |
+
except subprocess.CalledProcessError:
|
| 55 |
+
pass
|
| 56 |
+
except Exception as e:
|
| 57 |
+
print(f"⚠️ Warning: Could not kill processes on port {port}: {e}")
|
| 58 |
+
|
| 59 |
+
def update_frontend_config(port):
|
| 60 |
+
"""Update frontend configuration to use the correct backend port"""
|
| 61 |
+
frontend_files = [
|
| 62 |
+
'frontend/src/pages/Models.tsx',
|
| 63 |
+
'frontend/src/pages/Playground.tsx'
|
| 64 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
for file_path in frontend_files:
|
| 67 |
+
if os.path.exists(file_path):
|
| 68 |
+
try:
|
| 69 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 70 |
+
content = f.read()
|
| 71 |
+
|
| 72 |
+
# Update the baseUrl to use the current port (no longer needed with dynamic ports)
|
| 73 |
+
old_pattern = "window.location.hostname === 'localhost' ? `${window.location.protocol}//${window.location.host}` : ''"
|
| 74 |
+
new_pattern = old_pattern # No change needed since it's already dynamic
|
| 75 |
+
|
| 76 |
+
# No need to update frontend files since they use dynamic origins now
|
| 77 |
+
print(f"✅ Frontend uses dynamic origins - no port updates needed")
|
| 78 |
+
except Exception as e:
|
| 79 |
+
print(f"⚠️ Warning: Could not update {file_path}: {e}")
|
| 80 |
+
|
| 81 |
+
def build_frontend():
|
| 82 |
+
"""Build the frontend if needed"""
|
| 83 |
+
if not os.path.exists('frontend/dist') or not os.listdir('frontend/dist'):
|
| 84 |
+
print("🔨 Building frontend...")
|
| 85 |
+
try:
|
| 86 |
+
os.chdir('frontend')
|
| 87 |
+
subprocess.run(['npm', 'install'], check=True, capture_output=True)
|
| 88 |
+
subprocess.run(['npm', 'run', 'build'], check=True, capture_output=True)
|
| 89 |
+
os.chdir('..')
|
| 90 |
+
print("✅ Frontend built successfully")
|
| 91 |
+
except subprocess.CalledProcessError as e:
|
| 92 |
+
print(f"❌ Frontend build failed: {e}")
|
| 93 |
+
os.chdir('..')
|
| 94 |
+
return False
|
| 95 |
+
except FileNotFoundError:
|
| 96 |
+
print("❌ npm not found. Please install Node.js")
|
| 97 |
+
return False
|
| 98 |
+
return True
|
| 99 |
+
|
| 100 |
+
def should_rebuild_frontend():
|
| 101 |
+
"""Check if frontend needs to be rebuilt"""
|
| 102 |
+
# Check if build exists
|
| 103 |
+
if not (os.path.exists('frontend/dist/index.html') and os.path.exists('frontend/dist/assets')):
|
| 104 |
+
print("⚠️ Frontend build not found - will build it")
|
| 105 |
return True
|
| 106 |
|
| 107 |
+
# Check if source is newer than build
|
|
|
|
|
|
|
| 108 |
try:
|
| 109 |
+
dist_time = os.path.getmtime('frontend/dist/index.html')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
# Check key source files
|
| 112 |
+
source_files = [
|
| 113 |
+
'frontend/src',
|
| 114 |
+
'frontend/package.json',
|
| 115 |
+
'frontend/vite.config.ts',
|
| 116 |
+
'frontend/tsconfig.json'
|
| 117 |
+
]
|
| 118 |
+
|
| 119 |
+
for src_path in source_files:
|
| 120 |
+
if os.path.exists(src_path):
|
| 121 |
+
if os.path.isdir(src_path):
|
| 122 |
+
# Check all files in directory
|
| 123 |
+
for root, dirs, files in os.walk(src_path):
|
| 124 |
+
for file in files:
|
| 125 |
+
file_path = os.path.join(root, file)
|
| 126 |
+
if os.path.getmtime(file_path) > dist_time:
|
| 127 |
+
print(f"🔄 Source files changed - will rebuild frontend")
|
| 128 |
+
return True
|
| 129 |
+
else:
|
| 130 |
+
if os.path.getmtime(src_path) > dist_time:
|
| 131 |
+
print(f"🔄 {src_path} changed - will rebuild frontend")
|
| 132 |
+
return True
|
| 133 |
+
|
| 134 |
+
print("✅ Frontend build is up to date")
|
| 135 |
return False
|
| 136 |
+
|
| 137 |
+
except Exception as e:
|
| 138 |
+
print(f"⚠️ Error checking build status: {e} - will rebuild")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
+
def cleanup_handler(signum, frame):
|
| 142 |
+
"""Handle cleanup on exit"""
|
| 143 |
+
print("\n🛑 Shutting down Edge LLM...")
|
| 144 |
+
sys.exit(0)
|
| 145 |
|
| 146 |
+
if __name__ == "__main__":
|
| 147 |
+
# Set up signal handlers
|
| 148 |
+
signal.signal(signal.SIGINT, cleanup_handler)
|
| 149 |
+
signal.signal(signal.SIGTERM, cleanup_handler)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
+
print("🚀 Starting Edge LLM with auto-build frontend...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
+
# Find available port
|
| 154 |
+
import os
|
| 155 |
+
original_port = int(os.getenv("PORT", "0")) # Use env var or auto-assign
|
| 156 |
+
if original_port == 0:
|
| 157 |
+
# Auto-assign a free port starting from 8000
|
| 158 |
+
original_port = find_free_port(8000)
|
| 159 |
+
print(f"🔍 Auto-assigned port: {original_port}")
|
| 160 |
else:
|
| 161 |
+
kill_processes_on_port(original_port)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
try:
|
| 164 |
+
port = find_free_port(original_port)
|
| 165 |
+
print(f"📡 Using port: {port}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
+
if port != original_port:
|
| 168 |
+
print(f"⚠️ Port {original_port} was busy, switched to {port}")
|
| 169 |
+
update_frontend_config(port)
|
| 170 |
|
| 171 |
+
# Auto-build frontend if needed
|
| 172 |
+
if should_rebuild_frontend():
|
| 173 |
+
print("🔨 Building frontend...")
|
| 174 |
+
build_frontend()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
+
# Start the backend server
|
| 177 |
+
print(f"🌐 Starting server on http://localhost:{port}")
|
| 178 |
+
print("🎯 Frontend and Backend integrated - ready to use!")
|
| 179 |
|
| 180 |
+
# Auto-open browser after a short delay
|
| 181 |
+
def open_browser():
|
| 182 |
+
time.sleep(2)
|
| 183 |
+
webbrowser.open(f'http://localhost:{port}')
|
| 184 |
|
| 185 |
+
import threading
|
| 186 |
+
browser_thread = threading.Thread(target=open_browser)
|
| 187 |
+
browser_thread.daemon = True
|
| 188 |
+
browser_thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
+
# Start the server
|
| 191 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
except Exception as e:
|
| 194 |
+
print(f"❌ Error starting server: {e}")
|
| 195 |
+
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
backend/__init__.py
DELETED
|
File without changes
|
backend/api/__init__.py
DELETED
|
File without changes
|
backend/api/endpoints/__init__.py
DELETED
|
File without changes
|
backend/api/routes.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
API routes for Edge LLM
|
| 3 |
"""
|
| 4 |
-
from fastapi import APIRouter, HTTPException
|
| 5 |
from fastapi.responses import FileResponse
|
| 6 |
from ..models import (
|
| 7 |
PromptRequest, PromptResponse, ModelInfo, ModelsResponse,
|
|
@@ -18,7 +18,8 @@ router = APIRouter()
|
|
| 18 |
@router.get("/")
|
| 19 |
async def read_index():
|
| 20 |
"""Serve the React app"""
|
| 21 |
-
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
@router.get("/health")
|
|
@@ -38,7 +39,8 @@ async def get_models():
|
|
| 38 |
supports_thinking=info["supports_thinking"],
|
| 39 |
description=info["description"],
|
| 40 |
size_gb=info["size_gb"],
|
| 41 |
-
is_loaded=model_service.is_model_loaded(model_name)
|
|
|
|
| 42 |
))
|
| 43 |
|
| 44 |
return ModelsResponse(
|
|
@@ -124,6 +126,7 @@ async def generate_text(request: PromptRequest):
|
|
| 124 |
thinking_content, final_content, model_used, supports_thinking = chat_service.generate_response(
|
| 125 |
prompt=request.prompt,
|
| 126 |
model_name=model_to_use,
|
|
|
|
| 127 |
system_prompt=request.system_prompt,
|
| 128 |
temperature=request.temperature,
|
| 129 |
max_new_tokens=request.max_new_tokens
|
|
@@ -139,3 +142,14 @@ async def generate_text(request: PromptRequest):
|
|
| 139 |
except Exception as e:
|
| 140 |
print(f"Generation error: {e}")
|
| 141 |
raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
API routes for Edge LLM
|
| 3 |
"""
|
| 4 |
+
from fastapi import APIRouter, HTTPException, Request
|
| 5 |
from fastapi.responses import FileResponse
|
| 6 |
from ..models import (
|
| 7 |
PromptRequest, PromptResponse, ModelInfo, ModelsResponse,
|
|
|
|
| 18 |
@router.get("/")
|
| 19 |
async def read_index():
|
| 20 |
"""Serve the React app"""
|
| 21 |
+
from ..config import FRONTEND_DIST_DIR
|
| 22 |
+
return FileResponse(f'{FRONTEND_DIST_DIR}/index.html')
|
| 23 |
|
| 24 |
|
| 25 |
@router.get("/health")
|
|
|
|
| 39 |
supports_thinking=info["supports_thinking"],
|
| 40 |
description=info["description"],
|
| 41 |
size_gb=info["size_gb"],
|
| 42 |
+
is_loaded=model_service.is_model_loaded(model_name),
|
| 43 |
+
type=info["type"]
|
| 44 |
))
|
| 45 |
|
| 46 |
return ModelsResponse(
|
|
|
|
| 126 |
thinking_content, final_content, model_used, supports_thinking = chat_service.generate_response(
|
| 127 |
prompt=request.prompt,
|
| 128 |
model_name=model_to_use,
|
| 129 |
+
messages=[msg.dict() for msg in request.messages] if request.messages else [],
|
| 130 |
system_prompt=request.system_prompt,
|
| 131 |
temperature=request.temperature,
|
| 132 |
max_new_tokens=request.max_new_tokens
|
|
|
|
| 142 |
except Exception as e:
|
| 143 |
print(f"Generation error: {e}")
|
| 144 |
raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# Catch-all route for SPA - must be last
|
| 148 |
+
@router.get("/{full_path:path}")
|
| 149 |
+
async def catch_all(request: Request, full_path: str):
|
| 150 |
+
"""
|
| 151 |
+
Catch-all route to serve index.html for any unmatched paths.
|
| 152 |
+
This enables client-side routing for the React SPA.
|
| 153 |
+
"""
|
| 154 |
+
from ..config import FRONTEND_DIST_DIR
|
| 155 |
+
return FileResponse(f'{FRONTEND_DIST_DIR}/index.html')
|
backend/app.py
DELETED
|
@@ -1,243 +0,0 @@
|
|
| 1 |
-
from fastapi import FastAPI, HTTPException
|
| 2 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
-
from pydantic import BaseModel
|
| 4 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 5 |
-
import torch
|
| 6 |
-
from typing import Optional, Dict, Any
|
| 7 |
-
|
| 8 |
-
app = FastAPI(title="Edge LLM API")
|
| 9 |
-
|
| 10 |
-
# Enable CORS for frontend
|
| 11 |
-
app.add_middleware(
|
| 12 |
-
CORSMiddleware,
|
| 13 |
-
allow_origins=["http://localhost:5173", "http://localhost:5174"], # Vite ports
|
| 14 |
-
allow_credentials=True,
|
| 15 |
-
allow_methods=["*"],
|
| 16 |
-
allow_headers=["*"],
|
| 17 |
-
)
|
| 18 |
-
|
| 19 |
-
# Available models
|
| 20 |
-
AVAILABLE_MODELS = {
|
| 21 |
-
"Qwen/Qwen3-4B-Thinking-2507": {
|
| 22 |
-
"name": "Qwen3-4B-Thinking-2507",
|
| 23 |
-
"supports_thinking": True,
|
| 24 |
-
"description": "Shows thinking process",
|
| 25 |
-
"size_gb": "~8GB"
|
| 26 |
-
},
|
| 27 |
-
"Qwen/Qwen3-4B-Instruct-2507": {
|
| 28 |
-
"name": "Qwen3-4B-Instruct-2507",
|
| 29 |
-
"supports_thinking": False,
|
| 30 |
-
"description": "Direct instruction following",
|
| 31 |
-
"size_gb": "~8GB"
|
| 32 |
-
}
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
# Global model cache
|
| 36 |
-
models_cache: Dict[str, Dict[str, Any]] = {}
|
| 37 |
-
current_model_name = None # No model loaded by default
|
| 38 |
-
|
| 39 |
-
class PromptRequest(BaseModel):
|
| 40 |
-
prompt: str
|
| 41 |
-
system_prompt: Optional[str] = None
|
| 42 |
-
model_name: Optional[str] = None
|
| 43 |
-
temperature: Optional[float] = 0.7
|
| 44 |
-
max_new_tokens: Optional[int] = 1024
|
| 45 |
-
|
| 46 |
-
class PromptResponse(BaseModel):
|
| 47 |
-
thinking_content: str
|
| 48 |
-
content: str
|
| 49 |
-
model_used: str
|
| 50 |
-
supports_thinking: bool
|
| 51 |
-
|
| 52 |
-
class ModelInfo(BaseModel):
|
| 53 |
-
model_name: str
|
| 54 |
-
name: str
|
| 55 |
-
supports_thinking: bool
|
| 56 |
-
description: str
|
| 57 |
-
size_gb: str
|
| 58 |
-
is_loaded: bool
|
| 59 |
-
|
| 60 |
-
class ModelLoadRequest(BaseModel):
|
| 61 |
-
model_name: str
|
| 62 |
-
|
| 63 |
-
class ModelUnloadRequest(BaseModel):
|
| 64 |
-
model_name: str
|
| 65 |
-
|
| 66 |
-
async def load_model_by_name(model_name: str):
|
| 67 |
-
"""Load a specific model and cache it (without setting as current)"""
|
| 68 |
-
global models_cache
|
| 69 |
-
|
| 70 |
-
if model_name not in AVAILABLE_MODELS:
|
| 71 |
-
raise HTTPException(status_code=400, detail=f"Model {model_name} not available")
|
| 72 |
-
|
| 73 |
-
if model_name not in models_cache:
|
| 74 |
-
print(f"Loading model: {model_name}...")
|
| 75 |
-
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 76 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 77 |
-
model_name,
|
| 78 |
-
torch_dtype="auto",
|
| 79 |
-
device_map="auto"
|
| 80 |
-
)
|
| 81 |
-
models_cache[model_name] = {
|
| 82 |
-
"model": model,
|
| 83 |
-
"tokenizer": tokenizer
|
| 84 |
-
}
|
| 85 |
-
print(f"Model {model_name} loaded successfully!")
|
| 86 |
-
|
| 87 |
-
return models_cache[model_name]
|
| 88 |
-
|
| 89 |
-
def unload_model_by_name(model_name: str):
|
| 90 |
-
"""Unload a specific model from cache"""
|
| 91 |
-
global models_cache, current_model_name
|
| 92 |
-
|
| 93 |
-
if model_name in models_cache:
|
| 94 |
-
del models_cache[model_name]
|
| 95 |
-
print(f"Model {model_name} unloaded from cache")
|
| 96 |
-
|
| 97 |
-
# If current model was unloaded, reset current model
|
| 98 |
-
if current_model_name == model_name:
|
| 99 |
-
current_model_name = None
|
| 100 |
-
|
| 101 |
-
@app.on_event("startup")
|
| 102 |
-
async def startup_event():
|
| 103 |
-
"""Startup without loading any models"""
|
| 104 |
-
print("Backend started. Models will be loaded on demand.")
|
| 105 |
-
|
| 106 |
-
@app.get("/")
|
| 107 |
-
async def root():
|
| 108 |
-
return {"message": "Edge LLM API is running"}
|
| 109 |
-
|
| 110 |
-
@app.get("/models")
|
| 111 |
-
async def get_available_models():
|
| 112 |
-
"""Get list of available models with their status"""
|
| 113 |
-
models_info = []
|
| 114 |
-
for model_name, info in AVAILABLE_MODELS.items():
|
| 115 |
-
models_info.append(ModelInfo(
|
| 116 |
-
model_name=model_name,
|
| 117 |
-
name=info["name"],
|
| 118 |
-
supports_thinking=info["supports_thinking"],
|
| 119 |
-
description=info["description"],
|
| 120 |
-
size_gb=info["size_gb"],
|
| 121 |
-
is_loaded=model_name in models_cache
|
| 122 |
-
))
|
| 123 |
-
return {
|
| 124 |
-
"models": models_info,
|
| 125 |
-
"current_model": current_model_name
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
@app.post("/load-model")
|
| 129 |
-
async def load_model(request: ModelLoadRequest):
|
| 130 |
-
"""Load a model into memory"""
|
| 131 |
-
try:
|
| 132 |
-
model_data = await load_model_by_name(request.model_name)
|
| 133 |
-
return {
|
| 134 |
-
"message": f"Model loaded: {request.model_name}",
|
| 135 |
-
"model_name": request.model_name,
|
| 136 |
-
"supports_thinking": AVAILABLE_MODELS[request.model_name]["supports_thinking"]
|
| 137 |
-
}
|
| 138 |
-
except Exception as e:
|
| 139 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 140 |
-
|
| 141 |
-
@app.post("/unload-model")
|
| 142 |
-
async def unload_model(request: ModelUnloadRequest):
|
| 143 |
-
"""Unload a model from memory"""
|
| 144 |
-
try:
|
| 145 |
-
unload_model_by_name(request.model_name)
|
| 146 |
-
return {
|
| 147 |
-
"message": f"Model unloaded: {request.model_name}",
|
| 148 |
-
"model_name": request.model_name
|
| 149 |
-
}
|
| 150 |
-
except Exception as e:
|
| 151 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 152 |
-
|
| 153 |
-
@app.post("/set-current-model")
|
| 154 |
-
async def set_current_model(request: ModelLoadRequest):
|
| 155 |
-
"""Set the current active model (must be loaded first)"""
|
| 156 |
-
global current_model_name
|
| 157 |
-
|
| 158 |
-
if request.model_name not in models_cache:
|
| 159 |
-
raise HTTPException(status_code=400, detail=f"Model {request.model_name} is not loaded. Please load it first.")
|
| 160 |
-
|
| 161 |
-
current_model_name = request.model_name
|
| 162 |
-
return {
|
| 163 |
-
"message": f"Current model set to: {request.model_name}",
|
| 164 |
-
"model_name": request.model_name,
|
| 165 |
-
"supports_thinking": AVAILABLE_MODELS[request.model_name]["supports_thinking"]
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
@app.post("/generate", response_model=PromptResponse)
|
| 169 |
-
async def generate_response(request: PromptRequest):
|
| 170 |
-
global current_model_name
|
| 171 |
-
|
| 172 |
-
# Determine which model to use
|
| 173 |
-
target_model = request.model_name if request.model_name else current_model_name
|
| 174 |
-
|
| 175 |
-
if not target_model:
|
| 176 |
-
raise HTTPException(status_code=400, detail="No model specified and no current model set")
|
| 177 |
-
|
| 178 |
-
# Check if the target model is loaded
|
| 179 |
-
if target_model not in models_cache:
|
| 180 |
-
raise HTTPException(
|
| 181 |
-
status_code=400,
|
| 182 |
-
detail=f"Model {target_model} is not loaded. Please load the model first using the load button."
|
| 183 |
-
)
|
| 184 |
-
|
| 185 |
-
# Set as current model if it's different
|
| 186 |
-
if target_model != current_model_name:
|
| 187 |
-
current_model_name = target_model
|
| 188 |
-
|
| 189 |
-
# Get model and tokenizer
|
| 190 |
-
model_data = models_cache[current_model_name]
|
| 191 |
-
model = model_data["model"]
|
| 192 |
-
tokenizer = model_data["tokenizer"]
|
| 193 |
-
supports_thinking = AVAILABLE_MODELS[current_model_name]["supports_thinking"]
|
| 194 |
-
|
| 195 |
-
# Prepare the model input with optional system prompt
|
| 196 |
-
messages = []
|
| 197 |
-
if request.system_prompt:
|
| 198 |
-
messages.append({"role": "system", "content": request.system_prompt})
|
| 199 |
-
messages.append({"role": "user", "content": request.prompt})
|
| 200 |
-
|
| 201 |
-
text = tokenizer.apply_chat_template(
|
| 202 |
-
messages,
|
| 203 |
-
tokenize=False,
|
| 204 |
-
add_generation_prompt=True,
|
| 205 |
-
)
|
| 206 |
-
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
|
| 207 |
-
|
| 208 |
-
# Generate response with parameters
|
| 209 |
-
generated_ids = model.generate(
|
| 210 |
-
**model_inputs,
|
| 211 |
-
max_new_tokens=request.max_new_tokens,
|
| 212 |
-
temperature=request.temperature,
|
| 213 |
-
do_sample=True if request.temperature > 0 else False,
|
| 214 |
-
pad_token_id=tokenizer.eos_token_id
|
| 215 |
-
)
|
| 216 |
-
output_ids = generated_ids[0][len(model_inputs.input_ids[0]):].tolist()
|
| 217 |
-
|
| 218 |
-
thinking_content = ""
|
| 219 |
-
content = ""
|
| 220 |
-
|
| 221 |
-
if supports_thinking:
|
| 222 |
-
# Parse thinking content for thinking models
|
| 223 |
-
try:
|
| 224 |
-
index = len(output_ids) - output_ids[::-1].index(151668)
|
| 225 |
-
except ValueError:
|
| 226 |
-
index = 0
|
| 227 |
-
|
| 228 |
-
thinking_content = tokenizer.decode(output_ids[:index], skip_special_tokens=True).strip("\n")
|
| 229 |
-
content = tokenizer.decode(output_ids[index:], skip_special_tokens=True).strip("\n")
|
| 230 |
-
else:
|
| 231 |
-
# For non-thinking models, everything is content
|
| 232 |
-
content = tokenizer.decode(output_ids, skip_special_tokens=True).strip("\n")
|
| 233 |
-
|
| 234 |
-
return PromptResponse(
|
| 235 |
-
thinking_content=thinking_content,
|
| 236 |
-
content=content,
|
| 237 |
-
model_used=current_model_name,
|
| 238 |
-
supports_thinking=supports_thinking
|
| 239 |
-
)
|
| 240 |
-
|
| 241 |
-
if __name__ == "__main__":
|
| 242 |
-
import uvicorn
|
| 243 |
-
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/config.py
CHANGED
|
@@ -1,30 +1,64 @@
|
|
| 1 |
"""
|
| 2 |
Configuration settings for the Edge LLM API
|
| 3 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
# Available models configuration
|
| 6 |
AVAILABLE_MODELS = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
"Qwen/Qwen3-4B-Thinking-2507": {
|
| 8 |
"name": "Qwen3-4B-Thinking-2507",
|
| 9 |
"supports_thinking": True,
|
| 10 |
-
"description": "Shows thinking process",
|
| 11 |
-
"size_gb": "~8GB"
|
|
|
|
| 12 |
},
|
| 13 |
"Qwen/Qwen3-4B-Instruct-2507": {
|
| 14 |
-
"name": "Qwen3-4B-Instruct-2507",
|
| 15 |
"supports_thinking": False,
|
| 16 |
-
"description": "Direct instruction following",
|
| 17 |
-
"size_gb": "~8GB"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
}
|
| 20 |
|
| 21 |
# CORS settings
|
| 22 |
CORS_ORIGINS = ["*"] # Allow all origins for HF Space
|
| 23 |
|
| 24 |
-
# Static files directory
|
| 25 |
-
|
| 26 |
-
ASSETS_DIR = "
|
| 27 |
|
| 28 |
-
# Server settings
|
| 29 |
HOST = "0.0.0.0"
|
| 30 |
-
|
|
|
|
| 1 |
"""
|
| 2 |
Configuration settings for the Edge LLM API
|
| 3 |
"""
|
| 4 |
+
import os
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
|
| 7 |
+
# Load environment variables
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
# API Configuration
|
| 11 |
+
API_KEY = os.getenv("api_key", "")
|
| 12 |
+
BASE_URL = os.getenv("base_url", "https://aihubmix.com/v1")
|
| 13 |
|
| 14 |
# Available models configuration
|
| 15 |
AVAILABLE_MODELS = {
|
| 16 |
+
# API models (AiHubMix) - Prioritized first
|
| 17 |
+
"Qwen/Qwen3-30B-A3B": {
|
| 18 |
+
"name": "Qwen3-30B-A3B",
|
| 19 |
+
"supports_thinking": True,
|
| 20 |
+
"description": "API: Qwen3 with dynamic thinking modes",
|
| 21 |
+
"size_gb": "API",
|
| 22 |
+
"type": "api"
|
| 23 |
+
},
|
| 24 |
+
# Local models (for local development)
|
| 25 |
"Qwen/Qwen3-4B-Thinking-2507": {
|
| 26 |
"name": "Qwen3-4B-Thinking-2507",
|
| 27 |
"supports_thinking": True,
|
| 28 |
+
"description": "Local: Shows thinking process",
|
| 29 |
+
"size_gb": "~8GB",
|
| 30 |
+
"type": "local"
|
| 31 |
},
|
| 32 |
"Qwen/Qwen3-4B-Instruct-2507": {
|
| 33 |
+
"name": "Qwen3-4B-Instruct-2507",
|
| 34 |
"supports_thinking": False,
|
| 35 |
+
"description": "Local: Direct instruction following",
|
| 36 |
+
"size_gb": "~8GB",
|
| 37 |
+
"type": "local"
|
| 38 |
+
},
|
| 39 |
+
"qwen2.5-vl-72b-instruct": {
|
| 40 |
+
"name": "Qwen2.5-VL-72B-Instruct",
|
| 41 |
+
"supports_thinking": False,
|
| 42 |
+
"description": "API: Multimodal model with vision",
|
| 43 |
+
"size_gb": "API",
|
| 44 |
+
"type": "api"
|
| 45 |
+
},
|
| 46 |
+
"Qwen/QVQ-72B-Preview": {
|
| 47 |
+
"name": "QVQ-72B-Preview",
|
| 48 |
+
"supports_thinking": True,
|
| 49 |
+
"description": "API: Visual reasoning with thinking",
|
| 50 |
+
"size_gb": "API",
|
| 51 |
+
"type": "api"
|
| 52 |
}
|
| 53 |
}
|
| 54 |
|
| 55 |
# CORS settings
|
| 56 |
CORS_ORIGINS = ["*"] # Allow all origins for HF Space
|
| 57 |
|
| 58 |
+
# Static files directory - point directly to frontend build
|
| 59 |
+
FRONTEND_DIST_DIR = "frontend/dist"
|
| 60 |
+
ASSETS_DIR = "frontend/dist/assets"
|
| 61 |
|
| 62 |
+
# Server settings (port will be dynamically determined)
|
| 63 |
HOST = "0.0.0.0"
|
| 64 |
+
DEFAULT_PORT = int(os.getenv("PORT", "0")) # 0 means auto-assign a free port
|
backend/core/__init__.py
DELETED
|
File without changes
|
backend/main.py
CHANGED
|
@@ -5,7 +5,7 @@ from fastapi import FastAPI
|
|
| 5 |
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
from fastapi.staticfiles import StaticFiles
|
| 7 |
from .api.routes import router
|
| 8 |
-
from .config import CORS_ORIGINS, ASSETS_DIR
|
| 9 |
|
| 10 |
|
| 11 |
def create_app() -> FastAPI:
|
|
|
|
| 5 |
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
from fastapi.staticfiles import StaticFiles
|
| 7 |
from .api.routes import router
|
| 8 |
+
from .config import CORS_ORIGINS, ASSETS_DIR, FRONTEND_DIST_DIR
|
| 9 |
|
| 10 |
|
| 11 |
def create_app() -> FastAPI:
|
backend/models.py
CHANGED
|
@@ -5,8 +5,13 @@ from pydantic import BaseModel
|
|
| 5 |
from typing import Optional, List
|
| 6 |
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
class PromptRequest(BaseModel):
|
| 9 |
prompt: str
|
|
|
|
| 10 |
system_prompt: Optional[str] = None
|
| 11 |
model_name: Optional[str] = None
|
| 12 |
temperature: Optional[float] = 0.7
|
|
@@ -27,6 +32,7 @@ class ModelInfo(BaseModel):
|
|
| 27 |
description: str
|
| 28 |
size_gb: str
|
| 29 |
is_loaded: bool
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
class ModelsResponse(BaseModel):
|
|
|
|
| 5 |
from typing import Optional, List
|
| 6 |
|
| 7 |
|
| 8 |
+
class ChatMessage(BaseModel):
|
| 9 |
+
role: str # 'user', 'assistant', 'system'
|
| 10 |
+
content: str
|
| 11 |
+
|
| 12 |
class PromptRequest(BaseModel):
|
| 13 |
prompt: str
|
| 14 |
+
messages: Optional[List[ChatMessage]] = [] # Full conversation history
|
| 15 |
system_prompt: Optional[str] = None
|
| 16 |
model_name: Optional[str] = None
|
| 17 |
temperature: Optional[float] = 0.7
|
|
|
|
| 32 |
description: str
|
| 33 |
size_gb: str
|
| 34 |
is_loaded: bool
|
| 35 |
+
type: str
|
| 36 |
|
| 37 |
|
| 38 |
class ModelsResponse(BaseModel):
|
backend/services/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# Services module
|
|
|
|
|
|
backend/services/chat_service.py
CHANGED
|
@@ -1,26 +1,94 @@
|
|
| 1 |
"""
|
| 2 |
-
Chat generation service
|
| 3 |
"""
|
| 4 |
import torch
|
| 5 |
from typing import Tuple
|
|
|
|
| 6 |
from .model_service import model_service
|
| 7 |
-
from ..config import AVAILABLE_MODELS
|
| 8 |
|
| 9 |
|
| 10 |
class ChatService:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
prompt: str,
|
| 15 |
model_name: str,
|
|
|
|
| 16 |
system_prompt: str = None,
|
| 17 |
temperature: float = 0.7,
|
| 18 |
max_new_tokens: int = 1024
|
| 19 |
) -> Tuple[str, str, str, bool]:
|
| 20 |
-
"""
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
if not model_service.is_model_loaded(model_name):
|
| 25 |
raise ValueError(f"Model {model_name} is not loaded")
|
| 26 |
|
|
@@ -30,15 +98,22 @@ class ChatService:
|
|
| 30 |
tokenizer = model_data["tokenizer"]
|
| 31 |
model_info = AVAILABLE_MODELS[model_name]
|
| 32 |
|
| 33 |
-
# Build the
|
| 34 |
-
|
| 35 |
if system_prompt:
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
# Apply chat template
|
| 40 |
formatted_prompt = tokenizer.apply_chat_template(
|
| 41 |
-
|
| 42 |
tokenize=False,
|
| 43 |
add_generation_prompt=True
|
| 44 |
)
|
|
@@ -79,6 +154,33 @@ class ChatService:
|
|
| 79 |
model_name,
|
| 80 |
model_info["supports_thinking"]
|
| 81 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
# Global chat service instance
|
|
|
|
| 1 |
"""
|
| 2 |
+
Chat generation service supporting both local models and API calls
|
| 3 |
"""
|
| 4 |
import torch
|
| 5 |
from typing import Tuple
|
| 6 |
+
from openai import OpenAI
|
| 7 |
from .model_service import model_service
|
| 8 |
+
from ..config import AVAILABLE_MODELS, API_KEY, BASE_URL
|
| 9 |
|
| 10 |
|
| 11 |
class ChatService:
|
| 12 |
+
def __init__(self):
|
| 13 |
+
# Initialize OpenAI client for API calls
|
| 14 |
+
self.api_client = OpenAI(
|
| 15 |
+
api_key=API_KEY,
|
| 16 |
+
base_url=BASE_URL
|
| 17 |
+
) if API_KEY else None
|
| 18 |
|
| 19 |
+
def _generate_api_response(
|
| 20 |
+
self,
|
| 21 |
prompt: str,
|
| 22 |
model_name: str,
|
| 23 |
+
messages: list = None,
|
| 24 |
system_prompt: str = None,
|
| 25 |
temperature: float = 0.7,
|
| 26 |
max_new_tokens: int = 1024
|
| 27 |
) -> Tuple[str, str, str, bool]:
|
| 28 |
+
"""Generate response using API"""
|
| 29 |
+
if not self.api_client:
|
| 30 |
+
raise ValueError("API client not configured. Please check API_KEY.")
|
| 31 |
+
|
| 32 |
+
# Build messages with conversation history
|
| 33 |
+
api_messages = []
|
| 34 |
+
if system_prompt:
|
| 35 |
+
api_messages.append({"role": "system", "content": system_prompt})
|
| 36 |
+
|
| 37 |
+
# Add conversation history
|
| 38 |
+
if messages:
|
| 39 |
+
for msg in messages:
|
| 40 |
+
api_messages.append({"role": msg.get("role"), "content": msg.get("content")})
|
| 41 |
+
|
| 42 |
+
# Add current prompt as the latest user message
|
| 43 |
+
api_messages.append({"role": "user", "content": prompt})
|
| 44 |
+
|
| 45 |
+
model_info = AVAILABLE_MODELS[model_name]
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
# Make API call
|
| 49 |
+
completion = self.api_client.chat.completions.create(
|
| 50 |
+
model=model_name,
|
| 51 |
+
messages=api_messages,
|
| 52 |
+
temperature=temperature,
|
| 53 |
+
max_tokens=max_new_tokens,
|
| 54 |
+
stream=False
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
generated_text = completion.choices[0].message.content
|
| 58 |
+
|
| 59 |
+
# Parse thinking vs final content for thinking models
|
| 60 |
+
thinking_content = ""
|
| 61 |
+
final_content = generated_text
|
| 62 |
+
|
| 63 |
+
if model_info["supports_thinking"] and "<thinking>" in generated_text:
|
| 64 |
+
parts = generated_text.split("<thinking>")
|
| 65 |
+
if len(parts) > 1:
|
| 66 |
+
thinking_part = parts[1]
|
| 67 |
+
if "</thinking>" in thinking_part:
|
| 68 |
+
thinking_content = thinking_part.split("</thinking>")[0].strip()
|
| 69 |
+
remaining = thinking_part.split("</thinking>", 1)[1] if "</thinking>" in thinking_part else ""
|
| 70 |
+
final_content = remaining.strip()
|
| 71 |
+
|
| 72 |
+
return (
|
| 73 |
+
thinking_content,
|
| 74 |
+
final_content,
|
| 75 |
+
model_name,
|
| 76 |
+
model_info["supports_thinking"]
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
except Exception as e:
|
| 80 |
+
raise ValueError(f"API call failed: {str(e)}")
|
| 81 |
+
|
| 82 |
+
def _generate_local_response(
|
| 83 |
+
self,
|
| 84 |
+
prompt: str,
|
| 85 |
+
model_name: str,
|
| 86 |
+
messages: list = None,
|
| 87 |
+
system_prompt: str = None,
|
| 88 |
+
temperature: float = 0.7,
|
| 89 |
+
max_new_tokens: int = 1024
|
| 90 |
+
) -> Tuple[str, str, str, bool]:
|
| 91 |
+
"""Generate response using local model"""
|
| 92 |
if not model_service.is_model_loaded(model_name):
|
| 93 |
raise ValueError(f"Model {model_name} is not loaded")
|
| 94 |
|
|
|
|
| 98 |
tokenizer = model_data["tokenizer"]
|
| 99 |
model_info = AVAILABLE_MODELS[model_name]
|
| 100 |
|
| 101 |
+
# Build the conversation with full history
|
| 102 |
+
conversation = []
|
| 103 |
if system_prompt:
|
| 104 |
+
conversation.append({"role": "system", "content": system_prompt})
|
| 105 |
+
|
| 106 |
+
# Add conversation history
|
| 107 |
+
if messages:
|
| 108 |
+
for msg in messages:
|
| 109 |
+
conversation.append({"role": msg.get("role"), "content": msg.get("content")})
|
| 110 |
+
|
| 111 |
+
# Add current prompt as the latest user message
|
| 112 |
+
conversation.append({"role": "user", "content": prompt})
|
| 113 |
|
| 114 |
# Apply chat template
|
| 115 |
formatted_prompt = tokenizer.apply_chat_template(
|
| 116 |
+
conversation,
|
| 117 |
tokenize=False,
|
| 118 |
add_generation_prompt=True
|
| 119 |
)
|
|
|
|
| 154 |
model_name,
|
| 155 |
model_info["supports_thinking"]
|
| 156 |
)
|
| 157 |
+
|
| 158 |
+
def generate_response(
|
| 159 |
+
self,
|
| 160 |
+
prompt: str,
|
| 161 |
+
model_name: str,
|
| 162 |
+
messages: list = None,
|
| 163 |
+
system_prompt: str = None,
|
| 164 |
+
temperature: float = 0.7,
|
| 165 |
+
max_new_tokens: int = 1024
|
| 166 |
+
) -> Tuple[str, str, str, bool]:
|
| 167 |
+
"""
|
| 168 |
+
Generate chat response using appropriate method (API or local)
|
| 169 |
+
Returns: (thinking_content, final_content, model_used, supports_thinking)
|
| 170 |
+
"""
|
| 171 |
+
model_info = AVAILABLE_MODELS.get(model_name)
|
| 172 |
+
if not model_info:
|
| 173 |
+
raise ValueError(f"Unknown model: {model_name}")
|
| 174 |
+
|
| 175 |
+
# Route to appropriate generation method
|
| 176 |
+
if model_info["type"] == "api":
|
| 177 |
+
return self._generate_api_response(
|
| 178 |
+
prompt, model_name, messages, system_prompt, temperature, max_new_tokens
|
| 179 |
+
)
|
| 180 |
+
else:
|
| 181 |
+
return self._generate_local_response(
|
| 182 |
+
prompt, model_name, messages, system_prompt, temperature, max_new_tokens
|
| 183 |
+
)
|
| 184 |
|
| 185 |
|
| 186 |
# Global chat service instance
|
backend/services/model_service.py
CHANGED
|
@@ -1,46 +1,60 @@
|
|
| 1 |
"""
|
| 2 |
Model loading and management service
|
| 3 |
"""
|
| 4 |
-
import torch
|
| 5 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 6 |
-
|
|
|
|
| 7 |
from ..config import AVAILABLE_MODELS
|
| 8 |
|
| 9 |
|
| 10 |
class ModelService:
|
| 11 |
def __init__(self):
|
| 12 |
self.models_cache: Dict[str, Dict[str, Any]] = {}
|
| 13 |
-
self.current_model_name:
|
| 14 |
-
|
| 15 |
def load_model(self, model_name: str) -> bool:
|
| 16 |
-
"""Load a model into
|
| 17 |
-
if model_name in self.models_cache:
|
| 18 |
-
return True
|
| 19 |
-
|
| 20 |
if model_name not in AVAILABLE_MODELS:
|
|
|
|
| 21 |
return False
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
try:
|
| 24 |
-
print(f"Loading model: {model_name}")
|
| 25 |
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 26 |
model = AutoModelForCausalLM.from_pretrained(
|
| 27 |
model_name,
|
| 28 |
torch_dtype=torch.float16,
|
| 29 |
device_map="auto"
|
| 30 |
)
|
| 31 |
-
|
| 32 |
-
self.models_cache[model_name] = {
|
| 33 |
-
"model": model,
|
| 34 |
-
"tokenizer": tokenizer
|
| 35 |
-
}
|
| 36 |
print(f"Model {model_name} loaded successfully")
|
| 37 |
return True
|
| 38 |
except Exception as e:
|
| 39 |
print(f"Error loading model {model_name}: {e}")
|
| 40 |
return False
|
| 41 |
-
|
| 42 |
def unload_model(self, model_name: str) -> bool:
|
| 43 |
-
"""Unload a model from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
if model_name in self.models_cache:
|
| 45 |
del self.models_cache[model_name]
|
| 46 |
if self.current_model_name == model_name:
|
|
@@ -48,27 +62,47 @@ class ModelService:
|
|
| 48 |
print(f"Model {model_name} unloaded")
|
| 49 |
return True
|
| 50 |
return False
|
| 51 |
-
|
| 52 |
def set_current_model(self, model_name: str) -> bool:
|
| 53 |
"""Set the current active model"""
|
| 54 |
-
if model_name in
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
self.current_model_name = model_name
|
| 56 |
return True
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
def is_model_loaded(self, model_name: str) -> bool:
|
| 64 |
-
"""Check if a model is loaded"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
return model_name in self.models_cache
|
| 66 |
-
|
| 67 |
def get_loaded_models(self) -> list:
|
| 68 |
-
"""Get list of currently loaded models"""
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
"""Get the current active model"""
|
| 73 |
return self.current_model_name
|
| 74 |
|
|
|
|
| 1 |
"""
|
| 2 |
Model loading and management service
|
| 3 |
"""
|
|
|
|
| 4 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 5 |
+
import torch
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
from ..config import AVAILABLE_MODELS
|
| 8 |
|
| 9 |
|
| 10 |
class ModelService:
|
| 11 |
def __init__(self):
|
| 12 |
self.models_cache: Dict[str, Dict[str, Any]] = {}
|
| 13 |
+
self.current_model_name: str = None
|
| 14 |
+
|
| 15 |
def load_model(self, model_name: str) -> bool:
|
| 16 |
+
"""Load a model into memory"""
|
|
|
|
|
|
|
|
|
|
| 17 |
if model_name not in AVAILABLE_MODELS:
|
| 18 |
+
print(f"Model {model_name} not available.")
|
| 19 |
return False
|
| 20 |
|
| 21 |
+
model_info = AVAILABLE_MODELS[model_name]
|
| 22 |
+
|
| 23 |
+
# API models don't need to be "loaded" - they're always available
|
| 24 |
+
if model_info["type"] == "api":
|
| 25 |
+
print(f"API model {model_name} is always available")
|
| 26 |
+
return True
|
| 27 |
+
|
| 28 |
+
# Handle local models
|
| 29 |
+
if model_name in self.models_cache:
|
| 30 |
+
print(f"Model {model_name} already loaded.")
|
| 31 |
+
return True
|
| 32 |
+
|
| 33 |
try:
|
| 34 |
+
print(f"Loading local model: {model_name}")
|
| 35 |
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 36 |
model = AutoModelForCausalLM.from_pretrained(
|
| 37 |
model_name,
|
| 38 |
torch_dtype=torch.float16,
|
| 39 |
device_map="auto"
|
| 40 |
)
|
| 41 |
+
self.models_cache[model_name] = {"model": model, "tokenizer": tokenizer}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
print(f"Model {model_name} loaded successfully")
|
| 43 |
return True
|
| 44 |
except Exception as e:
|
| 45 |
print(f"Error loading model {model_name}: {e}")
|
| 46 |
return False
|
| 47 |
+
|
| 48 |
def unload_model(self, model_name: str) -> bool:
|
| 49 |
+
"""Unload a model from memory"""
|
| 50 |
+
model_info = AVAILABLE_MODELS.get(model_name, {})
|
| 51 |
+
|
| 52 |
+
# API models can't be "unloaded"
|
| 53 |
+
if model_info.get("type") == "api":
|
| 54 |
+
print(f"API model {model_name} cannot be unloaded")
|
| 55 |
+
return True
|
| 56 |
+
|
| 57 |
+
# Handle local models
|
| 58 |
if model_name in self.models_cache:
|
| 59 |
del self.models_cache[model_name]
|
| 60 |
if self.current_model_name == model_name:
|
|
|
|
| 62 |
print(f"Model {model_name} unloaded")
|
| 63 |
return True
|
| 64 |
return False
|
| 65 |
+
|
| 66 |
def set_current_model(self, model_name: str) -> bool:
|
| 67 |
"""Set the current active model"""
|
| 68 |
+
if model_name not in AVAILABLE_MODELS:
|
| 69 |
+
return False
|
| 70 |
+
|
| 71 |
+
model_info = AVAILABLE_MODELS[model_name]
|
| 72 |
+
|
| 73 |
+
# API models are always "available"
|
| 74 |
+
if model_info["type"] == "api":
|
| 75 |
self.current_model_name = model_name
|
| 76 |
return True
|
| 77 |
+
|
| 78 |
+
# Local models need to be loaded first
|
| 79 |
+
if model_name not in self.models_cache:
|
| 80 |
+
if not self.load_model(model_name):
|
| 81 |
+
return False
|
| 82 |
+
|
| 83 |
+
self.current_model_name = model_name
|
| 84 |
+
return True
|
| 85 |
+
|
| 86 |
def is_model_loaded(self, model_name: str) -> bool:
|
| 87 |
+
"""Check if a model is loaded/available"""
|
| 88 |
+
model_info = AVAILABLE_MODELS.get(model_name, {})
|
| 89 |
+
|
| 90 |
+
# API models are always available
|
| 91 |
+
if model_info.get("type") == "api":
|
| 92 |
+
return True
|
| 93 |
+
|
| 94 |
+
# Local models need to be in cache
|
| 95 |
return model_name in self.models_cache
|
| 96 |
+
|
| 97 |
def get_loaded_models(self) -> list:
|
| 98 |
+
"""Get list of currently loaded/available models"""
|
| 99 |
+
loaded = []
|
| 100 |
+
for model_name, model_info in AVAILABLE_MODELS.items():
|
| 101 |
+
if model_info["type"] == "api" or model_name in self.models_cache:
|
| 102 |
+
loaded.append(model_name)
|
| 103 |
+
return loaded
|
| 104 |
+
|
| 105 |
+
def get_current_model(self) -> str:
|
| 106 |
"""Get the current active model"""
|
| 107 |
return self.current_model_name
|
| 108 |
|
backend/utils/__init__.py
DELETED
|
File without changes
|
frontend/components.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://ui.shadcn.com/schema.json",
|
| 3 |
+
"style": "default",
|
| 4 |
+
"rsc": false,
|
| 5 |
+
"tsx": true,
|
| 6 |
+
"tailwind": {
|
| 7 |
+
"config": "tailwind.config.js",
|
| 8 |
+
"css": "src/index.css",
|
| 9 |
+
"baseColor": "slate",
|
| 10 |
+
"cssVariables": true,
|
| 11 |
+
"prefix": ""
|
| 12 |
+
},
|
| 13 |
+
"aliases": {
|
| 14 |
+
"components": "@/components",
|
| 15 |
+
"utils": "@/lib/utils",
|
| 16 |
+
"ui": "@/components/ui",
|
| 17 |
+
"lib": "@/lib",
|
| 18 |
+
"hooks": "@/hooks"
|
| 19 |
+
},
|
| 20 |
+
"iconLibrary": "lucide"
|
| 21 |
+
}
|
frontend/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<title>Edge LLM
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
<div id="root"></div>
|
|
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Edge LLM</title>
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
<div id="root"></div>
|
frontend/package-lock.json
CHANGED
|
@@ -16,9 +16,10 @@
|
|
| 16 |
"@radix-ui/react-slot": "^1.2.3",
|
| 17 |
"@radix-ui/react-switch": "^1.2.6",
|
| 18 |
"@tailwindcss/typography": "^0.5.16",
|
|
|
|
| 19 |
"class-variance-authority": "^0.7.1",
|
| 20 |
"clsx": "^2.1.1",
|
| 21 |
-
"lucide-react": "^0.
|
| 22 |
"react": "^18.2.0",
|
| 23 |
"react-dom": "^18.2.0",
|
| 24 |
"react-markdown": "^10.1.0",
|
|
@@ -37,6 +38,51 @@
|
|
| 37 |
"vite": "^4.4.5"
|
| 38 |
}
|
| 39 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
"node_modules/@alloc/quick-lru": {
|
| 41 |
"version": "5.2.0",
|
| 42 |
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
|
@@ -844,6 +890,15 @@
|
|
| 844 |
"node": ">= 8"
|
| 845 |
}
|
| 846 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 847 |
"node_modules/@pkgjs/parseargs": {
|
| 848 |
"version": "0.11.0",
|
| 849 |
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
|
@@ -1569,6 +1624,12 @@
|
|
| 1569 |
"dev": true,
|
| 1570 |
"license": "MIT"
|
| 1571 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1572 |
"node_modules/@tailwindcss/typography": {
|
| 1573 |
"version": "0.5.16",
|
| 1574 |
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
|
|
@@ -1736,6 +1797,24 @@
|
|
| 1736 |
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
| 1737 |
}
|
| 1738 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1739 |
"node_modules/ansi-regex": {
|
| 1740 |
"version": "6.2.0",
|
| 1741 |
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
|
|
@@ -2277,6 +2356,15 @@
|
|
| 2277 |
"url": "https://opencollective.com/unified"
|
| 2278 |
}
|
| 2279 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2280 |
"node_modules/extend": {
|
| 2281 |
"version": "3.0.2",
|
| 2282 |
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
|
@@ -2675,6 +2763,12 @@
|
|
| 2675 |
"node": ">=6"
|
| 2676 |
}
|
| 2677 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2678 |
"node_modules/json5": {
|
| 2679 |
"version": "2.2.3",
|
| 2680 |
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
|
@@ -2757,12 +2851,12 @@
|
|
| 2757 |
}
|
| 2758 |
},
|
| 2759 |
"node_modules/lucide-react": {
|
| 2760 |
-
"version": "0.
|
| 2761 |
-
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.
|
| 2762 |
-
"integrity": "sha512-
|
| 2763 |
"license": "ISC",
|
| 2764 |
"peerDependencies": {
|
| 2765 |
-
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
| 2766 |
}
|
| 2767 |
},
|
| 2768 |
"node_modules/mdast-util-from-markdown": {
|
|
@@ -4819,6 +4913,16 @@
|
|
| 4819 |
"node": ">= 14.6"
|
| 4820 |
}
|
| 4821 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4822 |
"node_modules/zwitch": {
|
| 4823 |
"version": "2.0.4",
|
| 4824 |
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
|
|
|
| 16 |
"@radix-ui/react-slot": "^1.2.3",
|
| 17 |
"@radix-ui/react-switch": "^1.2.6",
|
| 18 |
"@tailwindcss/typography": "^0.5.16",
|
| 19 |
+
"ai": "^5.0.27",
|
| 20 |
"class-variance-authority": "^0.7.1",
|
| 21 |
"clsx": "^2.1.1",
|
| 22 |
+
"lucide-react": "^0.542.0",
|
| 23 |
"react": "^18.2.0",
|
| 24 |
"react-dom": "^18.2.0",
|
| 25 |
"react-markdown": "^10.1.0",
|
|
|
|
| 38 |
"vite": "^4.4.5"
|
| 39 |
}
|
| 40 |
},
|
| 41 |
+
"node_modules/@ai-sdk/gateway": {
|
| 42 |
+
"version": "1.0.15",
|
| 43 |
+
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.15.tgz",
|
| 44 |
+
"integrity": "sha512-xySXoQ29+KbGuGfmDnABx+O6vc7Gj7qugmj1kGpn0rW0rQNn6UKUuvscKMzWyv1Uv05GyC1vqHq8ZhEOLfXscQ==",
|
| 45 |
+
"license": "Apache-2.0",
|
| 46 |
+
"dependencies": {
|
| 47 |
+
"@ai-sdk/provider": "2.0.0",
|
| 48 |
+
"@ai-sdk/provider-utils": "3.0.7"
|
| 49 |
+
},
|
| 50 |
+
"engines": {
|
| 51 |
+
"node": ">=18"
|
| 52 |
+
},
|
| 53 |
+
"peerDependencies": {
|
| 54 |
+
"zod": "^3.25.76 || ^4"
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"node_modules/@ai-sdk/provider": {
|
| 58 |
+
"version": "2.0.0",
|
| 59 |
+
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz",
|
| 60 |
+
"integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==",
|
| 61 |
+
"license": "Apache-2.0",
|
| 62 |
+
"dependencies": {
|
| 63 |
+
"json-schema": "^0.4.0"
|
| 64 |
+
},
|
| 65 |
+
"engines": {
|
| 66 |
+
"node": ">=18"
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
"node_modules/@ai-sdk/provider-utils": {
|
| 70 |
+
"version": "3.0.7",
|
| 71 |
+
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.7.tgz",
|
| 72 |
+
"integrity": "sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA==",
|
| 73 |
+
"license": "Apache-2.0",
|
| 74 |
+
"dependencies": {
|
| 75 |
+
"@ai-sdk/provider": "2.0.0",
|
| 76 |
+
"@standard-schema/spec": "^1.0.0",
|
| 77 |
+
"eventsource-parser": "^3.0.5"
|
| 78 |
+
},
|
| 79 |
+
"engines": {
|
| 80 |
+
"node": ">=18"
|
| 81 |
+
},
|
| 82 |
+
"peerDependencies": {
|
| 83 |
+
"zod": "^3.25.76 || ^4"
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
"node_modules/@alloc/quick-lru": {
|
| 87 |
"version": "5.2.0",
|
| 88 |
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
|
|
|
| 890 |
"node": ">= 8"
|
| 891 |
}
|
| 892 |
},
|
| 893 |
+
"node_modules/@opentelemetry/api": {
|
| 894 |
+
"version": "1.9.0",
|
| 895 |
+
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
| 896 |
+
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
| 897 |
+
"license": "Apache-2.0",
|
| 898 |
+
"engines": {
|
| 899 |
+
"node": ">=8.0.0"
|
| 900 |
+
}
|
| 901 |
+
},
|
| 902 |
"node_modules/@pkgjs/parseargs": {
|
| 903 |
"version": "0.11.0",
|
| 904 |
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
|
|
|
| 1624 |
"dev": true,
|
| 1625 |
"license": "MIT"
|
| 1626 |
},
|
| 1627 |
+
"node_modules/@standard-schema/spec": {
|
| 1628 |
+
"version": "1.0.0",
|
| 1629 |
+
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
| 1630 |
+
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
| 1631 |
+
"license": "MIT"
|
| 1632 |
+
},
|
| 1633 |
"node_modules/@tailwindcss/typography": {
|
| 1634 |
"version": "0.5.16",
|
| 1635 |
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
|
|
|
|
| 1797 |
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
| 1798 |
}
|
| 1799 |
},
|
| 1800 |
+
"node_modules/ai": {
|
| 1801 |
+
"version": "5.0.27",
|
| 1802 |
+
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.27.tgz",
|
| 1803 |
+
"integrity": "sha512-V7I9Rvrap5+3ozAjOrETA5Mv9Z1LmQobyY13U88IkFRahFp0xrEwjvYTwjQa4q5lPgLxwKgbIZRLnZSbUQwnUg==",
|
| 1804 |
+
"license": "Apache-2.0",
|
| 1805 |
+
"dependencies": {
|
| 1806 |
+
"@ai-sdk/gateway": "1.0.15",
|
| 1807 |
+
"@ai-sdk/provider": "2.0.0",
|
| 1808 |
+
"@ai-sdk/provider-utils": "3.0.7",
|
| 1809 |
+
"@opentelemetry/api": "1.9.0"
|
| 1810 |
+
},
|
| 1811 |
+
"engines": {
|
| 1812 |
+
"node": ">=18"
|
| 1813 |
+
},
|
| 1814 |
+
"peerDependencies": {
|
| 1815 |
+
"zod": "^3.25.76 || ^4"
|
| 1816 |
+
}
|
| 1817 |
+
},
|
| 1818 |
"node_modules/ansi-regex": {
|
| 1819 |
"version": "6.2.0",
|
| 1820 |
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
|
|
|
|
| 2356 |
"url": "https://opencollective.com/unified"
|
| 2357 |
}
|
| 2358 |
},
|
| 2359 |
+
"node_modules/eventsource-parser": {
|
| 2360 |
+
"version": "3.0.5",
|
| 2361 |
+
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.5.tgz",
|
| 2362 |
+
"integrity": "sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==",
|
| 2363 |
+
"license": "MIT",
|
| 2364 |
+
"engines": {
|
| 2365 |
+
"node": ">=20.0.0"
|
| 2366 |
+
}
|
| 2367 |
+
},
|
| 2368 |
"node_modules/extend": {
|
| 2369 |
"version": "3.0.2",
|
| 2370 |
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
|
|
|
| 2763 |
"node": ">=6"
|
| 2764 |
}
|
| 2765 |
},
|
| 2766 |
+
"node_modules/json-schema": {
|
| 2767 |
+
"version": "0.4.0",
|
| 2768 |
+
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
| 2769 |
+
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
| 2770 |
+
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
| 2771 |
+
},
|
| 2772 |
"node_modules/json5": {
|
| 2773 |
"version": "2.2.3",
|
| 2774 |
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
|
|
|
| 2851 |
}
|
| 2852 |
},
|
| 2853 |
"node_modules/lucide-react": {
|
| 2854 |
+
"version": "0.542.0",
|
| 2855 |
+
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.542.0.tgz",
|
| 2856 |
+
"integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==",
|
| 2857 |
"license": "ISC",
|
| 2858 |
"peerDependencies": {
|
| 2859 |
+
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 2860 |
}
|
| 2861 |
},
|
| 2862 |
"node_modules/mdast-util-from-markdown": {
|
|
|
|
| 4913 |
"node": ">= 14.6"
|
| 4914 |
}
|
| 4915 |
},
|
| 4916 |
+
"node_modules/zod": {
|
| 4917 |
+
"version": "4.1.5",
|
| 4918 |
+
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz",
|
| 4919 |
+
"integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==",
|
| 4920 |
+
"license": "MIT",
|
| 4921 |
+
"peer": true,
|
| 4922 |
+
"funding": {
|
| 4923 |
+
"url": "https://github.com/sponsors/colinhacks"
|
| 4924 |
+
}
|
| 4925 |
+
},
|
| 4926 |
"node_modules/zwitch": {
|
| 4927 |
"version": "2.0.4",
|
| 4928 |
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
frontend/package.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
| 6 |
"scripts": {
|
| 7 |
"dev": "vite",
|
| 8 |
"build": "tsc && vite build",
|
|
|
|
| 9 |
"preview": "vite preview"
|
| 10 |
},
|
| 11 |
"dependencies": {
|
|
@@ -17,9 +18,10 @@
|
|
| 17 |
"@radix-ui/react-slot": "^1.2.3",
|
| 18 |
"@radix-ui/react-switch": "^1.2.6",
|
| 19 |
"@tailwindcss/typography": "^0.5.16",
|
|
|
|
| 20 |
"class-variance-authority": "^0.7.1",
|
| 21 |
"clsx": "^2.1.1",
|
| 22 |
-
"lucide-react": "^0.
|
| 23 |
"react": "^18.2.0",
|
| 24 |
"react-dom": "^18.2.0",
|
| 25 |
"react-markdown": "^10.1.0",
|
|
|
|
| 6 |
"scripts": {
|
| 7 |
"dev": "vite",
|
| 8 |
"build": "tsc && vite build",
|
| 9 |
+
"build:watch": "tsc && vite build --watch",
|
| 10 |
"preview": "vite preview"
|
| 11 |
},
|
| 12 |
"dependencies": {
|
|
|
|
| 18 |
"@radix-ui/react-slot": "^1.2.3",
|
| 19 |
"@radix-ui/react-switch": "^1.2.6",
|
| 20 |
"@tailwindcss/typography": "^0.5.16",
|
| 21 |
+
"ai": "^5.0.27",
|
| 22 |
"class-variance-authority": "^0.7.1",
|
| 23 |
"clsx": "^2.1.1",
|
| 24 |
+
"lucide-react": "^0.542.0",
|
| 25 |
"react": "^18.2.0",
|
| 26 |
"react-dom": "^18.2.0",
|
| 27 |
"react-markdown": "^10.1.0",
|
frontend/src/App.tsx
CHANGED
|
@@ -21,4 +21,3 @@ function App() {
|
|
| 21 |
}
|
| 22 |
|
| 23 |
export default App
|
| 24 |
-
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
export default App
|
|
|
frontend/src/components/Layout.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Outlet } from 'react-router-dom'
|
| 2 |
+
import { Sidebar } from './Sidebar'
|
| 3 |
+
|
| 4 |
+
export function Layout() {
|
| 5 |
+
return (
|
| 6 |
+
<div className="flex h-screen bg-background">
|
| 7 |
+
{/* Sidebar */}
|
| 8 |
+
<div className="w-64 border-r">
|
| 9 |
+
<Sidebar />
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
{/* Main content */}
|
| 13 |
+
<div className="flex-1 overflow-hidden">
|
| 14 |
+
<Outlet />
|
| 15 |
+
</div>
|
| 16 |
+
</div>
|
| 17 |
+
)
|
| 18 |
+
}
|
frontend/src/components/Sidebar.tsx
CHANGED
|
@@ -63,24 +63,24 @@ export function Sidebar() {
|
|
| 63 |
const location = useLocation()
|
| 64 |
|
| 65 |
return (
|
| 66 |
-
<div className="flex flex-col
|
| 67 |
-
{/*
|
| 68 |
-
<div className="
|
| 69 |
<div className="flex items-center gap-2">
|
| 70 |
-
<div className="w-8 h-8 bg-
|
| 71 |
-
<Brain className="
|
| 72 |
</div>
|
| 73 |
<div>
|
| 74 |
-
<h1 className="text-lg
|
| 75 |
<p className="text-xs text-muted-foreground">Local AI Platform</p>
|
| 76 |
</div>
|
| 77 |
</div>
|
| 78 |
</div>
|
| 79 |
|
| 80 |
{/* Navigation */}
|
| 81 |
-
<div className="flex-1
|
| 82 |
-
<div
|
| 83 |
-
<h2 className="mb-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-
|
| 84 |
Get started
|
| 85 |
</h2>
|
| 86 |
<nav className="space-y-1">
|
|
@@ -115,8 +115,8 @@ export function Sidebar() {
|
|
| 115 |
</div>
|
| 116 |
|
| 117 |
<div className="px-3">
|
| 118 |
-
<h2 className="mb-2
|
| 119 |
-
|
| 120 |
</h2>
|
| 121 |
<nav className="space-y-1">
|
| 122 |
{tools.map((item) => {
|
|
@@ -140,14 +140,6 @@ export function Sidebar() {
|
|
| 140 |
</nav>
|
| 141 |
</div>
|
| 142 |
</div>
|
| 143 |
-
|
| 144 |
-
{/* Footer */}
|
| 145 |
-
<div className="border-t p-4">
|
| 146 |
-
<div className="text-xs text-muted-foreground">
|
| 147 |
-
<p className="mb-1">Local Model Platform</p>
|
| 148 |
-
<p>Privacy-focused AI</p>
|
| 149 |
-
</div>
|
| 150 |
-
</div>
|
| 151 |
</div>
|
| 152 |
)
|
| 153 |
}
|
|
|
|
| 63 |
const location = useLocation()
|
| 64 |
|
| 65 |
return (
|
| 66 |
+
<div className="flex h-full flex-col bg-background border-r">
|
| 67 |
+
{/* Header */}
|
| 68 |
+
<div className="p-6 border-b">
|
| 69 |
<div className="flex items-center gap-2">
|
| 70 |
+
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
| 71 |
+
<Brain className="h-5 w-5 text-primary-foreground" />
|
| 72 |
</div>
|
| 73 |
<div>
|
| 74 |
+
<h1 className="font-semibold text-lg">Edge LLM</h1>
|
| 75 |
<p className="text-xs text-muted-foreground">Local AI Platform</p>
|
| 76 |
</div>
|
| 77 |
</div>
|
| 78 |
</div>
|
| 79 |
|
| 80 |
{/* Navigation */}
|
| 81 |
+
<div className="flex-1 px-3 py-4 space-y-8">
|
| 82 |
+
<div>
|
| 83 |
+
<h2 className="mb-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
| 84 |
Get started
|
| 85 |
</h2>
|
| 86 |
<nav className="space-y-1">
|
|
|
|
| 115 |
</div>
|
| 116 |
|
| 117 |
<div className="px-3">
|
| 118 |
+
<h2 className="mb-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
| 119 |
+
Advanced
|
| 120 |
</h2>
|
| 121 |
<nav className="space-y-1">
|
| 122 |
{tools.map((item) => {
|
|
|
|
| 140 |
</nav>
|
| 141 |
</div>
|
| 142 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
</div>
|
| 144 |
)
|
| 145 |
}
|
frontend/src/components/chat/ChatContainer.tsx
CHANGED
|
@@ -1,113 +1,185 @@
|
|
| 1 |
-
import
|
| 2 |
-
import
|
| 3 |
-
import {
|
|
|
|
|
|
|
|
|
|
| 4 |
import { Message } from '@/types/chat'
|
| 5 |
-
import {
|
| 6 |
-
import { cn } from '@/lib/utils'
|
| 7 |
|
| 8 |
interface ChatContainerProps {
|
| 9 |
messages: Message[]
|
| 10 |
input: string
|
| 11 |
-
|
| 12 |
onSubmit: () => void
|
| 13 |
-
onStop
|
| 14 |
-
isLoading
|
| 15 |
disabled?: boolean
|
| 16 |
-
className?: string
|
| 17 |
placeholder?: string
|
| 18 |
}
|
| 19 |
|
| 20 |
export function ChatContainer({
|
| 21 |
messages,
|
| 22 |
input,
|
| 23 |
-
|
| 24 |
onSubmit,
|
| 25 |
onStop,
|
| 26 |
-
isLoading
|
| 27 |
disabled = false,
|
| 28 |
-
|
| 29 |
-
placeholder = "Ask me anything..."
|
| 30 |
}: ChatContainerProps) {
|
| 31 |
-
const
|
| 32 |
-
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
-
}
|
| 40 |
|
| 41 |
-
const
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
return (
|
| 47 |
-
<div className=
|
| 48 |
-
{/* Messages
|
| 49 |
-
<div
|
| 50 |
-
ref={messagesContainerRef}
|
| 51 |
-
className="flex-1 overflow-y-auto p-4 space-y-4"
|
| 52 |
-
>
|
| 53 |
{messages.length === 0 ? (
|
| 54 |
-
<div className="flex
|
| 55 |
-
<div className="
|
| 56 |
-
<
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
</div>
|
| 62 |
</div>
|
| 63 |
</div>
|
| 64 |
) : (
|
| 65 |
-
|
| 66 |
-
{
|
| 67 |
-
<div
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
{/* Loading indicator */}
|
| 76 |
-
{isLoading && (
|
| 77 |
-
<div className="flex gap-3 mb-4">
|
| 78 |
-
{/* Assistant avatar */}
|
| 79 |
-
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-muted border flex items-center justify-center">
|
| 80 |
-
<Loader2 className="h-4 w-4 animate-spin" />
|
| 81 |
-
</div>
|
| 82 |
|
| 83 |
-
{
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
</div>
|
| 91 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
</div>
|
| 93 |
-
|
| 94 |
-
|
| 95 |
)}
|
| 96 |
-
|
| 97 |
-
{/* Scroll anchor */}
|
| 98 |
-
<div ref={messagesEndRef} />
|
| 99 |
</div>
|
| 100 |
|
| 101 |
-
{/* Input
|
| 102 |
-
<
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
</div>
|
| 112 |
)
|
| 113 |
}
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactMarkdown from 'react-markdown'
|
| 3 |
+
import { Button } from '@/components/ui/button'
|
| 4 |
+
import { Textarea } from '@/components/ui/textarea'
|
| 5 |
+
import { Card } from '@/components/ui/card'
|
| 6 |
+
import { Badge } from '@/components/ui/badge'
|
| 7 |
import { Message } from '@/types/chat'
|
| 8 |
+
import { Send, Square, Eye, EyeOff, Brain, User, Bot } from 'lucide-react'
|
|
|
|
| 9 |
|
| 10 |
interface ChatContainerProps {
|
| 11 |
messages: Message[]
|
| 12 |
input: string
|
| 13 |
+
setInput: (value: string) => void
|
| 14 |
onSubmit: () => void
|
| 15 |
+
onStop: () => void
|
| 16 |
+
isLoading: boolean
|
| 17 |
disabled?: boolean
|
|
|
|
| 18 |
placeholder?: string
|
| 19 |
}
|
| 20 |
|
| 21 |
export function ChatContainer({
|
| 22 |
messages,
|
| 23 |
input,
|
| 24 |
+
setInput,
|
| 25 |
onSubmit,
|
| 26 |
onStop,
|
| 27 |
+
isLoading,
|
| 28 |
disabled = false,
|
| 29 |
+
placeholder = "Type your message..."
|
|
|
|
| 30 |
}: ChatContainerProps) {
|
| 31 |
+
const [showThinking, setShowThinking] = React.useState<{ [key: string]: boolean }>({})
|
|
|
|
| 32 |
|
| 33 |
+
const handleKeyPress = (e: React.KeyboardEvent) => {
|
| 34 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 35 |
+
e.preventDefault()
|
| 36 |
+
if (!isLoading && !disabled) {
|
| 37 |
+
onSubmit()
|
| 38 |
+
}
|
| 39 |
}
|
| 40 |
+
}
|
| 41 |
|
| 42 |
+
const toggleThinking = (messageId: string) => {
|
| 43 |
+
setShowThinking(prev => ({
|
| 44 |
+
...prev,
|
| 45 |
+
[messageId]: !prev[messageId]
|
| 46 |
+
}))
|
| 47 |
}
|
| 48 |
|
| 49 |
return (
|
| 50 |
+
<div className="flex flex-col h-full">
|
| 51 |
+
{/* Messages */}
|
| 52 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
|
|
|
|
|
|
|
|
| 53 |
{messages.length === 0 ? (
|
| 54 |
+
<div className="flex items-center justify-center h-full">
|
| 55 |
+
<div className="text-center">
|
| 56 |
+
<Bot className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
| 57 |
+
<h3 className="text-lg font-medium mb-2">Start a conversation</h3>
|
| 58 |
+
<p className="text-muted-foreground">
|
| 59 |
+
Ask me anything and I'll help you out!
|
| 60 |
+
</p>
|
|
|
|
| 61 |
</div>
|
| 62 |
</div>
|
| 63 |
) : (
|
| 64 |
+
messages.map((message) => (
|
| 65 |
+
<div key={message.id} className="space-y-3">
|
| 66 |
+
<div className={`flex items-start gap-3 ${
|
| 67 |
+
message.role === 'user' ? 'justify-end' : 'justify-start'
|
| 68 |
+
}`}>
|
| 69 |
+
{message.role !== 'user' && (
|
| 70 |
+
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
|
| 71 |
+
<Bot className="h-4 w-4 text-primary-foreground" />
|
| 72 |
+
</div>
|
| 73 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
+
<Card className={`max-w-[80%] ${
|
| 76 |
+
message.role === 'user'
|
| 77 |
+
? 'bg-primary text-primary-foreground'
|
| 78 |
+
: 'bg-muted'
|
| 79 |
+
}`}>
|
| 80 |
+
<div className="p-4">
|
| 81 |
+
<div className="flex items-center gap-2 mb-2">
|
| 82 |
+
{message.role === 'user' ? (
|
| 83 |
+
<User className="h-4 w-4" />
|
| 84 |
+
) : (
|
| 85 |
+
<Bot className="h-4 w-4" />
|
| 86 |
+
)}
|
| 87 |
+
<span className="text-sm font-medium capitalize">
|
| 88 |
+
{message.role === 'user' ? 'You' : 'Assistant'}
|
| 89 |
+
</span>
|
| 90 |
+
{message.model_used && (
|
| 91 |
+
<Badge variant="outline" className="text-xs">
|
| 92 |
+
{message.model_used}
|
| 93 |
+
</Badge>
|
| 94 |
+
)}
|
| 95 |
</div>
|
| 96 |
+
|
| 97 |
+
{/* Thinking content */}
|
| 98 |
+
{message.thinking_content && message.supports_thinking && (
|
| 99 |
+
<div className="mb-3">
|
| 100 |
+
<Button
|
| 101 |
+
variant="ghost"
|
| 102 |
+
size="sm"
|
| 103 |
+
onClick={() => toggleThinking(message.id)}
|
| 104 |
+
className="p-1 h-auto"
|
| 105 |
+
>
|
| 106 |
+
<Brain className="h-3 w-3 mr-1" />
|
| 107 |
+
{showThinking[message.id] ? (
|
| 108 |
+
<>
|
| 109 |
+
<EyeOff className="h-3 w-3 mr-1" />
|
| 110 |
+
Hide thinking
|
| 111 |
+
</>
|
| 112 |
+
) : (
|
| 113 |
+
<>
|
| 114 |
+
<Eye className="h-3 w-3 mr-1" />
|
| 115 |
+
Show thinking
|
| 116 |
+
</>
|
| 117 |
+
)}
|
| 118 |
+
</Button>
|
| 119 |
+
|
| 120 |
+
{showThinking[message.id] && (
|
| 121 |
+
<div className="mt-2 p-3 bg-background/50 rounded border-l-4 border-blue-500">
|
| 122 |
+
<div className="text-xs text-muted-foreground mb-1">Thinking process:</div>
|
| 123 |
+
<ReactMarkdown
|
| 124 |
+
components={{
|
| 125 |
+
p: ({ children }) => <p className="text-sm prose prose-sm max-w-none">{children}</p>
|
| 126 |
+
}}
|
| 127 |
+
>
|
| 128 |
+
{message.thinking_content}
|
| 129 |
+
</ReactMarkdown>
|
| 130 |
+
</div>
|
| 131 |
+
)}
|
| 132 |
+
</div>
|
| 133 |
+
)}
|
| 134 |
+
|
| 135 |
+
{/* Main content */}
|
| 136 |
+
<ReactMarkdown
|
| 137 |
+
components={{
|
| 138 |
+
p: ({ children }) => <p className="prose prose-sm max-w-none">{children}</p>
|
| 139 |
+
}}
|
| 140 |
+
>
|
| 141 |
+
{message.content}
|
| 142 |
+
</ReactMarkdown>
|
| 143 |
</div>
|
| 144 |
+
</Card>
|
| 145 |
+
|
| 146 |
+
{message.role === 'user' && (
|
| 147 |
+
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
|
| 148 |
+
<User className="h-4 w-4" />
|
| 149 |
+
</div>
|
| 150 |
+
)}
|
| 151 |
</div>
|
| 152 |
+
</div>
|
| 153 |
+
))
|
| 154 |
)}
|
|
|
|
|
|
|
|
|
|
| 155 |
</div>
|
| 156 |
|
| 157 |
+
{/* Input area */}
|
| 158 |
+
<div className="border-t p-4">
|
| 159 |
+
<div className="flex gap-2">
|
| 160 |
+
<Textarea
|
| 161 |
+
value={input}
|
| 162 |
+
onChange={(e) => setInput(e.target.value)}
|
| 163 |
+
onKeyPress={handleKeyPress}
|
| 164 |
+
placeholder={placeholder}
|
| 165 |
+
disabled={disabled}
|
| 166 |
+
className="min-h-[60px] resize-none"
|
| 167 |
+
/>
|
| 168 |
+
{isLoading ? (
|
| 169 |
+
<Button onClick={onStop} variant="outline" size="icon">
|
| 170 |
+
<Square className="h-4 w-4" />
|
| 171 |
+
</Button>
|
| 172 |
+
) : (
|
| 173 |
+
<Button
|
| 174 |
+
onClick={onSubmit}
|
| 175 |
+
disabled={disabled || !input.trim()}
|
| 176 |
+
size="icon"
|
| 177 |
+
>
|
| 178 |
+
<Send className="h-4 w-4" />
|
| 179 |
+
</Button>
|
| 180 |
+
)}
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
</div>
|
| 184 |
)
|
| 185 |
}
|
frontend/src/components/chat/ChatInput.tsx
DELETED
|
@@ -1,138 +0,0 @@
|
|
| 1 |
-
import { useState, useRef, useEffect } from 'react'
|
| 2 |
-
import { Button } from '@/components/ui/button'
|
| 3 |
-
import { Textarea } from '@/components/ui/textarea'
|
| 4 |
-
import {
|
| 5 |
-
Send,
|
| 6 |
-
Square,
|
| 7 |
-
Paperclip
|
| 8 |
-
} from 'lucide-react'
|
| 9 |
-
import { cn } from '@/lib/utils'
|
| 10 |
-
|
| 11 |
-
interface ChatInputProps {
|
| 12 |
-
value: string
|
| 13 |
-
onChange: (value: string) => void
|
| 14 |
-
onSubmit: () => void
|
| 15 |
-
onStop?: () => void
|
| 16 |
-
isLoading?: boolean
|
| 17 |
-
disabled?: boolean
|
| 18 |
-
placeholder?: string
|
| 19 |
-
maxRows?: number
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
export function ChatInput({
|
| 23 |
-
value,
|
| 24 |
-
onChange,
|
| 25 |
-
onSubmit,
|
| 26 |
-
onStop,
|
| 27 |
-
isLoading = false,
|
| 28 |
-
disabled = false,
|
| 29 |
-
placeholder = "Type your message...",
|
| 30 |
-
maxRows = 6
|
| 31 |
-
}: ChatInputProps) {
|
| 32 |
-
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
| 33 |
-
const [rows, setRows] = useState(1)
|
| 34 |
-
|
| 35 |
-
// Auto-resize textarea
|
| 36 |
-
useEffect(() => {
|
| 37 |
-
if (textareaRef.current) {
|
| 38 |
-
textareaRef.current.style.height = 'auto'
|
| 39 |
-
const scrollHeight = textareaRef.current.scrollHeight
|
| 40 |
-
const rowHeight = 24 // Approximate line height
|
| 41 |
-
const newRows = Math.min(Math.max(Math.ceil(scrollHeight / rowHeight), 1), maxRows)
|
| 42 |
-
setRows(newRows)
|
| 43 |
-
}
|
| 44 |
-
}, [value, maxRows])
|
| 45 |
-
|
| 46 |
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
| 47 |
-
if (e.key === 'Enter' && !e.shiftKey) {
|
| 48 |
-
e.preventDefault()
|
| 49 |
-
if (!isLoading && value.trim() && !disabled) {
|
| 50 |
-
onSubmit()
|
| 51 |
-
}
|
| 52 |
-
}
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
const handleSubmit = (e: React.FormEvent) => {
|
| 56 |
-
e.preventDefault()
|
| 57 |
-
if (!isLoading && value.trim() && !disabled) {
|
| 58 |
-
onSubmit()
|
| 59 |
-
}
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
const canSend = value.trim() && !disabled && !isLoading
|
| 63 |
-
|
| 64 |
-
return (
|
| 65 |
-
<div className="border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
| 66 |
-
<div className="p-4">
|
| 67 |
-
<form onSubmit={handleSubmit} className="space-y-3">
|
| 68 |
-
{/* Main input area */}
|
| 69 |
-
<div className="relative flex items-end gap-2">
|
| 70 |
-
<div className="flex-1 relative">
|
| 71 |
-
<Textarea
|
| 72 |
-
ref={textareaRef}
|
| 73 |
-
value={value}
|
| 74 |
-
onChange={(e) => onChange(e.target.value)}
|
| 75 |
-
onKeyDown={handleKeyDown}
|
| 76 |
-
placeholder={placeholder}
|
| 77 |
-
disabled={disabled}
|
| 78 |
-
rows={rows}
|
| 79 |
-
className={cn(
|
| 80 |
-
"min-h-[40px] max-h-[150px] resize-none pr-12",
|
| 81 |
-
"focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
| 82 |
-
"placeholder:text-muted-foreground"
|
| 83 |
-
)}
|
| 84 |
-
style={{
|
| 85 |
-
lineHeight: '1.5',
|
| 86 |
-
}}
|
| 87 |
-
/>
|
| 88 |
-
|
| 89 |
-
{/* Attachment button (placeholder) */}
|
| 90 |
-
<Button
|
| 91 |
-
type="button"
|
| 92 |
-
variant="ghost"
|
| 93 |
-
size="sm"
|
| 94 |
-
className="absolute right-2 bottom-2 h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
| 95 |
-
disabled={disabled}
|
| 96 |
-
>
|
| 97 |
-
<Paperclip className="h-4 w-4" />
|
| 98 |
-
</Button>
|
| 99 |
-
</div>
|
| 100 |
-
|
| 101 |
-
{/* Send/Stop button */}
|
| 102 |
-
{isLoading ? (
|
| 103 |
-
<Button
|
| 104 |
-
type="button"
|
| 105 |
-
variant="destructive"
|
| 106 |
-
size="sm"
|
| 107 |
-
onClick={onStop}
|
| 108 |
-
className="h-10 w-10 p-0"
|
| 109 |
-
>
|
| 110 |
-
<Square className="h-4 w-4" />
|
| 111 |
-
</Button>
|
| 112 |
-
) : (
|
| 113 |
-
<Button
|
| 114 |
-
type="submit"
|
| 115 |
-
size="sm"
|
| 116 |
-
disabled={!canSend}
|
| 117 |
-
className={cn(
|
| 118 |
-
"h-10 w-10 p-0 transition-colors",
|
| 119 |
-
canSend
|
| 120 |
-
? "bg-blue-500 hover:bg-blue-600 text-white"
|
| 121 |
-
: "bg-muted text-muted-foreground"
|
| 122 |
-
)}
|
| 123 |
-
>
|
| 124 |
-
<Send className="h-4 w-4" />
|
| 125 |
-
</Button>
|
| 126 |
-
)}
|
| 127 |
-
</div>
|
| 128 |
-
|
| 129 |
-
{/* Helper text */}
|
| 130 |
-
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
| 131 |
-
<span>Press Enter to send, Shift+Enter for new line</span>
|
| 132 |
-
<span>{value.length} characters</span>
|
| 133 |
-
</div>
|
| 134 |
-
</form>
|
| 135 |
-
</div>
|
| 136 |
-
</div>
|
| 137 |
-
)
|
| 138 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/chat/ChatMessage.tsx
DELETED
|
@@ -1,192 +0,0 @@
|
|
| 1 |
-
import { useState } from 'react'
|
| 2 |
-
import { Card, CardContent } from '@/components/ui/card'
|
| 3 |
-
import { Button } from '@/components/ui/button'
|
| 4 |
-
import { Badge } from '@/components/ui/badge'
|
| 5 |
-
import { Message } from '@/types/chat'
|
| 6 |
-
import ReactMarkdown from 'react-markdown'
|
| 7 |
-
import {
|
| 8 |
-
Copy,
|
| 9 |
-
User,
|
| 10 |
-
Bot,
|
| 11 |
-
Brain,
|
| 12 |
-
Zap,
|
| 13 |
-
ChevronDown,
|
| 14 |
-
ChevronUp,
|
| 15 |
-
MessageSquare
|
| 16 |
-
} from 'lucide-react'
|
| 17 |
-
import { cn } from '@/lib/utils'
|
| 18 |
-
|
| 19 |
-
interface ChatMessageProps {
|
| 20 |
-
message: Message
|
| 21 |
-
onCopy?: (content: string) => void
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
export function ChatMessage({ message, onCopy }: ChatMessageProps) {
|
| 25 |
-
const [showThinking, setShowThinking] = useState(false)
|
| 26 |
-
const isUser = message.role === 'user'
|
| 27 |
-
const isSystem = message.role === 'system'
|
| 28 |
-
|
| 29 |
-
const handleCopy = () => {
|
| 30 |
-
if (onCopy) {
|
| 31 |
-
onCopy(message.content)
|
| 32 |
-
} else {
|
| 33 |
-
navigator.clipboard.writeText(message.content)
|
| 34 |
-
}
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
const formatTime = (timestamp: number) => {
|
| 38 |
-
return new Date(timestamp).toLocaleTimeString([], {
|
| 39 |
-
hour: '2-digit',
|
| 40 |
-
minute: '2-digit'
|
| 41 |
-
})
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
if (isSystem) {
|
| 45 |
-
return (
|
| 46 |
-
<div className="flex justify-center my-4">
|
| 47 |
-
<Badge variant="outline" className="text-xs">
|
| 48 |
-
<MessageSquare className="h-3 w-3 mr-1" />
|
| 49 |
-
System prompt set
|
| 50 |
-
</Badge>
|
| 51 |
-
</div>
|
| 52 |
-
)
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
return (
|
| 56 |
-
<div className={cn(
|
| 57 |
-
"flex gap-3 mb-4",
|
| 58 |
-
isUser ? "flex-row-reverse" : "flex-row"
|
| 59 |
-
)}>
|
| 60 |
-
{/* Avatar */}
|
| 61 |
-
<div className={cn(
|
| 62 |
-
"flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center",
|
| 63 |
-
isUser
|
| 64 |
-
? "bg-blue-500 text-white"
|
| 65 |
-
: "bg-muted border"
|
| 66 |
-
)}>
|
| 67 |
-
{isUser ? (
|
| 68 |
-
<User className="h-4 w-4" />
|
| 69 |
-
) : message.supports_thinking ? (
|
| 70 |
-
<Brain className="h-4 w-4" />
|
| 71 |
-
) : (
|
| 72 |
-
<Bot className="h-4 w-4" />
|
| 73 |
-
)}
|
| 74 |
-
</div>
|
| 75 |
-
|
| 76 |
-
{/* Message Content */}
|
| 77 |
-
<div className={cn(
|
| 78 |
-
"flex-1 max-w-[80%] space-y-2",
|
| 79 |
-
isUser ? "items-end" : "items-start"
|
| 80 |
-
)}>
|
| 81 |
-
{/* Message Bubble */}
|
| 82 |
-
<Card className={cn(
|
| 83 |
-
"relative",
|
| 84 |
-
isUser
|
| 85 |
-
? "bg-blue-500 text-white border-blue-500"
|
| 86 |
-
: "bg-muted/50"
|
| 87 |
-
)}>
|
| 88 |
-
<CardContent className="p-3">
|
| 89 |
-
{/* Model info for assistant messages */}
|
| 90 |
-
{!isUser && message.model_used && (
|
| 91 |
-
<div className="flex items-center gap-2 mb-2 text-xs text-muted-foreground">
|
| 92 |
-
{message.supports_thinking ? <Brain className="h-3 w-3" /> : <Zap className="h-3 w-3" />}
|
| 93 |
-
<span>{message.model_used}</span>
|
| 94 |
-
<Badge variant="secondary" className="text-xs">
|
| 95 |
-
{message.supports_thinking ? "Thinking" : "Instruct"}
|
| 96 |
-
</Badge>
|
| 97 |
-
</div>
|
| 98 |
-
)}
|
| 99 |
-
|
| 100 |
-
{/* Thinking Content Toggle */}
|
| 101 |
-
{!isUser && message.thinking_content && (
|
| 102 |
-
<div className="mb-3">
|
| 103 |
-
<Button
|
| 104 |
-
variant="ghost"
|
| 105 |
-
size="sm"
|
| 106 |
-
onClick={() => setShowThinking(!showThinking)}
|
| 107 |
-
className="h-auto p-2 text-xs font-normal"
|
| 108 |
-
>
|
| 109 |
-
<Brain className="h-3 w-3 mr-2" />
|
| 110 |
-
Thinking Process
|
| 111 |
-
{showThinking ? (
|
| 112 |
-
<ChevronUp className="h-3 w-3 ml-2" />
|
| 113 |
-
) : (
|
| 114 |
-
<ChevronDown className="h-3 w-3 ml-2" />
|
| 115 |
-
)}
|
| 116 |
-
</Button>
|
| 117 |
-
|
| 118 |
-
{showThinking && (
|
| 119 |
-
<Card className="mt-2 bg-background/50">
|
| 120 |
-
<CardContent className="p-3">
|
| 121 |
-
<pre className="text-xs font-mono whitespace-pre-wrap text-muted-foreground">
|
| 122 |
-
{message.thinking_content}
|
| 123 |
-
</pre>
|
| 124 |
-
</CardContent>
|
| 125 |
-
</Card>
|
| 126 |
-
)}
|
| 127 |
-
</div>
|
| 128 |
-
)}
|
| 129 |
-
|
| 130 |
-
{/* Main Message Content */}
|
| 131 |
-
<div className="text-sm">
|
| 132 |
-
{isUser ? (
|
| 133 |
-
<div className="whitespace-pre-wrap">{message.content}</div>
|
| 134 |
-
) : (
|
| 135 |
-
<div className="prose prose-sm max-w-none dark:prose-invert
|
| 136 |
-
prose-headings:font-semibold prose-headings:text-foreground
|
| 137 |
-
prose-p:text-foreground prose-p:leading-relaxed
|
| 138 |
-
prose-strong:text-foreground prose-strong:font-semibold
|
| 139 |
-
prose-em:text-muted-foreground
|
| 140 |
-
prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm
|
| 141 |
-
prose-pre:bg-muted prose-pre:border prose-pre:rounded-md
|
| 142 |
-
prose-ul:text-foreground prose-ol:text-foreground
|
| 143 |
-
prose-li:text-foreground
|
| 144 |
-
prose-blockquote:border-l-muted-foreground prose-blockquote:text-muted-foreground">
|
| 145 |
-
<ReactMarkdown
|
| 146 |
-
components={{
|
| 147 |
-
// Custom component for better styling
|
| 148 |
-
h1: ({children}) => <h1 className="text-lg font-bold mb-2 text-foreground">{children}</h1>,
|
| 149 |
-
h2: ({children}) => <h2 className="text-base font-semibold mb-2 text-foreground">{children}</h2>,
|
| 150 |
-
h3: ({children}) => <h3 className="text-sm font-semibold mb-1 text-foreground">{children}</h3>,
|
| 151 |
-
p: ({children}) => <p className="mb-2 last:mb-0 text-foreground leading-relaxed">{children}</p>,
|
| 152 |
-
strong: ({children}) => <strong className="font-semibold text-foreground">{children}</strong>,
|
| 153 |
-
em: ({children}) => <em className="italic text-muted-foreground">{children}</em>,
|
| 154 |
-
code: ({children}) => <code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{children}</code>,
|
| 155 |
-
ul: ({children}) => <ul className="mb-2 space-y-1 text-foreground">{children}</ul>,
|
| 156 |
-
ol: ({children}) => <ol className="mb-2 space-y-1 text-foreground">{children}</ol>,
|
| 157 |
-
li: ({children}) => <li className="text-foreground">{children}</li>,
|
| 158 |
-
}}
|
| 159 |
-
>
|
| 160 |
-
{message.content}
|
| 161 |
-
</ReactMarkdown>
|
| 162 |
-
</div>
|
| 163 |
-
)}
|
| 164 |
-
</div>
|
| 165 |
-
</CardContent>
|
| 166 |
-
|
| 167 |
-
{/* Message Actions */}
|
| 168 |
-
{!isUser && (
|
| 169 |
-
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 170 |
-
<Button
|
| 171 |
-
variant="ghost"
|
| 172 |
-
size="sm"
|
| 173 |
-
onClick={handleCopy}
|
| 174 |
-
className="h-6 w-6 p-0"
|
| 175 |
-
>
|
| 176 |
-
<Copy className="h-3 w-3" />
|
| 177 |
-
</Button>
|
| 178 |
-
</div>
|
| 179 |
-
)}
|
| 180 |
-
</Card>
|
| 181 |
-
|
| 182 |
-
{/* Timestamp */}
|
| 183 |
-
<div className={cn(
|
| 184 |
-
"text-xs text-muted-foreground px-1",
|
| 185 |
-
isUser ? "text-right" : "text-left"
|
| 186 |
-
)}>
|
| 187 |
-
{formatTime(message.timestamp)}
|
| 188 |
-
</div>
|
| 189 |
-
</div>
|
| 190 |
-
</div>
|
| 191 |
-
)
|
| 192 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/chat/ChatSessions.tsx
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
-
import
|
| 2 |
import { Button } from '@/components/ui/button'
|
| 3 |
-
import { Card
|
| 4 |
import { Badge } from '@/components/ui/badge'
|
|
|
|
| 5 |
import {
|
| 6 |
Plus,
|
| 7 |
MessageSquare,
|
| 8 |
Trash2,
|
| 9 |
-
|
| 10 |
-
|
|
|
|
| 11 |
} from 'lucide-react'
|
| 12 |
-
import { ChatSession } from '@/types/chat'
|
| 13 |
-
import { cn } from '@/lib/utils'
|
| 14 |
|
| 15 |
interface ChatSessionsProps {
|
| 16 |
sessions: ChatSession[]
|
|
@@ -18,7 +18,7 @@ interface ChatSessionsProps {
|
|
| 18 |
onSelectSession: (sessionId: string) => void
|
| 19 |
onNewSession: () => void
|
| 20 |
onDeleteSession: (sessionId: string) => void
|
| 21 |
-
onRenameSession
|
| 22 |
}
|
| 23 |
|
| 24 |
export function ChatSessions({
|
|
@@ -29,182 +29,141 @@ export function ChatSessions({
|
|
| 29 |
onDeleteSession,
|
| 30 |
onRenameSession
|
| 31 |
}: ChatSessionsProps) {
|
| 32 |
-
const [
|
| 33 |
-
const [editTitle, setEditTitle] = useState('')
|
| 34 |
|
| 35 |
-
const
|
| 36 |
-
|
| 37 |
setEditTitle(session.title)
|
| 38 |
}
|
| 39 |
|
| 40 |
-
const
|
| 41 |
-
if (
|
| 42 |
-
onRenameSession(
|
| 43 |
}
|
| 44 |
-
|
| 45 |
setEditTitle('')
|
| 46 |
}
|
| 47 |
|
| 48 |
-
const
|
| 49 |
-
|
| 50 |
setEditTitle('')
|
| 51 |
}
|
| 52 |
|
| 53 |
-
const formatDate = (timestamp: number) => {
|
| 54 |
-
const date = new Date(timestamp)
|
| 55 |
-
const now = new Date()
|
| 56 |
-
const diffTime = now.getTime() - date.getTime()
|
| 57 |
-
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
| 58 |
-
|
| 59 |
-
if (diffDays === 0) {
|
| 60 |
-
return 'Today'
|
| 61 |
-
} else if (diffDays === 1) {
|
| 62 |
-
return 'Yesterday'
|
| 63 |
-
} else if (diffDays < 7) {
|
| 64 |
-
return `${diffDays} days ago`
|
| 65 |
-
} else {
|
| 66 |
-
return date.toLocaleDateString()
|
| 67 |
-
}
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
const groupedSessions = sessions.reduce((groups, session) => {
|
| 71 |
-
const date = formatDate(session.updated_at)
|
| 72 |
-
if (!groups[date]) {
|
| 73 |
-
groups[date] = []
|
| 74 |
-
}
|
| 75 |
-
groups[date].push(session)
|
| 76 |
-
return groups
|
| 77 |
-
}, {} as Record<string, ChatSession[]>)
|
| 78 |
-
|
| 79 |
return (
|
| 80 |
<div className="h-full flex flex-col">
|
| 81 |
{/* Header */}
|
| 82 |
<div className="p-4 border-b">
|
| 83 |
-
<div className="flex items-center justify-between mb-
|
| 84 |
-
<h2 className="font-semibold
|
| 85 |
-
<Button
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
className="h-8 w-8 p-0"
|
| 89 |
-
>
|
| 90 |
-
<Plus className="h-4 w-4" />
|
| 91 |
</Button>
|
| 92 |
</div>
|
| 93 |
-
|
| 94 |
-
<Button
|
| 95 |
-
onClick={onNewSession}
|
| 96 |
-
variant="outline"
|
| 97 |
-
className="w-full justify-start"
|
| 98 |
-
size="sm"
|
| 99 |
-
>
|
| 100 |
-
<Plus className="h-4 w-4 mr-2" />
|
| 101 |
-
New Chat
|
| 102 |
-
</Button>
|
| 103 |
</div>
|
| 104 |
|
| 105 |
-
{/* Sessions
|
| 106 |
-
<div className="flex-1 overflow-y-auto p-
|
| 107 |
-
{
|
| 108 |
-
<div className="
|
| 109 |
-
<MessageSquare className="h-8 w-8 text-muted-foreground mb-2" />
|
| 110 |
-
<p className="text-sm text-muted-foreground">No
|
| 111 |
-
<
|
|
|
|
|
|
|
| 112 |
</div>
|
| 113 |
) : (
|
| 114 |
-
|
| 115 |
-
<
|
| 116 |
-
{
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
)}
|
| 203 |
-
</CardContent>
|
| 204 |
-
</Card>
|
| 205 |
-
))}
|
| 206 |
</div>
|
| 207 |
-
</
|
| 208 |
))
|
| 209 |
)}
|
| 210 |
</div>
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
import { Button } from '@/components/ui/button'
|
| 3 |
+
import { Card } from '@/components/ui/card'
|
| 4 |
import { Badge } from '@/components/ui/badge'
|
| 5 |
+
import { ChatSession } from '@/types/chat'
|
| 6 |
import {
|
| 7 |
Plus,
|
| 8 |
MessageSquare,
|
| 9 |
Trash2,
|
| 10 |
+
Edit2,
|
| 11 |
+
Check,
|
| 12 |
+
X
|
| 13 |
} from 'lucide-react'
|
|
|
|
|
|
|
| 14 |
|
| 15 |
interface ChatSessionsProps {
|
| 16 |
sessions: ChatSession[]
|
|
|
|
| 18 |
onSelectSession: (sessionId: string) => void
|
| 19 |
onNewSession: () => void
|
| 20 |
onDeleteSession: (sessionId: string) => void
|
| 21 |
+
onRenameSession: (sessionId: string, newTitle: string) => void
|
| 22 |
}
|
| 23 |
|
| 24 |
export function ChatSessions({
|
|
|
|
| 29 |
onDeleteSession,
|
| 30 |
onRenameSession
|
| 31 |
}: ChatSessionsProps) {
|
| 32 |
+
const [editingId, setEditingId] = React.useState<string | null>(null)
|
| 33 |
+
const [editTitle, setEditTitle] = React.useState('')
|
| 34 |
|
| 35 |
+
const startEditing = (session: ChatSession) => {
|
| 36 |
+
setEditingId(session.id)
|
| 37 |
setEditTitle(session.title)
|
| 38 |
}
|
| 39 |
|
| 40 |
+
const finishEditing = () => {
|
| 41 |
+
if (editingId && editTitle.trim()) {
|
| 42 |
+
onRenameSession(editingId, editTitle.trim())
|
| 43 |
}
|
| 44 |
+
setEditingId(null)
|
| 45 |
setEditTitle('')
|
| 46 |
}
|
| 47 |
|
| 48 |
+
const cancelEditing = () => {
|
| 49 |
+
setEditingId(null)
|
| 50 |
setEditTitle('')
|
| 51 |
}
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
return (
|
| 54 |
<div className="h-full flex flex-col">
|
| 55 |
{/* Header */}
|
| 56 |
<div className="p-4 border-b">
|
| 57 |
+
<div className="flex items-center justify-between mb-4">
|
| 58 |
+
<h2 className="font-semibold">Chat Sessions</h2>
|
| 59 |
+
<Button onClick={onNewSession} size="sm">
|
| 60 |
+
<Plus className="h-4 w-4 mr-1" />
|
| 61 |
+
New
|
|
|
|
|
|
|
|
|
|
| 62 |
</Button>
|
| 63 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
</div>
|
| 65 |
|
| 66 |
+
{/* Sessions list */}
|
| 67 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-2">
|
| 68 |
+
{sessions.length === 0 ? (
|
| 69 |
+
<div className="text-center py-8">
|
| 70 |
+
<MessageSquare className="h-8 w-8 mx-auto text-muted-foreground mb-2" />
|
| 71 |
+
<p className="text-sm text-muted-foreground">No conversations yet</p>
|
| 72 |
+
<Button onClick={onNewSession} variant="outline" size="sm" className="mt-2">
|
| 73 |
+
Start your first chat
|
| 74 |
+
</Button>
|
| 75 |
</div>
|
| 76 |
) : (
|
| 77 |
+
sessions.map((session) => (
|
| 78 |
+
<Card
|
| 79 |
+
key={session.id}
|
| 80 |
+
className={`p-3 cursor-pointer transition-colors hover:bg-accent ${
|
| 81 |
+
currentSessionId === session.id ? 'bg-accent border-primary' : ''
|
| 82 |
+
}`}
|
| 83 |
+
onClick={() => onSelectSession(session.id)}
|
| 84 |
+
>
|
| 85 |
+
<div className="space-y-2">
|
| 86 |
+
{/* Title */}
|
| 87 |
+
{editingId === session.id ? (
|
| 88 |
+
<div className="flex items-center gap-1">
|
| 89 |
+
<input
|
| 90 |
+
value={editTitle}
|
| 91 |
+
onChange={(e) => setEditTitle(e.target.value)}
|
| 92 |
+
onKeyPress={(e) => {
|
| 93 |
+
if (e.key === 'Enter') finishEditing()
|
| 94 |
+
if (e.key === 'Escape') cancelEditing()
|
| 95 |
+
}}
|
| 96 |
+
className="flex-1 text-sm bg-background border rounded px-2 py-1"
|
| 97 |
+
autoFocus
|
| 98 |
+
onClick={(e) => e.stopPropagation()}
|
| 99 |
+
/>
|
| 100 |
+
<Button
|
| 101 |
+
size="sm"
|
| 102 |
+
variant="ghost"
|
| 103 |
+
onClick={(e) => {
|
| 104 |
+
e.stopPropagation()
|
| 105 |
+
finishEditing()
|
| 106 |
+
}}
|
| 107 |
+
>
|
| 108 |
+
<Check className="h-3 w-3" />
|
| 109 |
+
</Button>
|
| 110 |
+
<Button
|
| 111 |
+
size="sm"
|
| 112 |
+
variant="ghost"
|
| 113 |
+
onClick={(e) => {
|
| 114 |
+
e.stopPropagation()
|
| 115 |
+
cancelEditing()
|
| 116 |
+
}}
|
| 117 |
+
>
|
| 118 |
+
<X className="h-3 w-3" />
|
| 119 |
+
</Button>
|
| 120 |
+
</div>
|
| 121 |
+
) : (
|
| 122 |
+
<div className="flex items-start justify-between">
|
| 123 |
+
<h3 className="font-medium text-sm line-clamp-2">
|
| 124 |
+
{session.title}
|
| 125 |
+
</h3>
|
| 126 |
+
<div className="flex items-center gap-1 ml-2">
|
| 127 |
+
<Button
|
| 128 |
+
size="sm"
|
| 129 |
+
variant="ghost"
|
| 130 |
+
onClick={(e) => {
|
| 131 |
+
e.stopPropagation()
|
| 132 |
+
startEditing(session)
|
| 133 |
+
}}
|
| 134 |
+
className="h-6 w-6 p-0"
|
| 135 |
+
>
|
| 136 |
+
<Edit2 className="h-3 w-3" />
|
| 137 |
+
</Button>
|
| 138 |
+
<Button
|
| 139 |
+
size="sm"
|
| 140 |
+
variant="ghost"
|
| 141 |
+
onClick={(e) => {
|
| 142 |
+
e.stopPropagation()
|
| 143 |
+
onDeleteSession(session.id)
|
| 144 |
+
}}
|
| 145 |
+
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
|
| 146 |
+
>
|
| 147 |
+
<Trash2 className="h-3 w-3" />
|
| 148 |
+
</Button>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
)}
|
| 152 |
+
|
| 153 |
+
{/* Metadata */}
|
| 154 |
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
| 155 |
+
<span>{session.messages.length} messages</span>
|
| 156 |
+
<span>{new Date(session.updatedAt).toLocaleDateString()}</span>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
{/* Model info */}
|
| 160 |
+
{session.model && (
|
| 161 |
+
<Badge variant="outline" className="text-xs">
|
| 162 |
+
{session.model}
|
| 163 |
+
</Badge>
|
| 164 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
</div>
|
| 166 |
+
</Card>
|
| 167 |
))
|
| 168 |
)}
|
| 169 |
</div>
|
frontend/src/components/chat/index.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
export { ChatContainer } from './ChatContainer'
|
| 2 |
-
export { ChatInput } from './ChatInput'
|
| 3 |
-
export { ChatMessage } from './ChatMessage'
|
| 4 |
-
export { ChatSessions } from './ChatSessions'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/ui/alert-dialog.tsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
import { buttonVariants } from "@/components/ui/button"
|
| 5 |
+
|
| 6 |
+
const AlertDialog = AlertDialogPrimitive.Root
|
| 7 |
+
|
| 8 |
+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
| 9 |
+
|
| 10 |
+
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
| 11 |
+
|
| 12 |
+
const AlertDialogOverlay = React.forwardRef<
|
| 13 |
+
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
| 14 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
| 15 |
+
>(({ className, ...props }, ref) => (
|
| 16 |
+
<AlertDialogPrimitive.Overlay
|
| 17 |
+
className={cn(
|
| 18 |
+
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
| 19 |
+
className
|
| 20 |
+
)}
|
| 21 |
+
{...props}
|
| 22 |
+
ref={ref}
|
| 23 |
+
/>
|
| 24 |
+
))
|
| 25 |
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
| 26 |
+
|
| 27 |
+
const AlertDialogContent = React.forwardRef<
|
| 28 |
+
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
| 29 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
| 30 |
+
>(({ className, ...props }, ref) => (
|
| 31 |
+
<AlertDialogPortal>
|
| 32 |
+
<AlertDialogOverlay />
|
| 33 |
+
<AlertDialogPrimitive.Content
|
| 34 |
+
ref={ref}
|
| 35 |
+
className={cn(
|
| 36 |
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
| 37 |
+
className
|
| 38 |
+
)}
|
| 39 |
+
{...props}
|
| 40 |
+
/>
|
| 41 |
+
</AlertDialogPortal>
|
| 42 |
+
))
|
| 43 |
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
| 44 |
+
|
| 45 |
+
const AlertDialogHeader = ({
|
| 46 |
+
className,
|
| 47 |
+
...props
|
| 48 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 49 |
+
<div
|
| 50 |
+
className={cn(
|
| 51 |
+
"flex flex-col space-y-2 text-center sm:text-left",
|
| 52 |
+
className
|
| 53 |
+
)}
|
| 54 |
+
{...props}
|
| 55 |
+
/>
|
| 56 |
+
)
|
| 57 |
+
AlertDialogHeader.displayName = "AlertDialogHeader"
|
| 58 |
+
|
| 59 |
+
const AlertDialogFooter = ({
|
| 60 |
+
className,
|
| 61 |
+
...props
|
| 62 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 63 |
+
<div
|
| 64 |
+
className={cn(
|
| 65 |
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
| 66 |
+
className
|
| 67 |
+
)}
|
| 68 |
+
{...props}
|
| 69 |
+
/>
|
| 70 |
+
)
|
| 71 |
+
AlertDialogFooter.displayName = "AlertDialogFooter"
|
| 72 |
+
|
| 73 |
+
const AlertDialogTitle = React.forwardRef<
|
| 74 |
+
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
| 75 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
| 76 |
+
>(({ className, ...props }, ref) => (
|
| 77 |
+
<AlertDialogPrimitive.Title
|
| 78 |
+
ref={ref}
|
| 79 |
+
className={cn("text-lg font-semibold", className)}
|
| 80 |
+
{...props}
|
| 81 |
+
/>
|
| 82 |
+
))
|
| 83 |
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
| 84 |
+
|
| 85 |
+
const AlertDialogDescription = React.forwardRef<
|
| 86 |
+
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
| 87 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
| 88 |
+
>(({ className, ...props }, ref) => (
|
| 89 |
+
<AlertDialogPrimitive.Description
|
| 90 |
+
ref={ref}
|
| 91 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 92 |
+
{...props}
|
| 93 |
+
/>
|
| 94 |
+
))
|
| 95 |
+
AlertDialogDescription.displayName =
|
| 96 |
+
AlertDialogPrimitive.Description.displayName
|
| 97 |
+
|
| 98 |
+
const AlertDialogAction = React.forwardRef<
|
| 99 |
+
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
| 100 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
| 101 |
+
>(({ className, ...props }, ref) => (
|
| 102 |
+
<AlertDialogPrimitive.Action
|
| 103 |
+
ref={ref}
|
| 104 |
+
className={cn(buttonVariants(), className)}
|
| 105 |
+
{...props}
|
| 106 |
+
/>
|
| 107 |
+
))
|
| 108 |
+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
| 109 |
+
|
| 110 |
+
const AlertDialogCancel = React.forwardRef<
|
| 111 |
+
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
| 112 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
| 113 |
+
>(({ className, ...props }, ref) => (
|
| 114 |
+
<AlertDialogPrimitive.Cancel
|
| 115 |
+
ref={ref}
|
| 116 |
+
className={cn(
|
| 117 |
+
buttonVariants({ variant: "outline" }),
|
| 118 |
+
"mt-2 sm:mt-0",
|
| 119 |
+
className
|
| 120 |
+
)}
|
| 121 |
+
{...props}
|
| 122 |
+
/>
|
| 123 |
+
))
|
| 124 |
+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
| 125 |
+
|
| 126 |
+
export {
|
| 127 |
+
AlertDialog,
|
| 128 |
+
AlertDialogPortal,
|
| 129 |
+
AlertDialogOverlay,
|
| 130 |
+
AlertDialogTrigger,
|
| 131 |
+
AlertDialogContent,
|
| 132 |
+
AlertDialogHeader,
|
| 133 |
+
AlertDialogFooter,
|
| 134 |
+
AlertDialogTitle,
|
| 135 |
+
AlertDialogDescription,
|
| 136 |
+
AlertDialogAction,
|
| 137 |
+
AlertDialogCancel,
|
| 138 |
+
}
|
frontend/src/components/ui/badge.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
const badgeVariants = cva(
|
| 6 |
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
| 7 |
+
{
|
| 8 |
+
variants: {
|
| 9 |
+
variant: {
|
| 10 |
+
default:
|
| 11 |
+
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
| 12 |
+
secondary:
|
| 13 |
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
| 14 |
+
destructive:
|
| 15 |
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
| 16 |
+
outline: "text-foreground",
|
| 17 |
+
},
|
| 18 |
+
},
|
| 19 |
+
defaultVariants: {
|
| 20 |
+
variant: "default",
|
| 21 |
+
},
|
| 22 |
+
}
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
export interface BadgeProps
|
| 26 |
+
extends React.HTMLAttributes<HTMLDivElement>,
|
| 27 |
+
VariantProps<typeof badgeVariants> {}
|
| 28 |
+
|
| 29 |
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
| 30 |
+
return (
|
| 31 |
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
| 32 |
+
)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export { Badge, badgeVariants }
|
frontend/src/components/ui/button.tsx
CHANGED
|
@@ -1,30 +1,28 @@
|
|
| 1 |
import * as React from "react"
|
| 2 |
import { Slot } from "@radix-ui/react-slot"
|
| 3 |
import { cva, type VariantProps } from "class-variance-authority"
|
| 4 |
-
|
| 5 |
import { cn } from "@/lib/utils"
|
| 6 |
|
| 7 |
const buttonVariants = cva(
|
| 8 |
-
"inline-flex items-center justify-center
|
| 9 |
{
|
| 10 |
variants: {
|
| 11 |
variant: {
|
| 12 |
-
default:
|
| 13 |
-
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
| 14 |
destructive:
|
| 15 |
-
"bg-destructive text-destructive-foreground
|
| 16 |
outline:
|
| 17 |
-
"border border-input bg-background
|
| 18 |
secondary:
|
| 19 |
-
"bg-secondary text-secondary-foreground
|
| 20 |
ghost: "hover:bg-accent hover:text-accent-foreground",
|
| 21 |
link: "text-primary underline-offset-4 hover:underline",
|
| 22 |
},
|
| 23 |
size: {
|
| 24 |
-
default: "h-
|
| 25 |
-
sm: "h-
|
| 26 |
-
lg: "h-
|
| 27 |
-
icon: "h-
|
| 28 |
},
|
| 29 |
},
|
| 30 |
defaultVariants: {
|
|
|
|
| 1 |
import * as React from "react"
|
| 2 |
import { Slot } from "@radix-ui/react-slot"
|
| 3 |
import { cva, type VariantProps } from "class-variance-authority"
|
|
|
|
| 4 |
import { cn } from "@/lib/utils"
|
| 5 |
|
| 6 |
const buttonVariants = cva(
|
| 7 |
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
| 8 |
{
|
| 9 |
variants: {
|
| 10 |
variant: {
|
| 11 |
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
|
|
| 12 |
destructive:
|
| 13 |
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
| 14 |
outline:
|
| 15 |
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
| 16 |
secondary:
|
| 17 |
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
| 18 |
ghost: "hover:bg-accent hover:text-accent-foreground",
|
| 19 |
link: "text-primary underline-offset-4 hover:underline",
|
| 20 |
},
|
| 21 |
size: {
|
| 22 |
+
default: "h-10 px-4 py-2",
|
| 23 |
+
sm: "h-9 rounded-md px-3",
|
| 24 |
+
lg: "h-11 rounded-md px-8",
|
| 25 |
+
icon: "h-10 w-10",
|
| 26 |
},
|
| 27 |
},
|
| 28 |
defaultVariants: {
|
frontend/src/components/ui/card.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import * as React from "react"
|
| 2 |
-
|
| 3 |
import { cn } from "@/lib/utils"
|
| 4 |
|
| 5 |
const Card = React.forwardRef<
|
|
@@ -9,7 +8,7 @@ const Card = React.forwardRef<
|
|
| 9 |
<div
|
| 10 |
ref={ref}
|
| 11 |
className={cn(
|
| 12 |
-
"rounded-
|
| 13 |
className
|
| 14 |
)}
|
| 15 |
{...props}
|
|
@@ -21,31 +20,30 @@ const CardHeader = React.forwardRef<
|
|
| 21 |
HTMLDivElement,
|
| 22 |
React.HTMLAttributes<HTMLDivElement>
|
| 23 |
>(({ className, ...props }, ref) => (
|
| 24 |
-
<div
|
| 25 |
-
ref={ref}
|
| 26 |
-
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
| 27 |
-
{...props}
|
| 28 |
-
/>
|
| 29 |
))
|
| 30 |
CardHeader.displayName = "CardHeader"
|
| 31 |
|
| 32 |
const CardTitle = React.forwardRef<
|
| 33 |
-
|
| 34 |
-
React.HTMLAttributes<
|
| 35 |
>(({ className, ...props }, ref) => (
|
| 36 |
-
<
|
| 37 |
ref={ref}
|
| 38 |
-
className={cn(
|
|
|
|
|
|
|
|
|
|
| 39 |
{...props}
|
| 40 |
/>
|
| 41 |
))
|
| 42 |
CardTitle.displayName = "CardTitle"
|
| 43 |
|
| 44 |
const CardDescription = React.forwardRef<
|
| 45 |
-
|
| 46 |
-
React.HTMLAttributes<
|
| 47 |
>(({ className, ...props }, ref) => (
|
| 48 |
-
<
|
| 49 |
ref={ref}
|
| 50 |
className={cn("text-sm text-muted-foreground", className)}
|
| 51 |
{...props}
|
|
@@ -65,11 +63,7 @@ const CardFooter = React.forwardRef<
|
|
| 65 |
HTMLDivElement,
|
| 66 |
React.HTMLAttributes<HTMLDivElement>
|
| 67 |
>(({ className, ...props }, ref) => (
|
| 68 |
-
<div
|
| 69 |
-
ref={ref}
|
| 70 |
-
className={cn("flex items-center p-6 pt-0", className)}
|
| 71 |
-
{...props}
|
| 72 |
-
/>
|
| 73 |
))
|
| 74 |
CardFooter.displayName = "CardFooter"
|
| 75 |
|
|
|
|
| 1 |
import * as React from "react"
|
|
|
|
| 2 |
import { cn } from "@/lib/utils"
|
| 3 |
|
| 4 |
const Card = React.forwardRef<
|
|
|
|
| 8 |
<div
|
| 9 |
ref={ref}
|
| 10 |
className={cn(
|
| 11 |
+
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
| 12 |
className
|
| 13 |
)}
|
| 14 |
{...props}
|
|
|
|
| 20 |
HTMLDivElement,
|
| 21 |
React.HTMLAttributes<HTMLDivElement>
|
| 22 |
>(({ className, ...props }, ref) => (
|
| 23 |
+
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
))
|
| 25 |
CardHeader.displayName = "CardHeader"
|
| 26 |
|
| 27 |
const CardTitle = React.forwardRef<
|
| 28 |
+
HTMLParagraphElement,
|
| 29 |
+
React.HTMLAttributes<HTMLHeadingElement>
|
| 30 |
>(({ className, ...props }, ref) => (
|
| 31 |
+
<h3
|
| 32 |
ref={ref}
|
| 33 |
+
className={cn(
|
| 34 |
+
"text-2xl font-semibold leading-none tracking-tight",
|
| 35 |
+
className
|
| 36 |
+
)}
|
| 37 |
{...props}
|
| 38 |
/>
|
| 39 |
))
|
| 40 |
CardTitle.displayName = "CardTitle"
|
| 41 |
|
| 42 |
const CardDescription = React.forwardRef<
|
| 43 |
+
HTMLParagraphElement,
|
| 44 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
| 45 |
>(({ className, ...props }, ref) => (
|
| 46 |
+
<p
|
| 47 |
ref={ref}
|
| 48 |
className={cn("text-sm text-muted-foreground", className)}
|
| 49 |
{...props}
|
|
|
|
| 63 |
HTMLDivElement,
|
| 64 |
React.HTMLAttributes<HTMLDivElement>
|
| 65 |
>(({ className, ...props }, ref) => (
|
| 66 |
+
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
))
|
| 68 |
CardFooter.displayName = "CardFooter"
|
| 69 |
|
frontend/src/components/ui/chat.tsx
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react'
|
| 2 |
+
import { cn } from '@/lib/utils'
|
| 3 |
+
import { Button } from '@/components/ui/button'
|
| 4 |
+
import { Textarea } from '@/components/ui/textarea'
|
| 5 |
+
import { Send, Square, User, Bot } from 'lucide-react'
|
| 6 |
+
|
| 7 |
+
export interface ChatProps extends React.HTMLAttributes<HTMLDivElement> {
|
| 8 |
+
messages: Array<{
|
| 9 |
+
id: string
|
| 10 |
+
role: 'user' | 'assistant' | 'system'
|
| 11 |
+
content: string
|
| 12 |
+
createdAt?: Date
|
| 13 |
+
}>
|
| 14 |
+
input: string
|
| 15 |
+
handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
| 16 |
+
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void
|
| 17 |
+
isGenerating?: boolean
|
| 18 |
+
stop?: () => void
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const Chat = React.forwardRef<HTMLDivElement, ChatProps>(
|
| 22 |
+
({ className, messages, input, handleInputChange, handleSubmit, isGenerating, stop, ...props }, ref) => {
|
| 23 |
+
const messagesEndRef = React.useRef<HTMLDivElement>(null)
|
| 24 |
+
|
| 25 |
+
const scrollToBottom = () => {
|
| 26 |
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
React.useEffect(() => {
|
| 30 |
+
console.log('Chat component - messages updated:', messages.length, messages.map(m => ({ id: m.id, role: m.role, content: m.content.slice(0, 50) + '...' })))
|
| 31 |
+
scrollToBottom()
|
| 32 |
+
}, [messages])
|
| 33 |
+
|
| 34 |
+
return (
|
| 35 |
+
<div
|
| 36 |
+
className={cn('flex h-full flex-col', className)}
|
| 37 |
+
ref={ref}
|
| 38 |
+
{...props}
|
| 39 |
+
>
|
| 40 |
+
{/* Messages */}
|
| 41 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
| 42 |
+
{messages.length === 0 ? (
|
| 43 |
+
<div className="flex items-center justify-center h-full text-muted-foreground">
|
| 44 |
+
<p>No messages yet. Start a conversation!</p>
|
| 45 |
+
</div>
|
| 46 |
+
) : (
|
| 47 |
+
messages.map((message, index) => (
|
| 48 |
+
<div
|
| 49 |
+
key={`${message.id}-${index}`}
|
| 50 |
+
className={cn(
|
| 51 |
+
'flex gap-3 w-full',
|
| 52 |
+
message.role === 'user' ? 'justify-end' : 'justify-start'
|
| 53 |
+
)}
|
| 54 |
+
>
|
| 55 |
+
{/* Avatar for assistant */}
|
| 56 |
+
{message.role !== 'user' && (
|
| 57 |
+
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
|
| 58 |
+
<Bot className="h-4 w-4 text-primary-foreground" />
|
| 59 |
+
</div>
|
| 60 |
+
)}
|
| 61 |
+
|
| 62 |
+
{/* Message content */}
|
| 63 |
+
<div
|
| 64 |
+
className={cn(
|
| 65 |
+
'max-w-[75%] flex flex-col gap-2 rounded-lg px-3 py-2 text-sm',
|
| 66 |
+
message.role === 'user'
|
| 67 |
+
? 'bg-primary text-primary-foreground'
|
| 68 |
+
: 'bg-muted'
|
| 69 |
+
)}
|
| 70 |
+
>
|
| 71 |
+
<div className="text-xs opacity-70">
|
| 72 |
+
{message.role === 'user' ? 'You' : 'Assistant'} • #{index + 1}
|
| 73 |
+
</div>
|
| 74 |
+
<div className="leading-relaxed">
|
| 75 |
+
{message.content}
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
{/* Avatar for user */}
|
| 80 |
+
{message.role === 'user' && (
|
| 81 |
+
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
|
| 82 |
+
<User className="h-4 w-4" />
|
| 83 |
+
</div>
|
| 84 |
+
)}
|
| 85 |
+
</div>
|
| 86 |
+
))
|
| 87 |
+
)}
|
| 88 |
+
<div ref={messagesEndRef} />
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
{/* Input */}
|
| 92 |
+
<div className="border-t p-4">
|
| 93 |
+
<form onSubmit={handleSubmit} className="flex gap-2">
|
| 94 |
+
<Textarea
|
| 95 |
+
value={input}
|
| 96 |
+
onChange={handleInputChange}
|
| 97 |
+
placeholder="Type your message..."
|
| 98 |
+
className="min-h-[60px] resize-none"
|
| 99 |
+
onKeyDown={(e) => {
|
| 100 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 101 |
+
e.preventDefault()
|
| 102 |
+
handleSubmit(e as any)
|
| 103 |
+
}
|
| 104 |
+
}}
|
| 105 |
+
/>
|
| 106 |
+
{isGenerating ? (
|
| 107 |
+
<Button type="button" onClick={stop} variant="outline" size="icon">
|
| 108 |
+
<Square className="h-4 w-4" />
|
| 109 |
+
</Button>
|
| 110 |
+
) : (
|
| 111 |
+
<Button type="submit" disabled={!input.trim()} size="icon">
|
| 112 |
+
<Send className="h-4 w-4" />
|
| 113 |
+
</Button>
|
| 114 |
+
)}
|
| 115 |
+
</form>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
)
|
| 119 |
+
}
|
| 120 |
+
)
|
| 121 |
+
Chat.displayName = 'Chat'
|
| 122 |
+
|
| 123 |
+
export { Chat }
|
frontend/src/components/ui/collapsible.tsx
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
| 2 |
+
|
| 3 |
+
const Collapsible = CollapsiblePrimitive.Root
|
| 4 |
+
|
| 5 |
+
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
| 6 |
+
|
| 7 |
+
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
| 8 |
+
|
| 9 |
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
frontend/src/components/ui/label.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
| 3 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const labelVariants = cva(
|
| 7 |
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
const Label = React.forwardRef<
|
| 11 |
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
| 12 |
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
| 13 |
+
VariantProps<typeof labelVariants>
|
| 14 |
+
>(({ className, ...props }, ref) => (
|
| 15 |
+
<LabelPrimitive.Root
|
| 16 |
+
ref={ref}
|
| 17 |
+
className={cn(labelVariants(), className)}
|
| 18 |
+
{...props}
|
| 19 |
+
/>
|
| 20 |
+
))
|
| 21 |
+
Label.displayName = LabelPrimitive.Root.displayName
|
| 22 |
+
|
| 23 |
+
export { Label }
|
frontend/src/components/ui/select.tsx
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
| 3 |
+
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const Select = SelectPrimitive.Root
|
| 7 |
+
|
| 8 |
+
const SelectGroup = SelectPrimitive.Group
|
| 9 |
+
|
| 10 |
+
const SelectValue = SelectPrimitive.Value
|
| 11 |
+
|
| 12 |
+
const SelectTrigger = React.forwardRef<
|
| 13 |
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
| 14 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
| 15 |
+
>(({ className, children, ...props }, ref) => (
|
| 16 |
+
<SelectPrimitive.Trigger
|
| 17 |
+
ref={ref}
|
| 18 |
+
className={cn(
|
| 19 |
+
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
| 20 |
+
className
|
| 21 |
+
)}
|
| 22 |
+
{...props}
|
| 23 |
+
>
|
| 24 |
+
{children}
|
| 25 |
+
<SelectPrimitive.Icon asChild>
|
| 26 |
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
| 27 |
+
</SelectPrimitive.Icon>
|
| 28 |
+
</SelectPrimitive.Trigger>
|
| 29 |
+
))
|
| 30 |
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
| 31 |
+
|
| 32 |
+
const SelectScrollUpButton = React.forwardRef<
|
| 33 |
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
| 34 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
| 35 |
+
>(({ className, ...props }, ref) => (
|
| 36 |
+
<SelectPrimitive.ScrollUpButton
|
| 37 |
+
ref={ref}
|
| 38 |
+
className={cn(
|
| 39 |
+
"flex cursor-default items-center justify-center py-1",
|
| 40 |
+
className
|
| 41 |
+
)}
|
| 42 |
+
{...props}
|
| 43 |
+
>
|
| 44 |
+
<ChevronUp className="h-4 w-4" />
|
| 45 |
+
</SelectPrimitive.ScrollUpButton>
|
| 46 |
+
))
|
| 47 |
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
| 48 |
+
|
| 49 |
+
const SelectScrollDownButton = React.forwardRef<
|
| 50 |
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
| 51 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
| 52 |
+
>(({ className, ...props }, ref) => (
|
| 53 |
+
<SelectPrimitive.ScrollDownButton
|
| 54 |
+
ref={ref}
|
| 55 |
+
className={cn(
|
| 56 |
+
"flex cursor-default items-center justify-center py-1",
|
| 57 |
+
className
|
| 58 |
+
)}
|
| 59 |
+
{...props}
|
| 60 |
+
>
|
| 61 |
+
<ChevronDown className="h-4 w-4" />
|
| 62 |
+
</SelectPrimitive.ScrollDownButton>
|
| 63 |
+
))
|
| 64 |
+
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
| 65 |
+
|
| 66 |
+
const SelectContent = React.forwardRef<
|
| 67 |
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
| 68 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
| 69 |
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
| 70 |
+
<SelectPrimitive.Portal>
|
| 71 |
+
<SelectPrimitive.Content
|
| 72 |
+
ref={ref}
|
| 73 |
+
className={cn(
|
| 74 |
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
| 75 |
+
position === "popper" &&
|
| 76 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
| 77 |
+
className
|
| 78 |
+
)}
|
| 79 |
+
position={position}
|
| 80 |
+
{...props}
|
| 81 |
+
>
|
| 82 |
+
<SelectScrollUpButton />
|
| 83 |
+
<SelectPrimitive.Viewport
|
| 84 |
+
className={cn(
|
| 85 |
+
"p-1",
|
| 86 |
+
position === "popper" &&
|
| 87 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
| 88 |
+
)}
|
| 89 |
+
>
|
| 90 |
+
{children}
|
| 91 |
+
</SelectPrimitive.Viewport>
|
| 92 |
+
<SelectScrollDownButton />
|
| 93 |
+
</SelectPrimitive.Content>
|
| 94 |
+
</SelectPrimitive.Portal>
|
| 95 |
+
))
|
| 96 |
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
| 97 |
+
|
| 98 |
+
const SelectLabel = React.forwardRef<
|
| 99 |
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
| 100 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
| 101 |
+
>(({ className, ...props }, ref) => (
|
| 102 |
+
<SelectPrimitive.Label
|
| 103 |
+
ref={ref}
|
| 104 |
+
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
| 105 |
+
{...props}
|
| 106 |
+
/>
|
| 107 |
+
))
|
| 108 |
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
| 109 |
+
|
| 110 |
+
const SelectItem = React.forwardRef<
|
| 111 |
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
| 112 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
| 113 |
+
>(({ className, children, ...props }, ref) => (
|
| 114 |
+
<SelectPrimitive.Item
|
| 115 |
+
ref={ref}
|
| 116 |
+
className={cn(
|
| 117 |
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 118 |
+
className
|
| 119 |
+
)}
|
| 120 |
+
{...props}
|
| 121 |
+
>
|
| 122 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 123 |
+
<SelectPrimitive.ItemIndicator>
|
| 124 |
+
<Check className="h-4 w-4" />
|
| 125 |
+
</SelectPrimitive.ItemIndicator>
|
| 126 |
+
</span>
|
| 127 |
+
|
| 128 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
| 129 |
+
</SelectPrimitive.Item>
|
| 130 |
+
))
|
| 131 |
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
| 132 |
+
|
| 133 |
+
const SelectSeparator = React.forwardRef<
|
| 134 |
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
| 135 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
| 136 |
+
>(({ className, ...props }, ref) => (
|
| 137 |
+
<SelectPrimitive.Separator
|
| 138 |
+
ref={ref}
|
| 139 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
| 140 |
+
{...props}
|
| 141 |
+
/>
|
| 142 |
+
))
|
| 143 |
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
| 144 |
+
|
| 145 |
+
export {
|
| 146 |
+
Select,
|
| 147 |
+
SelectGroup,
|
| 148 |
+
SelectValue,
|
| 149 |
+
SelectTrigger,
|
| 150 |
+
SelectContent,
|
| 151 |
+
SelectLabel,
|
| 152 |
+
SelectItem,
|
| 153 |
+
SelectSeparator,
|
| 154 |
+
SelectScrollUpButton,
|
| 155 |
+
SelectScrollDownButton,
|
| 156 |
+
}
|
frontend/src/components/ui/slider.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as SliderPrimitive from "@radix-ui/react-slider"
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
const Slider = React.forwardRef<
|
| 6 |
+
React.ElementRef<typeof SliderPrimitive.Root>,
|
| 7 |
+
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
| 8 |
+
>(({ className, ...props }, ref) => (
|
| 9 |
+
<SliderPrimitive.Root
|
| 10 |
+
ref={ref}
|
| 11 |
+
className={cn(
|
| 12 |
+
"relative flex w-full touch-none select-none items-center",
|
| 13 |
+
className
|
| 14 |
+
)}
|
| 15 |
+
{...props}
|
| 16 |
+
>
|
| 17 |
+
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
| 18 |
+
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
| 19 |
+
</SliderPrimitive.Track>
|
| 20 |
+
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
| 21 |
+
</SliderPrimitive.Root>
|
| 22 |
+
))
|
| 23 |
+
Slider.displayName = SliderPrimitive.Root.displayName
|
| 24 |
+
|
| 25 |
+
export { Slider }
|
frontend/src/components/ui/switch.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
const Switch = React.forwardRef<
|
| 6 |
+
React.ElementRef<typeof SwitchPrimitives.Root>,
|
| 7 |
+
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
| 8 |
+
>(({ className, ...props }, ref) => (
|
| 9 |
+
<SwitchPrimitives.Root
|
| 10 |
+
className={cn(
|
| 11 |
+
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
| 12 |
+
className
|
| 13 |
+
)}
|
| 14 |
+
{...props}
|
| 15 |
+
ref={ref}
|
| 16 |
+
>
|
| 17 |
+
<SwitchPrimitives.Thumb
|
| 18 |
+
className={cn(
|
| 19 |
+
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
| 20 |
+
)}
|
| 21 |
+
/>
|
| 22 |
+
</SwitchPrimitives.Root>
|
| 23 |
+
))
|
| 24 |
+
Switch.displayName = SwitchPrimitives.Root.displayName
|
| 25 |
+
|
| 26 |
+
export { Switch }
|
frontend/src/components/ui/textarea.tsx
CHANGED
|
@@ -1,22 +1,23 @@
|
|
| 1 |
import * as React from "react"
|
| 2 |
-
|
| 3 |
import { cn } from "@/lib/utils"
|
| 4 |
|
| 5 |
-
|
| 6 |
-
HTMLTextAreaElement
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
Textarea.displayName = "Textarea"
|
| 21 |
|
| 22 |
export { Textarea }
|
|
|
|
| 1 |
import * as React from "react"
|
|
|
|
| 2 |
import { cn } from "@/lib/utils"
|
| 3 |
|
| 4 |
+
export interface TextareaProps
|
| 5 |
+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
| 6 |
+
|
| 7 |
+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
| 8 |
+
({ className, ...props }, ref) => {
|
| 9 |
+
return (
|
| 10 |
+
<textarea
|
| 11 |
+
className={cn(
|
| 12 |
+
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
| 13 |
+
className
|
| 14 |
+
)}
|
| 15 |
+
ref={ref}
|
| 16 |
+
{...props}
|
| 17 |
+
/>
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
)
|
| 21 |
Textarea.displayName = "Textarea"
|
| 22 |
|
| 23 |
export { Textarea }
|
frontend/src/hooks/useChat.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { useState, useEffect, useCallback } from 'react'
|
| 2 |
import { Message, ChatSession, MessageStatus } from '@/types/chat'
|
| 3 |
import { chatStorage } from '@/lib/chat-storage'
|
| 4 |
|
|
@@ -17,8 +17,8 @@ interface ApiResponse {
|
|
| 17 |
|
| 18 |
export function useChat(options: UseChatOptions = {}) {
|
| 19 |
const {
|
| 20 |
-
api_endpoint =
|
| 21 |
-
defaultModel = 'Qwen/Qwen3-
|
| 22 |
defaultSystemPrompt = ''
|
| 23 |
} = options
|
| 24 |
|
|
@@ -37,9 +37,22 @@ export function useChat(options: UseChatOptions = {}) {
|
|
| 37 |
const [temperature, setTemperature] = useState(0.7)
|
| 38 |
const [maxTokens, setMaxTokens] = useState(1024)
|
| 39 |
|
| 40 |
-
// Current session
|
| 41 |
-
const currentSession =
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
// Load sessions on mount
|
| 45 |
useEffect(() => {
|
|
@@ -66,7 +79,8 @@ export function useChat(options: UseChatOptions = {}) {
|
|
| 66 |
)
|
| 67 |
|
| 68 |
// Update React state with all sessions from localStorage
|
| 69 |
-
|
|
|
|
| 70 |
setCurrentSessionId(newSession.id)
|
| 71 |
chatStorage.setCurrentSession(newSession.id)
|
| 72 |
|
|
@@ -83,7 +97,7 @@ export function useChat(options: UseChatOptions = {}) {
|
|
| 83 |
const deleteSession = useCallback((sessionId: string) => {
|
| 84 |
chatStorage.deleteSession(sessionId)
|
| 85 |
const updatedSessions = chatStorage.getAllSessions()
|
| 86 |
-
setSessions(updatedSessions)
|
| 87 |
|
| 88 |
if (currentSessionId === sessionId) {
|
| 89 |
if (updatedSessions.length > 0) {
|
|
@@ -98,7 +112,8 @@ export function useChat(options: UseChatOptions = {}) {
|
|
| 98 |
// Rename session
|
| 99 |
const renameSession = useCallback((sessionId: string, newTitle: string) => {
|
| 100 |
chatStorage.updateSession(sessionId, { title: newTitle })
|
| 101 |
-
|
|
|
|
| 102 |
}, [])
|
| 103 |
|
| 104 |
// Add message to current session
|
|
@@ -106,7 +121,9 @@ export function useChat(options: UseChatOptions = {}) {
|
|
| 106 |
if (!currentSessionId) return
|
| 107 |
|
| 108 |
chatStorage.addMessageToSession(currentSessionId, message)
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
}, [currentSessionId])
|
| 111 |
|
| 112 |
// Send message
|
|
@@ -130,7 +147,9 @@ export function useChat(options: UseChatOptions = {}) {
|
|
| 130 |
role: 'user',
|
| 131 |
content: userMessage
|
| 132 |
})
|
| 133 |
-
|
|
|
|
|
|
|
| 134 |
}
|
| 135 |
|
| 136 |
// Add system message if system prompt is set
|
|
@@ -143,10 +162,21 @@ export function useChat(options: UseChatOptions = {}) {
|
|
| 143 |
role: 'system',
|
| 144 |
content: systemPrompt
|
| 145 |
})
|
| 146 |
-
|
|
|
|
|
|
|
| 147 |
}
|
| 148 |
|
| 149 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
const response = await fetch(api_endpoint, {
|
| 151 |
method: 'POST',
|
| 152 |
headers: {
|
|
@@ -154,6 +184,7 @@ export function useChat(options: UseChatOptions = {}) {
|
|
| 154 |
},
|
| 155 |
body: JSON.stringify({
|
| 156 |
prompt: userMessage,
|
|
|
|
| 157 |
system_prompt: systemPrompt || null,
|
| 158 |
model_name: selectedModel,
|
| 159 |
temperature,
|
|
@@ -177,7 +208,9 @@ export function useChat(options: UseChatOptions = {}) {
|
|
| 177 |
model_used: data.model_used,
|
| 178 |
supports_thinking: data.supports_thinking
|
| 179 |
})
|
| 180 |
-
|
|
|
|
|
|
|
| 181 |
}
|
| 182 |
|
| 183 |
setStatus({ isLoading: false, error: null })
|
|
@@ -191,7 +224,9 @@ export function useChat(options: UseChatOptions = {}) {
|
|
| 191 |
role: 'assistant',
|
| 192 |
content: `Sorry, I encountered an error: ${errorMessage}`
|
| 193 |
})
|
| 194 |
-
|
|
|
|
|
|
|
| 195 |
}
|
| 196 |
}
|
| 197 |
}, [
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useCallback } from 'react'
|
| 2 |
import { Message, ChatSession, MessageStatus } from '@/types/chat'
|
| 3 |
import { chatStorage } from '@/lib/chat-storage'
|
| 4 |
|
|
|
|
| 17 |
|
| 18 |
export function useChat(options: UseChatOptions = {}) {
|
| 19 |
const {
|
| 20 |
+
api_endpoint = `${window.location.origin}/generate`,
|
| 21 |
+
defaultModel = 'Qwen/Qwen3-30B-A3B',
|
| 22 |
defaultSystemPrompt = ''
|
| 23 |
} = options
|
| 24 |
|
|
|
|
| 37 |
const [temperature, setTemperature] = useState(0.7)
|
| 38 |
const [maxTokens, setMaxTokens] = useState(1024)
|
| 39 |
|
| 40 |
+
// Current session - add dependency on sessions to force re-render
|
| 41 |
+
const currentSession = React.useMemo(() => {
|
| 42 |
+
const session = sessions.find((s: any) => s.id === currentSessionId) || null
|
| 43 |
+
console.log('useChat - currentSession updated:', {
|
| 44 |
+
sessionId: currentSessionId,
|
| 45 |
+
found: !!session,
|
| 46 |
+
messageCount: session?.messages?.length || 0
|
| 47 |
+
})
|
| 48 |
+
return session
|
| 49 |
+
}, [sessions, currentSessionId])
|
| 50 |
+
|
| 51 |
+
const messages = React.useMemo(() => {
|
| 52 |
+
const msgs = currentSession?.messages || []
|
| 53 |
+
console.log('useChat - messages computed:', msgs.length, msgs.map(m => ({ id: m.id, role: m.role })))
|
| 54 |
+
return msgs
|
| 55 |
+
}, [currentSession?.messages])
|
| 56 |
|
| 57 |
// Load sessions on mount
|
| 58 |
useEffect(() => {
|
|
|
|
| 79 |
)
|
| 80 |
|
| 81 |
// Update React state with all sessions from localStorage
|
| 82 |
+
const updatedSessions = chatStorage.getAllSessions()
|
| 83 |
+
setSessions([...updatedSessions]) // Force update with new array reference
|
| 84 |
setCurrentSessionId(newSession.id)
|
| 85 |
chatStorage.setCurrentSession(newSession.id)
|
| 86 |
|
|
|
|
| 97 |
const deleteSession = useCallback((sessionId: string) => {
|
| 98 |
chatStorage.deleteSession(sessionId)
|
| 99 |
const updatedSessions = chatStorage.getAllSessions()
|
| 100 |
+
setSessions([...updatedSessions]) // Force update with new array reference
|
| 101 |
|
| 102 |
if (currentSessionId === sessionId) {
|
| 103 |
if (updatedSessions.length > 0) {
|
|
|
|
| 112 |
// Rename session
|
| 113 |
const renameSession = useCallback((sessionId: string, newTitle: string) => {
|
| 114 |
chatStorage.updateSession(sessionId, { title: newTitle })
|
| 115 |
+
const updatedSessions = chatStorage.getAllSessions()
|
| 116 |
+
setSessions([...updatedSessions]) // Force update with new array reference
|
| 117 |
}, [])
|
| 118 |
|
| 119 |
// Add message to current session
|
|
|
|
| 121 |
if (!currentSessionId) return
|
| 122 |
|
| 123 |
chatStorage.addMessageToSession(currentSessionId, message)
|
| 124 |
+
// Force update with new array reference
|
| 125 |
+
const updatedSessions = chatStorage.getAllSessions()
|
| 126 |
+
setSessions([...updatedSessions])
|
| 127 |
}, [currentSessionId])
|
| 128 |
|
| 129 |
// Send message
|
|
|
|
| 147 |
role: 'user',
|
| 148 |
content: userMessage
|
| 149 |
})
|
| 150 |
+
// Force update sessions state with fresh data
|
| 151 |
+
const updatedSessions = chatStorage.getAllSessions()
|
| 152 |
+
setSessions([...updatedSessions]) // Create new array reference to force re-render
|
| 153 |
}
|
| 154 |
|
| 155 |
// Add system message if system prompt is set
|
|
|
|
| 162 |
role: 'system',
|
| 163 |
content: systemPrompt
|
| 164 |
})
|
| 165 |
+
// Force update sessions state with fresh data
|
| 166 |
+
const updatedSessions = chatStorage.getAllSessions()
|
| 167 |
+
setSessions([...updatedSessions]) // Create new array reference to force re-render
|
| 168 |
}
|
| 169 |
|
| 170 |
try {
|
| 171 |
+
// Get conversation history (excluding system messages for the API)
|
| 172 |
+
const actualSession = chatStorage.getSession(sessionId!)
|
| 173 |
+
const conversationHistory = actualSession?.messages
|
| 174 |
+
?.filter((msg: any) => msg.role !== 'system')
|
| 175 |
+
?.map((msg: any) => ({
|
| 176 |
+
role: msg.role,
|
| 177 |
+
content: msg.content
|
| 178 |
+
})) || []
|
| 179 |
+
|
| 180 |
const response = await fetch(api_endpoint, {
|
| 181 |
method: 'POST',
|
| 182 |
headers: {
|
|
|
|
| 184 |
},
|
| 185 |
body: JSON.stringify({
|
| 186 |
prompt: userMessage,
|
| 187 |
+
messages: conversationHistory,
|
| 188 |
system_prompt: systemPrompt || null,
|
| 189 |
model_name: selectedModel,
|
| 190 |
temperature,
|
|
|
|
| 208 |
model_used: data.model_used,
|
| 209 |
supports_thinking: data.supports_thinking
|
| 210 |
})
|
| 211 |
+
// Force update sessions state with fresh data
|
| 212 |
+
const updatedSessions = chatStorage.getAllSessions()
|
| 213 |
+
setSessions([...updatedSessions]) // Create new array reference to force re-render
|
| 214 |
}
|
| 215 |
|
| 216 |
setStatus({ isLoading: false, error: null })
|
|
|
|
| 224 |
role: 'assistant',
|
| 225 |
content: `Sorry, I encountered an error: ${errorMessage}`
|
| 226 |
})
|
| 227 |
+
// Force update sessions state with fresh data
|
| 228 |
+
const updatedSessions = chatStorage.getAllSessions()
|
| 229 |
+
setSessions([...updatedSessions]) // Create new array reference to force re-render
|
| 230 |
}
|
| 231 |
}
|
| 232 |
}, [
|
frontend/src/index.css
CHANGED
|
@@ -3,115 +3,52 @@
|
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
@layer base {
|
| 6 |
-
body {
|
| 7 |
-
@apply bg-gray-50 text-gray-900;
|
| 8 |
-
}
|
| 9 |
:root {
|
| 10 |
-
|
| 11 |
--background: 0 0% 100%;
|
| 12 |
-
|
| 13 |
-
--foreground: 0 0% 3.9%;
|
| 14 |
-
|
| 15 |
--card: 0 0% 100%;
|
| 16 |
-
|
| 17 |
-
--card-foreground: 0 0% 3.9%;
|
| 18 |
-
|
| 19 |
--popover: 0 0% 100%;
|
| 20 |
-
|
| 21 |
-
--
|
| 22 |
-
|
| 23 |
-
--
|
| 24 |
-
|
| 25 |
-
--
|
| 26 |
-
|
| 27 |
-
--
|
| 28 |
-
|
| 29 |
-
--secondary-foreground: 0 0% 9%;
|
| 30 |
-
|
| 31 |
-
--muted: 0 0% 96.1%;
|
| 32 |
-
|
| 33 |
-
--muted-foreground: 0 0% 45.1%;
|
| 34 |
-
|
| 35 |
-
--accent: 0 0% 96.1%;
|
| 36 |
-
|
| 37 |
-
--accent-foreground: 0 0% 9%;
|
| 38 |
-
|
| 39 |
--destructive: 0 84.2% 60.2%;
|
| 40 |
-
|
| 41 |
-
--
|
| 42 |
-
|
| 43 |
-
--
|
| 44 |
-
|
| 45 |
-
--input: 0 0% 89.8%;
|
| 46 |
-
|
| 47 |
-
--ring: 0 0% 3.9%;
|
| 48 |
-
|
| 49 |
-
--chart-1: 12 76% 61%;
|
| 50 |
-
|
| 51 |
-
--chart-2: 173 58% 39%;
|
| 52 |
-
|
| 53 |
-
--chart-3: 197 37% 24%;
|
| 54 |
-
|
| 55 |
-
--chart-4: 43 74% 66%;
|
| 56 |
-
|
| 57 |
-
--chart-5: 27 87% 67%;
|
| 58 |
-
|
| 59 |
-
--radius: 0.5rem
|
| 60 |
}
|
| 61 |
-
.dark {
|
| 62 |
-
|
| 63 |
-
--background: 0 0% 3.9%;
|
| 64 |
-
|
| 65 |
-
--foreground: 0 0% 98%;
|
| 66 |
-
|
| 67 |
-
--card: 0 0% 3.9%;
|
| 68 |
-
|
| 69 |
-
--card-foreground: 0 0% 98%;
|
| 70 |
-
|
| 71 |
-
--popover: 0 0% 3.9%;
|
| 72 |
-
|
| 73 |
-
--popover-foreground: 0 0% 98%;
|
| 74 |
-
|
| 75 |
-
--primary: 0 0% 98%;
|
| 76 |
-
|
| 77 |
-
--primary-foreground: 0 0% 9%;
|
| 78 |
-
|
| 79 |
-
--secondary: 0 0% 14.9%;
|
| 80 |
-
|
| 81 |
-
--secondary-foreground: 0 0% 98%;
|
| 82 |
-
|
| 83 |
-
--muted: 0 0% 14.9%;
|
| 84 |
-
|
| 85 |
-
--muted-foreground: 0 0% 63.9%;
|
| 86 |
-
|
| 87 |
-
--accent: 0 0% 14.9%;
|
| 88 |
-
|
| 89 |
-
--accent-foreground: 0 0% 98%;
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
--destructive: 0 62.8% 30.6%;
|
| 92 |
-
|
| 93 |
-
--
|
| 94 |
-
|
| 95 |
-
--
|
| 96 |
-
|
| 97 |
-
--input: 0 0% 14.9%;
|
| 98 |
-
|
| 99 |
-
--ring: 0 0% 83.1%;
|
| 100 |
-
|
| 101 |
-
--chart-1: 220 70% 50%;
|
| 102 |
-
|
| 103 |
-
--chart-2: 160 60% 45%;
|
| 104 |
-
|
| 105 |
-
--chart-3: 30 80% 55%;
|
| 106 |
-
|
| 107 |
-
--chart-4: 280 65% 60%;
|
| 108 |
-
|
| 109 |
-
--chart-5: 340 75% 55%
|
| 110 |
}
|
| 111 |
}
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
| 115 |
@layer base {
|
| 116 |
* {
|
| 117 |
@apply border-border;
|
|
@@ -120,19 +57,3 @@
|
|
| 120 |
@apply bg-background text-foreground;
|
| 121 |
}
|
| 122 |
}
|
| 123 |
-
|
| 124 |
-
@layer utilities {
|
| 125 |
-
.line-clamp-2 {
|
| 126 |
-
display: -webkit-box;
|
| 127 |
-
-webkit-line-clamp: 2;
|
| 128 |
-
-webkit-box-orient: vertical;
|
| 129 |
-
overflow: hidden;
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
.line-clamp-3 {
|
| 133 |
-
display: -webkit-box;
|
| 134 |
-
-webkit-line-clamp: 3;
|
| 135 |
-
-webkit-box-orient: vertical;
|
| 136 |
-
overflow: hidden;
|
| 137 |
-
}
|
| 138 |
-
}
|
|
|
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
@layer base {
|
|
|
|
|
|
|
|
|
|
| 6 |
:root {
|
|
|
|
| 7 |
--background: 0 0% 100%;
|
| 8 |
+
--foreground: 222.2 84% 4.9%;
|
|
|
|
|
|
|
| 9 |
--card: 0 0% 100%;
|
| 10 |
+
--card-foreground: 222.2 84% 4.9%;
|
|
|
|
|
|
|
| 11 |
--popover: 0 0% 100%;
|
| 12 |
+
--popover-foreground: 222.2 84% 4.9%;
|
| 13 |
+
--primary: 221.2 83.2% 53.3%;
|
| 14 |
+
--primary-foreground: 210 40% 98%;
|
| 15 |
+
--secondary: 210 40% 96%;
|
| 16 |
+
--secondary-foreground: 222.2 84% 4.9%;
|
| 17 |
+
--muted: 210 40% 96%;
|
| 18 |
+
--muted-foreground: 215.4 16.3% 46.9%;
|
| 19 |
+
--accent: 210 40% 96%;
|
| 20 |
+
--accent-foreground: 222.2 84% 4.9%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
--destructive: 0 84.2% 60.2%;
|
| 22 |
+
--destructive-foreground: 210 40% 98%;
|
| 23 |
+
--border: 214.3 31.8% 91.4%;
|
| 24 |
+
--input: 214.3 31.8% 91.4%;
|
| 25 |
+
--ring: 221.2 83.2% 53.3%;
|
| 26 |
+
--radius: 0.5rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
.dark {
|
| 30 |
+
--background: 222.2 84% 4.9%;
|
| 31 |
+
--foreground: 210 40% 98%;
|
| 32 |
+
--card: 222.2 84% 4.9%;
|
| 33 |
+
--card-foreground: 210 40% 98%;
|
| 34 |
+
--popover: 222.2 84% 4.9%;
|
| 35 |
+
--popover-foreground: 210 40% 98%;
|
| 36 |
+
--primary: 217.2 91.2% 59.8%;
|
| 37 |
+
--primary-foreground: 222.2 84% 4.9%;
|
| 38 |
+
--secondary: 217.2 32.6% 17.5%;
|
| 39 |
+
--secondary-foreground: 210 40% 98%;
|
| 40 |
+
--muted: 217.2 32.6% 17.5%;
|
| 41 |
+
--muted-foreground: 215 20.2% 65.1%;
|
| 42 |
+
--accent: 217.2 32.6% 17.5%;
|
| 43 |
+
--accent-foreground: 210 40% 98%;
|
| 44 |
--destructive: 0 62.8% 30.6%;
|
| 45 |
+
--destructive-foreground: 210 40% 98%;
|
| 46 |
+
--border: 217.2 32.6% 17.5%;
|
| 47 |
+
--input: 217.2 32.6% 17.5%;
|
| 48 |
+
--ring: 224.3 76.3% 94.1%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
}
|
| 50 |
}
|
| 51 |
|
|
|
|
|
|
|
| 52 |
@layer base {
|
| 53 |
* {
|
| 54 |
@apply border-border;
|
|
|
|
| 57 |
@apply bg-background text-foreground;
|
| 58 |
}
|
| 59 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/lib/chat-storage.ts
CHANGED
|
@@ -1,132 +1,105 @@
|
|
| 1 |
-
import { ChatSession, Message
|
| 2 |
|
| 3 |
-
const
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
try {
|
| 9 |
-
const stored = localStorage.getItem(STORAGE_KEY)
|
| 10 |
-
if (!stored) {
|
| 11 |
-
return { sessions: [], current_session_id: null }
|
| 12 |
-
}
|
| 13 |
-
return JSON.parse(stored)
|
| 14 |
-
} catch (error) {
|
| 15 |
-
console.error('Failed to load chat store:', error)
|
| 16 |
-
return { sessions: [], current_session_id: null }
|
| 17 |
-
}
|
| 18 |
-
},
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
|
| 30 |
-
createSession(title?: string, model_name?: string, system_prompt?: string): ChatSession {
|
| 31 |
-
const now = Date.now()
|
| 32 |
const newSession: ChatSession = {
|
| 33 |
-
id:
|
| 34 |
-
title: title || `New Chat ${new Date(
|
| 35 |
messages: [],
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
}
|
| 41 |
-
|
| 42 |
-
// Save the new session to localStorage immediately
|
| 43 |
-
const store = this.load()
|
| 44 |
-
store.sessions.unshift(newSession) // Add to beginning of array
|
| 45 |
-
this.save(store)
|
| 46 |
-
|
| 47 |
-
return newSession
|
| 48 |
-
},
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
const session = store.sessions.find(s => s.id === sessionId)
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
...message,
|
| 58 |
-
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
| 59 |
-
timestamp: Date.now(),
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
session.messages.push(newMessage)
|
| 63 |
-
session.updated_at = Date.now()
|
| 64 |
-
|
| 65 |
-
// Update session title based on first user message
|
| 66 |
-
if (session.messages.length === 1 && message.role === 'user') {
|
| 67 |
-
session.title = message.content.slice(0, 50) + (message.content.length > 50 ? '...' : '')
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
this.save(store)
|
| 71 |
-
}
|
| 72 |
-
},
|
| 73 |
-
|
| 74 |
-
// Get session by ID
|
| 75 |
-
getSession(sessionId: string): ChatSession | null {
|
| 76 |
-
const store = this.load()
|
| 77 |
-
return store.sessions.find(s => s.id === sessionId) || null
|
| 78 |
-
},
|
| 79 |
|
| 80 |
-
// Update session
|
| 81 |
updateSession(sessionId: string, updates: Partial<ChatSession>): void {
|
| 82 |
-
const
|
| 83 |
-
const sessionIndex =
|
| 84 |
|
| 85 |
if (sessionIndex !== -1) {
|
| 86 |
-
|
| 87 |
-
...
|
| 88 |
...updates,
|
| 89 |
-
|
| 90 |
}
|
| 91 |
-
|
| 92 |
}
|
| 93 |
-
}
|
| 94 |
|
| 95 |
-
// Delete session
|
| 96 |
deleteSession(sessionId: string): void {
|
| 97 |
-
const
|
| 98 |
-
|
|
|
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
store.current_session_id = store.sessions.length > 0 ? store.sessions[0].id : null
|
| 103 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
store.current_session_id = sessionId
|
| 112 |
-
this.save(store)
|
| 113 |
-
},
|
| 114 |
|
| 115 |
-
// Get current session
|
| 116 |
getCurrentSession(): ChatSession | null {
|
| 117 |
-
const
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
},
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
return store.sessions.sort((a, b) => b.updated_at - a.updated_at)
|
| 126 |
-
},
|
| 127 |
|
| 128 |
-
// Clear all data
|
| 129 |
clear(): void {
|
| 130 |
-
localStorage.removeItem(
|
| 131 |
-
|
|
|
|
| 132 |
}
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ChatSession, Message } from '@/types/chat'
|
| 2 |
|
| 3 |
+
const STORAGE_KEYS = {
|
| 4 |
+
sessions: 'edge-llm-sessions',
|
| 5 |
+
currentSession: 'edge-llm-current-session'
|
| 6 |
+
}
|
| 7 |
|
| 8 |
+
function generateId(): string {
|
| 9 |
+
return Math.random().toString(36).substring(2) + Date.now().toString(36)
|
| 10 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
function generateMessageId(): string {
|
| 13 |
+
return generateId()
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
class ChatStorageManager {
|
| 17 |
+
getAllSessions(): ChatSession[] {
|
| 18 |
+
const stored = localStorage.getItem(STORAGE_KEYS.sessions)
|
| 19 |
+
return stored ? JSON.parse(stored) : []
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
getSession(sessionId: string): ChatSession | null {
|
| 23 |
+
const sessions = this.getAllSessions()
|
| 24 |
+
return sessions.find(s => s.id === sessionId) || null
|
| 25 |
+
}
|
| 26 |
|
| 27 |
+
createSession(title?: string, model?: string, systemPrompt?: string): ChatSession {
|
|
|
|
|
|
|
| 28 |
const newSession: ChatSession = {
|
| 29 |
+
id: generateId(),
|
| 30 |
+
title: title || `New Chat ${new Date().toLocaleString()}`,
|
| 31 |
messages: [],
|
| 32 |
+
model,
|
| 33 |
+
systemPrompt,
|
| 34 |
+
createdAt: Date.now(),
|
| 35 |
+
updatedAt: Date.now()
|
| 36 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
const sessions = this.getAllSessions()
|
| 39 |
+
sessions.unshift(newSession)
|
| 40 |
+
localStorage.setItem(STORAGE_KEYS.sessions, JSON.stringify(sessions))
|
|
|
|
| 41 |
|
| 42 |
+
return newSession
|
| 43 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
|
|
|
| 45 |
updateSession(sessionId: string, updates: Partial<ChatSession>): void {
|
| 46 |
+
const sessions = this.getAllSessions()
|
| 47 |
+
const sessionIndex = sessions.findIndex(s => s.id === sessionId)
|
| 48 |
|
| 49 |
if (sessionIndex !== -1) {
|
| 50 |
+
sessions[sessionIndex] = {
|
| 51 |
+
...sessions[sessionIndex],
|
| 52 |
...updates,
|
| 53 |
+
updatedAt: Date.now()
|
| 54 |
}
|
| 55 |
+
localStorage.setItem(STORAGE_KEYS.sessions, JSON.stringify(sessions))
|
| 56 |
}
|
| 57 |
+
}
|
| 58 |
|
|
|
|
| 59 |
deleteSession(sessionId: string): void {
|
| 60 |
+
const sessions = this.getAllSessions()
|
| 61 |
+
const filtered = sessions.filter(s => s.id !== sessionId)
|
| 62 |
+
localStorage.setItem(STORAGE_KEYS.sessions, JSON.stringify(filtered))
|
| 63 |
|
| 64 |
+
if (this.getCurrentSessionId() === sessionId) {
|
| 65 |
+
localStorage.removeItem(STORAGE_KEYS.currentSession)
|
|
|
|
| 66 |
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
addMessageToSession(sessionId: string, message: Omit<Message, 'id' | 'timestamp'>): void {
|
| 70 |
+
const sessions = this.getAllSessions()
|
| 71 |
+
const sessionIndex = sessions.findIndex(s => s.id === sessionId)
|
| 72 |
|
| 73 |
+
if (sessionIndex !== -1) {
|
| 74 |
+
const newMessage: Message = {
|
| 75 |
+
...message,
|
| 76 |
+
id: generateMessageId(),
|
| 77 |
+
timestamp: Date.now()
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
sessions[sessionIndex].messages.push(newMessage)
|
| 81 |
+
sessions[sessionIndex].updatedAt = Date.now()
|
| 82 |
+
localStorage.setItem(STORAGE_KEYS.sessions, JSON.stringify(sessions))
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
|
| 86 |
+
getCurrentSessionId(): string | null {
|
| 87 |
+
return localStorage.getItem(STORAGE_KEYS.currentSession)
|
| 88 |
+
}
|
|
|
|
|
|
|
|
|
|
| 89 |
|
|
|
|
| 90 |
getCurrentSession(): ChatSession | null {
|
| 91 |
+
const currentId = this.getCurrentSessionId()
|
| 92 |
+
return currentId ? this.getSession(currentId) : null
|
| 93 |
+
}
|
|
|
|
| 94 |
|
| 95 |
+
setCurrentSession(sessionId: string): void {
|
| 96 |
+
localStorage.setItem(STORAGE_KEYS.currentSession, sessionId)
|
| 97 |
+
}
|
|
|
|
|
|
|
| 98 |
|
|
|
|
| 99 |
clear(): void {
|
| 100 |
+
localStorage.removeItem(STORAGE_KEYS.sessions)
|
| 101 |
+
localStorage.removeItem(STORAGE_KEYS.currentSession)
|
| 102 |
+
}
|
| 103 |
}
|
| 104 |
+
|
| 105 |
+
export const chatStorage = new ChatStorageManager()
|
frontend/src/lib/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
import { twMerge } from "tailwind-merge"
|
| 3 |
|
| 4 |
export function cn(...inputs: ClassValue[]) {
|
|
|
|
| 1 |
+
import { type ClassValue, clsx } from "clsx"
|
| 2 |
import { twMerge } from "tailwind-merge"
|
| 3 |
|
| 4 |
export function cn(...inputs: ClassValue[]) {
|
frontend/src/pages/Home.tsx
CHANGED
|
@@ -8,8 +8,7 @@ import {
|
|
| 8 |
Zap,
|
| 9 |
Shield,
|
| 10 |
Cpu,
|
| 11 |
-
ArrowRight
|
| 12 |
-
Download
|
| 13 |
} from 'lucide-react'
|
| 14 |
import { Link } from 'react-router-dom'
|
| 15 |
|
|
@@ -67,115 +66,106 @@ const quickActions = [
|
|
| 67 |
export function Home() {
|
| 68 |
return (
|
| 69 |
<div className="min-h-screen bg-background">
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
<div className="
|
| 73 |
-
<div className="flex items-center
|
| 74 |
-
<
|
| 75 |
-
|
| 76 |
-
</div>
|
| 77 |
-
</div>
|
| 78 |
-
</div>
|
| 79 |
-
|
| 80 |
-
<div className="flex-1 p-6">
|
| 81 |
-
<div className="max-w-6xl mx-auto space-y-8">
|
| 82 |
-
|
| 83 |
-
{/* Hero Section */}
|
| 84 |
-
<div className="text-center space-y-4">
|
| 85 |
-
<div className="inline-flex items-center gap-2 px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm">
|
| 86 |
-
<Cpu className="h-4 w-4" />
|
| 87 |
-
Local AI Platform
|
| 88 |
</div>
|
| 89 |
-
<h1 className="text-4xl font-bold tracking-tight">
|
| 90 |
-
Welcome to Edge LLM
|
| 91 |
-
</h1>
|
| 92 |
-
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
| 93 |
-
A powerful local AI platform for running language models privately on your machine.
|
| 94 |
-
Experience the future of AI without compromising your privacy.
|
| 95 |
-
</p>
|
| 96 |
</div>
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
</Card>
|
| 115 |
-
))}
|
| 116 |
</div>
|
|
|
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
<
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
<CardContent className="p-6 space-y-3">
|
| 131 |
-
<feature.icon className={`h-8 w-8 ${feature.color}`} />
|
| 132 |
-
<div>
|
| 133 |
-
<h3 className="font-semibold">{feature.title}</h3>
|
| 134 |
-
<p className="text-sm text-muted-foreground">{feature.description}</p>
|
| 135 |
-
</div>
|
| 136 |
-
</CardContent>
|
| 137 |
-
</Card>
|
| 138 |
-
))}
|
| 139 |
-
</div>
|
| 140 |
-
</div>
|
| 141 |
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
<Card>
|
| 144 |
<CardHeader>
|
| 145 |
-
<CardTitle
|
| 146 |
-
<Download className="h-5 w-5" />
|
| 147 |
-
Getting Started
|
| 148 |
-
</CardTitle>
|
| 149 |
</CardHeader>
|
| 150 |
-
<CardContent
|
| 151 |
-
<div className="
|
| 152 |
<div className="space-y-2">
|
| 153 |
<div className="flex items-center gap-2">
|
| 154 |
-
<div className="w-6 h-6 bg-
|
| 155 |
1
|
| 156 |
</div>
|
| 157 |
-
<h4 className="font-medium">
|
| 158 |
</div>
|
| 159 |
<p className="text-sm text-muted-foreground pl-8">
|
| 160 |
-
|
| 161 |
</p>
|
| 162 |
</div>
|
| 163 |
|
| 164 |
<div className="space-y-2">
|
| 165 |
<div className="flex items-center gap-2">
|
| 166 |
-
<div className="w-6 h-6 bg-
|
| 167 |
2
|
| 168 |
</div>
|
| 169 |
-
<h4 className="font-medium">Load
|
| 170 |
</div>
|
| 171 |
<p className="text-sm text-muted-foreground pl-8">
|
| 172 |
-
|
| 173 |
</p>
|
| 174 |
</div>
|
| 175 |
|
| 176 |
<div className="space-y-2">
|
| 177 |
<div className="flex items-center gap-2">
|
| 178 |
-
<div className="w-6 h-6 bg-
|
| 179 |
3
|
| 180 |
</div>
|
| 181 |
<h4 className="font-medium">Start Chatting</h4>
|
|
@@ -203,29 +193,34 @@ export function Home() {
|
|
| 203 |
<CardTitle>System Status</CardTitle>
|
| 204 |
</CardHeader>
|
| 205 |
<CardContent>
|
| 206 |
-
<div className="
|
| 207 |
-
<div className="flex items-center
|
| 208 |
-
<
|
| 209 |
-
<
|
| 210 |
-
<
|
| 211 |
-
|
| 212 |
-
</
|
| 213 |
</div>
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
<
|
| 217 |
-
|
| 218 |
-
<p className="text-xs text-muted-foreground">Ready to load</p>
|
| 219 |
-
</div>
|
| 220 |
</div>
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
<
|
| 224 |
-
|
| 225 |
-
<p className="text-xs text-muted-foreground">Local</p>
|
| 226 |
-
</div>
|
| 227 |
</div>
|
| 228 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
</CardContent>
|
| 230 |
</Card>
|
| 231 |
</div>
|
|
|
|
| 8 |
Zap,
|
| 9 |
Shield,
|
| 10 |
Cpu,
|
| 11 |
+
ArrowRight
|
|
|
|
| 12 |
} from 'lucide-react'
|
| 13 |
import { Link } from 'react-router-dom'
|
| 14 |
|
|
|
|
| 66 |
export function Home() {
|
| 67 |
return (
|
| 68 |
<div className="min-h-screen bg-background">
|
| 69 |
+
<div className="container mx-auto px-4 py-8">
|
| 70 |
+
{/* Hero Section */}
|
| 71 |
+
<div className="text-center mb-12">
|
| 72 |
+
<div className="flex items-center justify-center mb-4">
|
| 73 |
+
<div className="w-16 h-16 bg-primary rounded-2xl flex items-center justify-center">
|
| 74 |
+
<Brain className="h-8 w-8 text-primary-foreground" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
</div>
|
| 77 |
+
<h1 className="text-4xl font-bold mb-4">Edge LLM</h1>
|
| 78 |
+
<p className="text-xl text-muted-foreground max-w-2xl mx-auto mb-8">
|
| 79 |
+
Your local AI companion. Run powerful language models on your own hardware with complete privacy and control.
|
| 80 |
+
</p>
|
| 81 |
+
<div className="flex items-center justify-center gap-4">
|
| 82 |
+
<Link to="/playground">
|
| 83 |
+
<Button size="lg">
|
| 84 |
+
<MessageSquare className="h-5 w-5 mr-2" />
|
| 85 |
+
Start Chatting
|
| 86 |
+
</Button>
|
| 87 |
+
</Link>
|
| 88 |
+
<Link to="/models">
|
| 89 |
+
<Button variant="outline" size="lg">
|
| 90 |
+
<BookOpen className="h-5 w-5 mr-2" />
|
| 91 |
+
Browse Models
|
| 92 |
+
</Button>
|
| 93 |
+
</Link>
|
|
|
|
|
|
|
| 94 |
</div>
|
| 95 |
+
</div>
|
| 96 |
|
| 97 |
+
{/* Features Grid */}
|
| 98 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
| 99 |
+
{features.map((feature, index) => (
|
| 100 |
+
<Card key={index} className="text-center">
|
| 101 |
+
<CardContent className="pt-6">
|
| 102 |
+
<feature.icon className={`h-12 w-12 mx-auto mb-4 ${feature.color}`} />
|
| 103 |
+
<h3 className="font-semibold mb-2">{feature.title}</h3>
|
| 104 |
+
<p className="text-sm text-muted-foreground">{feature.description}</p>
|
| 105 |
+
</CardContent>
|
| 106 |
+
</Card>
|
| 107 |
+
))}
|
| 108 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
+
{/* Quick Actions */}
|
| 111 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
| 112 |
+
{quickActions.map((action, index) => (
|
| 113 |
+
<Card key={index} className="hover:shadow-lg transition-shadow">
|
| 114 |
+
<CardContent className="pt-6">
|
| 115 |
+
<div className="flex items-center gap-3 mb-3">
|
| 116 |
+
<action.icon className="h-6 w-6 text-primary" />
|
| 117 |
+
<h3 className="font-semibold">{action.title}</h3>
|
| 118 |
+
</div>
|
| 119 |
+
<p className="text-sm text-muted-foreground mb-4">{action.description}</p>
|
| 120 |
+
<Link to={action.href}>
|
| 121 |
+
<Button
|
| 122 |
+
variant={action.primary ? "default" : "outline"}
|
| 123 |
+
className="w-full"
|
| 124 |
+
>
|
| 125 |
+
Get Started
|
| 126 |
+
<ArrowRight className="h-4 w-4 ml-2" />
|
| 127 |
+
</Button>
|
| 128 |
+
</Link>
|
| 129 |
+
</CardContent>
|
| 130 |
+
</Card>
|
| 131 |
+
))}
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
{/* Getting Started */}
|
| 135 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
| 136 |
<Card>
|
| 137 |
<CardHeader>
|
| 138 |
+
<CardTitle>Getting Started</CardTitle>
|
|
|
|
|
|
|
|
|
|
| 139 |
</CardHeader>
|
| 140 |
+
<CardContent>
|
| 141 |
+
<div className="space-y-4">
|
| 142 |
<div className="space-y-2">
|
| 143 |
<div className="flex items-center gap-2">
|
| 144 |
+
<div className="w-6 h-6 bg-primary text-white rounded-full flex items-center justify-center text-sm font-medium">
|
| 145 |
1
|
| 146 |
</div>
|
| 147 |
+
<h4 className="font-medium">Browse Available Models</h4>
|
| 148 |
</div>
|
| 149 |
<p className="text-sm text-muted-foreground pl-8">
|
| 150 |
+
Check out our model catalog to see what's available for your use case.
|
| 151 |
</p>
|
| 152 |
</div>
|
| 153 |
|
| 154 |
<div className="space-y-2">
|
| 155 |
<div className="flex items-center gap-2">
|
| 156 |
+
<div className="w-6 h-6 bg-primary text-white rounded-full flex items-center justify-center text-sm font-medium">
|
| 157 |
2
|
| 158 |
</div>
|
| 159 |
+
<h4 className="font-medium">Load a Model</h4>
|
| 160 |
</div>
|
| 161 |
<p className="text-sm text-muted-foreground pl-8">
|
| 162 |
+
Select and load a model that fits your hardware and requirements.
|
| 163 |
</p>
|
| 164 |
</div>
|
| 165 |
|
| 166 |
<div className="space-y-2">
|
| 167 |
<div className="flex items-center gap-2">
|
| 168 |
+
<div className="w-6 h-6 bg-primary text-white rounded-full flex items-center justify-center text-sm font-medium">
|
| 169 |
3
|
| 170 |
</div>
|
| 171 |
<h4 className="font-medium">Start Chatting</h4>
|
|
|
|
| 193 |
<CardTitle>System Status</CardTitle>
|
| 194 |
</CardHeader>
|
| 195 |
<CardContent>
|
| 196 |
+
<div className="space-y-4">
|
| 197 |
+
<div className="flex items-center justify-between">
|
| 198 |
+
<span className="text-sm">Backend Status</span>
|
| 199 |
+
<span className="inline-flex items-center gap-1 text-sm text-green-600">
|
| 200 |
+
<div className="w-2 h-2 bg-green-600 rounded-full"></div>
|
| 201 |
+
Online
|
| 202 |
+
</span>
|
| 203 |
</div>
|
| 204 |
+
|
| 205 |
+
<div className="flex items-center justify-between">
|
| 206 |
+
<span className="text-sm">Models Loaded</span>
|
| 207 |
+
<span className="text-sm font-medium">Ready</span>
|
|
|
|
|
|
|
| 208 |
</div>
|
| 209 |
+
|
| 210 |
+
<div className="flex items-center justify-between">
|
| 211 |
+
<span className="text-sm">Memory Usage</span>
|
| 212 |
+
<span className="text-sm font-medium">Optimized</span>
|
|
|
|
|
|
|
| 213 |
</div>
|
| 214 |
</div>
|
| 215 |
+
|
| 216 |
+
<div className="pt-4 border-t">
|
| 217 |
+
<Link to="/settings">
|
| 218 |
+
<Button variant="outline" className="w-full md:w-auto">
|
| 219 |
+
<Cpu className="h-4 w-4 mr-2" />
|
| 220 |
+
View Settings
|
| 221 |
+
</Button>
|
| 222 |
+
</Link>
|
| 223 |
+
</div>
|
| 224 |
</CardContent>
|
| 225 |
</Card>
|
| 226 |
</div>
|
frontend/src/pages/Models.tsx
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react'
|
| 2 |
+
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
|
| 3 |
+
import { Button } from '@/components/ui/button'
|
| 4 |
+
import { Badge } from '@/components/ui/badge'
|
| 5 |
+
import {
|
| 6 |
+
BookOpen,
|
| 7 |
+
Brain,
|
| 8 |
+
Zap,
|
| 9 |
+
Download,
|
| 10 |
+
Trash2,
|
| 11 |
+
Loader2,
|
| 12 |
+
Info,
|
| 13 |
+
CheckCircle,
|
| 14 |
+
Cloud,
|
| 15 |
+
HardDrive
|
| 16 |
+
} from 'lucide-react'
|
| 17 |
+
|
| 18 |
+
interface ModelInfo {
|
| 19 |
+
model_name: string
|
| 20 |
+
name: string
|
| 21 |
+
supports_thinking: boolean
|
| 22 |
+
description: string
|
| 23 |
+
size_gb: string
|
| 24 |
+
is_loaded: boolean
|
| 25 |
+
type: 'local' | 'api'
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
interface ModelsResponse {
|
| 29 |
+
models: ModelInfo[]
|
| 30 |
+
current_model: string
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export function Models() {
|
| 34 |
+
const [models, setModels] = useState<ModelInfo[]>([])
|
| 35 |
+
const [loading, setLoading] = useState(true)
|
| 36 |
+
const [modelLoading, setModelLoading] = useState<string | null>(null)
|
| 37 |
+
|
| 38 |
+
useEffect(() => {
|
| 39 |
+
fetchModels()
|
| 40 |
+
}, [])
|
| 41 |
+
|
| 42 |
+
const fetchModels = async () => {
|
| 43 |
+
try {
|
| 44 |
+
const baseUrl = window.location.hostname === 'localhost' ? `${window.location.protocol}//${window.location.host}` : ''
|
| 45 |
+
const res = await fetch(`${baseUrl}/models`)
|
| 46 |
+
if (res.ok) {
|
| 47 |
+
const data: ModelsResponse = await res.json()
|
| 48 |
+
setModels(data.models)
|
| 49 |
+
}
|
| 50 |
+
} catch (err) {
|
| 51 |
+
console.error('Failed to fetch models:', err)
|
| 52 |
+
} finally {
|
| 53 |
+
setLoading(false)
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
const handleLoadModel = async (modelName: string) => {
|
| 58 |
+
setModelLoading(modelName)
|
| 59 |
+
try {
|
| 60 |
+
const baseUrl = window.location.hostname === 'localhost' ? `${window.location.protocol}//${window.location.host}` : ''
|
| 61 |
+
const res = await fetch(`${baseUrl}/load-model`, {
|
| 62 |
+
method: 'POST',
|
| 63 |
+
headers: { 'Content-Type': 'application/json' },
|
| 64 |
+
body: JSON.stringify({ model_name: modelName }),
|
| 65 |
+
})
|
| 66 |
+
|
| 67 |
+
if (res.ok) {
|
| 68 |
+
await fetchModels()
|
| 69 |
+
}
|
| 70 |
+
} catch (err) {
|
| 71 |
+
console.error('Failed to load model:', err)
|
| 72 |
+
} finally {
|
| 73 |
+
setModelLoading(null)
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const handleUnloadModel = async (modelName: string) => {
|
| 78 |
+
try {
|
| 79 |
+
const baseUrl = window.location.hostname === 'localhost' ? `${window.location.protocol}//${window.location.host}` : ''
|
| 80 |
+
const res = await fetch(`${baseUrl}/unload-model`, {
|
| 81 |
+
method: 'POST',
|
| 82 |
+
headers: { 'Content-Type': 'application/json' },
|
| 83 |
+
body: JSON.stringify({ model_name: modelName }),
|
| 84 |
+
})
|
| 85 |
+
|
| 86 |
+
if (res.ok) {
|
| 87 |
+
await fetchModels()
|
| 88 |
+
}
|
| 89 |
+
} catch (err) {
|
| 90 |
+
console.error('Failed to unload model:', err)
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
if (loading) {
|
| 95 |
+
return (
|
| 96 |
+
<div className="min-h-screen bg-background">
|
| 97 |
+
<div className="border-b">
|
| 98 |
+
<div className="flex h-14 items-center px-6">
|
| 99 |
+
<div className="flex items-center gap-2">
|
| 100 |
+
<BookOpen className="h-5 w-5" />
|
| 101 |
+
<h1 className="text-lg font-semibold">Model Catalog</h1>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
<div className="flex items-center justify-center h-64">
|
| 106 |
+
<Loader2 className="h-8 w-8 animate-spin" />
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
)
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
return (
|
| 113 |
+
<div className="min-h-screen bg-background">
|
| 114 |
+
{/* Header */}
|
| 115 |
+
<div className="border-b">
|
| 116 |
+
<div className="flex h-14 items-center px-6">
|
| 117 |
+
<div className="flex items-center gap-2">
|
| 118 |
+
<BookOpen className="h-5 w-5" />
|
| 119 |
+
<h1 className="text-lg font-semibold">Model Catalog</h1>
|
| 120 |
+
</div>
|
| 121 |
+
<div className="ml-auto">
|
| 122 |
+
<Button variant="outline" size="sm" onClick={fetchModels}>
|
| 123 |
+
Refresh
|
| 124 |
+
</Button>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
<div className="flex-1 p-6">
|
| 130 |
+
<div className="max-w-4xl mx-auto space-y-6">
|
| 131 |
+
|
| 132 |
+
{/* Info Card */}
|
| 133 |
+
<Card className="bg-blue-50 border-blue-200">
|
| 134 |
+
<CardContent className="pt-6">
|
| 135 |
+
<div className="flex items-start gap-3">
|
| 136 |
+
<Info className="h-5 w-5 text-blue-600 mt-0.5" />
|
| 137 |
+
<div>
|
| 138 |
+
<h3 className="font-medium text-blue-900">Model Management</h3>
|
| 139 |
+
<p className="text-sm text-blue-700 mt-1">
|
| 140 |
+
Load models to use them in the playground. Models are cached locally for faster access.
|
| 141 |
+
Each model requires significant storage space and initial download time.
|
| 142 |
+
</p>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</CardContent>
|
| 146 |
+
</Card>
|
| 147 |
+
|
| 148 |
+
{/* API Models Section */}
|
| 149 |
+
<div>
|
| 150 |
+
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
| 151 |
+
<Cloud className="h-5 w-5" />
|
| 152 |
+
API Models
|
| 153 |
+
<Badge variant="outline" className="text-xs">Cloud-Powered</Badge>
|
| 154 |
+
</h2>
|
| 155 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
| 156 |
+
{models.filter(m => m.type === 'api').map((model) => (
|
| 157 |
+
<ModelCard
|
| 158 |
+
key={model.model_name}
|
| 159 |
+
model={model}
|
| 160 |
+
modelLoading={modelLoading}
|
| 161 |
+
onLoad={handleLoadModel}
|
| 162 |
+
onUnload={handleUnloadModel}
|
| 163 |
+
/>
|
| 164 |
+
))}
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
{/* Local Models Section */}
|
| 169 |
+
<div>
|
| 170 |
+
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
| 171 |
+
<HardDrive className="h-5 w-5" />
|
| 172 |
+
Local Models
|
| 173 |
+
<Badge variant="outline" className="text-xs">Privacy-First</Badge>
|
| 174 |
+
</h2>
|
| 175 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 176 |
+
{models.filter(m => m.type === 'local').map((model) => (
|
| 177 |
+
<ModelCard
|
| 178 |
+
key={model.model_name}
|
| 179 |
+
model={model}
|
| 180 |
+
modelLoading={modelLoading}
|
| 181 |
+
onLoad={handleLoadModel}
|
| 182 |
+
onUnload={handleUnloadModel}
|
| 183 |
+
/>
|
| 184 |
+
))}
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
{/* Stats Card */}
|
| 189 |
+
<Card>
|
| 190 |
+
<CardHeader>
|
| 191 |
+
<CardTitle>Model Statistics</CardTitle>
|
| 192 |
+
</CardHeader>
|
| 193 |
+
<CardContent>
|
| 194 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 195 |
+
<div className="text-center">
|
| 196 |
+
<div className="text-2xl font-bold text-blue-600">{models.length}</div>
|
| 197 |
+
<div className="text-sm text-muted-foreground">Available Models</div>
|
| 198 |
+
</div>
|
| 199 |
+
<div className="text-center">
|
| 200 |
+
<div className="text-2xl font-bold text-green-600">
|
| 201 |
+
{models.filter(m => m.is_loaded).length}
|
| 202 |
+
</div>
|
| 203 |
+
<div className="text-sm text-muted-foreground">Loaded Models</div>
|
| 204 |
+
</div>
|
| 205 |
+
<div className="text-center">
|
| 206 |
+
<div className="text-2xl font-bold text-purple-600">
|
| 207 |
+
{models.filter(m => m.supports_thinking).length}
|
| 208 |
+
</div>
|
| 209 |
+
<div className="text-sm text-muted-foreground">Thinking Models</div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
</CardContent>
|
| 213 |
+
</Card>
|
| 214 |
+
</div>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
)
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// ModelCard component for reusability
|
| 221 |
+
interface ModelCardProps {
|
| 222 |
+
model: ModelInfo
|
| 223 |
+
modelLoading: string | null
|
| 224 |
+
onLoad: (modelName: string) => void
|
| 225 |
+
onUnload: (modelName: string) => void
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
function ModelCard({ model, modelLoading, onLoad, onUnload }: ModelCardProps) {
|
| 229 |
+
const isApiModel = model.type === 'api'
|
| 230 |
+
|
| 231 |
+
return (
|
| 232 |
+
<Card className="relative">
|
| 233 |
+
<CardHeader>
|
| 234 |
+
<div className="flex items-start justify-between">
|
| 235 |
+
<div className="flex items-center gap-3">
|
| 236 |
+
{isApiModel ? (
|
| 237 |
+
<Cloud className="h-6 w-6 text-blue-500" />
|
| 238 |
+
) : model.supports_thinking ? (
|
| 239 |
+
<Brain className="h-6 w-6 text-blue-500" />
|
| 240 |
+
) : (
|
| 241 |
+
<Zap className="h-6 w-6 text-green-500" />
|
| 242 |
+
)}
|
| 243 |
+
<div>
|
| 244 |
+
<CardTitle className="text-lg">{model.name}</CardTitle>
|
| 245 |
+
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
| 246 |
+
{isApiModel ? (
|
| 247 |
+
<Badge variant="default" className="bg-blue-600">
|
| 248 |
+
<Cloud className="h-3 w-3 mr-1" />
|
| 249 |
+
API Model
|
| 250 |
+
</Badge>
|
| 251 |
+
) : (
|
| 252 |
+
<Badge variant={model.supports_thinking ? "default" : "secondary"}>
|
| 253 |
+
<HardDrive className="h-3 w-3 mr-1" />
|
| 254 |
+
{model.supports_thinking ? "Thinking Model" : "Instruction Model"}
|
| 255 |
+
</Badge>
|
| 256 |
+
)}
|
| 257 |
+
{model.is_loaded && (
|
| 258 |
+
<Badge variant="outline" className="text-green-600 border-green-600">
|
| 259 |
+
<CheckCircle className="h-3 w-3 mr-1" />
|
| 260 |
+
{isApiModel ? "Ready" : "Loaded"}
|
| 261 |
+
</Badge>
|
| 262 |
+
)}
|
| 263 |
+
</div>
|
| 264 |
+
</div>
|
| 265 |
+
</div>
|
| 266 |
+
</div>
|
| 267 |
+
</CardHeader>
|
| 268 |
+
<CardContent className="space-y-4">
|
| 269 |
+
<div>
|
| 270 |
+
<p className="text-sm text-muted-foreground mb-2">{model.description}</p>
|
| 271 |
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
| 272 |
+
<span>Size: {model.size_gb}</span>
|
| 273 |
+
{!isApiModel && <span>Format: Safetensors</span>}
|
| 274 |
+
{isApiModel && <span>Type: Cloud API</span>}
|
| 275 |
+
</div>
|
| 276 |
+
</div>
|
| 277 |
+
|
| 278 |
+
<div className="space-y-2">
|
| 279 |
+
<h4 className="text-sm font-medium">Capabilities</h4>
|
| 280 |
+
<div className="flex flex-wrap gap-2">
|
| 281 |
+
<Badge variant="outline" className="text-xs">Text Generation</Badge>
|
| 282 |
+
<Badge variant="outline" className="text-xs">Conversation</Badge>
|
| 283 |
+
<Badge variant="outline" className="text-xs">Code</Badge>
|
| 284 |
+
{model.supports_thinking && (
|
| 285 |
+
<Badge variant="outline" className="text-xs">Reasoning</Badge>
|
| 286 |
+
)}
|
| 287 |
+
{isApiModel && model.model_name.includes('vl') && (
|
| 288 |
+
<Badge variant="outline" className="text-xs">Vision</Badge>
|
| 289 |
+
)}
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
|
| 293 |
+
<div className="pt-2 border-t">
|
| 294 |
+
{model.is_loaded ? (
|
| 295 |
+
<div className="flex gap-2">
|
| 296 |
+
{!isApiModel && (
|
| 297 |
+
<Button
|
| 298 |
+
variant="outline"
|
| 299 |
+
size="sm"
|
| 300 |
+
onClick={() => onUnload(model.model_name)}
|
| 301 |
+
className="flex-1"
|
| 302 |
+
>
|
| 303 |
+
<Trash2 className="h-4 w-4 mr-2" />
|
| 304 |
+
Unload
|
| 305 |
+
</Button>
|
| 306 |
+
)}
|
| 307 |
+
<Button size="sm" className="flex-1" asChild>
|
| 308 |
+
<a href="/playground">Use in Playground</a>
|
| 309 |
+
</Button>
|
| 310 |
+
</div>
|
| 311 |
+
) : (
|
| 312 |
+
<Button
|
| 313 |
+
onClick={() => onLoad(model.model_name)}
|
| 314 |
+
disabled={modelLoading === model.model_name}
|
| 315 |
+
className="w-full"
|
| 316 |
+
size="sm"
|
| 317 |
+
>
|
| 318 |
+
{modelLoading === model.model_name ? (
|
| 319 |
+
<>
|
| 320 |
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
| 321 |
+
{isApiModel ? "Connecting..." : "Loading..."}
|
| 322 |
+
</>
|
| 323 |
+
) : (
|
| 324 |
+
<>
|
| 325 |
+
{isApiModel ? (
|
| 326 |
+
<Cloud className="h-4 w-4 mr-2" />
|
| 327 |
+
) : (
|
| 328 |
+
<Download className="h-4 w-4 mr-2" />
|
| 329 |
+
)}
|
| 330 |
+
{isApiModel ? "Connect" : "Load Model"}
|
| 331 |
+
</>
|
| 332 |
+
)}
|
| 333 |
+
</Button>
|
| 334 |
+
)}
|
| 335 |
+
</div>
|
| 336 |
+
</CardContent>
|
| 337 |
+
</Card>
|
| 338 |
+
)
|
| 339 |
+
}
|
frontend/src/pages/Playground.tsx
CHANGED
|
@@ -5,29 +5,35 @@ import { Slider } from '@/components/ui/slider'
|
|
| 5 |
import { Label } from '@/components/ui/label'
|
| 6 |
import { Badge } from '@/components/ui/badge'
|
| 7 |
import {
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
import {
|
| 18 |
Collapsible,
|
| 19 |
CollapsibleContent,
|
| 20 |
CollapsibleTrigger
|
| 21 |
} from '@/components/ui/collapsible'
|
| 22 |
-
import {
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
import { useChat } from '@/hooks/useChat'
|
| 25 |
import {
|
| 26 |
-
Loader2,
|
| 27 |
Brain,
|
| 28 |
Zap,
|
| 29 |
-
Download,
|
| 30 |
-
Trash2,
|
| 31 |
ChevronDown,
|
| 32 |
MessageSquare,
|
| 33 |
RotateCcw,
|
|
@@ -37,7 +43,13 @@ import {
|
|
| 37 |
History,
|
| 38 |
Settings,
|
| 39 |
PanelLeftOpen,
|
| 40 |
-
PanelLeftClose
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
} from 'lucide-react'
|
| 42 |
|
| 43 |
interface ModelInfo {
|
|
@@ -47,6 +59,7 @@ interface ModelInfo {
|
|
| 47 |
description: string
|
| 48 |
size_gb: string
|
| 49 |
is_loaded: boolean
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
interface ModelsResponse {
|
|
@@ -63,7 +76,7 @@ export function Playground() {
|
|
| 63 |
createNewSession,
|
| 64 |
selectSession,
|
| 65 |
deleteSession,
|
| 66 |
-
|
| 67 |
messages,
|
| 68 |
input,
|
| 69 |
setInput,
|
|
@@ -83,16 +96,12 @@ export function Playground() {
|
|
| 83 |
// UI state
|
| 84 |
const [showSessions, setShowSessions] = useState(false)
|
| 85 |
const [isSystemPromptOpen, setIsSystemPromptOpen] = useState(false)
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
// Model management state
|
| 88 |
const [models, setModels] = useState<ModelInfo[]>([])
|
| 89 |
-
const [modelLoading, setModelLoading] = useState<string | null>(null)
|
| 90 |
-
const [showLoadConfirm, setShowLoadConfirm] = useState(false)
|
| 91 |
-
const [showUnloadConfirm, setShowUnloadConfirm] = useState(false)
|
| 92 |
-
const [pendingModelAction, setPendingModelAction] = useState<{
|
| 93 |
-
action: 'load' | 'unload'
|
| 94 |
-
model: ModelInfo | null
|
| 95 |
-
}>({ action: 'load', model: null })
|
| 96 |
|
| 97 |
// Preset system prompts
|
| 98 |
const systemPromptPresets = [
|
|
@@ -153,108 +162,168 @@ export function Playground() {
|
|
| 153 |
|
| 154 |
// Update selected model when models change
|
| 155 |
useEffect(() => {
|
| 156 |
-
if
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
| 160 |
}
|
| 161 |
}
|
| 162 |
}, [models, selectedModel, setSelectedModel])
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
if (
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
}
|
|
|
|
|
|
|
| 179 |
}
|
| 180 |
}
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
-
}
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
setShowLoadConfirm(true)
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
const handleUnloadModelClick = (model: ModelInfo) => {
|
| 192 |
-
setPendingModelAction({ action: 'unload', model })
|
| 193 |
-
setShowUnloadConfirm(true)
|
| 194 |
-
}
|
| 195 |
|
| 196 |
-
const
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
setModelLoading(model.model_name)
|
| 201 |
setShowLoadConfirm(false)
|
|
|
|
| 202 |
|
| 203 |
try {
|
| 204 |
-
const
|
|
|
|
| 205 |
method: 'POST',
|
| 206 |
headers: { 'Content-Type': 'application/json' },
|
| 207 |
-
body: JSON.stringify({ model_name:
|
| 208 |
})
|
| 209 |
|
| 210 |
-
if (
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
// Set as selected model
|
| 214 |
-
setSelectedModel(model.model_name)
|
| 215 |
} else {
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
}
|
| 219 |
-
} catch (err) {
|
| 220 |
-
console.error(`Failed to load model: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
| 221 |
} finally {
|
| 222 |
-
|
|
|
|
| 223 |
}
|
| 224 |
}
|
| 225 |
|
| 226 |
-
const
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
setShowUnloadConfirm(false)
|
| 231 |
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
if (res.ok) {
|
| 240 |
-
await
|
|
|
|
| 241 |
|
| 242 |
-
//
|
| 243 |
-
if (selectedModel
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
|
|
|
|
|
|
| 248 |
}
|
| 249 |
-
} else {
|
| 250 |
-
const errorData = await res.json()
|
| 251 |
-
console.error(`Failed to unload model: ${errorData.detail || 'Unknown error'}`)
|
| 252 |
}
|
| 253 |
} catch (err) {
|
| 254 |
-
console.error(
|
| 255 |
}
|
| 256 |
}
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
const handleSamplePromptClick = (samplePrompt: string) => {
|
| 259 |
setInput(samplePrompt)
|
| 260 |
}
|
|
@@ -267,14 +336,44 @@ export function Playground() {
|
|
| 267 |
fixed inset-y-0 left-0 z-50 w-80 bg-background border-r transition-transform duration-300 ease-in-out
|
| 268 |
lg:translate-x-0 lg:static lg:inset-0
|
| 269 |
`}>
|
| 270 |
-
<
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
</div>
|
| 279 |
|
| 280 |
{/* Overlay for mobile */}
|
|
@@ -368,19 +467,22 @@ export function Playground() {
|
|
| 368 |
)}
|
| 369 |
|
| 370 |
{/* Chat Messages and Input */}
|
| 371 |
-
|
| 372 |
-
messages={messages
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
input={input}
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
: "Ask me anything..."
|
| 383 |
-
}
|
| 384 |
className="flex-1"
|
| 385 |
/>
|
| 386 |
</div>
|
|
@@ -393,87 +495,94 @@ export function Playground() {
|
|
| 393 |
<h2 className="font-semibold text-sm">Configuration</h2>
|
| 394 |
</div>
|
| 395 |
|
| 396 |
-
{/* Model
|
| 397 |
<Card>
|
| 398 |
<CardHeader>
|
| 399 |
-
<CardTitle className="text-sm">Model
|
| 400 |
</CardHeader>
|
| 401 |
<CardContent className="space-y-3">
|
| 402 |
-
{
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
>
|
| 449 |
-
<Trash2 className="h-3 w-3 mr-2" />
|
| 450 |
-
Unload
|
| 451 |
-
</Button>
|
| 452 |
-
) : (
|
| 453 |
-
<Button
|
| 454 |
-
variant="outline"
|
| 455 |
-
size="sm"
|
| 456 |
-
onClick={() => handleLoadModelClick(model)}
|
| 457 |
-
disabled={isLoading || modelLoading === model.model_name}
|
| 458 |
-
className="h-8 px-3 text-xs flex-shrink-0 min-w-[80px]"
|
| 459 |
-
>
|
| 460 |
-
{modelLoading === model.model_name ? (
|
| 461 |
-
<>
|
| 462 |
-
<Loader2 className="h-3 w-3 mr-2 animate-spin" />
|
| 463 |
-
Loading...
|
| 464 |
-
</>
|
| 465 |
-
) : (
|
| 466 |
-
<>
|
| 467 |
-
<Download className="h-3 w-3 mr-2" />
|
| 468 |
-
Load
|
| 469 |
-
</>
|
| 470 |
-
)}
|
| 471 |
-
</Button>
|
| 472 |
-
)}
|
| 473 |
-
</div>
|
| 474 |
-
</div>
|
| 475 |
-
</div>
|
| 476 |
-
))}
|
| 477 |
</CardContent>
|
| 478 |
</Card>
|
| 479 |
|
|
@@ -490,7 +599,7 @@ export function Playground() {
|
|
| 490 |
</Label>
|
| 491 |
<Slider
|
| 492 |
value={[temperature]}
|
| 493 |
-
onValueChange={(value) => setTemperature(value[0])}
|
| 494 |
min={0}
|
| 495 |
max={2}
|
| 496 |
step={0.01}
|
|
@@ -509,7 +618,7 @@ export function Playground() {
|
|
| 509 |
</Label>
|
| 510 |
<Slider
|
| 511 |
value={[maxTokens]}
|
| 512 |
-
onValueChange={(value) => setMaxTokens(value[0])}
|
| 513 |
min={100}
|
| 514 |
max={4096}
|
| 515 |
step={100}
|
|
@@ -581,7 +690,7 @@ export function Playground() {
|
|
| 581 |
<textarea
|
| 582 |
id="system-prompt"
|
| 583 |
value={systemPrompt}
|
| 584 |
-
onChange={(e) => setSystemPrompt(e.target.value)}
|
| 585 |
placeholder="Enter custom system prompt to define how the model should behave..."
|
| 586 |
className="w-full min-h-[80px] text-xs p-2 border rounded-md bg-background"
|
| 587 |
disabled={isLoading}
|
|
@@ -599,47 +708,59 @@ export function Playground() {
|
|
| 599 |
</div>
|
| 600 |
</div>
|
| 601 |
|
| 602 |
-
{/* Load
|
| 603 |
<AlertDialog open={showLoadConfirm} onOpenChange={setShowLoadConfirm}>
|
| 604 |
<AlertDialogContent>
|
| 605 |
<AlertDialogHeader>
|
| 606 |
-
<AlertDialogTitle
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
<
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
</AlertDialogDescription>
|
| 638 |
</AlertDialogHeader>
|
| 639 |
<AlertDialogFooter>
|
| 640 |
-
<AlertDialogCancel>
|
| 641 |
-
|
| 642 |
-
|
|
|
|
|
|
|
| 643 |
</AlertDialogAction>
|
| 644 |
</AlertDialogFooter>
|
| 645 |
</AlertDialogContent>
|
|
|
|
| 5 |
import { Label } from '@/components/ui/label'
|
| 6 |
import { Badge } from '@/components/ui/badge'
|
| 7 |
import {
|
| 8 |
+
Select,
|
| 9 |
+
SelectContent,
|
| 10 |
+
SelectGroup,
|
| 11 |
+
SelectItem,
|
| 12 |
+
SelectLabel,
|
| 13 |
+
SelectTrigger,
|
| 14 |
+
SelectValue,
|
| 15 |
+
} from '@/components/ui/select'
|
| 16 |
+
|
| 17 |
import {
|
| 18 |
Collapsible,
|
| 19 |
CollapsibleContent,
|
| 20 |
CollapsibleTrigger
|
| 21 |
} from '@/components/ui/collapsible'
|
| 22 |
+
import {
|
| 23 |
+
AlertDialog,
|
| 24 |
+
AlertDialogAction,
|
| 25 |
+
AlertDialogCancel,
|
| 26 |
+
AlertDialogContent,
|
| 27 |
+
AlertDialogDescription,
|
| 28 |
+
AlertDialogFooter,
|
| 29 |
+
AlertDialogHeader,
|
| 30 |
+
AlertDialogTitle,
|
| 31 |
+
} from '@/components/ui/alert-dialog'
|
| 32 |
+
import { Chat } from '@/components/ui/chat'
|
| 33 |
import { useChat } from '@/hooks/useChat'
|
| 34 |
import {
|
|
|
|
| 35 |
Brain,
|
| 36 |
Zap,
|
|
|
|
|
|
|
| 37 |
ChevronDown,
|
| 38 |
MessageSquare,
|
| 39 |
RotateCcw,
|
|
|
|
| 43 |
History,
|
| 44 |
Settings,
|
| 45 |
PanelLeftOpen,
|
| 46 |
+
PanelLeftClose,
|
| 47 |
+
Cloud,
|
| 48 |
+
BookOpen,
|
| 49 |
+
Download,
|
| 50 |
+
AlertTriangle,
|
| 51 |
+
Plus,
|
| 52 |
+
Trash2
|
| 53 |
} from 'lucide-react'
|
| 54 |
|
| 55 |
interface ModelInfo {
|
|
|
|
| 59 |
description: string
|
| 60 |
size_gb: string
|
| 61 |
is_loaded: boolean
|
| 62 |
+
type: 'local' | 'api'
|
| 63 |
}
|
| 64 |
|
| 65 |
interface ModelsResponse {
|
|
|
|
| 76 |
createNewSession,
|
| 77 |
selectSession,
|
| 78 |
deleteSession,
|
| 79 |
+
|
| 80 |
messages,
|
| 81 |
input,
|
| 82 |
setInput,
|
|
|
|
| 96 |
// UI state
|
| 97 |
const [showSessions, setShowSessions] = useState(false)
|
| 98 |
const [isSystemPromptOpen, setIsSystemPromptOpen] = useState(false)
|
| 99 |
+
const [autoLoadingModel, setAutoLoadingModel] = useState<string | null>(null)
|
| 100 |
+
const [showLoadConfirm, setShowLoadConfirm] = useState(false)
|
| 101 |
+
const [pendingModelToLoad, setPendingModelToLoad] = useState<ModelInfo | null>(null)
|
| 102 |
|
| 103 |
// Model management state
|
| 104 |
const [models, setModels] = useState<ModelInfo[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
// Preset system prompts
|
| 107 |
const systemPromptPresets = [
|
|
|
|
| 162 |
|
| 163 |
// Update selected model when models change
|
| 164 |
useEffect(() => {
|
| 165 |
+
// Only reset if the selected model no longer exists in the models list
|
| 166 |
+
if (selectedModel && !models.find(m => m.model_name === selectedModel)) {
|
| 167 |
+
const firstModel = models[0]
|
| 168 |
+
if (firstModel) {
|
| 169 |
+
setSelectedModel(firstModel.model_name)
|
| 170 |
}
|
| 171 |
}
|
| 172 |
}, [models, selectedModel, setSelectedModel])
|
| 173 |
|
| 174 |
+
// Auto-load/unload local models when selection changes
|
| 175 |
+
useEffect(() => {
|
| 176 |
+
const handleModelChange = async () => {
|
| 177 |
+
if (!selectedModel || !models.length) return
|
| 178 |
+
|
| 179 |
+
const selectedModelInfo = models.find(m => m.model_name === selectedModel)
|
| 180 |
+
if (!selectedModelInfo) return
|
| 181 |
+
|
| 182 |
+
const baseUrl = window.location.hostname === 'localhost' ? `${window.location.protocol}//${window.location.host}` : ''
|
| 183 |
+
|
| 184 |
+
// If selected model is a local model and not loaded, show confirmation
|
| 185 |
+
if (selectedModelInfo.type === 'local' && !selectedModelInfo.is_loaded) {
|
| 186 |
+
setPendingModelToLoad(selectedModelInfo)
|
| 187 |
+
setShowLoadConfirm(true)
|
| 188 |
+
return // Don't auto-load, wait for user confirmation
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// Unload other local models that are loaded but not selected
|
| 192 |
+
const loadedLocalModels = models.filter(m =>
|
| 193 |
+
m.type === 'local' &&
|
| 194 |
+
m.is_loaded &&
|
| 195 |
+
m.model_name !== selectedModel
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
for (const model of loadedLocalModels) {
|
| 199 |
+
try {
|
| 200 |
+
const response = await fetch(`${baseUrl}/unload-model`, {
|
| 201 |
+
method: 'POST',
|
| 202 |
+
headers: { 'Content-Type': 'application/json' },
|
| 203 |
+
body: JSON.stringify({ model_name: model.model_name })
|
| 204 |
+
})
|
| 205 |
+
|
| 206 |
+
if (response.ok) {
|
| 207 |
+
console.log(`✅ Auto-unloaded local model: ${model.model_name}`)
|
| 208 |
}
|
| 209 |
+
} catch (error) {
|
| 210 |
+
console.error(`Error auto-unloading model ${model.model_name}:`, error)
|
| 211 |
}
|
| 212 |
}
|
| 213 |
+
|
| 214 |
+
// Refresh models after any unloading
|
| 215 |
+
if (loadedLocalModels.length > 0) {
|
| 216 |
+
fetchModels()
|
| 217 |
+
}
|
| 218 |
}
|
|
|
|
| 219 |
|
| 220 |
+
handleModelChange()
|
| 221 |
+
}, [selectedModel, models])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
+
const handleLoadModelConfirm = async () => {
|
| 224 |
+
if (!pendingModelToLoad) return
|
| 225 |
+
|
|
|
|
|
|
|
| 226 |
setShowLoadConfirm(false)
|
| 227 |
+
setAutoLoadingModel(pendingModelToLoad.model_name)
|
| 228 |
|
| 229 |
try {
|
| 230 |
+
const baseUrl = window.location.hostname === 'localhost' ? `${window.location.protocol}//${window.location.host}` : ''
|
| 231 |
+
const response = await fetch(`${baseUrl}/load-model`, {
|
| 232 |
method: 'POST',
|
| 233 |
headers: { 'Content-Type': 'application/json' },
|
| 234 |
+
body: JSON.stringify({ model_name: pendingModelToLoad.model_name })
|
| 235 |
})
|
| 236 |
|
| 237 |
+
if (response.ok) {
|
| 238 |
+
console.log(`✅ User confirmed and loaded: ${pendingModelToLoad.model_name}`)
|
| 239 |
+
fetchModels() // Refresh model states
|
|
|
|
|
|
|
| 240 |
} else {
|
| 241 |
+
console.error(`❌ Failed to load model: ${pendingModelToLoad.model_name}`)
|
| 242 |
+
// Revert to an API model if load failed
|
| 243 |
+
const apiModel = models.find(m => m.type === 'api')
|
| 244 |
+
if (apiModel) {
|
| 245 |
+
setSelectedModel(apiModel.model_name)
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
} catch (error) {
|
| 249 |
+
console.error('Error loading model:', error)
|
| 250 |
+
// Revert to an API model if error
|
| 251 |
+
const apiModel = models.find(m => m.type === 'api')
|
| 252 |
+
if (apiModel) {
|
| 253 |
+
setSelectedModel(apiModel.model_name)
|
| 254 |
}
|
|
|
|
|
|
|
| 255 |
} finally {
|
| 256 |
+
setAutoLoadingModel(null)
|
| 257 |
+
setPendingModelToLoad(null)
|
| 258 |
}
|
| 259 |
}
|
| 260 |
|
| 261 |
+
const handleLoadModelCancel = () => {
|
| 262 |
+
setShowLoadConfirm(false)
|
| 263 |
+
setPendingModelToLoad(null)
|
|
|
|
|
|
|
| 264 |
|
| 265 |
+
// Revert to an API model
|
| 266 |
+
const apiModel = models.find(m => m.type === 'api')
|
| 267 |
+
if (apiModel) {
|
| 268 |
+
setSelectedModel(apiModel.model_name)
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
// Cleanup: unload all local models when component unmounts or user leaves
|
| 273 |
+
useEffect(() => {
|
| 274 |
+
const handlePageUnload = async () => {
|
| 275 |
+
const baseUrl = window.location.hostname === 'localhost' ? `${window.location.protocol}//${window.location.host}` : ''
|
| 276 |
+
const loadedLocalModels = models.filter(m => m.type === 'local' && m.is_loaded)
|
| 277 |
|
| 278 |
+
for (const model of loadedLocalModels) {
|
| 279 |
+
try {
|
| 280 |
+
await fetch(`${baseUrl}/unload-model`, {
|
| 281 |
+
method: 'POST',
|
| 282 |
+
headers: { 'Content-Type': 'application/json' },
|
| 283 |
+
body: JSON.stringify({ model_name: model.model_name })
|
| 284 |
+
})
|
| 285 |
+
console.log(`✅ Cleanup: unloaded ${model.model_name}`)
|
| 286 |
+
} catch (error) {
|
| 287 |
+
console.error(`Error cleaning up model ${model.model_name}:`, error)
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// Cleanup on component unmount
|
| 293 |
+
return () => {
|
| 294 |
+
handlePageUnload()
|
| 295 |
+
}
|
| 296 |
+
}, [models])
|
| 297 |
+
|
| 298 |
+
const fetchModels = async () => {
|
| 299 |
+
try {
|
| 300 |
+
const baseUrl = window.location.hostname === 'localhost' ? `${window.location.protocol}//${window.location.host}` : ''
|
| 301 |
+
const res = await fetch(`${baseUrl}/models`)
|
| 302 |
if (res.ok) {
|
| 303 |
+
const data: ModelsResponse = await res.json()
|
| 304 |
+
setModels(data.models)
|
| 305 |
|
| 306 |
+
// Set selected model to current model if available, otherwise first API model
|
| 307 |
+
if (data.current_model && selectedModel !== data.current_model) {
|
| 308 |
+
setSelectedModel(data.current_model)
|
| 309 |
+
} else if (!selectedModel && data.models.length > 0) {
|
| 310 |
+
// Prefer API models as default
|
| 311 |
+
const apiModel = data.models.find(m => m.type === 'api')
|
| 312 |
+
const defaultModel = apiModel || data.models[0]
|
| 313 |
+
setSelectedModel(defaultModel.model_name)
|
| 314 |
}
|
|
|
|
|
|
|
|
|
|
| 315 |
}
|
| 316 |
} catch (err) {
|
| 317 |
+
console.error('Failed to fetch models:', err)
|
| 318 |
}
|
| 319 |
}
|
| 320 |
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
|
| 327 |
const handleSamplePromptClick = (samplePrompt: string) => {
|
| 328 |
setInput(samplePrompt)
|
| 329 |
}
|
|
|
|
| 336 |
fixed inset-y-0 left-0 z-50 w-80 bg-background border-r transition-transform duration-300 ease-in-out
|
| 337 |
lg:translate-x-0 lg:static lg:inset-0
|
| 338 |
`}>
|
| 339 |
+
<div className="p-4 space-y-4">
|
| 340 |
+
<div className="flex items-center justify-between">
|
| 341 |
+
<h2 className="font-semibold">Chat Sessions</h2>
|
| 342 |
+
<Button onClick={createNewSession} size="sm">
|
| 343 |
+
<Plus className="h-4 w-4 mr-1" />
|
| 344 |
+
New
|
| 345 |
+
</Button>
|
| 346 |
+
</div>
|
| 347 |
+
<div className="space-y-2">
|
| 348 |
+
{sessions.map((session) => (
|
| 349 |
+
<Card
|
| 350 |
+
key={session.id}
|
| 351 |
+
className={`p-3 cursor-pointer transition-colors hover:bg-accent ${
|
| 352 |
+
currentSessionId === session.id ? 'bg-accent border-primary' : ''
|
| 353 |
+
}`}
|
| 354 |
+
onClick={() => selectSession(session.id)}
|
| 355 |
+
>
|
| 356 |
+
<div className="flex items-center justify-between">
|
| 357 |
+
<span className="text-sm font-medium truncate">{session.title}</span>
|
| 358 |
+
<Button
|
| 359 |
+
size="sm"
|
| 360 |
+
variant="ghost"
|
| 361 |
+
onClick={(e) => {
|
| 362 |
+
e.stopPropagation()
|
| 363 |
+
deleteSession(session.id)
|
| 364 |
+
}}
|
| 365 |
+
className="h-6 w-6 p-0"
|
| 366 |
+
>
|
| 367 |
+
<Trash2 className="h-3 w-3" />
|
| 368 |
+
</Button>
|
| 369 |
+
</div>
|
| 370 |
+
<div className="text-xs text-muted-foreground">
|
| 371 |
+
{session.messages.length} messages
|
| 372 |
+
</div>
|
| 373 |
+
</Card>
|
| 374 |
+
))}
|
| 375 |
+
</div>
|
| 376 |
+
</div>
|
| 377 |
</div>
|
| 378 |
|
| 379 |
{/* Overlay for mobile */}
|
|
|
|
| 467 |
)}
|
| 468 |
|
| 469 |
{/* Chat Messages and Input */}
|
| 470 |
+
<Chat
|
| 471 |
+
messages={messages.map(msg => ({
|
| 472 |
+
id: msg.id,
|
| 473 |
+
role: msg.role as 'user' | 'assistant' | 'system',
|
| 474 |
+
content: msg.content,
|
| 475 |
+
createdAt: new Date(msg.timestamp)
|
| 476 |
+
}))}
|
| 477 |
input={input}
|
| 478 |
+
handleInputChange={(e) => setInput(e.target.value)}
|
| 479 |
+
handleSubmit={async (e) => {
|
| 480 |
+
e.preventDefault()
|
| 481 |
+
if (!selectedModel || !models.find(m => m.model_name === selectedModel)) return
|
| 482 |
+
await sendMessage()
|
| 483 |
+
}}
|
| 484 |
+
isGenerating={isLoading}
|
| 485 |
+
stop={stopGeneration}
|
|
|
|
|
|
|
| 486 |
className="flex-1"
|
| 487 |
/>
|
| 488 |
</div>
|
|
|
|
| 495 |
<h2 className="font-semibold text-sm">Configuration</h2>
|
| 496 |
</div>
|
| 497 |
|
| 498 |
+
{/* Model Selection */}
|
| 499 |
<Card>
|
| 500 |
<CardHeader>
|
| 501 |
+
<CardTitle className="text-sm">Model Selection</CardTitle>
|
| 502 |
</CardHeader>
|
| 503 |
<CardContent className="space-y-3">
|
| 504 |
+
{/* Simple Model Dropdown */}
|
| 505 |
+
<div>
|
| 506 |
+
<Label className="text-xs font-medium mb-2">Active Model</Label>
|
| 507 |
+
<Select value={selectedModel || ""} onValueChange={setSelectedModel}>
|
| 508 |
+
<SelectTrigger className="w-full">
|
| 509 |
+
<SelectValue placeholder="Select a model...">
|
| 510 |
+
{selectedModel && (() => {
|
| 511 |
+
const model = models.find(m => m.model_name === selectedModel)
|
| 512 |
+
if (!model) return selectedModel
|
| 513 |
+
const isApiModel = model.type === 'api'
|
| 514 |
+
return (
|
| 515 |
+
<div className="flex items-center gap-2">
|
| 516 |
+
{isApiModel ? (
|
| 517 |
+
<Cloud className="h-4 w-4 text-blue-500" />
|
| 518 |
+
) : model.supports_thinking ? (
|
| 519 |
+
<Brain className="h-4 w-4 text-purple-500" />
|
| 520 |
+
) : (
|
| 521 |
+
<Zap className="h-4 w-4 text-green-500" />
|
| 522 |
+
)}
|
| 523 |
+
<span className="truncate">{model.name}</span>
|
| 524 |
+
{autoLoadingModel === selectedModel ? (
|
| 525 |
+
<Badge variant="outline" className="text-xs">
|
| 526 |
+
Loading...
|
| 527 |
+
</Badge>
|
| 528 |
+
) : (
|
| 529 |
+
<Badge variant="outline" className="text-xs">
|
| 530 |
+
{isApiModel ? "API" : model.is_loaded ? "Loaded" : "Available"}
|
| 531 |
+
</Badge>
|
| 532 |
+
)}
|
| 533 |
+
</div>
|
| 534 |
+
)
|
| 535 |
+
})()}
|
| 536 |
+
</SelectValue>
|
| 537 |
+
</SelectTrigger>
|
| 538 |
+
<SelectContent>
|
| 539 |
+
<SelectGroup>
|
| 540 |
+
<SelectLabel>🌐 API Models</SelectLabel>
|
| 541 |
+
{models.filter(m => m.type === 'api').map((model) => (
|
| 542 |
+
<SelectItem key={model.model_name} value={model.model_name}>
|
| 543 |
+
<div className="flex items-center gap-2">
|
| 544 |
+
<Cloud className="h-4 w-4 text-blue-500" />
|
| 545 |
+
<span>{model.name}</span>
|
| 546 |
+
<Badge variant="outline" className="text-xs bg-blue-50">API</Badge>
|
| 547 |
+
</div>
|
| 548 |
+
</SelectItem>
|
| 549 |
+
))}
|
| 550 |
+
</SelectGroup>
|
| 551 |
+
<SelectGroup>
|
| 552 |
+
<SelectLabel>💻 Local Models</SelectLabel>
|
| 553 |
+
{models.filter(m => m.type === 'local').map((model) => (
|
| 554 |
+
<SelectItem key={model.model_name} value={model.model_name}>
|
| 555 |
+
<div className="flex items-center gap-2">
|
| 556 |
+
{model.supports_thinking ? (
|
| 557 |
+
<Brain className="h-4 w-4 text-purple-500" />
|
| 558 |
+
) : (
|
| 559 |
+
<Zap className="h-4 w-4 text-green-500" />
|
| 560 |
+
)}
|
| 561 |
+
<span>{model.name}</span>
|
| 562 |
+
{autoLoadingModel === model.model_name ? (
|
| 563 |
+
<Badge variant="outline" className="text-xs bg-yellow-50">Loading...</Badge>
|
| 564 |
+
) : model.is_loaded ? (
|
| 565 |
+
<Badge variant="outline" className="text-xs bg-green-50">Loaded</Badge>
|
| 566 |
+
) : (
|
| 567 |
+
<Badge variant="outline" className="text-xs bg-gray-50">Available</Badge>
|
| 568 |
+
)}
|
| 569 |
+
</div>
|
| 570 |
+
</SelectItem>
|
| 571 |
+
))}
|
| 572 |
+
</SelectGroup>
|
| 573 |
+
</SelectContent>
|
| 574 |
+
</Select>
|
| 575 |
+
</div>
|
| 576 |
|
| 577 |
+
{/* Model Catalog Link */}
|
| 578 |
+
<div className="pt-2 border-t">
|
| 579 |
+
<Button variant="outline" size="sm" className="w-full" asChild>
|
| 580 |
+
<a href="/models" className="flex items-center gap-2">
|
| 581 |
+
<BookOpen className="h-4 w-4" />
|
| 582 |
+
View Model Catalog
|
| 583 |
+
</a>
|
| 584 |
+
</Button>
|
| 585 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
</CardContent>
|
| 587 |
</Card>
|
| 588 |
|
|
|
|
| 599 |
</Label>
|
| 600 |
<Slider
|
| 601 |
value={[temperature]}
|
| 602 |
+
onValueChange={(value: number[]) => setTemperature(value[0])}
|
| 603 |
min={0}
|
| 604 |
max={2}
|
| 605 |
step={0.01}
|
|
|
|
| 618 |
</Label>
|
| 619 |
<Slider
|
| 620 |
value={[maxTokens]}
|
| 621 |
+
onValueChange={(value: number[]) => setMaxTokens(value[0])}
|
| 622 |
min={100}
|
| 623 |
max={4096}
|
| 624 |
step={100}
|
|
|
|
| 690 |
<textarea
|
| 691 |
id="system-prompt"
|
| 692 |
value={systemPrompt}
|
| 693 |
+
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setSystemPrompt(e.target.value)}
|
| 694 |
placeholder="Enter custom system prompt to define how the model should behave..."
|
| 695 |
className="w-full min-h-[80px] text-xs p-2 border rounded-md bg-background"
|
| 696 |
disabled={isLoading}
|
|
|
|
| 708 |
</div>
|
| 709 |
</div>
|
| 710 |
|
| 711 |
+
{/* Model Load Confirmation Dialog */}
|
| 712 |
<AlertDialog open={showLoadConfirm} onOpenChange={setShowLoadConfirm}>
|
| 713 |
<AlertDialogContent>
|
| 714 |
<AlertDialogHeader>
|
| 715 |
+
<AlertDialogTitle className="flex items-center gap-2">
|
| 716 |
+
<Download className="h-5 w-5 text-blue-500" />
|
| 717 |
+
Load Local Model
|
| 718 |
+
</AlertDialogTitle>
|
| 719 |
+
<AlertDialogDescription asChild>
|
| 720 |
+
<div className="space-y-3">
|
| 721 |
+
<p>
|
| 722 |
+
You're about to load <strong>{pendingModelToLoad?.name}</strong> locally.
|
| 723 |
+
</p>
|
| 724 |
+
|
| 725 |
+
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
| 726 |
+
<div className="flex items-start gap-2">
|
| 727 |
+
<AlertTriangle className="h-4 w-4 text-yellow-600 mt-0.5" />
|
| 728 |
+
<div className="text-sm">
|
| 729 |
+
<p className="font-medium text-yellow-800">Resource Requirements:</p>
|
| 730 |
+
<ul className="mt-1 text-yellow-700 space-y-1">
|
| 731 |
+
<li>• <strong>Storage:</strong> {pendingModelToLoad?.size_gb}</li>
|
| 732 |
+
<li>• <strong>RAM:</strong> ~{pendingModelToLoad?.size_gb} (while running)</li>
|
| 733 |
+
<li>• <strong>Download:</strong> First-time loading will download the model</li>
|
| 734 |
+
</ul>
|
| 735 |
+
</div>
|
| 736 |
+
</div>
|
| 737 |
+
</div>
|
| 738 |
|
| 739 |
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
| 740 |
+
<div className="text-sm text-blue-700">
|
| 741 |
+
<p className="font-medium text-blue-800">Model Features:</p>
|
| 742 |
+
<p className="mt-1">{pendingModelToLoad?.description}</p>
|
| 743 |
+
{pendingModelToLoad?.supports_thinking && (
|
| 744 |
+
<p className="mt-1 flex items-center gap-1">
|
| 745 |
+
<Brain className="h-3 w-3" />
|
| 746 |
+
Supports thinking process
|
| 747 |
+
</p>
|
| 748 |
+
)}
|
| 749 |
+
</div>
|
| 750 |
+
</div>
|
| 751 |
+
|
| 752 |
+
<p className="text-sm text-muted-foreground">
|
| 753 |
+
The model will be cached locally for faster future access. You can unload it anytime to free up memory.
|
| 754 |
+
</p>
|
| 755 |
+
</div>
|
| 756 |
</AlertDialogDescription>
|
| 757 |
</AlertDialogHeader>
|
| 758 |
<AlertDialogFooter>
|
| 759 |
+
<AlertDialogCancel onClick={handleLoadModelCancel}>
|
| 760 |
+
Cancel
|
| 761 |
+
</AlertDialogCancel>
|
| 762 |
+
<AlertDialogAction onClick={handleLoadModelConfirm}>
|
| 763 |
+
Load Model
|
| 764 |
</AlertDialogAction>
|
| 765 |
</AlertDialogFooter>
|
| 766 |
</AlertDialogContent>
|