wu981526092 commited on
Commit
6a50e97
·
1 Parent(s): d8e039b
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +0 -35
  2. .gitignore +36 -21
  3. CONTRIBUTING.md +0 -230
  4. Dockerfile +1 -1
  5. LICENSE +0 -21
  6. README.md +137 -10
  7. app.py +172 -269
  8. backend/__init__.py +0 -0
  9. backend/api/__init__.py +0 -0
  10. backend/api/endpoints/__init__.py +0 -0
  11. backend/api/routes.py +17 -3
  12. backend/app.py +0 -243
  13. backend/config.py +44 -10
  14. backend/core/__init__.py +0 -0
  15. backend/main.py +1 -1
  16. backend/models.py +6 -0
  17. backend/services/__init__.py +0 -1
  18. backend/services/chat_service.py +115 -13
  19. backend/services/model_service.py +64 -30
  20. backend/utils/__init__.py +0 -0
  21. frontend/components.json +21 -0
  22. frontend/index.html +1 -1
  23. frontend/package-lock.json +109 -5
  24. frontend/package.json +3 -1
  25. frontend/src/App.tsx +0 -1
  26. frontend/src/components/Layout.tsx +18 -0
  27. frontend/src/components/Sidebar.tsx +11 -19
  28. frontend/src/components/chat/ChatContainer.tsx +148 -76
  29. frontend/src/components/chat/ChatInput.tsx +0 -138
  30. frontend/src/components/chat/ChatMessage.tsx +0 -192
  31. frontend/src/components/chat/ChatSessions.tsx +120 -161
  32. frontend/src/components/chat/index.ts +0 -4
  33. frontend/src/components/ui/alert-dialog.tsx +138 -0
  34. frontend/src/components/ui/badge.tsx +35 -0
  35. frontend/src/components/ui/button.tsx +9 -11
  36. frontend/src/components/ui/card.tsx +13 -19
  37. frontend/src/components/ui/chat.tsx +123 -0
  38. frontend/src/components/ui/collapsible.tsx +9 -0
  39. frontend/src/components/ui/label.tsx +23 -0
  40. frontend/src/components/ui/select.tsx +156 -0
  41. frontend/src/components/ui/slider.tsx +25 -0
  42. frontend/src/components/ui/switch.tsx +26 -0
  43. frontend/src/components/ui/textarea.tsx +17 -16
  44. frontend/src/hooks/useChat.ts +49 -14
  45. frontend/src/index.css +35 -114
  46. frontend/src/lib/chat-storage.ts +77 -104
  47. frontend/src/lib/utils.ts +1 -1
  48. frontend/src/pages/Home.tsx +97 -102
  49. frontend/src/pages/Models.tsx +339 -0
  50. 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
- # Dependencies
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
- # Cache
36
- .cache/
37
- .pytest_cache/
38
- .mypy_cache/
39
-
40
- # Model cache (uncomment to ignore downloaded models)
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 ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
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
- title: EdgeLLM
3
- emoji: 🏆
4
- colorFrom: blue
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- from fastapi import FastAPI, HTTPException
2
- from fastapi.middleware.cors import CORSMiddleware
3
- from fastapi.staticfiles import StaticFiles
4
- from fastapi.responses import FileResponse
5
- from pydantic import BaseModel
6
- from transformers import AutoModelForCausalLM, AutoTokenizer
7
- import torch
8
- from typing import Optional, Dict, Any
 
 
9
  import os
10
-
11
- app = FastAPI(title="Edge LLM API")
12
-
13
- # Enable CORS for Hugging Face Space
14
- app.add_middleware(
15
- CORSMiddleware,
16
- allow_origins=["*"], # Allow all origins for HF Space
17
- allow_credentials=True,
18
- allow_methods=["*"],
19
- allow_headers=["*"],
20
- )
21
-
22
- # Mount static files
23
- app.mount("/assets", StaticFiles(directory="static/assets"), name="assets")
24
-
25
- # Available models
26
- AVAILABLE_MODELS = {
27
- "Qwen/Qwen3-4B-Thinking-2507": {
28
- "name": "Qwen3-4B-Thinking-2507",
29
- "supports_thinking": True,
30
- "description": "Shows thinking process",
31
- "size_gb": "~8GB"
32
- },
33
- "Qwen/Qwen3-4B-Instruct-2507": {
34
- "name": "Qwen3-4B-Instruct-2507",
35
- "supports_thinking": False,
36
- "description": "Direct instruction following",
37
- "size_gb": "~8GB"
38
- }
39
- }
40
-
41
- # Global model cache
42
- models_cache: Dict[str, Dict[str, Any]] = {}
43
- current_model_name = None # No model loaded by default
44
-
45
- class PromptRequest(BaseModel):
46
- prompt: str
47
- system_prompt: Optional[str] = None
48
- model_name: Optional[str] = None
49
- temperature: Optional[float] = 0.7
50
- max_new_tokens: Optional[int] = 1024
51
-
52
- class PromptResponse(BaseModel):
53
- thinking_content: str
54
- content: str
55
- model_used: str
56
- supports_thinking: bool
57
-
58
- class ModelInfo(BaseModel):
59
- model_name: str
60
- name: str
61
- supports_thinking: bool
62
- description: str
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
- if model_name in models_cache:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  return True
82
 
