wu981526092 commited on
Commit
d8e039b
Β·
1 Parent(s): 9701528

Production ready build with modular backend

Browse files
Files changed (47) hide show
  1. .gitignore +46 -0
  2. CONTRIBUTING.md +230 -0
  3. LICENSE +21 -0
  4. backend/__init__.py +0 -0
  5. backend/api/__init__.py +0 -0
  6. backend/api/endpoints/__init__.py +0 -0
  7. backend/api/routes.py +141 -0
  8. backend/app.py +243 -0
  9. backend/config.py +30 -0
  10. backend/core/__init__.py +0 -0
  11. backend/main.py +40 -0
  12. backend/models.py +42 -0
  13. backend/services/__init__.py +1 -0
  14. backend/services/chat_service.py +85 -0
  15. backend/services/model_service.py +77 -0
  16. backend/utils/__init__.py +0 -0
  17. frontend/index.html +13 -0
  18. frontend/package-lock.json +0 -0
  19. frontend/package.json +40 -0
  20. frontend/postcss.config.js +6 -0
  21. frontend/src/App.tsx +24 -0
  22. frontend/src/components/Sidebar.tsx +153 -0
  23. frontend/src/components/chat/ChatContainer.tsx +113 -0
  24. frontend/src/components/chat/ChatInput.tsx +138 -0
  25. frontend/src/components/chat/ChatMessage.tsx +192 -0
  26. frontend/src/components/chat/ChatSessions.tsx +213 -0
  27. frontend/src/components/chat/index.ts +4 -0
  28. frontend/src/components/ui/button.tsx +57 -0
  29. frontend/src/components/ui/card.tsx +76 -0
  30. frontend/src/components/ui/textarea.tsx +22 -0
  31. frontend/src/hooks/useChat.ts +257 -0
  32. frontend/src/index.css +138 -0
  33. frontend/src/lib/chat-storage.ts +132 -0
  34. frontend/src/lib/utils.ts +6 -0
  35. frontend/src/main.tsx +10 -0
  36. frontend/src/pages/Home.tsx +235 -0
  37. frontend/src/pages/Playground.tsx +649 -0
  38. frontend/src/types/chat.ts +29 -0
  39. frontend/tailwind.config.js +91 -0
  40. frontend/tsconfig.json +25 -0
  41. frontend/tsconfig.node.json +10 -0
  42. frontend/vite.config.ts +13 -0
  43. package.json +42 -0
  44. scripts/start_both.bat +51 -0
  45. scripts/start_platform.py +279 -0
  46. scripts/start_platform.sh +216 -0
  47. scripts/stop_both.bat +34 -0
.gitignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
29
+ Thumbs.db
30
+
31
+ # Logs
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
CONTRIBUTING.md ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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!** πŸš€
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.
backend/__init__.py ADDED
File without changes
backend/api/__init__.py ADDED
File without changes
backend/api/endpoints/__init__.py ADDED
File without changes
backend/api/routes.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,
8
+ ModelLoadRequest, ModelUnloadRequest
9
+ )
10
+ from ..services.model_service import model_service
11
+ from ..services.chat_service import chat_service
12
+ from ..config import AVAILABLE_MODELS
13
+
14
+ # Create API router
15
+ router = APIRouter()
16
+
17
+
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")
25
+ async def health_check():
26
+ """Health check endpoint"""
27
+ return {"status": "healthy", "message": "Edge LLM API is running"}
28
+
29
+
30
+ @router.get("/models", response_model=ModelsResponse)
31
+ async def get_models():
32
+ """Get available models and their status"""
33
+ models = []
34
+ for model_name, info in AVAILABLE_MODELS.items():
35
+ models.append(ModelInfo(
36
+ model_name=model_name,
37
+ name=info["name"],
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(
45
+ models=models,
46
+ current_model=model_service.get_current_model() or ""
47
+ )
48
+
49
+
50
+ @router.post("/load-model")
51
+ async def load_model(request: ModelLoadRequest):
52
+ """Load a specific model"""
53
+ if request.model_name not in AVAILABLE_MODELS:
54
+ raise HTTPException(
55
+ status_code=400,
56
+ detail=f"Model {request.model_name} not available"
57
+ )
58
+
59
+ success = model_service.load_model(request.model_name)
60
+ if success:
61
+ model_service.set_current_model(request.model_name)
62
+ return {
63
+ "message": f"Model {request.model_name} loaded successfully",
64
+ "current_model": model_service.get_current_model()
65
+ }
66
+ else:
67
+ raise HTTPException(
68
+ status_code=500,
69
+ detail=f"Failed to load model {request.model_name}"
70
+ )
71
+
72
+
73
+ @router.post("/unload-model")
74
+ async def unload_model(request: ModelUnloadRequest):
75
+ """Unload a specific model"""
76
+ success = model_service.unload_model(request.model_name)
77
+ if success:
78
+ return {
79
+ "message": f"Model {request.model_name} unloaded successfully",
80
+ "current_model": model_service.get_current_model() or ""
81
+ }
82
+ else:
83
+ raise HTTPException(
84
+ status_code=404,
85
+ detail=f"Model {request.model_name} not found in cache"
86
+ )
87
+
88
+
89
+ @router.post("/set-current-model")
90
+ async def set_current_model(request: ModelLoadRequest):
91
+ """Set the current active model"""
92
+ if not model_service.is_model_loaded(request.model_name):
93
+ raise HTTPException(
94
+ status_code=400,
95
+ detail=f"Model {request.model_name} is not loaded. Please load it first."
96
+ )
97
+
98
+ model_service.set_current_model(request.model_name)
99
+ return {
100
+ "message": f"Current model set to {request.model_name}",
101
+ "current_model": model_service.get_current_model()
102
+ }
103
+
104
+
105
+ @router.post("/generate", response_model=PromptResponse)
106
+ async def generate_text(request: PromptRequest):
107
+ """Generate text using the loaded model"""
108
+ # Use the model specified in request, or fall back to current model
109
+ model_to_use = request.model_name if request.model_name else model_service.get_current_model()
110
+
111
+ if not model_to_use:
112
+ raise HTTPException(
113
+ status_code=400,
114
+ detail="No model specified. Please load a model first."
115
+ )
116
+
117
+ if not model_service.is_model_loaded(model_to_use):
118
+ raise HTTPException(
119
+ status_code=400,
120
+ detail=f"Model {model_to_use} is not loaded. Please load it first."
121
+ )
122
+
123
+ try:
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
130
+ )
131
+
132
+ return PromptResponse(
133
+ thinking_content=thinking_content,
134
+ content=final_content,
135
+ model_used=model_used,
136
+ supports_thinking=supports_thinking
137
+ )
138
+
139
+ except Exception as e:
140
+ print(f"Generation error: {e}")
141
+ raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
backend/app.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
backend/core/__init__.py ADDED
File without changes
backend/main.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main FastAPI application
3
+ """
4
+ 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:
12
+ """Create and configure the FastAPI application"""
13
+ app = FastAPI(title="Edge LLM API")
14
+
15
+ # Enable CORS for Hugging Face Space
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=CORS_ORIGINS,
19
+ allow_credentials=True,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+ # Mount static files
25
+ app.mount("/assets", StaticFiles(directory=ASSETS_DIR), name="assets")
26
+
27
+ # Include API routes
28
+ app.include_router(router)
29
+
30
+ @app.on_event("startup")
31
+ async def startup_event():
32
+ """Startup event - don't load models by default"""
33
+ print("πŸš€ Edge LLM API is starting up...")
34
+ print("πŸ’‘ Models will be loaded on demand")
35
+
36
+ return app
37
+
38
+
39
+ # Create the app instance
40
+ app = create_app()
backend/models.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic models for API requests and responses
3
+ """
4
+ 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
13
+ max_new_tokens: Optional[int] = 1024
14
+
15
+
16
+ class PromptResponse(BaseModel):
17
+ thinking_content: str
18
+ content: str
19
+ model_used: str
20
+ supports_thinking: bool
21
+
22
+
23
+ class ModelInfo(BaseModel):
24
+ model_name: str
25
+ name: str
26
+ supports_thinking: bool
27
+ description: str
28
+ size_gb: str
29
+ is_loaded: bool
30
+
31
+
32
+ class ModelsResponse(BaseModel):
33
+ models: List[ModelInfo]
34
+ current_model: str
35
+
36
+
37
+ class ModelLoadRequest(BaseModel):
38
+ model_name: str
39
+
40
+
41
+ class ModelUnloadRequest(BaseModel):
42
+ model_name: str
backend/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Services module
backend/services/chat_service.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
27
+ # Get model and tokenizer
28
+ model_data = model_service.models_cache[model_name]
29
+ model = model_data["model"]
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
+ )
45
+
46
+ # Tokenize
47
+ inputs = tokenizer(formatted_prompt, return_tensors="pt").to(model.device)
48
+
49
+ # Generate
50
+ with torch.no_grad():
51
+ outputs = model.generate(
52
+ **inputs,
53
+ max_new_tokens=max_new_tokens,
54
+ temperature=temperature,
55
+ do_sample=True,
56
+ pad_token_id=tokenizer.eos_token_id
57
+ )
58
+
59
+ # Decode
60
+ generated_tokens = outputs[0][inputs['input_ids'].shape[1]:]
61
+ generated_text = tokenizer.decode(generated_tokens, skip_special_tokens=True)
62
+
63
+ # Parse thinking vs final content for thinking models
64
+ thinking_content = ""
65
+ final_content = generated_text
66
+
67
+ if model_info["supports_thinking"] and "<thinking>" in generated_text:
68
+ parts = generated_text.split("<thinking>")
69
+ if len(parts) > 1:
70
+ thinking_part = parts[1]
71
+ if "</thinking>" in thinking_part:
72
+ thinking_content = thinking_part.split("</thinking>")[0].strip()
73
+ remaining = thinking_part.split("</thinking>", 1)[1] if "</thinking>" in thinking_part else ""
74
+ final_content = remaining.strip()
75
+
76
+ return (
77
+ thinking_content,
78
+ final_content,
79
+ model_name,
80
+ model_info["supports_thinking"]
81
+ )
82
+
83
+
84
+ # Global chat service instance
85
+ chat_service = ChatService()
backend/services/model_service.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:
47
+ self.current_model_name = None
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
+
75
+
76
+ # Global model service instance
77
+ model_service = ModelService()
backend/utils/__init__.py ADDED
File without changes
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
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>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "edge-llm-frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@radix-ui/react-alert-dialog": "^1.1.15",
13
+ "@radix-ui/react-collapsible": "^1.1.12",
14
+ "@radix-ui/react-label": "^2.1.7",
15
+ "@radix-ui/react-select": "^2.2.6",
16
+ "@radix-ui/react-slider": "^1.3.6",
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",
26
+ "react-router-dom": "^6.15.0",
27
+ "tailwind-merge": "^1.14.0",
28
+ "tailwindcss-animate": "^1.0.7"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^18.2.15",
32
+ "@types/react-dom": "^18.2.7",
33
+ "@vitejs/plugin-react": "^4.0.3",
34
+ "autoprefixer": "^10.4.14",
35
+ "postcss": "^8.4.24",
36
+ "tailwindcss": "^3.3.0",
37
+ "typescript": "^5.0.2",
38
+ "vite": "^4.4.5"
39
+ }
40
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
2
+ import { Layout } from './components/Layout'
3
+ import { Home } from './pages/Home'
4
+ import { Playground } from './pages/Playground'
5
+ import { Models } from './pages/Models'
6
+ import { Settings } from './pages/Settings'
7
+
8
+ function App() {
9
+ return (
10
+ <Router>
11
+ <Routes>
12
+ <Route path="/" element={<Layout />}>
13
+ <Route index element={<Home />} />
14
+ <Route path="playground" element={<Playground />} />
15
+ <Route path="models" element={<Models />} />
16
+ <Route path="settings" element={<Settings />} />
17
+ </Route>
18
+ </Routes>
19
+ </Router>
20
+ )
21
+ }
22
+
23
+ export default App
24
+
frontend/src/components/Sidebar.tsx ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Link, useLocation } from 'react-router-dom'
2
+ import { cn } from '@/lib/utils'
3
+ import {
4
+ Home,
5
+ BookOpen,
6
+ MessageSquare,
7
+ Bot,
8
+ Zap,
9
+ Settings,
10
+ Brain
11
+ } from 'lucide-react'
12
+
13
+ const navigation = [
14
+ {
15
+ name: 'Home',
16
+ href: '/',
17
+ icon: Home,
18
+ description: 'Overview and getting started'
19
+ },
20
+ {
21
+ name: 'Chat Playground',
22
+ href: '/playground',
23
+ icon: MessageSquare,
24
+ description: 'AI chatbot with conversation history'
25
+ },
26
+ {
27
+ name: 'Model Catalog',
28
+ href: '/models',
29
+ icon: BookOpen,
30
+ description: 'Browse and manage models'
31
+ },
32
+ {
33
+ name: 'Assistants',
34
+ href: '/assistants',
35
+ icon: Bot,
36
+ description: 'Custom AI assistants',
37
+ badge: 'Preview'
38
+ }
39
+ ]
40
+
41
+ const tools = [
42
+ {
43
+ name: 'Completions',
44
+ href: '/completions',
45
+ icon: Zap,
46
+ description: 'Text completion endpoint'
47
+ },
48
+ {
49
+ name: 'Fine-tuning',
50
+ href: '/fine-tuning',
51
+ icon: Brain,
52
+ description: 'Train custom models'
53
+ },
54
+ {
55
+ name: 'Settings',
56
+ href: '/settings',
57
+ icon: Settings,
58
+ description: 'Application settings'
59
+ }
60
+ ]
61
+
62
+ 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">
87
+ {navigation.map((item) => {
88
+ const isActive = location.pathname === item.href
89
+ return (
90
+ <Link
91
+ key={item.name}
92
+ to={item.href}
93
+ className={cn(
94
+ 'flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-all hover:bg-accent',
95
+ isActive
96
+ ? 'bg-accent text-accent-foreground font-medium'
97
+ : 'text-muted-foreground hover:text-foreground'
98
+ )}
99
+ >
100
+ <item.icon className="h-4 w-4" />
101
+ <div className="flex-1">
102
+ <div className="flex items-center gap-2">
103
+ {item.name}
104
+ {item.badge && (
105
+ <span className="px-1.5 py-0.5 text-xs bg-blue-100 text-blue-700 rounded-full">
106
+ {item.badge}
107
+ </span>
108
+ )}
109
+ </div>
110
+ </div>
111
+ </Link>
112
+ )
113
+ })}
114
+ </nav>
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) => {
123
+ const isActive = location.pathname === item.href
124
+ return (
125
+ <Link
126
+ key={item.name}
127
+ to={item.href}
128
+ className={cn(
129
+ 'flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-all hover:bg-accent',
130
+ isActive
131
+ ? 'bg-accent text-accent-foreground font-medium'
132
+ : 'text-muted-foreground hover:text-foreground'
133
+ )}
134
+ >
135
+ <item.icon className="h-4 w-4" />
136
+ {item.name}
137
+ </Link>
138
+ )
139
+ })}
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
+ }
frontend/src/components/chat/ChatContainer.tsx ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
frontend/src/components/chat/ChatInput.tsx ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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[]
17
+ currentSessionId: string | null
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({
25
+ sessions,
26
+ currentSessionId,
27
+ onSelectSession,
28
+ onNewSession,
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>
211
+ </div>
212
+ )
213
+ }
frontend/src/components/chat/index.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export { ChatContainer } from './ChatContainer'
2
+ export { ChatInput } from './ChatInput'
3
+ export { ChatMessage } from './ChatMessage'
4
+ export { ChatSessions } from './ChatSessions'
frontend/src/components/ui/button.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ }
35
+ )
36
+
37
+ export interface ButtonProps
38
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
+ VariantProps<typeof buttonVariants> {
40
+ asChild?: boolean
41
+ }
42
+
43
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
45
+ const Comp = asChild ? Slot : "button"
46
+ return (
47
+ <Comp
48
+ className={cn(buttonVariants({ variant, size, className }))}
49
+ ref={ref}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+ )
55
+ Button.displayName = "Button"
56
+
57
+ export { Button, buttonVariants }
frontend/src/components/ui/card.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-xl border bg-card text-card-foreground shadow",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ 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}
52
+ />
53
+ ))
54
+ CardDescription.displayName = "CardDescription"
55
+
56
+ const CardContent = React.forwardRef<
57
+ HTMLDivElement,
58
+ React.HTMLAttributes<HTMLDivElement>
59
+ >(({ className, ...props }, ref) => (
60
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
61
+ ))
62
+ CardContent.displayName = "CardContent"
63
+
64
+ 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
+
76
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
frontend/src/components/ui/textarea.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 }
frontend/src/hooks/useChat.ts ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import { Message, ChatSession, MessageStatus } from '@/types/chat'
3
+ import { chatStorage } from '@/lib/chat-storage'
4
+
5
+ interface UseChatOptions {
6
+ api_endpoint?: string
7
+ defaultModel?: string
8
+ defaultSystemPrompt?: string
9
+ }
10
+
11
+ interface ApiResponse {
12
+ thinking_content: string
13
+ content: string
14
+ model_used: string
15
+ supports_thinking: boolean
16
+ }
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
+
25
+ // Chat state
26
+ const [sessions, setSessions] = useState<ChatSession[]>([])
27
+ const [currentSessionId, setCurrentSessionId] = useState<string | null>(null)
28
+ const [input, setInput] = useState('')
29
+ const [status, setStatus] = useState<MessageStatus>({
30
+ isLoading: false,
31
+ error: null
32
+ })
33
+
34
+ // Model settings
35
+ const [selectedModel, setSelectedModel] = useState(defaultModel)
36
+ const [systemPrompt, setSystemPrompt] = useState(defaultSystemPrompt)
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(() => {
46
+ const loadedSessions = chatStorage.getAllSessions()
47
+ setSessions(loadedSessions)
48
+
49
+ const currentSession = chatStorage.getCurrentSession()
50
+ if (currentSession) {
51
+ setCurrentSessionId(currentSession.id)
52
+ } else if (loadedSessions.length > 0) {
53
+ setCurrentSessionId(loadedSessions[0].id)
54
+ chatStorage.setCurrentSession(loadedSessions[0].id)
55
+ }
56
+ }, [])
57
+
58
+
59
+
60
+ // Create new session
61
+ const createNewSession = useCallback(() => {
62
+ const newSession = chatStorage.createSession(
63
+ undefined, // Auto-generate title
64
+ selectedModel,
65
+ systemPrompt
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
+
73
+ return newSession.id
74
+ }, [selectedModel, systemPrompt])
75
+
76
+ // Switch to session
77
+ const selectSession = useCallback((sessionId: string) => {
78
+ setCurrentSessionId(sessionId)
79
+ chatStorage.setCurrentSession(sessionId)
80
+ }, [])
81
+
82
+ // Delete session
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) {
90
+ setCurrentSessionId(updatedSessions[0].id)
91
+ chatStorage.setCurrentSession(updatedSessions[0].id)
92
+ } else {
93
+ setCurrentSessionId(null)
94
+ }
95
+ }
96
+ }, [currentSessionId])
97
+
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
105
+ const addMessage = useCallback((message: Omit<Message, 'id' | 'timestamp'>) => {
106
+ if (!currentSessionId) return
107
+
108
+ chatStorage.addMessageToSession(currentSessionId, message)
109
+ setSessions(chatStorage.getAllSessions())
110
+ }, [currentSessionId])
111
+
112
+ // Send message
113
+ const sendMessage = useCallback(async () => {
114
+ if (!input.trim() || status.isLoading) return
115
+
116
+ let sessionId = currentSessionId
117
+
118
+ // Create new session if none exists
119
+ if (!sessionId) {
120
+ sessionId = createNewSession()
121
+ }
122
+
123
+ const userMessage = input.trim()
124
+ setInput('')
125
+ setStatus({ isLoading: true, error: null })
126
+
127
+ // Add user message directly to the specific session
128
+ if (sessionId) {
129
+ chatStorage.addMessageToSession(sessionId, {
130
+ role: 'user',
131
+ content: userMessage
132
+ })
133
+ setSessions(chatStorage.getAllSessions())
134
+ }
135
+
136
+ // Add system message if system prompt is set
137
+ // Check actual session messages from storage, not React state
138
+ const actualSession = chatStorage.getSession(sessionId!)
139
+ const hasMessages = actualSession?.messages && actualSession.messages.length > 1 // >1 because user message was just added
140
+
141
+ if (systemPrompt && !hasMessages && sessionId) {
142
+ chatStorage.addMessageToSession(sessionId, {
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: {
153
+ 'Content-Type': 'application/json',
154
+ },
155
+ body: JSON.stringify({
156
+ prompt: userMessage,
157
+ system_prompt: systemPrompt || null,
158
+ model_name: selectedModel,
159
+ temperature,
160
+ max_new_tokens: maxTokens
161
+ }),
162
+ })
163
+
164
+ if (!response.ok) {
165
+ const errorData = await response.json()
166
+ throw new Error(errorData.detail || `HTTP error! status: ${response.status}`)
167
+ }
168
+
169
+ const data: ApiResponse = await response.json()
170
+
171
+ // Add assistant message directly to the specific session
172
+ if (sessionId) {
173
+ chatStorage.addMessageToSession(sessionId, {
174
+ role: 'assistant',
175
+ content: data.content,
176
+ thinking_content: data.thinking_content,
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 })
184
+ } catch (error) {
185
+ const errorMessage = error instanceof Error ? error.message : 'An error occurred'
186
+ setStatus({ isLoading: false, error: errorMessage })
187
+
188
+ // Add error message directly to the specific session
189
+ if (sessionId) {
190
+ chatStorage.addMessageToSession(sessionId, {
191
+ role: 'assistant',
192
+ content: `Sorry, I encountered an error: ${errorMessage}`
193
+ })
194
+ setSessions(chatStorage.getAllSessions())
195
+ }
196
+ }
197
+ }, [
198
+ input,
199
+ status.isLoading,
200
+ currentSessionId,
201
+ createNewSession,
202
+ addMessage,
203
+ systemPrompt,
204
+ messages.length,
205
+ api_endpoint,
206
+ selectedModel,
207
+ temperature,
208
+ maxTokens
209
+ ])
210
+
211
+ // Stop generation (placeholder for future implementation)
212
+ const stopGeneration = useCallback(() => {
213
+ setStatus({ isLoading: false, error: null })
214
+ }, [])
215
+
216
+ // Clear all sessions
217
+ const clearAllSessions = useCallback(() => {
218
+ chatStorage.clear()
219
+ setSessions([])
220
+ setCurrentSessionId(null)
221
+ }, [])
222
+
223
+ return {
224
+ // Session management
225
+ sessions,
226
+ currentSession,
227
+ currentSessionId,
228
+ createNewSession,
229
+ selectSession,
230
+ deleteSession,
231
+ renameSession,
232
+ clearAllSessions,
233
+
234
+ // Messages
235
+ messages,
236
+ input,
237
+ setInput,
238
+
239
+ // Chat actions
240
+ sendMessage,
241
+ stopGeneration,
242
+
243
+ // Status
244
+ isLoading: status.isLoading,
245
+ error: status.error,
246
+
247
+ // Model settings
248
+ selectedModel,
249
+ setSelectedModel,
250
+ systemPrompt,
251
+ setSystemPrompt,
252
+ temperature,
253
+ setTemperature,
254
+ maxTokens,
255
+ setMaxTokens
256
+ }
257
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
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;
118
+ }
119
+ body {
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
+ }
frontend/src/lib/chat-storage.ts ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
frontend/src/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.tsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
frontend/src/pages/Home.tsx ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
2
+ import { Button } from '@/components/ui/button'
3
+
4
+ import {
5
+ Brain,
6
+ MessageSquare,
7
+ BookOpen,
8
+ Zap,
9
+ Shield,
10
+ Cpu,
11
+ ArrowRight,
12
+ Download
13
+ } from 'lucide-react'
14
+ import { Link } from 'react-router-dom'
15
+
16
+ const features = [
17
+ {
18
+ icon: Brain,
19
+ title: "Local AI Models",
20
+ description: "Run powerful language models locally on your machine with full privacy control.",
21
+ color: "text-blue-500"
22
+ },
23
+ {
24
+ icon: MessageSquare,
25
+ title: "Interactive Chat",
26
+ description: "Playground interface for testing prompts and exploring model capabilities.",
27
+ color: "text-green-500"
28
+ },
29
+ {
30
+ icon: Shield,
31
+ title: "Privacy First",
32
+ description: "Your data never leaves your machine. Complete privacy and security guaranteed.",
33
+ color: "text-purple-500"
34
+ },
35
+ {
36
+ icon: Zap,
37
+ title: "High Performance",
38
+ description: "Optimized for speed with model caching and efficient resource management.",
39
+ color: "text-yellow-500"
40
+ }
41
+ ]
42
+
43
+ const quickActions = [
44
+ {
45
+ title: "Start Chatting",
46
+ description: "Jump into the playground and start experimenting",
47
+ href: "/playground",
48
+ icon: MessageSquare,
49
+ primary: true
50
+ },
51
+ {
52
+ title: "Browse Models",
53
+ description: "Explore available models and their capabilities",
54
+ href: "/models",
55
+ icon: BookOpen,
56
+ primary: false
57
+ },
58
+ {
59
+ title: "View Settings",
60
+ description: "Configure your application preferences",
61
+ href: "/settings",
62
+ icon: Cpu,
63
+ primary: false
64
+ }
65
+ ]
66
+
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>
182
+ </div>
183
+ <p className="text-sm text-muted-foreground pl-8">
184
+ Go to the playground and start experimenting with prompts.
185
+ </p>
186
+ </div>
187
+ </div>
188
+
189
+ <div className="pt-4 border-t">
190
+ <Link to="/playground">
191
+ <Button className="w-full md:w-auto">
192
+ <MessageSquare className="h-4 w-4 mr-2" />
193
+ Open Playground
194
+ </Button>
195
+ </Link>
196
+ </div>
197
+ </CardContent>
198
+ </Card>
199
+
200
+ {/* Status */}
201
+ <Card>
202
+ <CardHeader>
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>
232
+ </div>
233
+ </div>
234
+ )
235
+ }
frontend/src/pages/Playground.tsx ADDED
@@ -0,0 +1,649 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import { Button } from '@/components/ui/button'
3
+ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
4
+ 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,
34
+ Code,
35
+ Upload,
36
+ Share,
37
+ History,
38
+ Settings,
39
+ PanelLeftOpen,
40
+ PanelLeftClose
41
+ } from 'lucide-react'
42
+
43
+ interface ModelInfo {
44
+ model_name: string
45
+ name: string
46
+ supports_thinking: boolean
47
+ description: string
48
+ size_gb: string
49
+ is_loaded: boolean
50
+ }
51
+
52
+ interface ModelsResponse {
53
+ models: ModelInfo[]
54
+ current_model: string
55
+ }
56
+
57
+ export function Playground() {
58
+ // Chat functionality
59
+ const {
60
+ sessions,
61
+ currentSession,
62
+ currentSessionId,
63
+ createNewSession,
64
+ selectSession,
65
+ deleteSession,
66
+ renameSession,
67
+ messages,
68
+ input,
69
+ setInput,
70
+ sendMessage,
71
+ stopGeneration,
72
+ isLoading,
73
+ selectedModel,
74
+ setSelectedModel,
75
+ systemPrompt,
76
+ setSystemPrompt,
77
+ temperature,
78
+ setTemperature,
79
+ maxTokens,
80
+ setMaxTokens
81
+ } = useChat()
82
+
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 = [
99
+ {
100
+ name: "Default Assistant",
101
+ prompt: "You are a helpful, harmless, and honest AI assistant. Provide clear, accurate, and well-structured responses."
102
+ },
103
+ {
104
+ name: "Code Expert",
105
+ prompt: "You are an expert software developer. Provide clean, efficient code with clear explanations. Always follow best practices and include comments where helpful."
106
+ },
107
+ {
108
+ name: "Technical Writer",
109
+ prompt: "You are a technical writer. Create clear, comprehensive documentation and explanations. Use proper formatting and structure your responses logically."
110
+ },
111
+ {
112
+ name: "Creative Writer",
113
+ prompt: "You are a creative writer. Use vivid language, engaging storytelling, and imaginative descriptions. Be expressive and artistic in your responses."
114
+ },
115
+ {
116
+ name: "Research Assistant",
117
+ prompt: "You are a research assistant. Provide detailed, well-researched responses with clear reasoning. Cite sources when relevant and present information objectively."
118
+ },
119
+ {
120
+ name: "Teacher",
121
+ prompt: "You are an experienced teacher. Explain concepts clearly, use examples, and break down complex topics into understandable parts. Be encouraging and patient."
122
+ }
123
+ ]
124
+
125
+ // Sample prompts for quick start
126
+ const samplePrompts = [
127
+ {
128
+ title: "Marketing Slogan",
129
+ description: "Create a catchy marketing slogan for a new eco-friendly product.",
130
+ prompt: "Create a catchy marketing slogan for a new eco-friendly water bottle that keeps drinks cold for 24 hours. The target audience is environmentally conscious millennials and Gen Z consumers."
131
+ },
132
+ {
133
+ title: "Creative Storytelling",
134
+ description: "Write a short story about a time traveler.",
135
+ prompt: "Write a 300-word short story about a time traveler who accidentally changes a major historical event while trying to observe ancient Rome."
136
+ },
137
+ {
138
+ title: "Technical Explanation",
139
+ description: "Explain a complex technical concept simply.",
140
+ prompt: "Explain how blockchain technology works in simple terms that a 12-year-old could understand, using analogies and examples."
141
+ },
142
+ {
143
+ title: "Code Generation",
144
+ description: "Generate code with explanations.",
145
+ prompt: "Write a Python function that takes a list of numbers and returns the second largest number. Include error handling and detailed comments explaining each step."
146
+ }
147
+ ]
148
+
149
+ // Load available models on startup
150
+ useEffect(() => {
151
+ fetchModels()
152
+ }, [])
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
+ }
261
+
262
+ return (
263
+ <div className="min-h-screen bg-background flex">
264
+ {/* Chat Sessions Sidebar */}
265
+ <div className={`
266
+ ${showSessions ? 'translate-x-0' : '-translate-x-full'}
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 */}
281
+ {showSessions && (
282
+ <div
283
+ className="fixed inset-0 z-40 bg-black/50 lg:hidden"
284
+ onClick={() => setShowSessions(false)}
285
+ />
286
+ )}
287
+
288
+ {/* Main Content */}
289
+ <div className="flex-1 flex flex-col overflow-hidden">
290
+ {/* Header */}
291
+ <div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
292
+ <div className="flex h-14 items-center px-6">
293
+ <div className="flex items-center gap-2">
294
+ <Button
295
+ variant="ghost"
296
+ size="sm"
297
+ onClick={() => setShowSessions(!showSessions)}
298
+ className="lg:hidden"
299
+ >
300
+ {showSessions ? <PanelLeftClose className="h-4 w-4" /> : <PanelLeftOpen className="h-4 w-4" />}
301
+ </Button>
302
+ <MessageSquare className="h-5 w-5" />
303
+ <h1 className="text-lg font-semibold">Chat Playground</h1>
304
+ {currentSession && (
305
+ <Badge variant="outline" className="text-xs">
306
+ {currentSession.title.slice(0, 20)}...
307
+ </Badge>
308
+ )}
309
+ </div>
310
+ <div className="ml-auto flex items-center gap-2 overflow-x-auto">
311
+ <Button
312
+ variant="outline"
313
+ size="sm"
314
+ onClick={() => setShowSessions(!showSessions)}
315
+ className="hidden lg:flex flex-shrink-0"
316
+ >
317
+ <History className="h-4 w-4 mr-2" />
318
+ <span className="hidden sm:inline">Sessions</span>
319
+ </Button>
320
+ <Button variant="outline" size="sm" className="flex-shrink-0">
321
+ <Code className="h-4 w-4 mr-2" />
322
+ <span className="hidden sm:inline">View code</span>
323
+ <span className="sm:hidden">Code</span>
324
+ </Button>
325
+ <Button variant="outline" size="sm" className="flex-shrink-0">
326
+ <Upload className="h-4 w-4 mr-2" />
327
+ <span className="hidden sm:inline">Import</span>
328
+ </Button>
329
+ <Button variant="outline" size="sm" className="flex-shrink-0">
330
+ <Share className="h-4 w-4 mr-2" />
331
+ <span className="hidden sm:inline">Export</span>
332
+ </Button>
333
+ </div>
334
+ </div>
335
+ </div>
336
+
337
+ {/* Content Area */}
338
+ <div className="flex-1 flex overflow-hidden">
339
+ {/* Chat Area */}
340
+ <div className="flex-1 flex flex-col">
341
+ {/* Sample Prompts */}
342
+ {messages.length === 0 && (
343
+ <div className="p-6 border-b">
344
+ <Card>
345
+ <CardHeader>
346
+ <CardTitle className="text-base">Start with a sample prompt</CardTitle>
347
+ </CardHeader>
348
+ <CardContent>
349
+ <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
350
+ {samplePrompts.map((sample, index) => (
351
+ <Button
352
+ key={index}
353
+ variant="outline"
354
+ className="h-auto p-4 text-left justify-start min-w-0"
355
+ onClick={() => handleSamplePromptClick(sample.prompt)}
356
+ disabled={isLoading}
357
+ >
358
+ <div className="min-w-0">
359
+ <div className="font-medium text-sm mb-1 truncate">{sample.title}</div>
360
+ <div className="text-xs text-muted-foreground line-clamp-2">{sample.description}</div>
361
+ </div>
362
+ </Button>
363
+ ))}
364
+ </div>
365
+ </CardContent>
366
+ </Card>
367
+ </div>
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>
387
+
388
+ {/* Settings Panel */}
389
+ <div className="w-80 border-l bg-muted/30 overflow-y-auto">
390
+ <div className="p-4 space-y-6">
391
+ <div className="flex items-center gap-2">
392
+ <Settings className="h-4 w-4" />
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
+
480
+ {/* Parameters */}
481
+ <Card>
482
+ <CardHeader>
483
+ <CardTitle className="text-sm">Parameters</CardTitle>
484
+ </CardHeader>
485
+ <CardContent className="space-y-4">
486
+ {/* Temperature */}
487
+ <div>
488
+ <Label className="text-xs font-medium">
489
+ Temperature: {temperature.toFixed(2)}
490
+ </Label>
491
+ <Slider
492
+ value={[temperature]}
493
+ onValueChange={(value) => setTemperature(value[0])}
494
+ min={0}
495
+ max={2}
496
+ step={0.01}
497
+ className="mt-2"
498
+ disabled={isLoading}
499
+ />
500
+ <p className="text-xs text-muted-foreground mt-1">
501
+ Lower = more focused, Higher = more creative
502
+ </p>
503
+ </div>
504
+
505
+ {/* Max Tokens */}
506
+ <div>
507
+ <Label className="text-xs font-medium">
508
+ Max Tokens: {maxTokens}
509
+ </Label>
510
+ <Slider
511
+ value={[maxTokens]}
512
+ onValueChange={(value) => setMaxTokens(value[0])}
513
+ min={100}
514
+ max={4096}
515
+ step={100}
516
+ className="mt-2"
517
+ disabled={isLoading}
518
+ />
519
+ </div>
520
+ </CardContent>
521
+ </Card>
522
+
523
+ {/* System Prompt */}
524
+ <Card>
525
+ <Collapsible
526
+ open={isSystemPromptOpen}
527
+ onOpenChange={setIsSystemPromptOpen}
528
+ >
529
+ <CardHeader>
530
+ <CollapsibleTrigger asChild>
531
+ <Button variant="ghost" className="w-full justify-between p-0" disabled={isLoading}>
532
+ <div className="flex items-center gap-2">
533
+ <MessageSquare className="h-4 w-4" />
534
+ <span className="text-sm font-medium">System Prompt</span>
535
+ {systemPrompt && <Badge variant="secondary" className="text-xs">Custom</Badge>}
536
+ </div>
537
+ <ChevronDown className={`h-4 w-4 transition-transform ${isSystemPromptOpen ? 'transform rotate-180' : ''}`} />
538
+ </Button>
539
+ </CollapsibleTrigger>
540
+ </CardHeader>
541
+ <CollapsibleContent>
542
+ <CardContent className="space-y-3">
543
+ {/* Preset System Prompts */}
544
+ <div>
545
+ <Label className="text-xs font-medium text-muted-foreground">Quick Presets</Label>
546
+ <div className="grid grid-cols-1 gap-1 mt-1">
547
+ {systemPromptPresets.map((preset) => (
548
+ <Button
549
+ key={preset.name}
550
+ variant="outline"
551
+ size="sm"
552
+ className="h-auto p-2 text-xs justify-start"
553
+ onClick={() => setSystemPrompt(preset.prompt)}
554
+ disabled={isLoading}
555
+ >
556
+ {preset.name}
557
+ </Button>
558
+ ))}
559
+ </div>
560
+ </div>
561
+
562
+ {/* Custom System Prompt */}
563
+ <div>
564
+ <div className="flex items-center justify-between mb-2">
565
+ <Label htmlFor="system-prompt" className="text-xs font-medium">
566
+ Custom System Prompt
567
+ </Label>
568
+ {systemPrompt && (
569
+ <Button
570
+ variant="ghost"
571
+ size="sm"
572
+ onClick={() => setSystemPrompt('')}
573
+ className="h-6 px-2 text-xs"
574
+ disabled={isLoading}
575
+ >
576
+ <RotateCcw className="h-3 w-3 mr-1" />
577
+ Clear
578
+ </Button>
579
+ )}
580
+ </div>
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}
588
+ />
589
+ <p className="text-xs text-muted-foreground mt-1">
590
+ System prompts define the model's role and behavior.
591
+ </p>
592
+ </div>
593
+ </CardContent>
594
+ </CollapsibleContent>
595
+ </Collapsible>
596
+ </Card>
597
+ </div>
598
+ </div>
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>
646
+ </AlertDialog>
647
+ </div>
648
+ )
649
+ }
frontend/src/types/chat.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Message {
2
+ id: string
3
+ role: 'user' | 'assistant' | 'system'
4
+ content: string
5
+ thinking_content?: string
6
+ timestamp: number
7
+ model_used?: string
8
+ supports_thinking?: boolean
9
+ }
10
+
11
+ export interface ChatSession {
12
+ id: string
13
+ title: string
14
+ messages: Message[]
15
+ created_at: number
16
+ updated_at: number
17
+ model_name?: string
18
+ system_prompt?: string
19
+ }
20
+
21
+ export interface ChatStore {
22
+ sessions: ChatSession[]
23
+ current_session_id: string | null
24
+ }
25
+
26
+ export interface MessageStatus {
27
+ isLoading: boolean
28
+ error: string | null
29
+ }
frontend/tailwind.config.js ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ darkMode: ["class"],
4
+ content: [
5
+ './pages/**/*.{ts,tsx}',
6
+ './components/**/*.{ts,tsx}',
7
+ './app/**/*.{ts,tsx}',
8
+ './src/**/*.{ts,tsx}',
9
+ ],
10
+ theme: {
11
+ container: {
12
+ center: true,
13
+ padding: '2rem',
14
+ screens: {
15
+ '2xl': '1400px'
16
+ }
17
+ },
18
+ extend: {
19
+ colors: {
20
+ border: 'hsl(var(--border))',
21
+ input: 'hsl(var(--input))',
22
+ ring: 'hsl(var(--ring))',
23
+ background: 'hsl(var(--background))',
24
+ foreground: 'hsl(var(--foreground))',
25
+ primary: {
26
+ DEFAULT: 'hsl(var(--primary))',
27
+ foreground: 'hsl(var(--primary-foreground))'
28
+ },
29
+ secondary: {
30
+ DEFAULT: 'hsl(var(--secondary))',
31
+ foreground: 'hsl(var(--secondary-foreground))'
32
+ },
33
+ destructive: {
34
+ DEFAULT: 'hsl(var(--destructive))',
35
+ foreground: 'hsl(var(--destructive-foreground))'
36
+ },
37
+ muted: {
38
+ DEFAULT: 'hsl(var(--muted))',
39
+ foreground: 'hsl(var(--muted-foreground))'
40
+ },
41
+ accent: {
42
+ DEFAULT: 'hsl(var(--accent))',
43
+ foreground: 'hsl(var(--accent-foreground))'
44
+ },
45
+ popover: {
46
+ DEFAULT: 'hsl(var(--popover))',
47
+ foreground: 'hsl(var(--popover-foreground))'
48
+ },
49
+ card: {
50
+ DEFAULT: 'hsl(var(--card))',
51
+ foreground: 'hsl(var(--card-foreground))'
52
+ },
53
+ chart: {
54
+ '1': 'hsl(var(--chart-1))',
55
+ '2': 'hsl(var(--chart-2))',
56
+ '3': 'hsl(var(--chart-3))',
57
+ '4': 'hsl(var(--chart-4))',
58
+ '5': 'hsl(var(--chart-5))'
59
+ }
60
+ },
61
+ borderRadius: {
62
+ lg: 'var(--radius)',
63
+ md: 'calc(var(--radius) - 2px)',
64
+ sm: 'calc(var(--radius) - 4px)'
65
+ },
66
+ keyframes: {
67
+ 'accordion-down': {
68
+ from: {
69
+ height: 0
70
+ },
71
+ to: {
72
+ height: 'var(--radix-accordion-content-height)'
73
+ }
74
+ },
75
+ 'accordion-up': {
76
+ from: {
77
+ height: 'var(--radix-accordion-content-height)'
78
+ },
79
+ to: {
80
+ height: 0
81
+ }
82
+ }
83
+ },
84
+ animation: {
85
+ 'accordion-down': 'accordion-down 0.2s ease-out',
86
+ 'accordion-up': 'accordion-up 0.2s ease-out'
87
+ }
88
+ }
89
+ },
90
+ plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
91
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "baseUrl": ".",
19
+ "paths": {
20
+ "@/*": ["./src/*"]
21
+ }
22
+ },
23
+ "include": ["src"],
24
+ "references": [{ "path": "./tsconfig.node.json" }]
25
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import path from 'path'
4
+
5
+ // https://vitejs.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react()],
8
+ resolve: {
9
+ alias: {
10
+ "@": path.resolve(__dirname, "./src"),
11
+ },
12
+ },
13
+ })
package.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "edge-llm",
3
+ "version": "1.0.0",
4
+ "description": "Local AI Chat Platform with Modern UI",
5
+ "scripts": {
6
+ "dev": "concurrently \"npm run backend\" \"npm run frontend\"",
7
+ "backend": "uvicorn app:app --host 0.0.0.0 --port 8000 --reload",
8
+ "frontend": "cd frontend && npm run dev",
9
+ "build": "cd frontend && npm run build && cp -r dist/* ../static/",
10
+ "build:frontend": "cd frontend && npm run build",
11
+ "build:docker": "docker build -t edge-llm .",
12
+ "preview": "cd frontend && npm run preview",
13
+ "install:all": "pip install -r requirements.txt && cd frontend && npm install",
14
+ "start": "python scripts/start_platform.py",
15
+ "stop": "python scripts/stop_platform.py",
16
+ "test": "cd frontend && npm run test",
17
+ "deploy": "npm run build && echo 'Ready for deployment'",
18
+ "clean": "rm -rf frontend/dist frontend/node_modules __pycache__ .cache"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://huggingface.co/spaces/wu981526092/EdgeLLM"
23
+ },
24
+ "keywords": [
25
+ "ai",
26
+ "llm",
27
+ "chat",
28
+ "fastapi",
29
+ "react",
30
+ "local",
31
+ "privacy"
32
+ ],
33
+ "author": "EdgeLLM Contributors",
34
+ "license": "MIT",
35
+ "devDependencies": {
36
+ "concurrently": "^8.2.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=18.0.0",
40
+ "python": ">=3.9.0"
41
+ }
42
+ }
scripts/start_both.bat ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo.
3
+ echo ========================================
4
+ echo Edge LLM Platform Startup Script
5
+ echo ========================================
6
+ echo.
7
+
8
+ echo [1/4] Checking prerequisites...
9
+
10
+ REM Check if virtual environment exists
11
+ if not exist ".venv\Scripts\activate.bat" (
12
+ echo ERROR: Virtual environment not found!
13
+ echo Please run: python -m venv .venv
14
+ echo Then: pip install -r requirements.txt
15
+ pause
16
+ exit /b 1
17
+ )
18
+
19
+ REM Check if frontend dependencies exist
20
+ if not exist "frontend\node_modules" (
21
+ echo ERROR: Frontend dependencies not found!
22
+ echo Please run: cd frontend && npm install
23
+ pause
24
+ exit /b 1
25
+ )
26
+
27
+ echo [2/4] Starting backend server...
28
+ start "Edge LLM Backend" cmd /k "call .venv\Scripts\activate.bat && cd backend && python app.py"
29
+
30
+ echo [3/4] Waiting for backend to initialize...
31
+ timeout /t 3 /nobreak >nul
32
+
33
+ echo [4/4] Starting frontend development server...
34
+ start "Edge LLM Frontend" cmd /k "cd frontend && npm run dev"
35
+
36
+ echo.
37
+ echo ========================================
38
+ echo πŸš€ Edge LLM Platform Starting...
39
+ echo ========================================
40
+ echo.
41
+ echo Backend: http://localhost:8000
42
+ echo Frontend: http://localhost:5173
43
+ echo.
44
+ echo Both services are starting in separate windows.
45
+ echo Close this window to keep services running.
46
+ echo.
47
+ echo To stop services:
48
+ echo - Close the backend and frontend windows, OR
49
+ echo - Run: stop_both.bat
50
+ echo.
51
+ pause
scripts/start_platform.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Edge LLM Platform Startup Script
4
+ Starts both backend and frontend services simultaneously
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import subprocess
10
+ import time
11
+ import signal
12
+ import platform
13
+ import webbrowser
14
+ from pathlib import Path
15
+
16
+ class EdgeLLMStarter:
17
+ def __init__(self):
18
+ self.processes = []
19
+ self.is_windows = platform.system() == "Windows"
20
+ self.project_root = Path(__file__).parent
21
+
22
+ def print_banner(self):
23
+ print("\n" + "="*50)
24
+ print(" πŸ€– Edge LLM Platform Startup")
25
+ print("="*50)
26
+
27
+ def check_prerequisites(self):
28
+ print("\n[1/5] πŸ” Checking prerequisites...")
29
+
30
+ # Check virtual environment
31
+ venv_path = self.project_root / ".venv"
32
+ if self.is_windows:
33
+ venv_python = venv_path / "Scripts" / "python.exe"
34
+ venv_activate = venv_path / "Scripts" / "activate.bat"
35
+ else:
36
+ venv_python = venv_path / "bin" / "python"
37
+ venv_activate = venv_path / "bin" / "activate"
38
+
39
+ if not venv_path.exists() or not venv_python.exists():
40
+ print("❌ Virtual environment not found!")
41
+ print("Please run: python -m venv .venv")
42
+ print("Then: pip install -r requirements.txt")
43
+ return False
44
+
45
+ # Check backend dependencies
46
+ requirements_file = self.project_root / "requirements.txt"
47
+ if not requirements_file.exists():
48
+ print("❌ requirements.txt not found!")
49
+ return False
50
+
51
+ # Check frontend dependencies
52
+ node_modules = self.project_root / "frontend" / "node_modules"
53
+ package_json = self.project_root / "frontend" / "package.json"
54
+
55
+ if not package_json.exists():
56
+ print("❌ Frontend package.json not found!")
57
+ return False
58
+
59
+ if not node_modules.exists():
60
+ print("❌ Frontend dependencies not installed!")
61
+ print("Please run: cd frontend && npm install")
62
+ return False
63
+
64
+ print("βœ… All prerequisites satisfied")
65
+ return True
66
+
67
+ def start_backend(self):
68
+ print("\n[2/5] 🐍 Starting backend server...")
69
+
70
+ backend_dir = self.project_root / "backend"
71
+ if not backend_dir.exists():
72
+ print("❌ Backend directory not found!")
73
+ return None
74
+
75
+ try:
76
+ if self.is_windows:
77
+ # Windows: use call to activate venv and run python
78
+ cmd = [
79
+ "cmd", "/c",
80
+ f"call {self.project_root}/.venv/Scripts/activate.bat && "
81
+ f"cd {backend_dir} && python app.py"
82
+ ]
83
+ process = subprocess.Popen(
84
+ cmd,
85
+ creationflags=subprocess.CREATE_NEW_CONSOLE,
86
+ cwd=str(self.project_root)
87
+ )
88
+ else:
89
+ # Unix: use source to activate venv
90
+ cmd = f"source {self.project_root}/.venv/bin/activate && cd {backend_dir} && python app.py"
91
+ process = subprocess.Popen(
92
+ cmd,
93
+ shell=True,
94
+ cwd=str(self.project_root)
95
+ )
96
+
97
+ self.processes.append(("Backend", process))
98
+ print("βœ… Backend starting...")
99
+ return process
100
+
101
+ except Exception as e:
102
+ print(f"❌ Failed to start backend: {e}")
103
+ return None
104
+
105
+ def start_frontend(self):
106
+ print("\n[3/5] βš›οΈ Starting frontend development server...")
107
+
108
+ frontend_dir = self.project_root / "frontend"
109
+ if not frontend_dir.exists():
110
+ print("❌ Frontend directory not found!")
111
+ return None
112
+
113
+ try:
114
+ if self.is_windows:
115
+ cmd = ["cmd", "/c", "npm run dev"]
116
+ process = subprocess.Popen(
117
+ cmd,
118
+ creationflags=subprocess.CREATE_NEW_CONSOLE,
119
+ cwd=str(frontend_dir)
120
+ )
121
+ else:
122
+ cmd = ["npm", "run", "dev"]
123
+ process = subprocess.Popen(
124
+ cmd,
125
+ cwd=str(frontend_dir)
126
+ )
127
+
128
+ self.processes.append(("Frontend", process))
129
+ print("βœ… Frontend starting...")
130
+ return process
131
+
132
+ except Exception as e:
133
+ print(f"❌ Failed to start frontend: {e}")
134
+ return None
135
+
136
+ def wait_for_services(self):
137
+ print("\n[4/5] ⏳ Waiting for services to initialize...")
138
+
139
+ # Wait a bit for services to start
140
+ for i in range(5, 0, -1):
141
+ print(f" Waiting {i} seconds...", end="\r")
142
+ time.sleep(1)
143
+ print(" Services should be ready! ")
144
+
145
+ def check_services(self):
146
+ print("\n[5/5] πŸ” Checking service status...")
147
+
148
+ try:
149
+ import requests
150
+
151
+ # Check backend
152
+ try:
153
+ response = requests.get("http://localhost:8000/", timeout=5)
154
+ if response.status_code == 200:
155
+ print("βœ… Backend: Running on http://localhost:8000")
156
+ else:
157
+ print(f"⚠️ Backend: HTTP {response.status_code}")
158
+ except:
159
+ print("⏳ Backend: Still starting up...")
160
+
161
+ # Check frontend
162
+ try:
163
+ response = requests.get("http://localhost:5173/", timeout=5)
164
+ if response.status_code == 200:
165
+ print("βœ… Frontend: Running on http://localhost:5173")
166
+ else:
167
+ print(f"⚠️ Frontend: HTTP {response.status_code}")
168
+ except:
169
+ print("⏳ Frontend: Still starting up...")
170
+
171
+ except ImportError:
172
+ print("ℹ️ Install 'requests' package to check service status")
173
+ print(" pip install requests")
174
+
175
+ def open_browser(self):
176
+ """Open the application in default browser"""
177
+ try:
178
+ print("\n🌐 Opening Edge LLM in your browser...")
179
+ webbrowser.open("http://localhost:5173")
180
+ except:
181
+ pass
182
+
183
+ def show_info(self):
184
+ print("\n" + "="*50)
185
+ print(" πŸš€ Edge LLM Platform Started!")
186
+ print("="*50)
187
+ print("\nπŸ“ Access URLs:")
188
+ print(" Frontend: http://localhost:5173")
189
+ print(" Backend: http://localhost:8000")
190
+ print(" API Docs: http://localhost:8000/docs")
191
+
192
+ print("\nπŸ’‘ Usage:")
193
+ print(" 1. Go to http://localhost:5173/playground")
194
+ print(" 2. Load a model from the right panel")
195
+ print(" 3. Start chatting!")
196
+
197
+ print("\nπŸ›‘ To stop services:")
198
+ if self.is_windows:
199
+ print(" - Close the backend and frontend windows, OR")
200
+ print(" - Run: stop_both.bat, OR")
201
+ print(" - Press Ctrl+C in this window")
202
+
203
+ def cleanup(self):
204
+ """Clean up processes when shutting down"""
205
+ print("\nπŸ›‘ Shutting down Edge LLM Platform...")
206
+
207
+ for name, process in self.processes:
208
+ try:
209
+ print(f" Stopping {name}...")
210
+ if self.is_windows:
211
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(process.pid)],
212
+ capture_output=True)
213
+ else:
214
+ process.terminate()
215
+ process.wait(timeout=5)
216
+ except:
217
+ pass
218
+
219
+ print("βœ… All services stopped")
220
+
221
+ def run(self):
222
+ """Main execution function"""
223
+ try:
224
+ self.print_banner()
225
+
226
+ if not self.check_prerequisites():
227
+ input("\nPress Enter to exit...")
228
+ return 1
229
+
230
+ backend_process = self.start_backend()
231
+ if not backend_process:
232
+ return 1
233
+
234
+ frontend_process = self.start_frontend()
235
+ if not frontend_process:
236
+ return 1
237
+
238
+ self.wait_for_services()
239
+ self.check_services()
240
+ self.show_info()
241
+
242
+ # Open browser after a short delay
243
+ time.sleep(2)
244
+ self.open_browser()
245
+
246
+ print("\n⌨️ Press Ctrl+C to stop all services...")
247
+
248
+ # Keep the script running
249
+ while True:
250
+ time.sleep(1)
251
+
252
+ # Check if processes are still running
253
+ for name, process in self.processes:
254
+ if process.poll() is not None:
255
+ print(f"\n⚠️ {name} process stopped unexpectedly")
256
+
257
+ except KeyboardInterrupt:
258
+ print("\n\n⌨️ Received stop signal...")
259
+
260
+ except Exception as e:
261
+ print(f"\n❌ Unexpected error: {e}")
262
+
263
+ finally:
264
+ self.cleanup()
265
+
266
+ return 0
267
+
268
+ def signal_handler(signum, frame):
269
+ """Handle Ctrl+C gracefully"""
270
+ print("\n\n⌨️ Received stop signal...")
271
+ sys.exit(0)
272
+
273
+ if __name__ == "__main__":
274
+ # Handle Ctrl+C gracefully
275
+ signal.signal(signal.SIGINT, signal_handler)
276
+
277
+ starter = EdgeLLMStarter()
278
+ exit_code = starter.run()
279
+ sys.exit(exit_code)
scripts/start_platform.sh ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Edge LLM Platform Startup Script for Linux/macOS
4
+ # Starts both backend and frontend services
5
+
6
+ # Colors for output
7
+ RED='\033[0;31m'
8
+ GREEN='\033[0;32m'
9
+ YELLOW='\033[1;33m'
10
+ BLUE='\033[0;34m'
11
+ NC='\033[0m' # No Color
12
+
13
+ # Function to print colored output
14
+ print_status() {
15
+ echo -e "${GREEN}[INFO]${NC} $1"
16
+ }
17
+
18
+ print_error() {
19
+ echo -e "${RED}[ERROR]${NC} $1"
20
+ }
21
+
22
+ print_warning() {
23
+ echo -e "${YELLOW}[WARNING]${NC} $1"
24
+ }
25
+
26
+ print_banner() {
27
+ echo ""
28
+ echo "=================================================="
29
+ echo " πŸ€– Edge LLM Platform Startup Script"
30
+ echo "=================================================="
31
+ echo ""
32
+ }
33
+
34
+ # Cleanup function
35
+ cleanup() {
36
+ echo ""
37
+ print_status "πŸ›‘ Shutting down Edge LLM Platform..."
38
+
39
+ # Kill background jobs
40
+ jobs -p | xargs -r kill 2>/dev/null
41
+
42
+ # Kill any remaining python/node processes related to our project
43
+ pkill -f "backend/app.py" 2>/dev/null
44
+ pkill -f "npm run dev" 2>/dev/null
45
+
46
+ print_status "βœ… All services stopped"
47
+ exit 0
48
+ }
49
+
50
+ # Set up signal handlers
51
+ trap cleanup SIGINT SIGTERM
52
+
53
+ check_prerequisites() {
54
+ print_status "[1/5] πŸ” Checking prerequisites..."
55
+
56
+ # Check if virtual environment exists
57
+ if [ ! -d ".venv" ] || [ ! -f ".venv/bin/python" ]; then
58
+ print_error "Virtual environment not found!"
59
+ echo "Please run: python -m venv .venv"
60
+ echo "Then: source .venv/bin/activate && pip install -r requirements.txt"
61
+ return 1
62
+ fi
63
+
64
+ # Check if frontend dependencies exist
65
+ if [ ! -d "frontend/node_modules" ] || [ ! -f "frontend/package.json" ]; then
66
+ print_error "Frontend dependencies not found!"
67
+ echo "Please run: cd frontend && npm install"
68
+ return 1
69
+ fi
70
+
71
+ print_status "βœ… All prerequisites satisfied"
72
+ return 0
73
+ }
74
+
75
+ start_backend() {
76
+ print_status "[2/5] 🐍 Starting backend server..."
77
+
78
+ if [ ! -d "backend" ]; then
79
+ print_error "Backend directory not found!"
80
+ return 1
81
+ fi
82
+
83
+ # Start backend in background
84
+ (
85
+ source .venv/bin/activate
86
+ cd backend
87
+ python app.py
88
+ ) &
89
+
90
+ BACKEND_PID=$!
91
+ print_status "βœ… Backend starting (PID: $BACKEND_PID)..."
92
+ return 0
93
+ }
94
+
95
+ start_frontend() {
96
+ print_status "[3/5] βš›οΈ Starting frontend development server..."
97
+
98
+ if [ ! -d "frontend" ]; then
99
+ print_error "Frontend directory not found!"
100
+ return 1
101
+ fi
102
+
103
+ # Start frontend in background
104
+ (
105
+ cd frontend
106
+ npm run dev
107
+ ) &
108
+
109
+ FRONTEND_PID=$!
110
+ print_status "βœ… Frontend starting (PID: $FRONTEND_PID)..."
111
+ return 0
112
+ }
113
+
114
+ wait_for_services() {
115
+ print_status "[4/5] ⏳ Waiting for services to initialize..."
116
+
117
+ for i in {5..1}; do
118
+ printf "\r Waiting %d seconds..." $i
119
+ sleep 1
120
+ done
121
+ printf "\r Services should be ready! \n"
122
+ }
123
+
124
+ check_services() {
125
+ print_status "[5/5] πŸ” Checking service status..."
126
+
127
+ # Check if curl is available
128
+ if command -v curl &> /dev/null; then
129
+ # Check backend
130
+ if curl -s http://localhost:8000/ >/dev/null 2>&1; then
131
+ print_status "βœ… Backend: Running on http://localhost:8000"
132
+ else
133
+ print_warning "⏳ Backend: Still starting up..."
134
+ fi
135
+
136
+ # Check frontend
137
+ if curl -s http://localhost:5173/ >/dev/null 2>&1; then
138
+ print_status "βœ… Frontend: Running on http://localhost:5173"
139
+ else
140
+ print_warning "⏳ Frontend: Still starting up..."
141
+ fi
142
+ else
143
+ print_warning "Install 'curl' to check service status"
144
+ fi
145
+ }
146
+
147
+ open_browser() {
148
+ print_status "🌐 Opening Edge LLM in your browser..."
149
+
150
+ # Try to open browser (works on most Linux distributions and macOS)
151
+ if command -v xdg-open &> /dev/null; then
152
+ xdg-open http://localhost:5173 >/dev/null 2>&1 &
153
+ elif command -v open &> /dev/null; then
154
+ open http://localhost:5173 >/dev/null 2>&1 &
155
+ fi
156
+ }
157
+
158
+ show_info() {
159
+ echo ""
160
+ echo "=================================================="
161
+ echo " πŸš€ Edge LLM Platform Started!"
162
+ echo "=================================================="
163
+ echo ""
164
+ echo "πŸ“ Access URLs:"
165
+ echo " Frontend: http://localhost:5173"
166
+ echo " Backend: http://localhost:8000"
167
+ echo " API Docs: http://localhost:8000/docs"
168
+ echo ""
169
+ echo "πŸ’‘ Usage:"
170
+ echo " 1. Go to http://localhost:5173/playground"
171
+ echo " 2. Load a model from the right panel"
172
+ echo " 3. Start chatting!"
173
+ echo ""
174
+ echo "πŸ›‘ To stop services:"
175
+ echo " - Press Ctrl+C in this terminal"
176
+ echo ""
177
+ }
178
+
179
+ main() {
180
+ print_banner
181
+
182
+ # Check prerequisites
183
+ if ! check_prerequisites; then
184
+ read -p "Press Enter to exit..."
185
+ exit 1
186
+ fi
187
+
188
+ # Start services
189
+ if ! start_backend; then
190
+ exit 1
191
+ fi
192
+
193
+ if ! start_frontend; then
194
+ exit 1
195
+ fi
196
+
197
+ # Wait and check
198
+ wait_for_services
199
+ check_services
200
+ show_info
201
+
202
+ # Open browser after a short delay
203
+ sleep 2
204
+ open_browser
205
+
206
+ print_status "⌨️ Press Ctrl+C to stop all services..."
207
+
208
+ # Keep script running and wait for services
209
+ wait
210
+ }
211
+
212
+ # Make sure we're in the right directory
213
+ cd "$(dirname "$0")"
214
+
215
+ # Run main function
216
+ main
scripts/stop_both.bat ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo.
3
+ echo ========================================
4
+ echo Edge LLM Platform Stop Script
5
+ echo ========================================
6
+ echo.
7
+
8
+ echo [1/3] Stopping backend processes...
9
+ taskkill /F /IM python.exe 2>nul
10
+ if %errorlevel% == 0 (
11
+ echo βœ… Backend processes stopped
12
+ ) else (
13
+ echo ⚠️ No backend processes found
14
+ )
15
+
16
+ echo [2/3] Stopping frontend processes...
17
+ taskkill /F /IM node.exe 2>nul
18
+ if %errorlevel% == 0 (
19
+ echo βœ… Frontend processes stopped
20
+ ) else (
21
+ echo ⚠️ No frontend processes found
22
+ )
23
+
24
+ echo [3/3] Stopping any remaining Edge LLM processes...
25
+ for /f "tokens=2" %%i in ('tasklist /FI "WINDOWTITLE eq Edge LLM*" /FO CSV ^| find /v "PID"') do (
26
+ taskkill /F /PID %%i 2>nul
27
+ )
28
+
29
+ echo.
30
+ echo ========================================
31
+ echo πŸ›‘ All Edge LLM services stopped
32
+ echo ========================================
33
+ echo.
34
+ pause