83
- if model_name not in AVAILABLE_MODELS:
84
- return False
85
-
86
  try:
87
- print(f"Loading model: {model_name}")
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
- models_cache[model_name] = {
96
- "model": model,
97
- "tokenizer": tokenizer
98
- }
99
- print(f"Model {model_name} loaded successfully")
100
- return True
101
- except Exception as e:
102
- print(f"Error loading model {model_name}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  return False
104
-
105
- def unload_model_by_name(model_name: str):
106
- """Unload a model from the cache"""
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
- @app.get("/")
124
- async def read_index():
125
- """Serve the React app"""
126
- return FileResponse('static/index.html')
127
 
128
- @app.get("/health")
129
- async def health_check():
130
- return {"status": "healthy", "message": "Edge LLM API is running"}
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
- success = load_model_by_name(request.model_name)
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
- success = unload_model_by_name(request.model_name)
183
- if success:
184
- return {
185
- "message": f"Model {request.model_name} unloaded successfully",
186
- "current_model": current_model_name or ""
187
- }
 
188
  else:
189
- raise HTTPException(
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
- model = models_cache[model_to_use]["model"]
233
- tokenizer = models_cache[model_to_use]["tokenizer"]
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
- # Tokenize
250
- inputs = tokenizer(formatted_prompt, return_tensors="pt").to(model.device)
 
251
 
252
- # Generate
253
- with torch.no_grad():
254
- outputs = model.generate(
255
- **inputs,
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
- # Decode
263
- generated_tokens = outputs[0][inputs['input_ids'].shape[1]:]
264
- generated_text = tokenizer.decode(generated_tokens, skip_special_tokens=True)
265
 
266
- # Parse thinking vs final content for thinking models
267
- thinking_content = ""
268
- final_content = generated_text
 
269
 
270
- if model_info["supports_thinking"] and "<thinking>" in generated_text:
271
- parts = generated_text.split("<thinking>")
272
- if len(parts) > 1:
273
- thinking_part = parts[1]
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
- return PromptResponse(
280
- thinking_content=thinking_content,
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"Generation error: {e}")
288
- raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
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
- return FileResponse('static/index.html')
 
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
- STATIC_DIR = "static"
26
- ASSETS_DIR = "static/assets"
27
 
28
- # Server settings
29
  HOST = "0.0.0.0"
30
- PORT = 7860
 
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
- @staticmethod
13
- def generate_response(
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
- Generate chat response
22
- Returns: (thinking_content, final_content, model_used, supports_thinking)
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 prompt
34
- messages = []
35
  if system_prompt:
36
- messages.append({"role": "system", "content": system_prompt})
37
- messages.append({"role": "user", "content": prompt})
 
 
 
 
 
 
 
38
 
39
  # Apply chat template
40
  formatted_prompt = tokenizer.apply_chat_template(
41
- messages,
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
- from typing import Dict, Any, Optional
 
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: Optional[str] = None
14
-
15
  def load_model(self, model_name: str) -> bool:
16
- """Load a model into the cache"""
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 the cache"""
 
 
 
 
 
 
 
 
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 self.models_cache:
 
 
 
 
 
 
55
  self.current_model_name = model_name
56
  return True
57
- return False
58
-
59
- def get_model_info(self, model_name: str) -> Dict[str, Any]:
60
- """Get model configuration info"""
61
- return AVAILABLE_MODELS.get(model_name, {})
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
- return list(self.models_cache.keys())
70
-
71
- def get_current_model(self) -> Optional[str]:
 
 
 
 
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 Platform</title>
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.263.1",
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.263.1",
2761
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.263.1.tgz",
2762
- "integrity": "sha512-keqxAx97PlaEN89PXZ6ki1N8nRjGWtDa4021GFYLNj0RgruM5odbpl8GHTExj0hhPq3sF6Up0gnxt6TSHu+ovw==",
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.263.1",
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 h-full bg-muted/30 border-r">
67
- {/* Logo/Brand */}
68
- <div className="flex items-center h-16 px-6 border-b">
69
  <div className="flex items-center gap-2">
70
- <div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
71
- <Brain className="w-5 h-5 text-white" />
72
  </div>
73
  <div>
74
- <h1 className="text-lg font-semibold">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 overflow-y-auto py-4">
82
- <div className="px-3 mb-4">
83
- <h2 className="mb-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
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 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
119
- Tools
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 { useEffect, useRef } from 'react'
2
- import { ChatMessage } from './ChatMessage'
3
- import { ChatInput } from './ChatInput'
 
 
 
4
  import { Message } from '@/types/chat'
5
- import { Loader2 } from 'lucide-react'
6
- import { cn } from '@/lib/utils'
7
 
8
  interface ChatContainerProps {
9
  messages: Message[]
10
  input: string
11
- onInputChange: (value: string) => void
12
  onSubmit: () => void
13
- onStop?: () => void
14
- isLoading?: boolean
15
  disabled?: boolean
16
- className?: string
17
  placeholder?: string
18
  }
19
 
20
  export function ChatContainer({
21
  messages,
22
  input,
23
- onInputChange,
24
  onSubmit,
25
  onStop,
26
- isLoading = false,
27
  disabled = false,
28
- className,
29
- placeholder = "Ask me anything..."
30
  }: ChatContainerProps) {
31
- const messagesEndRef = useRef<HTMLDivElement>(null)
32
- const messagesContainerRef = useRef<HTMLDivElement>(null)
33
 
34
- // Auto-scroll to bottom when new messages arrive
35
- useEffect(() => {
36
- if (messagesEndRef.current) {
37
- messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
 
 
38
  }
39
- }, [messages, isLoading])
40
 
41
- const handleCopyMessage = (content: string) => {
42
- navigator.clipboard.writeText(content)
43
- // Could add a toast notification here
 
 
44
  }
45
 
46
  return (
47
- <div className={cn("flex flex-col h-full", className)}>
48
- {/* Messages Area */}
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-1 flex items-center justify-center text-center">
55
- <div className="max-w-md space-y-4">
56
- <div className="text-muted-foreground">
57
- <h3 className="text-lg font-medium">Start a conversation</h3>
58
- <p className="text-sm">
59
- Ask me anything! I can help with coding, writing, analysis, and more.
60
- </p>
61
- </div>
62
  </div>
63
  </div>
64
  ) : (
65
- <>
66
- {messages.map((message) => (
67
- <div key={message.id} className="group">
68
- <ChatMessage
69
- message={message}
70
- onCopy={handleCopyMessage}
71
- />
72
- </div>
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
- {/* Loading message */}
84
- <div className="flex-1 max-w-[80%]">
85
- <div className="bg-muted/50 rounded-lg p-3">
86
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
87
- <Loader2 className="h-4 w-4 animate-spin" />
88
- <span>Thinking...</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  </div>
91
- </div>
 
 
 
 
 
 
92
  </div>
93
- )}
94
- </>
95
  )}
96
-
97
- {/* Scroll anchor */}
98
- <div ref={messagesEndRef} />
99
  </div>
100
 
101
- {/* Input Area */}
102
- <ChatInput
103
- value={input}
104
- onChange={onInputChange}
105
- onSubmit={onSubmit}
106
- onStop={onStop}
107
- isLoading={isLoading}
108
- disabled={disabled}
109
- placeholder={placeholder}
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 { useState } from 'react'
2
  import { Button } from '@/components/ui/button'
3
- import { Card, CardContent } from '@/components/ui/card'
4
  import { Badge } from '@/components/ui/badge'
 
5
  import {
6
  Plus,
7
  MessageSquare,
8
  Trash2,
9
- Edit3,
10
- Calendar
 
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?: (sessionId: string, newTitle: string) => void
22
  }
23
 
24
  export function ChatSessions({
@@ -29,182 +29,141 @@ export function ChatSessions({
29
  onDeleteSession,
30
  onRenameSession
31
  }: ChatSessionsProps) {
32
- const [editingSession, setEditingSession] = useState<string | null>(null)
33
- const [editTitle, setEditTitle] = useState('')
34
 
35
- const handleStartEdit = (session: ChatSession) => {
36
- setEditingSession(session.id)
37
  setEditTitle(session.title)
38
  }
39
 
40
- const handleSaveEdit = () => {
41
- if (editingSession && editTitle.trim() && onRenameSession) {
42
- onRenameSession(editingSession, editTitle.trim())
43
  }
44
- setEditingSession(null)
45
  setEditTitle('')
46
  }
47
 
48
- const handleCancelEdit = () => {
49
- setEditingSession(null)
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-3">
84
- <h2 className="font-semibold text-sm">Chat Sessions</h2>
85
- <Button
86
- onClick={onNewSession}
87
- size="sm"
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 List */}
106
- <div className="flex-1 overflow-y-auto p-2 space-y-4">
107
- {Object.keys(groupedSessions).length === 0 ? (
108
- <div className="flex flex-col items-center justify-center h-32 text-center">
109
- <MessageSquare className="h-8 w-8 text-muted-foreground mb-2" />
110
- <p className="text-sm text-muted-foreground">No chat sessions yet</p>
111
- <p className="text-xs text-muted-foreground">Start a new conversation</p>
 
 
112
  </div>
113
  ) : (
114
- Object.entries(groupedSessions).map(([date, sessionGroup]) => (
115
- <div key={date} className="space-y-2">
116
- {/* Date Group Header */}
117
- <div className="flex items-center gap-2 px-2">
118
- <Calendar className="h-3 w-3 text-muted-foreground" />
119
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
120
- {date}
121
- </span>
122
- </div>
123
-
124
- {/* Sessions in this date group */}
125
- <div className="space-y-1">
126
- {sessionGroup.map((session) => (
127
- <Card
128
- key={session.id}
129
- className={cn(
130
- "cursor-pointer transition-colors hover:bg-accent/50 group",
131
- currentSessionId === session.id && "bg-accent border-primary"
132
- )}
133
- onClick={() => onSelectSession(session.id)}
134
- >
135
- <CardContent className="p-3">
136
- {editingSession === session.id ? (
137
- <div className="space-y-2">
138
- <input
139
- type="text"
140
- value={editTitle}
141
- onChange={(e) => setEditTitle(e.target.value)}
142
- className="w-full text-sm bg-transparent border border-input rounded px-2 py-1"
143
- onKeyDown={(e) => {
144
- if (e.key === 'Enter') handleSaveEdit()
145
- if (e.key === 'Escape') handleCancelEdit()
146
- }}
147
- autoFocus
148
- />
149
- <div className="flex gap-1">
150
- <Button size="sm" onClick={handleSaveEdit} className="h-6 px-2 text-xs">
151
- Save
152
- </Button>
153
- <Button size="sm" variant="outline" onClick={handleCancelEdit} className="h-6 px-2 text-xs">
154
- Cancel
155
- </Button>
156
- </div>
157
- </div>
158
- ) : (
159
- <div className="space-y-2">
160
- <div className="flex items-start justify-between">
161
- <h3 className="text-sm font-medium line-clamp-2 flex-1 mr-2">
162
- {session.title}
163
- </h3>
164
-
165
- <div className="opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
166
- {onRenameSession && (
167
- <Button
168
- variant="ghost"
169
- size="sm"
170
- onClick={(e) => {
171
- e.stopPropagation()
172
- handleStartEdit(session)
173
- }}
174
- className="h-6 w-6 p-0"
175
- >
176
- <Edit3 className="h-3 w-3" />
177
- </Button>
178
- )}
179
- <Button
180
- variant="ghost"
181
- size="sm"
182
- onClick={(e) => {
183
- e.stopPropagation()
184
- onDeleteSession(session.id)
185
- }}
186
- className="h-6 w-6 p-0 text-destructive hover:text-destructive"
187
- >
188
- <Trash2 className="h-3 w-3" />
189
- </Button>
190
- </div>
191
- </div>
192
-
193
- <div className="flex items-center justify-between text-xs text-muted-foreground">
194
- <span>{session.messages.length} messages</span>
195
- {session.model_name && (
196
- <Badge variant="outline" className="text-xs">
197
- {session.model_name.split('/').pop()?.split('-')[0]}
198
- </Badge>
199
- )}
200
- </div>
201
- </div>
202
- )}
203
- </CardContent>
204
- </Card>
205
- ))}
206
  </div>
207
- </div>
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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
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 shadow-sm hover:bg-destructive/90",
16
  outline:
17
- "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18
  secondary:
19
- "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20
  ghost: "hover:bg-accent hover:text-accent-foreground",
21
  link: "text-primary underline-offset-4 hover:underline",
22
  },
23
  size: {
24
- default: "h-9 px-4 py-2",
25
- sm: "h-8 rounded-md px-3 text-xs",
26
- lg: "h-10 rounded-md px-8",
27
- icon: "h-9 w-9",
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-xl border bg-card text-card-foreground shadow",
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
- HTMLDivElement,
34
- React.HTMLAttributes<HTMLDivElement>
35
  >(({ className, ...props }, ref) => (
36
- <div
37
  ref={ref}
38
- className={cn("font-semibold leading-none tracking-tight", className)}
 
 
 
39
  {...props}
40
  />
41
  ))
42
  CardTitle.displayName = "CardTitle"
43
 
44
  const CardDescription = React.forwardRef<
45
- HTMLDivElement,
46
- React.HTMLAttributes<HTMLDivElement>
47
  >(({ className, ...props }, ref) => (
48
- <div
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
- const Textarea = React.forwardRef<
6
- HTMLTextAreaElement,
7
- React.ComponentProps<"textarea">
8
- >(({ className, ...props }, ref) => {
9
- return (
10
- <textarea
11
- className={cn(
12
- "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
13
- className
14
- )}
15
- ref={ref}
16
- {...props}
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 = 'http://localhost:8000/generate',
21
- defaultModel = 'Qwen/Qwen3-4B-Instruct-2507',
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 = sessions.find(s => s.id === currentSessionId) || null
42
- const messages = currentSession?.messages || []
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- setSessions(chatStorage.getAllSessions())
 
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
- setSessions(chatStorage.getAllSessions())
 
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
- setSessions(chatStorage.getAllSessions())
 
 
110
  }, [currentSessionId])
111
 
112
  // Send message
@@ -130,7 +147,9 @@ export function useChat(options: UseChatOptions = {}) {
130
  role: 'user',
131
  content: userMessage
132
  })
133
- setSessions(chatStorage.getAllSessions())
 
 
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
- setSessions(chatStorage.getAllSessions())
 
 
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
- setSessions(chatStorage.getAllSessions())
 
 
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
- setSessions(chatStorage.getAllSessions())
 
 
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
- --popover-foreground: 0 0% 3.9%;
22
-
23
- --primary: 0 0% 9%;
24
-
25
- --primary-foreground: 0 0% 98%;
26
-
27
- --secondary: 0 0% 96.1%;
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
- --destructive-foreground: 0 0% 98%;
42
-
43
- --border: 0 0% 89.8%;
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
- --destructive-foreground: 0 0% 98%;
94
-
95
- --border: 0 0% 14.9%;
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, ChatStore } from '@/types/chat'
2
 
3
- const STORAGE_KEY = 'edge-llm-chat-store'
 
 
 
4
 
5
- export const chatStorage = {
6
- // Load all chat data from localStorage
7
- load(): ChatStore {
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
- // Save chat data to localStorage
21
- save(store: ChatStore): void {
22
- try {
23
- localStorage.setItem(STORAGE_KEY, JSON.stringify(store))
24
- } catch (error) {
25
- console.error('Failed to save chat store:', error)
26
- }
27
- },
 
 
 
 
 
 
28
 
29
- // Create a new chat session
30
- createSession(title?: string, model_name?: string, system_prompt?: string): ChatSession {
31
- const now = Date.now()
32
  const newSession: ChatSession = {
33
- id: `session_${now}_${Math.random().toString(36).substr(2, 9)}`,
34
- title: title || `New Chat ${new Date(now).toLocaleDateString()}`,
35
  messages: [],
36
- created_at: now,
37
- updated_at: now,
38
- model_name,
39
- system_prompt,
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
- // Add message to session
51
- addMessageToSession(sessionId: string, message: Omit<Message, 'id' | 'timestamp'>): void {
52
- const store = this.load()
53
- const session = store.sessions.find(s => s.id === sessionId)
54
 
55
- if (session) {
56
- const newMessage: Message = {
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 store = this.load()
83
- const sessionIndex = store.sessions.findIndex(s => s.id === sessionId)
84
 
85
  if (sessionIndex !== -1) {
86
- store.sessions[sessionIndex] = {
87
- ...store.sessions[sessionIndex],
88
  ...updates,
89
- updated_at: Date.now(),
90
  }
91
- this.save(store)
92
  }
93
- },
94
 
95
- // Delete session
96
  deleteSession(sessionId: string): void {
97
- const store = this.load()
98
- store.sessions = store.sessions.filter(s => s.id !== sessionId)
 
99
 
100
- // If deleting current session, clear current_session_id
101
- if (store.current_session_id === sessionId) {
102
- store.current_session_id = store.sessions.length > 0 ? store.sessions[0].id : null
103
  }
 
 
 
 
 
104
 
105
- this.save(store)
106
- },
 
 
 
 
 
 
 
 
 
 
107
 
108
- // Set current session
109
- setCurrentSession(sessionId: string): void {
110
- const store = this.load()
111
- store.current_session_id = sessionId
112
- this.save(store)
113
- },
114
 
115
- // Get current session
116
  getCurrentSession(): ChatSession | null {
117
- const store = this.load()
118
- if (!store.current_session_id) return null
119
- return this.getSession(store.current_session_id)
120
- },
121
 
122
- // Get all sessions sorted by updated_at
123
- getAllSessions(): ChatSession[] {
124
- const store = this.load()
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(STORAGE_KEY)
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 { clsx, type ClassValue } from "clsx"
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
- {/* Header */}
71
- <div className="border-b">
72
- <div className="flex h-14 items-center px-6">
73
- <div className="flex items-center gap-2">
74
- <Brain className="h-5 w-5" />
75
- <h1 className="text-lg font-semibold">Home</h1>
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
- {/* Quick Actions */}
99
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
100
- {quickActions.map((action) => (
101
- <Card key={action.href} className={action.primary ? "ring-2 ring-blue-500" : ""}>
102
- <CardContent className="p-6">
103
- <Link to={action.href} className="block space-y-3 group">
104
- <div className="flex items-center justify-between">
105
- <action.icon className={`h-8 w-8 ${action.primary ? 'text-blue-500' : 'text-muted-foreground'}`} />
106
- <ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
107
- </div>
108
- <div>
109
- <h3 className="font-semibold text-lg">{action.title}</h3>
110
- <p className="text-muted-foreground text-sm">{action.description}</p>
111
- </div>
112
- </Link>
113
- </CardContent>
114
- </Card>
115
- ))}
116
  </div>
 
117
 
118
- {/* Features Grid */}
119
- <div className="space-y-6">
120
- <div className="text-center">
121
- <h2 className="text-2xl font-bold">Key Features</h2>
122
- <p className="text-muted-foreground mt-2">
123
- Everything you need for local AI development and experimentation
124
- </p>
125
- </div>
126
-
127
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
128
- {features.map((feature, index) => (
129
- <Card key={index}>
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
- {/* Getting Started */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  <Card>
144
  <CardHeader>
145
- <CardTitle className="flex items-center gap-2">
146
- <Download className="h-5 w-5" />
147
- Getting Started
148
- </CardTitle>
149
  </CardHeader>
150
- <CardContent className="space-y-4">
151
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
152
  <div className="space-y-2">
153
  <div className="flex items-center gap-2">
154
- <div className="w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
155
  1
156
  </div>
157
- <h4 className="font-medium">Choose a Model</h4>
158
  </div>
159
  <p className="text-sm text-muted-foreground pl-8">
160
- Browse the model catalog and select a model that fits your needs.
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-blue-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
167
  2
168
  </div>
169
- <h4 className="font-medium">Load the Model</h4>
170
  </div>
171
  <p className="text-sm text-muted-foreground pl-8">
172
- Click the load button to download and prepare the model for use.
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-blue-500 text-white rounded-full flex items-center justify-center text-sm font-medium">
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="grid grid-cols-1 md:grid-cols-3 gap-4">
207
- <div className="flex items-center gap-3">
208
- <div className="w-2 h-2 bg-green-500 rounded-full"></div>
209
- <div>
210
- <p className="text-sm font-medium">Backend</p>
211
- <p className="text-xs text-muted-foreground">Running</p>
212
- </div>
213
  </div>
214
- <div className="flex items-center gap-3">
215
- <div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
216
- <div>
217
- <p className="text-sm font-medium">Models</p>
218
- <p className="text-xs text-muted-foreground">Ready to load</p>
219
- </div>
220
  </div>
221
- <div className="flex items-center gap-3">
222
- <div className="w-2 h-2 bg-blue-500 rounded-full"></div>
223
- <div>
224
- <p className="text-sm font-medium">Platform</p>
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
- AlertDialog,
9
- AlertDialogAction,
10
- AlertDialogCancel,
11
- AlertDialogContent,
12
- AlertDialogDescription,
13
- AlertDialogFooter,
14
- AlertDialogHeader,
15
- AlertDialogTitle
16
- } from '@/components/ui/alert-dialog'
17
  import {
18
  Collapsible,
19
  CollapsibleContent,
20
  CollapsibleTrigger
21
  } from '@/components/ui/collapsible'
22
- import { ChatContainer } from '@/components/chat/ChatContainer'
23
- import { ChatSessions } from '@/components/chat/ChatSessions'
 
 
 
 
 
 
 
 
 
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
- renameSession,
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 (selectedModel && !models.find(m => m.model_name === selectedModel && m.is_loaded)) {
157
- const loadedModel = models.find(m => m.is_loaded)
158
- if (loadedModel) {
159
- setSelectedModel(loadedModel.model_name)
 
160
  }
161
  }
162
  }, [models, selectedModel, setSelectedModel])
163
 
164
- const fetchModels = async () => {
165
- try {
166
- const res = await fetch('http://localhost:8000/models')
167
- if (res.ok) {
168
- const data: ModelsResponse = await res.json()
169
- setModels(data.models)
170
-
171
- // Set selected model to current model if available, otherwise first loaded model
172
- if (data.current_model && selectedModel !== data.current_model) {
173
- setSelectedModel(data.current_model)
174
- } else if (!selectedModel) {
175
- const loadedModel = data.models.find(m => m.is_loaded)
176
- if (loadedModel) {
177
- setSelectedModel(loadedModel.model_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  }
 
 
179
  }
180
  }
181
- } catch (err) {
182
- console.error('Failed to fetch models:', err)
 
 
 
183
  }
184
- }
185
 
186
- const handleLoadModelClick = (model: ModelInfo) => {
187
- setPendingModelAction({ action: 'load', model })
188
- setShowLoadConfirm(true)
189
- }
190
-
191
- const handleUnloadModelClick = (model: ModelInfo) => {
192
- setPendingModelAction({ action: 'unload', model })
193
- setShowUnloadConfirm(true)
194
- }
195
 
196
- const confirmLoadModel = async () => {
197
- const model = pendingModelAction.model
198
- if (!model) return
199
-
200
- setModelLoading(model.model_name)
201
  setShowLoadConfirm(false)
 
202
 
203
  try {
204
- const res = await fetch('http://localhost:8000/load-model', {
 
205
  method: 'POST',
206
  headers: { 'Content-Type': 'application/json' },
207
- body: JSON.stringify({ model_name: model.model_name }),
208
  })
209
 
210
- if (res.ok) {
211
- await fetchModels()
212
-
213
- // Set as selected model
214
- setSelectedModel(model.model_name)
215
  } else {
216
- const errorData = await res.json()
217
- console.error(`Failed to load model: ${errorData.detail || 'Unknown error'}`)
 
 
 
 
 
 
 
 
 
 
 
218
  }
219
- } catch (err) {
220
- console.error(`Failed to load model: ${err instanceof Error ? err.message : 'Unknown error'}`)
221
  } finally {
222
- setModelLoading(null)
 
223
  }
224
  }
225
 
226
- const confirmUnloadModel = async () => {
227
- const model = pendingModelAction.model
228
- if (!model) return
229
-
230
- setShowUnloadConfirm(false)
231
 
232
- try {
233
- const res = await fetch('http://localhost:8000/unload-model', {
234
- method: 'POST',
235
- headers: { 'Content-Type': 'application/json' },
236
- body: JSON.stringify({ model_name: model.model_name }),
237
- })
 
 
 
 
 
 
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  if (res.ok) {
240
- await fetchModels()
 
241
 
242
- // If we unloaded the selected model, find another loaded model
243
- if (selectedModel === model.model_name) {
244
- const remainingLoaded = models.find(m => m.is_loaded && m.model_name !== model.model_name)
245
- if (remainingLoaded) {
246
- setSelectedModel(remainingLoaded.model_name)
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(`Failed to unload model: ${err instanceof Error ? err.message : 'Unknown 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
- <ChatSessions
271
- sessions={sessions}
272
- currentSessionId={currentSessionId}
273
- onSelectSession={selectSession}
274
- onNewSession={createNewSession}
275
- onDeleteSession={deleteSession}
276
- onRenameSession={renameSession}
277
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  </div>
279
 
280
  {/* Overlay for mobile */}
@@ -368,19 +467,22 @@ export function Playground() {
368
  )}
369
 
370
  {/* Chat Messages and Input */}
371
- <ChatContainer
372
- messages={messages}
 
 
 
 
 
373
  input={input}
374
- onInputChange={setInput}
375
- onSubmit={sendMessage}
376
- onStop={stopGeneration}
377
- isLoading={isLoading}
378
- disabled={!selectedModel || !models.find(m => m.model_name === selectedModel)?.is_loaded}
379
- placeholder={
380
- !selectedModel || !models.find(m => m.model_name === selectedModel)?.is_loaded
381
- ? "Please load a model first..."
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 Management */}
397
  <Card>
398
  <CardHeader>
399
- <CardTitle className="text-sm">Model Management</CardTitle>
400
  </CardHeader>
401
  <CardContent className="space-y-3">
402
- {models.map((model) => (
403
- <div key={model.model_name} className="border rounded-lg p-3 overflow-hidden">
404
- <div className="space-y-3">
405
- {/* Model Header */}
406
- <div className="flex items-start gap-2">
407
- {model.supports_thinking ? <Brain className="h-4 w-4 flex-shrink-0" /> : <Zap className="h-4 w-4 flex-shrink-0" />}
408
- <div className="flex-1 min-w-0">
409
- <div className="flex items-center gap-2 mb-1 flex-wrap">
410
- <span className="font-medium text-sm truncate">{model.name}</span>
411
- {model.model_name === selectedModel && (
412
- <Badge variant="default" className="text-xs flex-shrink-0">Active</Badge>
413
- )}
414
- {model.is_loaded && model.model_name !== selectedModel && (
415
- <Badge variant="secondary" className="text-xs flex-shrink-0">Loaded</Badge>
416
- )}
417
- </div>
418
- <p className="text-xs text-muted-foreground break-words">
419
- {model.description} {model.size_gb}
420
- </p>
421
- </div>
422
- </div>
423
-
424
- {/* Model Selection */}
425
- {model.is_loaded && (
426
- <div className="flex items-center gap-2">
427
- <input
428
- type="radio"
429
- name="selectedModel"
430
- value={model.model_name}
431
- checked={selectedModel === model.model_name}
432
- onChange={() => setSelectedModel(model.model_name)}
433
- className="h-3 w-3 flex-shrink-0"
434
- />
435
- <Label className="text-xs">Use for generation</Label>
436
- </div>
437
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
 
439
- {/* Action Button */}
440
- <div className="flex justify-end">
441
- {model.is_loaded ? (
442
- <Button
443
- variant="outline"
444
- size="sm"
445
- onClick={() => handleUnloadModelClick(model)}
446
- disabled={isLoading}
447
- className="h-8 px-3 text-xs flex-shrink-0"
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 Model Confirmation Dialog */}
603
  <AlertDialog open={showLoadConfirm} onOpenChange={setShowLoadConfirm}>
604
  <AlertDialogContent>
605
  <AlertDialogHeader>
606
- <AlertDialogTitle>Load Model</AlertDialogTitle>
607
- <AlertDialogDescription>
608
- Do you want to load <strong>{pendingModelAction.model?.name}</strong>?
609
- <br /><br />
610
- <strong>Size:</strong> {pendingModelAction.model?.size_gb}
611
- <br />
612
- <strong>Note:</strong> This will download the model if it's not already cached locally.
613
- This may take several minutes and use significant bandwidth and storage.
614
- </AlertDialogDescription>
615
- </AlertDialogHeader>
616
- <AlertDialogFooter>
617
- <AlertDialogCancel>Cancel</AlertDialogCancel>
618
- <AlertDialogAction onClick={confirmLoadModel}>
619
- Load Model
620
- </AlertDialogAction>
621
- </AlertDialogFooter>
622
- </AlertDialogContent>
623
- </AlertDialog>
 
 
 
 
 
624
 
625
- {/* Unload Model Confirmation Dialog */}
626
- <AlertDialog open={showUnloadConfirm} onOpenChange={setShowUnloadConfirm}>
627
- <AlertDialogContent>
628
- <AlertDialogHeader>
629
- <AlertDialogTitle>Unload Model</AlertDialogTitle>
630
- <AlertDialogDescription>
631
- Are you sure you want to unload <strong>{pendingModelAction.model?.name}</strong>?
632
- <br /><br />
633
- This will free up memory but you'll need to reload it to use it again.
634
- {pendingModelAction.model?.model_name === selectedModel && (
635
- <><br /><br /><strong>Warning:</strong> This is the currently active model.</>
636
- )}
 
 
 
 
 
637
  </AlertDialogDescription>
638
  </AlertDialogHeader>
639
  <AlertDialogFooter>
640
- <AlertDialogCancel>Cancel</AlertDialogCancel>
641
- <AlertDialogAction onClick={confirmUnloadModel}>
642
- Unload Model
 
 
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>