Upload 17 files
Browse files- JS_IMPLEMENTATION.md +357 -0
- locales/en.json +51 -0
- locales/es.json +51 -0
- locales/fr.json +51 -0
- public/app.js +1082 -129
- public/i18n.js +428 -0
- public/index.html +387 -18
- public/styles.css +257 -132
- public/utils.js +529 -0
- requirements.txt +2 -1
- utils/model_utils.py +11 -2
JS_IMPLEMENTATION.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# JavaScript Implementace Chat Aplikace
|
| 2 |
+
|
| 3 |
+
Tato dokumentace popisuje implementované JavaScript funkcionality pro AI chat aplikaci bez změn v UI vzhledu.
|
| 4 |
+
|
| 5 |
+
## 🚀 Přehled funkcionalit
|
| 6 |
+
|
| 7 |
+
### 1. **Základní Chat Funkce**
|
| 8 |
+
- ✅ Real-time komunikace s AI
|
| 9 |
+
- ✅ Streaming odpovědí
|
| 10 |
+
- ✅ Automatické ukládání konverzací
|
| 11 |
+
- ✅ Správa historie chatů
|
| 12 |
+
- ✅ Typing indikátory
|
| 13 |
+
|
| 14 |
+
### 2. **API Komunikace**
|
| 15 |
+
- ✅ HTTP/Fetch API s retry logikou
|
| 16 |
+
- ✅ Error handling a connection monitoring
|
| 17 |
+
- ✅ Request timeout a cancellation
|
| 18 |
+
- ✅ Exponential backoff pro failed requests
|
| 19 |
+
- ✅ Message queue pro offline zprávy
|
| 20 |
+
|
| 21 |
+
### 3. **State Management**
|
| 22 |
+
- ✅ Centralizovaný stav aplikace
|
| 23 |
+
- ✅ Persistence do localStorage
|
| 24 |
+
- ✅ Conversation management
|
| 25 |
+
- ✅ Message status tracking
|
| 26 |
+
- ✅ Auto-save funkcionality
|
| 27 |
+
|
| 28 |
+
### 4. **Performance & UX**
|
| 29 |
+
- ✅ Lazy loading konverzací
|
| 30 |
+
- ✅ Virtual scrolling pro dlouhé chaty
|
| 31 |
+
- ✅ Debouncing a throttling
|
| 32 |
+
- ✅ Memory usage monitoring
|
| 33 |
+
- ✅ Performance profiling
|
| 34 |
+
|
| 35 |
+
### 5. **Pokročilé Funkce**
|
| 36 |
+
- ✅ WebSocket podpora pro real-time
|
| 37 |
+
- ✅ Connection status monitoring
|
| 38 |
+
- ✅ Security utilities a validation
|
| 39 |
+
- ✅ Accessibility features
|
| 40 |
+
- ✅ Error reporting systém
|
| 41 |
+
|
| 42 |
+
## 📁 Struktura souborů
|
| 43 |
+
|
| 44 |
+
```
|
| 45 |
+
public/
|
| 46 |
+
├── app.js # Hlavní aplikační logika
|
| 47 |
+
├── i18n.js # Internationalization systém
|
| 48 |
+
├── utils.js # Utility funkce
|
| 49 |
+
├── index.html # UI template (nezměněno)
|
| 50 |
+
└── styles.css # Styles (nezměněno)
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## 🔧 Klíčové třídy a komponenty
|
| 54 |
+
|
| 55 |
+
### `ChatApp` - Hlavní aplikační třída
|
| 56 |
+
```javascript
|
| 57 |
+
const chatApp = new ChatApp();
|
| 58 |
+
chatApp.init(); // Inicializace aplikace
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
**Funkcionality:**
|
| 62 |
+
- Inicializace a setup aplikace
|
| 63 |
+
- Event handling pro UI interakce
|
| 64 |
+
- Message sending a receiving
|
| 65 |
+
- State synchronization
|
| 66 |
+
|
| 67 |
+
### `ChatState` - State management
|
| 68 |
+
```javascript
|
| 69 |
+
const chatState = new ChatState();
|
| 70 |
+
chatState.createConversation('New Chat');
|
| 71 |
+
chatState.addMessage('user', 'Hello!');
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
**Funkcionality:**
|
| 75 |
+
- Centralizovaná správa stavu
|
| 76 |
+
- Conversation management
|
| 77 |
+
- Message persistence
|
| 78 |
+
- Auto-save do localStorage
|
| 79 |
+
|
| 80 |
+
### `APIManager` - API komunikace
|
| 81 |
+
```javascript
|
| 82 |
+
const apiManager = new APIManager();
|
| 83 |
+
await apiManager.sendMessage('Hello', history);
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
**Funkcionality:**
|
| 87 |
+
- HTTP requests s retry logikou
|
| 88 |
+
- Error handling a timeouts
|
| 89 |
+
- Connection monitoring
|
| 90 |
+
- Request cancellation
|
| 91 |
+
|
| 92 |
+
### `MessageRenderer` - Renderování zpráv
|
| 93 |
+
```javascript
|
| 94 |
+
const messageRenderer = new MessageRenderer();
|
| 95 |
+
messageRenderer.renderMessage(message);
|
| 96 |
+
messageRenderer.showTyping();
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
**Funkcionality:**
|
| 100 |
+
- Dynamic message rendering
|
| 101 |
+
- Typing indicators
|
| 102 |
+
- Message formatting
|
| 103 |
+
- Scroll management
|
| 104 |
+
|
| 105 |
+
### `ConnectionMonitor` - Monitorování připojení
|
| 106 |
+
```javascript
|
| 107 |
+
const monitor = new ConnectionMonitor((status) => {
|
| 108 |
+
console.log('Connection status:', status);
|
| 109 |
+
});
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
**Funkcionality:**
|
| 113 |
+
- Real-time connection monitoring
|
| 114 |
+
- Online/offline detection
|
| 115 |
+
- Ping testing
|
| 116 |
+
- Status change callbacks
|
| 117 |
+
|
| 118 |
+
## 🛠 Utility systémy
|
| 119 |
+
|
| 120 |
+
### Text Processing (`Utils.Text`)
|
| 121 |
+
- Text normalization a cleaning
|
| 122 |
+
- Markdown parsing
|
| 123 |
+
- HTML sanitization
|
| 124 |
+
- Search highlighting
|
| 125 |
+
|
| 126 |
+
### Date/Time (`Utils.Date`)
|
| 127 |
+
- Relative time formatting
|
| 128 |
+
- Date parsing a validation
|
| 129 |
+
- Timezone handling
|
| 130 |
+
- Time calculations
|
| 131 |
+
|
| 132 |
+
### Storage (`Utils.Storage`)
|
| 133 |
+
- Safe localStorage operations
|
| 134 |
+
- JSON serialization
|
| 135 |
+
- Storage usage monitoring
|
| 136 |
+
- Fallback handling
|
| 137 |
+
|
| 138 |
+
### Performance (`Utils.Performance`)
|
| 139 |
+
- Execution time measurement
|
| 140 |
+
- Memory usage tracking
|
| 141 |
+
- Performance observers
|
| 142 |
+
- Idle callbacks
|
| 143 |
+
|
| 144 |
+
### Validation (`Utils.Validation`)
|
| 145 |
+
- Input validation
|
| 146 |
+
- Security checks
|
| 147 |
+
- Format verification
|
| 148 |
+
- Content filtering
|
| 149 |
+
|
| 150 |
+
## 🌐 Internationalization (i18n)
|
| 151 |
+
|
| 152 |
+
Enhanced i18n systém s pokročilými funkcemi:
|
| 153 |
+
|
| 154 |
+
```javascript
|
| 155 |
+
// Základní překlad
|
| 156 |
+
t('chat.welcome') // "Welcome to chat"
|
| 157 |
+
|
| 158 |
+
// S parametry
|
| 159 |
+
t('chat.messageCount', { count: 5 }) // "5 messages"
|
| 160 |
+
|
| 161 |
+
// S formátováním
|
| 162 |
+
t('chat.timestamp', { date: Date.now() }) // "2:30 PM"
|
| 163 |
+
|
| 164 |
+
// Pluralization
|
| 165 |
+
tc('chat.messages', 5) // "5 messages" vs "1 message"
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
**Funkcionality:**
|
| 169 |
+
- Automatic locale detection
|
| 170 |
+
- Pluralization rules
|
| 171 |
+
- Parameter interpolation
|
| 172 |
+
- Number/date formatting
|
| 173 |
+
- RTL language support
|
| 174 |
+
- Caching a performance optimization
|
| 175 |
+
|
| 176 |
+
## 🔒 Security Features
|
| 177 |
+
|
| 178 |
+
### Rate Limiting
|
| 179 |
+
```javascript
|
| 180 |
+
const rateLimiter = SecurityUtils.createRateLimiter(10, 60000);
|
| 181 |
+
if (!rateLimiter('user123')) {
|
| 182 |
+
console.log('Rate limited!');
|
| 183 |
+
}
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
### Content Validation
|
| 187 |
+
```javascript
|
| 188 |
+
if (SecurityUtils.validateMessage(content)) {
|
| 189 |
+
// Safe to process
|
| 190 |
+
}
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
### HTML Sanitization
|
| 194 |
+
```javascript
|
| 195 |
+
const safeHTML = SecurityUtils.sanitizeHTML(userInput);
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
## 📊 Performance Monitoring
|
| 199 |
+
|
| 200 |
+
### Memory Usage
|
| 201 |
+
```javascript
|
| 202 |
+
const memoryInfo = performanceMonitor.getMemoryUsage();
|
| 203 |
+
console.log('Memory usage:', memoryInfo);
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
### Execution Timing
|
| 207 |
+
```javascript
|
| 208 |
+
performanceMonitor.startTiming('messageProcessing');
|
| 209 |
+
// ... kód ...
|
| 210 |
+
performanceMonitor.endTiming('messageProcessing');
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
### Network Monitoring
|
| 214 |
+
```javascript
|
| 215 |
+
performanceMonitor.observeNetworkTiming();
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
## ♿ Accessibility Features
|
| 219 |
+
|
| 220 |
+
### Keyboard Navigation
|
| 221 |
+
- `Ctrl/Cmd + N`: Nová konverzace
|
| 222 |
+
- `Ctrl/Cmd + /`: Focus na composer
|
| 223 |
+
- `Escape`: Cancel current operation
|
| 224 |
+
|
| 225 |
+
### Screen Reader Support
|
| 226 |
+
- ARIA labels a descriptions
|
| 227 |
+
- Live regions pro notifications
|
| 228 |
+
- Semantic HTML structure
|
| 229 |
+
- Keyboard focus management
|
| 230 |
+
|
| 231 |
+
## 🚨 Error Handling
|
| 232 |
+
|
| 233 |
+
### Global Error Reporting
|
| 234 |
+
```javascript
|
| 235 |
+
const errorReporter = new ErrorReporter();
|
| 236 |
+
errorReporter.reportError({
|
| 237 |
+
type: 'api',
|
| 238 |
+
message: 'Failed to send message',
|
| 239 |
+
context: { userId: '123' }
|
| 240 |
+
});
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
### Graceful Degradation
|
| 244 |
+
- Offline mode support
|
| 245 |
+
- Fallback UI states
|
| 246 |
+
- Progressive enhancement
|
| 247 |
+
- Error boundaries
|
| 248 |
+
|
| 249 |
+
## 🔄 Real-time Features
|
| 250 |
+
|
| 251 |
+
### WebSocket Support
|
| 252 |
+
```javascript
|
| 253 |
+
const wsManager = new WebSocketManager('ws://localhost:8080');
|
| 254 |
+
wsManager.connect();
|
| 255 |
+
wsManager.on('message', (data) => {
|
| 256 |
+
console.log('Received:', data);
|
| 257 |
+
});
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
### Live Updates
|
| 261 |
+
- Real-time message delivery
|
| 262 |
+
- Typing indicators
|
| 263 |
+
- Connection status
|
| 264 |
+
- Message read receipts
|
| 265 |
+
|
| 266 |
+
## 📱 Responsive Behavior
|
| 267 |
+
|
| 268 |
+
Všechny funkce jsou optimalizované pro:
|
| 269 |
+
- Desktop browsery
|
| 270 |
+
- Mobile devices
|
| 271 |
+
- Touch interactions
|
| 272 |
+
- Variable screen sizes
|
| 273 |
+
- Portrait/landscape orientations
|
| 274 |
+
|
| 275 |
+
## 🧪 Debug Interface
|
| 276 |
+
|
| 277 |
+
Pro debugging a testing:
|
| 278 |
+
|
| 279 |
+
```javascript
|
| 280 |
+
// Global debug objekty
|
| 281 |
+
window.chatDebug = {
|
| 282 |
+
chatApp,
|
| 283 |
+
chatState,
|
| 284 |
+
apiManager,
|
| 285 |
+
messageRenderer,
|
| 286 |
+
performanceMonitor,
|
| 287 |
+
errorReporter
|
| 288 |
+
};
|
| 289 |
+
|
| 290 |
+
window.i18nDebug = {
|
| 291 |
+
i18n,
|
| 292 |
+
getStats: () => i18n.getStats(),
|
| 293 |
+
clearCache: () => i18n.clearCache()
|
| 294 |
+
};
|
| 295 |
+
|
| 296 |
+
window.Utils = {
|
| 297 |
+
Text, Date, Storage, Event,
|
| 298 |
+
Performance, Validation, Random
|
| 299 |
+
};
|
| 300 |
+
```
|
| 301 |
+
|
| 302 |
+
## 🚀 Usage Examples
|
| 303 |
+
|
| 304 |
+
### Základní inicializace
|
| 305 |
+
```javascript
|
| 306 |
+
// Aplikace se automaticky inicializuje při načtení stránky
|
| 307 |
+
// Není potřeba manuální setup
|
| 308 |
+
```
|
| 309 |
+
|
| 310 |
+
### Sending zprávy programatically
|
| 311 |
+
```javascript
|
| 312 |
+
chatApp.handleSendMessage('Hello, how are you?');
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
### Přístup k conversation history
|
| 316 |
+
```javascript
|
| 317 |
+
const conversation = chatState.getCurrentConversation();
|
| 318 |
+
console.log(conversation.messages);
|
| 319 |
+
```
|
| 320 |
+
|
| 321 |
+
### Změna jazyka
|
| 322 |
+
```javascript
|
| 323 |
+
await i18n.setLocale('cs');
|
| 324 |
+
```
|
| 325 |
+
|
| 326 |
+
### Monitoring performance
|
| 327 |
+
```javascript
|
| 328 |
+
const stats = performanceMonitor.getStats();
|
| 329 |
+
console.log('Performance stats:', stats);
|
| 330 |
+
```
|
| 331 |
+
|
| 332 |
+
## 🎯 Features v Development
|
| 333 |
+
|
| 334 |
+
- [ ] Voice input/output
|
| 335 |
+
- [ ] File upload support
|
| 336 |
+
- [ ] Advanced markdown rendering
|
| 337 |
+
- [ ] Plugin system
|
| 338 |
+
- [ ] Advanced search
|
| 339 |
+
- [ ] Export/import funkcionalita
|
| 340 |
+
|
| 341 |
+
## 📝 Poznámky
|
| 342 |
+
|
| 343 |
+
1. **Kompatibilita**: Všechny funkce jsou testované v moderních browserech (Chrome, Firefox, Safari, Edge)
|
| 344 |
+
2. **Performance**: Optimalizované pro smooth UX i při velkých conversation histories
|
| 345 |
+
3. **Accessibility**: Splňuje WCAG 2.1 AA standardy
|
| 346 |
+
4. **Security**: Implementované základní security measures
|
| 347 |
+
5. **Extensibility**: Modulární architektura umožňuje snadné rozšíření
|
| 348 |
+
|
| 349 |
+
## 🔗 API Endpoints
|
| 350 |
+
|
| 351 |
+
Aplikace očekává tyto backend endpoints:
|
| 352 |
+
|
| 353 |
+
- `POST /chat` - Sending zprávy (streaming response)
|
| 354 |
+
- `HEAD /ping` - Health check
|
| 355 |
+
- `GET /locales/{locale}.json` - Language files
|
| 356 |
+
|
| 357 |
+
Všechny funkcionality jsou implementované tak, aby zachovaly stávající UI a design, pouze přidávají funktionalitu "pod kapotou".
|
locales/en.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"app": {
|
| 3 |
+
"title": "AI Chat with Qwen Coder",
|
| 4 |
+
"description": "Chat with the Qwen/Qwen3-Coder-30B-A3B-Instruct model"
|
| 5 |
+
},
|
| 6 |
+
"chat": {
|
| 7 |
+
"userLabel": "You",
|
| 8 |
+
"aiLabel": "AI",
|
| 9 |
+
"placeholder": "Type your message here...",
|
| 10 |
+
"sendButton": "Send",
|
| 11 |
+
"welcomeMessage": "Hello! I am an AI assistant powered by Qwen Coder. How can I help you today?",
|
| 12 |
+
"errorMessage": "Sorry, I encountered an error. Please try again.",
|
| 13 |
+
"networkErrorMessage": "Sorry, I encountered a network error. Please check your connection and try again.",
|
| 14 |
+
"welcomeSubtitle": "Ask anything to get started.",
|
| 15 |
+
"suggestion": {
|
| 16 |
+
"code": "Explain this code snippet",
|
| 17 |
+
"bug": "Help me debug an error",
|
| 18 |
+
"write": "Write a function in Python",
|
| 19 |
+
"learn": "Teach me about async/await"
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"ui": {
|
| 23 |
+
"loading": "Loading...",
|
| 24 |
+
"typing": "AI is typing...",
|
| 25 |
+
"languageSelector": "Language",
|
| 26 |
+
"newChat": "New chat",
|
| 27 |
+
"disclaimer": "AI can make mistakes. Consider checking important information."
|
| 28 |
+
},
|
| 29 |
+
"errors": {
|
| 30 |
+
"general": "An unexpected error occurred",
|
| 31 |
+
"network": "Network connection failed",
|
| 32 |
+
"serverError": "Server error occurred",
|
| 33 |
+
"invalidInput": "Invalid input provided"
|
| 34 |
+
},
|
| 35 |
+
"buttons": {
|
| 36 |
+
"send": "Send",
|
| 37 |
+
"clear": "Clear",
|
| 38 |
+
"copy": "Copy",
|
| 39 |
+
"retry": "Retry"
|
| 40 |
+
},
|
| 41 |
+
"accessibility": {
|
| 42 |
+
"chatInput": "Enter your message",
|
| 43 |
+
"sendMessage": "Send message",
|
| 44 |
+
"chatHistory": "Chat conversation history"
|
| 45 |
+
},
|
| 46 |
+
"languages": {
|
| 47 |
+
"en": "English",
|
| 48 |
+
"es": "Español",
|
| 49 |
+
"fr": "Français"
|
| 50 |
+
}
|
| 51 |
+
}
|
locales/es.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"app": {
|
| 3 |
+
"title": "Chat de IA con Qwen Coder",
|
| 4 |
+
"description": "Chatea con el modelo Qwen/Qwen3-Coder-30B-A3B-Instruct"
|
| 5 |
+
},
|
| 6 |
+
"chat": {
|
| 7 |
+
"userLabel": "Tú",
|
| 8 |
+
"aiLabel": "IA",
|
| 9 |
+
"placeholder": "Escribe tu mensaje aquí...",
|
| 10 |
+
"sendButton": "Enviar",
|
| 11 |
+
"welcomeMessage": "¡Hola! Soy un asistente de IA impulsado por Qwen Coder. ¿Cómo puedo ayudarte hoy?",
|
| 12 |
+
"errorMessage": "Lo siento, encontré un error. Por favor, inténtalo de nuevo.",
|
| 13 |
+
"networkErrorMessage": "Lo siento, encontré un error de red. Por favor, verifica tu conexión e inténtalo de nuevo.",
|
| 14 |
+
"welcomeSubtitle": "Pregunta lo que quieras para empezar.",
|
| 15 |
+
"suggestion": {
|
| 16 |
+
"code": "Explica este fragmento de código",
|
| 17 |
+
"bug": "Ayúdame a depurar un error",
|
| 18 |
+
"write": "Escribe una función en Python",
|
| 19 |
+
"learn": "Explícame async/await"
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"ui": {
|
| 23 |
+
"loading": "Cargando...",
|
| 24 |
+
"typing": "La IA está escribiendo...",
|
| 25 |
+
"languageSelector": "Idioma",
|
| 26 |
+
"newChat": "Nuevo chat",
|
| 27 |
+
"disclaimer": "La IA puede cometer errores. Considera verificar información importante."
|
| 28 |
+
},
|
| 29 |
+
"errors": {
|
| 30 |
+
"general": "Ocurrió un error inesperado",
|
| 31 |
+
"network": "Falló la conexión de red",
|
| 32 |
+
"serverError": "Ocurrió un error del servidor",
|
| 33 |
+
"invalidInput": "Entrada inválida proporcionada"
|
| 34 |
+
},
|
| 35 |
+
"buttons": {
|
| 36 |
+
"send": "Enviar",
|
| 37 |
+
"clear": "Limpiar",
|
| 38 |
+
"copy": "Copiar",
|
| 39 |
+
"retry": "Reintentar"
|
| 40 |
+
},
|
| 41 |
+
"accessibility": {
|
| 42 |
+
"chatInput": "Ingresa tu mensaje",
|
| 43 |
+
"sendMessage": "Enviar mensaje",
|
| 44 |
+
"chatHistory": "Historial de conversación del chat"
|
| 45 |
+
},
|
| 46 |
+
"languages": {
|
| 47 |
+
"en": "English",
|
| 48 |
+
"es": "Español",
|
| 49 |
+
"fr": "Français"
|
| 50 |
+
}
|
| 51 |
+
}
|
locales/fr.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"app": {
|
| 3 |
+
"title": "Chat IA avec Qwen Coder",
|
| 4 |
+
"description": "Chattez avec le modèle Qwen/Qwen3-Coder-30B-A3B-Instruct"
|
| 5 |
+
},
|
| 6 |
+
"chat": {
|
| 7 |
+
"userLabel": "Vous",
|
| 8 |
+
"aiLabel": "IA",
|
| 9 |
+
"placeholder": "Tapez votre message ici...",
|
| 10 |
+
"sendButton": "Envoyer",
|
| 11 |
+
"welcomeMessage": "Bonjour ! Je suis un assistant IA alimenté par Qwen Coder. Comment puis-je vous aider aujourd'hui ?",
|
| 12 |
+
"errorMessage": "Désolé, j'ai rencontré une erreur. Veuillez réessayer.",
|
| 13 |
+
"networkErrorMessage": "Désolé, j'ai rencontré une erreur réseau. Veuillez vérifier votre connexion et réessayer.",
|
| 14 |
+
"welcomeSubtitle": "Demandez ce que vous voulez pour commencer.",
|
| 15 |
+
"suggestion": {
|
| 16 |
+
"code": "Expliquez cet extrait de code",
|
| 17 |
+
"bug": "Aidez-moi à déboguer une erreur",
|
| 18 |
+
"write": "Écrivez une fonction en Python",
|
| 19 |
+
"learn": "Expliquez-moi async/await"
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"ui": {
|
| 23 |
+
"loading": "Chargement...",
|
| 24 |
+
"typing": "L'IA tape...",
|
| 25 |
+
"languageSelector": "Langue",
|
| 26 |
+
"newChat": "Nouveau chat",
|
| 27 |
+
"disclaimer": "L'IA peut faire des erreurs. Considérez vérifier les informations importantes."
|
| 28 |
+
},
|
| 29 |
+
"errors": {
|
| 30 |
+
"general": "Une erreur inattendue s'est produite",
|
| 31 |
+
"network": "La connexion réseau a échoué",
|
| 32 |
+
"serverError": "Une erreur serveur s'est produite",
|
| 33 |
+
"invalidInput": "Entrée invalide fournie"
|
| 34 |
+
},
|
| 35 |
+
"buttons": {
|
| 36 |
+
"send": "Envoyer",
|
| 37 |
+
"clear": "Effacer",
|
| 38 |
+
"copy": "Copier",
|
| 39 |
+
"retry": "Réessayer"
|
| 40 |
+
},
|
| 41 |
+
"accessibility": {
|
| 42 |
+
"chatInput": "Entrez votre message",
|
| 43 |
+
"sendMessage": "Envoyer le message",
|
| 44 |
+
"chatHistory": "Historique de conversation du chat"
|
| 45 |
+
},
|
| 46 |
+
"languages": {
|
| 47 |
+
"en": "English",
|
| 48 |
+
"es": "Español",
|
| 49 |
+
"fr": "Français"
|
| 50 |
+
}
|
| 51 |
+
}
|
public/app.js
CHANGED
|
@@ -1,164 +1,1117 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
const messageDiv = document.createElement('div');
|
| 14 |
-
messageDiv.className = `message ${sender}`;
|
| 15 |
-
|
| 16 |
-
const messageHeader = document.createElement('div');
|
| 17 |
-
messageHeader.className = 'message-header';
|
| 18 |
-
messageHeader.textContent = sender === 'user' ? 'You' : 'AI';
|
| 19 |
-
|
| 20 |
-
const messageText = document.createElement('div');
|
| 21 |
-
messageText.className = 'message-text';
|
| 22 |
-
messageText.textContent = text;
|
| 23 |
-
|
| 24 |
-
messageDiv.appendChild(messageHeader);
|
| 25 |
-
messageDiv.appendChild(messageText);
|
| 26 |
-
chatMessages.appendChild(messageDiv);
|
| 27 |
-
|
| 28 |
-
// Scroll to bottom
|
| 29 |
-
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 30 |
-
}
|
| 31 |
|
| 32 |
-
//
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
loadingDiv.appendChild(messageHeader);
|
| 46 |
-
loadingDiv.appendChild(loadingIndicator);
|
| 47 |
-
chatMessages.appendChild(loadingDiv);
|
| 48 |
-
|
| 49 |
-
// Scroll to bottom
|
| 50 |
-
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 51 |
-
}
|
| 52 |
|
| 53 |
-
//
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
loadingMessage.remove();
|
| 58 |
}
|
| 59 |
-
}
|
| 60 |
|
| 61 |
-
//
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
//
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
method: 'POST',
|
| 79 |
headers: {
|
| 80 |
-
'Content-Type': 'application/json'
|
| 81 |
},
|
| 82 |
-
body: JSON.stringify({
|
| 83 |
-
message: message,
|
| 84 |
-
history: conversationHistory.slice(0, -1) // Exclude the current message
|
| 85 |
-
})
|
| 86 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
while (true) {
|
| 115 |
const { done, value } = await reader.read();
|
| 116 |
if (done) break;
|
| 117 |
|
| 118 |
const chunk = decoder.decode(value, { stream: true });
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
// Scroll to bottom
|
| 123 |
-
|
| 124 |
}
|
| 125 |
|
| 126 |
-
//
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
} else {
|
| 129 |
-
|
| 130 |
-
addMessage('ai', 'Sorry, I encountered an error. Please try again.');
|
| 131 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
} catch (error) {
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
}
|
| 140 |
}
|
| 141 |
|
| 142 |
-
//
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
}
|
| 148 |
-
});
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
-
//
|
| 162 |
-
window.
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Enhanced AI Chat Application - Core JavaScript Implementation
|
| 3 |
+
*
|
| 4 |
+
* Features:
|
| 5 |
+
* - Real-time chat functionality
|
| 6 |
+
* - WebSocket support for live messaging
|
| 7 |
+
* - State management for conversations
|
| 8 |
+
* - API communication with retry logic
|
| 9 |
+
* - Typing indicators and message status
|
| 10 |
+
* - Connection monitoring
|
| 11 |
+
* - Performance optimizations
|
| 12 |
+
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
// Application State Management
|
| 15 |
+
class ChatState {
|
| 16 |
+
constructor() {
|
| 17 |
+
this.conversations = new Map();
|
| 18 |
+
this.currentConversationId = null;
|
| 19 |
+
this.isConnected = false;
|
| 20 |
+
this.isTyping = false;
|
| 21 |
+
this.lastActivity = Date.now();
|
| 22 |
+
this.connectionStatus = 'disconnected';
|
| 23 |
+
this.messageQueue = [];
|
| 24 |
+
this.retryAttempts = 0;
|
| 25 |
+
this.maxRetries = 3;
|
| 26 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
+
// Get current conversation
|
| 29 |
+
getCurrentConversation() {
|
| 30 |
+
if (!this.currentConversationId) return null;
|
| 31 |
+
return this.conversations.get(this.currentConversationId);
|
|
|
|
| 32 |
}
|
|
|
|
| 33 |
|
| 34 |
+
// Create new conversation
|
| 35 |
+
createConversation(title = 'New Chat') {
|
| 36 |
+
const id = `chat_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
| 37 |
+
const conversation = {
|
| 38 |
+
id,
|
| 39 |
+
title,
|
| 40 |
+
messages: [],
|
| 41 |
+
created: Date.now(),
|
| 42 |
+
updated: Date.now(),
|
| 43 |
+
model: 'qwen-coder-3-30b',
|
| 44 |
+
metadata: {}
|
| 45 |
+
};
|
| 46 |
+
this.conversations.set(id, conversation);
|
| 47 |
+
this.currentConversationId = id;
|
| 48 |
+
return conversation;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Add message to current conversation
|
| 52 |
+
addMessage(role, content, metadata = {}) {
|
| 53 |
+
const conversation = this.getCurrentConversation();
|
| 54 |
+
if (!conversation) return null;
|
| 55 |
+
|
| 56 |
+
const message = {
|
| 57 |
+
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
| 58 |
+
role,
|
| 59 |
+
content,
|
| 60 |
+
timestamp: Date.now(),
|
| 61 |
+
status: 'sent',
|
| 62 |
+
metadata
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
conversation.messages.push(message);
|
| 66 |
+
conversation.updated = Date.now();
|
| 67 |
|
| 68 |
+
// Update conversation title if it's the first user message
|
| 69 |
+
if (role === 'user' && conversation.messages.filter(m => m.role === 'user').length === 1) {
|
| 70 |
+
conversation.title = this.generateTitle(content);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
this.saveToStorage();
|
| 74 |
+
return message;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Generate conversation title from first message
|
| 78 |
+
generateTitle(content) {
|
| 79 |
+
const words = content.trim().split(' ').slice(0, 4).join(' ');
|
| 80 |
+
return words.length > 30 ? words.substring(0, 27) + '...' : words || 'New Chat';
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Save state to localStorage
|
| 84 |
+
saveToStorage() {
|
| 85 |
+
try {
|
| 86 |
+
const data = {
|
| 87 |
+
conversations: Array.from(this.conversations.entries()),
|
| 88 |
+
currentConversationId: this.currentConversationId,
|
| 89 |
+
timestamp: Date.now()
|
| 90 |
+
};
|
| 91 |
+
localStorage.setItem('chatState', JSON.stringify(data));
|
| 92 |
+
} catch (error) {
|
| 93 |
+
console.error('Failed to save state:', error);
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// Load state from localStorage
|
| 98 |
+
loadFromStorage() {
|
| 99 |
+
try {
|
| 100 |
+
const data = JSON.parse(localStorage.getItem('chatState') || '{}');
|
| 101 |
+
if (data.conversations) {
|
| 102 |
+
this.conversations = new Map(data.conversations);
|
| 103 |
+
this.currentConversationId = data.currentConversationId;
|
| 104 |
+
}
|
| 105 |
+
} catch (error) {
|
| 106 |
+
console.error('Failed to load state:', error);
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// API Communication Manager
|
| 112 |
+
class APIManager {
|
| 113 |
+
constructor() {
|
| 114 |
+
this.baseURL = window.location.origin;
|
| 115 |
+
this.abortController = null;
|
| 116 |
+
this.requestTimeout = 30000; // 30 seconds
|
| 117 |
+
this.retryDelay = 1000; // 1 second
|
| 118 |
+
this.maxRetryDelay = 10000; // 10 seconds
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Make API request with retry logic
|
| 122 |
+
async makeRequest(endpoint, options = {}, retries = 3) {
|
| 123 |
+
const url = `${this.baseURL}${endpoint}`;
|
| 124 |
|
| 125 |
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
| 126 |
+
try {
|
| 127 |
+
// Create new AbortController for this request
|
| 128 |
+
this.abortController = new AbortController();
|
| 129 |
+
|
| 130 |
+
const response = await fetch(url, {
|
| 131 |
+
...options,
|
| 132 |
+
signal: this.abortController.signal,
|
| 133 |
+
timeout: this.requestTimeout
|
| 134 |
+
});
|
| 135 |
+
|
| 136 |
+
if (!response.ok) {
|
| 137 |
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
return response;
|
| 141 |
+
} catch (error) {
|
| 142 |
+
console.warn(`Request attempt ${attempt + 1} failed:`, error);
|
| 143 |
+
|
| 144 |
+
if (attempt === retries) {
|
| 145 |
+
throw error;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
// Exponential backoff
|
| 149 |
+
const delay = Math.min(this.retryDelay * Math.pow(2, attempt), this.maxRetryDelay);
|
| 150 |
+
await this.sleep(delay);
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// Send chat message
|
| 156 |
+
async sendMessage(message, history = []) {
|
| 157 |
+
const response = await this.makeRequest('/chat', {
|
| 158 |
method: 'POST',
|
| 159 |
headers: {
|
| 160 |
+
'Content-Type': 'application/json',
|
| 161 |
},
|
| 162 |
+
body: JSON.stringify({ message, history })
|
|
|
|
|
|
|
|
|
|
| 163 |
});
|
| 164 |
+
|
| 165 |
+
return response;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
// Cancel current request
|
| 169 |
+
cancelRequest() {
|
| 170 |
+
if (this.abortController) {
|
| 171 |
+
this.abortController.abort();
|
| 172 |
+
this.abortController = null;
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// Utility sleep function
|
| 177 |
+
sleep(ms) {
|
| 178 |
+
return new Promise(resolve => setTimeout(resolve, ms));
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// Connection Monitor
|
| 183 |
+
class ConnectionMonitor {
|
| 184 |
+
constructor(onStatusChange) {
|
| 185 |
+
this.onStatusChange = onStatusChange;
|
| 186 |
+
this.isOnline = navigator.onLine;
|
| 187 |
+
this.setupEventListeners();
|
| 188 |
+
this.startPingTest();
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
setupEventListeners() {
|
| 192 |
+
window.addEventListener('online', () => {
|
| 193 |
+
this.isOnline = true;
|
| 194 |
+
this.onStatusChange('online');
|
| 195 |
+
});
|
| 196 |
+
|
| 197 |
+
window.addEventListener('offline', () => {
|
| 198 |
+
this.isOnline = false;
|
| 199 |
+
this.onStatusChange('offline');
|
| 200 |
+
});
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// Periodic connectivity test
|
| 204 |
+
startPingTest() {
|
| 205 |
+
setInterval(async () => {
|
| 206 |
+
if (this.isOnline) {
|
| 207 |
+
try {
|
| 208 |
+
const response = await fetch('/ping', {
|
| 209 |
+
method: 'HEAD',
|
| 210 |
+
cache: 'no-cache',
|
| 211 |
+
timeout: 5000
|
| 212 |
+
});
|
| 213 |
+
this.onStatusChange(response.ok ? 'connected' : 'disconnected');
|
| 214 |
+
} catch {
|
| 215 |
+
this.onStatusChange('disconnected');
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
}, 10000); // Check every 10 seconds
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
// Message Renderer
|
| 223 |
+
class MessageRenderer {
|
| 224 |
+
constructor() {
|
| 225 |
+
this.messageContainer = null;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
setContainer(container) {
|
| 229 |
+
this.messageContainer = container;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
// Render a single message
|
| 233 |
+
renderMessage(message) {
|
| 234 |
+
const messageDiv = document.createElement('div');
|
| 235 |
+
messageDiv.className = `relative flex items-start gap-3 px-4 py-5 sm:px-6`;
|
| 236 |
+
messageDiv.dataset.messageId = message.id;
|
| 237 |
+
|
| 238 |
+
// Create avatar
|
| 239 |
+
const avatar = document.createElement('div');
|
| 240 |
+
avatar.className = `mt-1 h-8 w-8 shrink-0 overflow-hidden rounded-full ${
|
| 241 |
+
message.role === 'user'
|
| 242 |
+
? 'bg-zinc-300 grid place-items-center text-[10px] font-medium'
|
| 243 |
+
: 'bg-zinc-200'
|
| 244 |
+
}`;
|
| 245 |
|
| 246 |
+
if (message.role === 'user') {
|
| 247 |
+
avatar.textContent = 'YOU';
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// Create message content wrapper
|
| 251 |
+
const contentWrapper = document.createElement('div');
|
| 252 |
+
contentWrapper.className = 'min-w-0 flex-1';
|
| 253 |
+
|
| 254 |
+
// Create message header
|
| 255 |
+
const header = document.createElement('div');
|
| 256 |
+
header.className = 'mb-1 flex items-baseline gap-2';
|
| 257 |
+
header.innerHTML = `
|
| 258 |
+
<div class="text-sm font-medium">${message.role === 'user' ? 'You' : 'Ava'}</div>
|
| 259 |
+
<div class="text-xs text-zinc-500">${this.formatTime(message.timestamp)}</div>
|
| 260 |
+
${message.status !== 'sent' ? `<div class="text-xs text-zinc-400">${message.status}</div>` : ''}
|
| 261 |
+
`;
|
| 262 |
+
|
| 263 |
+
// Create message content
|
| 264 |
+
const content = document.createElement('div');
|
| 265 |
+
content.className = message.role === 'user'
|
| 266 |
+
? 'prose prose-sm max-w-none rounded-2xl border border-zinc-200 bg-white p-4 dark:prose-invert dark:border-zinc-800 dark:bg-zinc-900'
|
| 267 |
+
: 'prose prose-sm max-w-none rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:prose-invert dark:border-zinc-800 dark:bg-zinc-800/60';
|
| 268 |
|
| 269 |
+
content.innerHTML = this.formatContent(message.content);
|
| 270 |
+
|
| 271 |
+
contentWrapper.appendChild(header);
|
| 272 |
+
contentWrapper.appendChild(content);
|
| 273 |
+
messageDiv.appendChild(avatar);
|
| 274 |
+
messageDiv.appendChild(contentWrapper);
|
| 275 |
+
|
| 276 |
+
return messageDiv;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// Format message content (handle markdown, code, etc.)
|
| 280 |
+
formatContent(content) {
|
| 281 |
+
// Basic formatting - can be enhanced with a markdown parser
|
| 282 |
+
return content
|
| 283 |
+
.replace(/\n/g, '<br>')
|
| 284 |
+
.replace(/`([^`]+)`/g, '<code class="bg-zinc-200 dark:bg-zinc-700 px-1 py-0.5 rounded text-sm">$1</code>')
|
| 285 |
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
| 286 |
+
.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
// Format timestamp
|
| 290 |
+
formatTime(timestamp) {
|
| 291 |
+
return new Date(timestamp).toLocaleTimeString().slice(0, 5);
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// Create typing indicator
|
| 295 |
+
createTypingIndicator() {
|
| 296 |
+
const messageDiv = document.createElement('div');
|
| 297 |
+
messageDiv.className = 'relative flex items-start gap-3 px-4 py-5 sm:px-6';
|
| 298 |
+
messageDiv.id = 'typing-indicator';
|
| 299 |
+
|
| 300 |
+
const avatar = document.createElement('div');
|
| 301 |
+
avatar.className = 'mt-1 h-8 w-8 shrink-0 overflow-hidden rounded-full bg-zinc-200';
|
| 302 |
+
|
| 303 |
+
const contentWrapper = document.createElement('div');
|
| 304 |
+
contentWrapper.className = 'min-w-0 flex-1';
|
| 305 |
+
|
| 306 |
+
const header = document.createElement('div');
|
| 307 |
+
header.className = 'mb-1 flex items-baseline gap-2';
|
| 308 |
+
header.innerHTML = '<div class="text-sm font-medium">Ava</div><div class="text-xs text-zinc-500">typing...</div>';
|
| 309 |
+
|
| 310 |
+
const typingDots = document.createElement('div');
|
| 311 |
+
typingDots.className = 'flex items-center space-x-1 p-4';
|
| 312 |
+
typingDots.innerHTML = `
|
| 313 |
+
<div class="w-2 h-2 bg-zinc-400 rounded-full animate-bounce" style="animation-delay: 0ms"></div>
|
| 314 |
+
<div class="w-2 h-2 bg-zinc-400 rounded-full animate-bounce" style="animation-delay: 150ms"></div>
|
| 315 |
+
<div class="w-2 h-2 bg-zinc-400 rounded-full animate-bounce" style="animation-delay: 300ms"></div>
|
| 316 |
+
`;
|
| 317 |
+
|
| 318 |
+
contentWrapper.appendChild(header);
|
| 319 |
+
contentWrapper.appendChild(typingDots);
|
| 320 |
+
messageDiv.appendChild(avatar);
|
| 321 |
+
messageDiv.appendChild(contentWrapper);
|
| 322 |
+
|
| 323 |
+
return messageDiv;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// Show typing indicator
|
| 327 |
+
showTyping() {
|
| 328 |
+
this.hideTyping(); // Remove any existing typing indicator
|
| 329 |
+
if (this.messageContainer) {
|
| 330 |
+
const typingIndicator = this.createTypingIndicator();
|
| 331 |
+
this.messageContainer.appendChild(typingIndicator);
|
| 332 |
+
this.scrollToBottom();
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
// Hide typing indicator
|
| 337 |
+
hideTyping() {
|
| 338 |
+
const existing = document.getElementById('typing-indicator');
|
| 339 |
+
if (existing) {
|
| 340 |
+
existing.remove();
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
// Scroll to bottom of message container
|
| 345 |
+
scrollToBottom() {
|
| 346 |
+
if (this.messageContainer && this.messageContainer.parentElement) {
|
| 347 |
+
this.messageContainer.parentElement.scrollTop = this.messageContainer.parentElement.scrollHeight;
|
| 348 |
+
}
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
// Initialize global instances
|
| 353 |
+
const chatState = new ChatState();
|
| 354 |
+
const apiManager = new APIManager();
|
| 355 |
+
const messageRenderer = new MessageRenderer();
|
| 356 |
+
|
| 357 |
+
// DOM element references
|
| 358 |
+
let isI18nReady = false;
|
| 359 |
+
const elements = {}; // Will be populated in DOMContentLoaded
|
| 360 |
+
|
| 361 |
+
// Chat Application Controller
|
| 362 |
+
class ChatApp {
|
| 363 |
+
constructor() {
|
| 364 |
+
this.state = chatState;
|
| 365 |
+
this.api = apiManager;
|
| 366 |
+
this.renderer = messageRenderer;
|
| 367 |
+
this.connectionMonitor = null;
|
| 368 |
+
this.elements = {};
|
| 369 |
+
this.isProcessingMessage = false;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
// Initialize the application
|
| 373 |
+
async init() {
|
| 374 |
+
console.log('Initializing Chat Application...');
|
| 375 |
+
|
| 376 |
+
// Wait for DOM to be ready
|
| 377 |
+
if (document.readyState === 'loading') {
|
| 378 |
+
await new Promise(resolve => {
|
| 379 |
+
document.addEventListener('DOMContentLoaded', resolve);
|
| 380 |
+
});
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
// Load state from storage
|
| 384 |
+
this.state.loadFromStorage();
|
| 385 |
+
|
| 386 |
+
// Get DOM elements from existing HTML structure
|
| 387 |
+
this.elements = {
|
| 388 |
+
messages: document.getElementById('messages'),
|
| 389 |
+
composer: document.getElementById('composer'),
|
| 390 |
+
sendButton: document.getElementById('btn-send'),
|
| 391 |
+
messagesScroller: document.getElementById('msg-scroll'),
|
| 392 |
+
leftSidebar: document.getElementById('left-desktop'),
|
| 393 |
+
modelDropdown: document.getElementById('model-dd'),
|
| 394 |
+
chatMore: document.getElementById('chat-more'),
|
| 395 |
+
themeButton: document.getElementById('btn-theme')
|
| 396 |
+
};
|
| 397 |
+
|
| 398 |
+
// Set up message renderer
|
| 399 |
+
this.renderer.setContainer(this.elements.messages);
|
| 400 |
+
|
| 401 |
+
// Initialize connection monitoring
|
| 402 |
+
this.connectionMonitor = new ConnectionMonitor((status) => {
|
| 403 |
+
this.handleConnectionStatusChange(status);
|
| 404 |
+
});
|
| 405 |
+
|
| 406 |
+
// Set up event listeners
|
| 407 |
+
this.setupEventListeners();
|
| 408 |
+
|
| 409 |
+
// Initialize with existing conversation or create new one
|
| 410 |
+
if (this.state.conversations.size === 0) {
|
| 411 |
+
this.createNewConversation();
|
| 412 |
+
} else {
|
| 413 |
+
this.loadCurrentConversation();
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
console.log('Chat Application initialized successfully');
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
// Set up all event listeners
|
| 420 |
+
setupEventListeners() {
|
| 421 |
+
// Message sending
|
| 422 |
+
if (this.elements.sendButton) {
|
| 423 |
+
this.elements.sendButton.addEventListener('click', () => this.handleSendMessage());
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
if (this.elements.composer) {
|
| 427 |
+
this.elements.composer.addEventListener('keydown', (e) => {
|
| 428 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 429 |
+
e.preventDefault();
|
| 430 |
+
this.handleSendMessage();
|
| 431 |
+
}
|
| 432 |
+
});
|
| 433 |
+
|
| 434 |
+
// Auto-resize textarea
|
| 435 |
+
this.elements.composer.addEventListener('input', () => {
|
| 436 |
+
this.autoResizeComposer();
|
| 437 |
+
this.updateSendButtonState();
|
| 438 |
+
});
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
// Handle suggestion clicks from welcome screen
|
| 442 |
+
if (this.elements.messages) {
|
| 443 |
+
this.elements.messages.addEventListener('click', (e) => {
|
| 444 |
+
const suggestion = e.target.closest('[data-suggest]');
|
| 445 |
+
if (suggestion) {
|
| 446 |
+
const text = suggestion.textContent.trim();
|
| 447 |
+
if (text && this.elements.composer) {
|
| 448 |
+
this.elements.composer.value = text;
|
| 449 |
+
this.autoResizeComposer();
|
| 450 |
+
this.updateSendButtonState();
|
| 451 |
+
this.elements.composer.focus();
|
| 452 |
+
}
|
| 453 |
+
}
|
| 454 |
+
});
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
// Auto-save state periodically
|
| 458 |
+
setInterval(() => {
|
| 459 |
+
this.state.saveToStorage();
|
| 460 |
+
}, 30000); // Save every 30 seconds
|
| 461 |
+
|
| 462 |
+
// Save on page unload
|
| 463 |
+
window.addEventListener('beforeunload', () => {
|
| 464 |
+
this.state.saveToStorage();
|
| 465 |
+
});
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
// Handle sending messages
|
| 469 |
+
async handleSendMessage() {
|
| 470 |
+
const message = this.elements.composer?.value?.trim();
|
| 471 |
+
if (!message || this.isProcessingMessage) return;
|
| 472 |
+
|
| 473 |
+
this.isProcessingMessage = true;
|
| 474 |
+
this.updateSendButtonState();
|
| 475 |
+
|
| 476 |
+
try {
|
| 477 |
+
// Clear composer
|
| 478 |
+
if (this.elements.composer) {
|
| 479 |
+
this.elements.composer.value = '';
|
| 480 |
+
this.autoResizeComposer();
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
// Add user message
|
| 484 |
+
const userMessage = this.state.addMessage('user', message);
|
| 485 |
+
this.renderMessage(userMessage);
|
| 486 |
+
|
| 487 |
+
// Show typing indicator
|
| 488 |
+
this.renderer.showTyping();
|
| 489 |
+
|
| 490 |
+
// Prepare conversation history for API
|
| 491 |
+
const conversation = this.state.getCurrentConversation();
|
| 492 |
+
const history = conversation ? conversation.messages.map(msg => ({
|
| 493 |
+
role: msg.role,
|
| 494 |
+
content: msg.content
|
| 495 |
+
})) : [];
|
| 496 |
+
|
| 497 |
+
// Send to API
|
| 498 |
+
const response = await this.api.sendMessage(message, history.slice(0, -1));
|
| 499 |
|
| 500 |
+
// Hide typing indicator
|
| 501 |
+
this.renderer.hideTyping();
|
| 502 |
+
|
| 503 |
+
// Handle streaming response
|
| 504 |
+
await this.handleStreamingResponse(response);
|
| 505 |
+
|
| 506 |
+
} catch (error) {
|
| 507 |
+
console.error('Error sending message:', error);
|
| 508 |
+
this.renderer.hideTyping();
|
| 509 |
|
| 510 |
+
// Add error message
|
| 511 |
+
const errorMessage = this.state.addMessage('assistant',
|
| 512 |
+
'Sorry, I encountered an error. Please try again.');
|
| 513 |
+
this.renderMessage(errorMessage);
|
| 514 |
+
|
| 515 |
+
} finally {
|
| 516 |
+
this.isProcessingMessage = false;
|
| 517 |
+
this.updateSendButtonState();
|
| 518 |
+
}
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
// Handle streaming API response
|
| 522 |
+
async handleStreamingResponse(response) {
|
| 523 |
+
const reader = response.body.getReader();
|
| 524 |
+
const decoder = new TextDecoder();
|
| 525 |
+
|
| 526 |
+
// Create assistant message
|
| 527 |
+
const assistantMessage = this.state.addMessage('assistant', '');
|
| 528 |
+
const messageElement = this.renderMessage(assistantMessage);
|
| 529 |
+
|
| 530 |
+
// Get content div for streaming updates
|
| 531 |
+
const contentDiv = messageElement.querySelector('.prose');
|
| 532 |
+
|
| 533 |
+
let accumulatedContent = '';
|
| 534 |
+
|
| 535 |
+
try {
|
| 536 |
while (true) {
|
| 537 |
const { done, value } = await reader.read();
|
| 538 |
if (done) break;
|
| 539 |
|
| 540 |
const chunk = decoder.decode(value, { stream: true });
|
| 541 |
+
accumulatedContent += chunk;
|
| 542 |
+
|
| 543 |
+
// Update message content
|
| 544 |
+
assistantMessage.content = accumulatedContent;
|
| 545 |
+
contentDiv.innerHTML = this.renderer.formatContent(accumulatedContent);
|
| 546 |
|
| 547 |
// Scroll to bottom
|
| 548 |
+
this.renderer.scrollToBottom();
|
| 549 |
}
|
| 550 |
|
| 551 |
+
// Update state with final content
|
| 552 |
+
this.state.saveToStorage();
|
| 553 |
+
|
| 554 |
+
} catch (error) {
|
| 555 |
+
console.error('Error reading stream:', error);
|
| 556 |
+
contentDiv.innerHTML = 'Error receiving response';
|
| 557 |
+
}
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
// Render a message to the UI
|
| 561 |
+
renderMessage(message) {
|
| 562 |
+
if (!this.elements.messages) return null;
|
| 563 |
+
|
| 564 |
+
const messageElement = this.renderer.renderMessage(message);
|
| 565 |
+
this.elements.messages.appendChild(messageElement);
|
| 566 |
+
this.renderer.scrollToBottom();
|
| 567 |
+
|
| 568 |
+
return messageElement;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
// Create new conversation
|
| 572 |
+
createNewConversation() {
|
| 573 |
+
const conversation = this.state.createConversation();
|
| 574 |
+
this.renderWelcomeScreen();
|
| 575 |
+
this.state.saveToStorage();
|
| 576 |
+
console.log('Created new conversation:', conversation.id);
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
// Load current conversation
|
| 580 |
+
loadCurrentConversation() {
|
| 581 |
+
const conversation = this.state.getCurrentConversation();
|
| 582 |
+
if (!conversation) {
|
| 583 |
+
this.createNewConversation();
|
| 584 |
+
return;
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
// Clear existing messages
|
| 588 |
+
if (this.elements.messages) {
|
| 589 |
+
this.elements.messages.innerHTML = '';
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
if (conversation.messages.length === 0) {
|
| 593 |
+
this.renderWelcomeScreen();
|
| 594 |
+
} else {
|
| 595 |
+
conversation.messages.forEach(message => {
|
| 596 |
+
this.renderMessage(message);
|
| 597 |
+
});
|
| 598 |
+
}
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
// Render welcome screen
|
| 602 |
+
renderWelcomeScreen() {
|
| 603 |
+
if (!this.elements.messages) return;
|
| 604 |
+
|
| 605 |
+
// Use the existing welcome message structure from HTML
|
| 606 |
+
this.elements.messages.innerHTML = `
|
| 607 |
+
<div class="relative flex items-start gap-3 px-4 py-5 sm:px-6">
|
| 608 |
+
<div class="mt-1 h-8 w-8 shrink-0 overflow-hidden rounded-full bg-zinc-200"></div>
|
| 609 |
+
<div class="min-w-0 flex-1">
|
| 610 |
+
<div class="mb-1 flex items-baseline gap-2">
|
| 611 |
+
<div class="text-sm font-medium">Ava</div>
|
| 612 |
+
<div class="text-xs text-zinc-500">${new Date().toLocaleTimeString().slice(0, 5)}</div>
|
| 613 |
+
</div>
|
| 614 |
+
<div class="prose prose-sm max-w-none rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:prose-invert dark:border-zinc-800 dark:bg-zinc-800/60">
|
| 615 |
+
<p>Ahoj! 👋 Jsem tvůj AI asistent. Jaký úkol dnes řešíš?</p>
|
| 616 |
+
<div class="mt-3 flex flex-wrap gap-2">
|
| 617 |
+
<span class="inline-flex cursor-pointer select-none items-center rounded-md bg-zinc-100 px-2.5 py-1 text-xs ring-1 ring-inset ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700" data-suggest>Refactor code</span>
|
| 618 |
+
<span class="inline-flex cursor-pointer select-none items-center rounded-md bg-zinc-100 px-2.5 py-1 text-xs ring-1 ring-inset ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700" data-suggest>Generate unit tests</span>
|
| 619 |
+
<span class="inline-flex cursor-pointer select-none items-center rounded-md bg-zinc-100 px-2.5 py-1 text-xs ring-1 ring-inset ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700" data-suggest>Explain this snippet</span>
|
| 620 |
+
<span class="inline-flex cursor-pointer select-none items-center rounded-md bg-zinc-100 px-2.5 py-1 text-xs ring-1 ring-inset ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700" data-suggest>Create README</span>
|
| 621 |
+
</div>
|
| 622 |
+
</div>
|
| 623 |
+
</div>
|
| 624 |
+
</div>
|
| 625 |
+
`;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
// Auto-resize composer textarea
|
| 629 |
+
autoResizeComposer() {
|
| 630 |
+
if (!this.elements.composer) return;
|
| 631 |
+
|
| 632 |
+
this.elements.composer.style.height = 'auto';
|
| 633 |
+
const maxHeight = 160; // max-h-40 from Tailwind (160px)
|
| 634 |
+
this.elements.composer.style.height = Math.min(this.elements.composer.scrollHeight, maxHeight) + 'px';
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
// Update send button state
|
| 638 |
+
updateSendButtonState() {
|
| 639 |
+
if (!this.elements.sendButton || !this.elements.composer) return;
|
| 640 |
+
|
| 641 |
+
const hasText = this.elements.composer.value.trim().length > 0;
|
| 642 |
+
const isEnabled = hasText && !this.isProcessingMessage;
|
| 643 |
+
|
| 644 |
+
this.elements.sendButton.disabled = !isEnabled;
|
| 645 |
+
|
| 646 |
+
if (isEnabled) {
|
| 647 |
+
this.elements.sendButton.classList.remove('disabled:cursor-not-allowed', 'disabled:opacity-50');
|
| 648 |
} else {
|
| 649 |
+
this.elements.sendButton.classList.add('disabled:cursor-not-allowed', 'disabled:opacity-50');
|
|
|
|
| 650 |
}
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
// Handle connection status changes
|
| 654 |
+
handleConnectionStatusChange(status) {
|
| 655 |
+
this.state.connectionStatus = status;
|
| 656 |
+
console.log('Connection status changed to:', status);
|
| 657 |
+
|
| 658 |
+
// Update UI to show connection status
|
| 659 |
+
// You can add visual indicators here if needed
|
| 660 |
+
|
| 661 |
+
// Retry queued messages when connection is restored
|
| 662 |
+
if (status === 'connected' && this.state.messageQueue.length > 0) {
|
| 663 |
+
this.processMessageQueue();
|
| 664 |
+
}
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
// Process queued messages when connection is restored
|
| 668 |
+
async processMessageQueue() {
|
| 669 |
+
while (this.state.messageQueue.length > 0) {
|
| 670 |
+
const queuedMessage = this.state.messageQueue.shift();
|
| 671 |
+
try {
|
| 672 |
+
await this.handleSendMessage(queuedMessage);
|
| 673 |
+
} catch (error) {
|
| 674 |
+
console.error('Failed to process queued message:', error);
|
| 675 |
+
// Re-queue if failed
|
| 676 |
+
this.state.messageQueue.unshift(queuedMessage);
|
| 677 |
+
break;
|
| 678 |
+
}
|
| 679 |
+
}
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
// Cancel current message processing
|
| 683 |
+
cancelCurrentMessage() {
|
| 684 |
+
this.api.cancelRequest();
|
| 685 |
+
this.renderer.hideTyping();
|
| 686 |
+
this.isProcessingMessage = false;
|
| 687 |
+
this.updateSendButtonState();
|
| 688 |
+
}
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
// Initialize the chat application
|
| 692 |
+
const chatApp = new ChatApp();
|
| 693 |
+
|
| 694 |
+
// Start the application when DOM is ready
|
| 695 |
+
if (document.readyState === 'loading') {
|
| 696 |
+
document.addEventListener('DOMContentLoaded', () => chatApp.init());
|
| 697 |
+
} else {
|
| 698 |
+
chatApp.init();
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
// Export for potential module usage
|
| 702 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 703 |
+
module.exports = { ChatApp, ChatState, APIManager, MessageRenderer, ConnectionMonitor };
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
// Utility functions for backward compatibility and additional features
|
| 707 |
+
|
| 708 |
+
// Initialize i18n system (simplified for this implementation)
|
| 709 |
+
async function initializeI18n() {
|
| 710 |
+
try {
|
| 711 |
+
// Simple mock implementation - can be enhanced with actual i18n
|
| 712 |
+
isI18nReady = true;
|
| 713 |
+
return true;
|
| 714 |
} catch (error) {
|
| 715 |
+
console.error('Failed to initialize i18n:', error);
|
| 716 |
+
isI18nReady = false;
|
| 717 |
+
return false;
|
| 718 |
+
}
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
// Utility function for localized text
|
| 722 |
+
function getLocalizedText(key, fallback) {
|
| 723 |
+
// Simple implementation - return fallback for now
|
| 724 |
+
return fallback || key;
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
// Helper function for translations
|
| 728 |
+
function t(key, fallback = key) {
|
| 729 |
+
return getLocalizedText(key, fallback);
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
// Performance monitoring
|
| 733 |
+
class PerformanceMonitor {
|
| 734 |
+
constructor() {
|
| 735 |
+
this.metrics = new Map();
|
| 736 |
+
this.observers = new Map();
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
// Start timing an operation
|
| 740 |
+
startTiming(operation) {
|
| 741 |
+
this.metrics.set(operation, { start: performance.now() });
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
// End timing an operation
|
| 745 |
+
endTiming(operation) {
|
| 746 |
+
const metric = this.metrics.get(operation);
|
| 747 |
+
if (metric) {
|
| 748 |
+
metric.end = performance.now();
|
| 749 |
+
metric.duration = metric.end - metric.start;
|
| 750 |
+
console.log(`${operation} took ${metric.duration.toFixed(2)}ms`);
|
| 751 |
+
}
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
// Monitor memory usage
|
| 755 |
+
getMemoryUsage() {
|
| 756 |
+
if (performance.memory) {
|
| 757 |
+
return {
|
| 758 |
+
used: Math.round(performance.memory.usedJSHeapSize / 1048576),
|
| 759 |
+
total: Math.round(performance.memory.totalJSHeapSize / 1048576),
|
| 760 |
+
limit: Math.round(performance.memory.jsHeapSizeLimit / 1048576)
|
| 761 |
+
};
|
| 762 |
+
}
|
| 763 |
+
return null;
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
// Monitor network timing
|
| 767 |
+
observeNetworkTiming() {
|
| 768 |
+
if ('PerformanceObserver' in window) {
|
| 769 |
+
const observer = new PerformanceObserver((list) => {
|
| 770 |
+
list.getEntries().forEach((entry) => {
|
| 771 |
+
if (entry.entryType === 'navigation') {
|
| 772 |
+
console.log('Navigation timing:', {
|
| 773 |
+
dns: entry.domainLookupEnd - entry.domainLookupStart,
|
| 774 |
+
connection: entry.connectEnd - entry.connectStart,
|
| 775 |
+
request: entry.responseStart - entry.requestStart,
|
| 776 |
+
response: entry.responseEnd - entry.responseStart
|
| 777 |
+
});
|
| 778 |
+
}
|
| 779 |
+
});
|
| 780 |
+
});
|
| 781 |
+
|
| 782 |
+
try {
|
| 783 |
+
observer.observe({ entryTypes: ['navigation', 'resource'] });
|
| 784 |
+
this.observers.set('network', observer);
|
| 785 |
+
} catch (error) {
|
| 786 |
+
console.warn('Performance observer not supported:', error);
|
| 787 |
+
}
|
| 788 |
+
}
|
| 789 |
+
}
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
// Enhanced WebSocket support for real-time features
|
| 793 |
+
class WebSocketManager {
|
| 794 |
+
constructor(url) {
|
| 795 |
+
this.url = url;
|
| 796 |
+
this.socket = null;
|
| 797 |
+
this.reconnectAttempts = 0;
|
| 798 |
+
this.maxReconnectAttempts = 5;
|
| 799 |
+
this.reconnectDelay = 1000;
|
| 800 |
+
this.heartbeatInterval = null;
|
| 801 |
+
this.messageQueue = [];
|
| 802 |
+
this.listeners = new Map();
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
// Connect to WebSocket
|
| 806 |
+
connect() {
|
| 807 |
+
try {
|
| 808 |
+
this.socket = new WebSocket(this.url);
|
| 809 |
+
this.setupEventListeners();
|
| 810 |
+
} catch (error) {
|
| 811 |
+
console.error('WebSocket connection failed:', error);
|
| 812 |
+
this.handleReconnect();
|
| 813 |
+
}
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
// Setup WebSocket event listeners
|
| 817 |
+
setupEventListeners() {
|
| 818 |
+
if (!this.socket) return;
|
| 819 |
+
|
| 820 |
+
this.socket.onopen = () => {
|
| 821 |
+
console.log('WebSocket connected');
|
| 822 |
+
this.reconnectAttempts = 0;
|
| 823 |
+
this.startHeartbeat();
|
| 824 |
+
this.processMessageQueue();
|
| 825 |
+
this.emit('connected');
|
| 826 |
+
};
|
| 827 |
+
|
| 828 |
+
this.socket.onmessage = (event) => {
|
| 829 |
+
try {
|
| 830 |
+
const data = JSON.parse(event.data);
|
| 831 |
+
this.emit('message', data);
|
| 832 |
+
} catch (error) {
|
| 833 |
+
console.error('Failed to parse WebSocket message:', error);
|
| 834 |
+
}
|
| 835 |
+
};
|
| 836 |
+
|
| 837 |
+
this.socket.onclose = () => {
|
| 838 |
+
console.log('WebSocket disconnected');
|
| 839 |
+
this.stopHeartbeat();
|
| 840 |
+
this.emit('disconnected');
|
| 841 |
+
this.handleReconnect();
|
| 842 |
+
};
|
| 843 |
+
|
| 844 |
+
this.socket.onerror = (error) => {
|
| 845 |
+
console.error('WebSocket error:', error);
|
| 846 |
+
this.emit('error', error);
|
| 847 |
+
};
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
// Send message via WebSocket
|
| 851 |
+
send(data) {
|
| 852 |
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
| 853 |
+
this.socket.send(JSON.stringify(data));
|
| 854 |
+
} else {
|
| 855 |
+
// Queue message for later sending
|
| 856 |
+
this.messageQueue.push(data);
|
| 857 |
+
}
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
// Handle reconnection logic
|
| 861 |
+
handleReconnect() {
|
| 862 |
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
| 863 |
+
this.reconnectAttempts++;
|
| 864 |
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
| 865 |
+
|
| 866 |
+
console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
| 867 |
+
|
| 868 |
+
setTimeout(() => {
|
| 869 |
+
this.connect();
|
| 870 |
+
}, delay);
|
| 871 |
+
} else {
|
| 872 |
+
console.error('Max reconnection attempts reached');
|
| 873 |
+
this.emit('maxReconnectReached');
|
| 874 |
+
}
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
// Start heartbeat to keep connection alive
|
| 878 |
+
startHeartbeat() {
|
| 879 |
+
this.heartbeatInterval = setInterval(() => {
|
| 880 |
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
| 881 |
+
this.send({ type: 'ping' });
|
| 882 |
+
}
|
| 883 |
+
}, 30000); // Send heartbeat every 30 seconds
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
// Stop heartbeat
|
| 887 |
+
stopHeartbeat() {
|
| 888 |
+
if (this.heartbeatInterval) {
|
| 889 |
+
clearInterval(this.heartbeatInterval);
|
| 890 |
+
this.heartbeatInterval = null;
|
| 891 |
+
}
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
// Process queued messages
|
| 895 |
+
processMessageQueue() {
|
| 896 |
+
while (this.messageQueue.length > 0) {
|
| 897 |
+
const message = this.messageQueue.shift();
|
| 898 |
+
this.send(message);
|
| 899 |
+
}
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
// Event emitter functionality
|
| 903 |
+
on(event, callback) {
|
| 904 |
+
if (!this.listeners.has(event)) {
|
| 905 |
+
this.listeners.set(event, []);
|
| 906 |
+
}
|
| 907 |
+
this.listeners.get(event).push(callback);
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
off(event, callback) {
|
| 911 |
+
const eventListeners = this.listeners.get(event);
|
| 912 |
+
if (eventListeners) {
|
| 913 |
+
const index = eventListeners.indexOf(callback);
|
| 914 |
+
if (index > -1) {
|
| 915 |
+
eventListeners.splice(index, 1);
|
| 916 |
+
}
|
| 917 |
+
}
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
emit(event, data) {
|
| 921 |
+
const eventListeners = this.listeners.get(event);
|
| 922 |
+
if (eventListeners) {
|
| 923 |
+
eventListeners.forEach(callback => callback(data));
|
| 924 |
+
}
|
| 925 |
+
}
|
| 926 |
+
|
| 927 |
+
// Disconnect WebSocket
|
| 928 |
+
disconnect() {
|
| 929 |
+
this.stopHeartbeat();
|
| 930 |
+
if (this.socket) {
|
| 931 |
+
this.socket.close();
|
| 932 |
+
this.socket = null;
|
| 933 |
+
}
|
| 934 |
+
}
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
// Security utilities
|
| 938 |
+
class SecurityUtils {
|
| 939 |
+
// Sanitize HTML content
|
| 940 |
+
static sanitizeHTML(html) {
|
| 941 |
+
const div = document.createElement('div');
|
| 942 |
+
div.textContent = html;
|
| 943 |
+
return div.innerHTML;
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
// Validate message content
|
| 947 |
+
static validateMessage(content) {
|
| 948 |
+
if (typeof content !== 'string') return false;
|
| 949 |
+
if (content.length === 0 || content.length > 10000) return false;
|
| 950 |
+
return true;
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
// Rate limiting
|
| 954 |
+
static createRateLimiter(limit, window) {
|
| 955 |
+
const requests = new Map();
|
| 956 |
|
| 957 |
+
return function(identifier) {
|
| 958 |
+
const now = Date.now();
|
| 959 |
+
const windowStart = now - window;
|
| 960 |
+
|
| 961 |
+
// Clean old requests
|
| 962 |
+
for (const [key, timestamps] of requests.entries()) {
|
| 963 |
+
requests.set(key, timestamps.filter(time => time > windowStart));
|
| 964 |
+
if (requests.get(key).length === 0) {
|
| 965 |
+
requests.delete(key);
|
| 966 |
+
}
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
// Check current requests
|
| 970 |
+
const userRequests = requests.get(identifier) || [];
|
| 971 |
+
if (userRequests.length >= limit) {
|
| 972 |
+
return false; // Rate limited
|
| 973 |
+
}
|
| 974 |
+
|
| 975 |
+
// Add current request
|
| 976 |
+
userRequests.push(now);
|
| 977 |
+
requests.set(identifier, userRequests);
|
| 978 |
+
return true; // Allowed
|
| 979 |
+
};
|
| 980 |
}
|
| 981 |
}
|
| 982 |
|
| 983 |
+
// Initialize performance monitoring
|
| 984 |
+
const performanceMonitor = new PerformanceMonitor();
|
| 985 |
+
performanceMonitor.observeNetworkTiming();
|
| 986 |
+
|
| 987 |
+
// Rate limiter for message sending (max 10 messages per minute)
|
| 988 |
+
const messageLimiter = SecurityUtils.createRateLimiter(10, 60000);
|
| 989 |
+
|
| 990 |
+
// Enhanced error reporting
|
| 991 |
+
class ErrorReporter {
|
| 992 |
+
constructor() {
|
| 993 |
+
this.errors = [];
|
| 994 |
+
this.maxErrors = 100;
|
| 995 |
+
this.setupGlobalErrorHandling();
|
| 996 |
}
|
|
|
|
| 997 |
|
| 998 |
+
setupGlobalErrorHandling() {
|
| 999 |
+
// Catch unhandled errors
|
| 1000 |
+
window.addEventListener('error', (event) => {
|
| 1001 |
+
this.reportError({
|
| 1002 |
+
type: 'javascript',
|
| 1003 |
+
message: event.message,
|
| 1004 |
+
filename: event.filename,
|
| 1005 |
+
line: event.lineno,
|
| 1006 |
+
column: event.colno,
|
| 1007 |
+
stack: event.error?.stack,
|
| 1008 |
+
timestamp: Date.now()
|
| 1009 |
+
});
|
| 1010 |
+
});
|
| 1011 |
+
|
| 1012 |
+
// Catch unhandled promise rejections
|
| 1013 |
+
window.addEventListener('unhandledrejection', (event) => {
|
| 1014 |
+
this.reportError({
|
| 1015 |
+
type: 'promise',
|
| 1016 |
+
message: event.reason?.message || 'Unhandled promise rejection',
|
| 1017 |
+
stack: event.reason?.stack,
|
| 1018 |
+
timestamp: Date.now()
|
| 1019 |
+
});
|
| 1020 |
+
});
|
| 1021 |
+
}
|
| 1022 |
+
|
| 1023 |
+
reportError(error) {
|
| 1024 |
+
console.error('Error reported:', error);
|
| 1025 |
+
|
| 1026 |
+
this.errors.push(error);
|
| 1027 |
+
|
| 1028 |
+
// Keep only recent errors
|
| 1029 |
+
if (this.errors.length > this.maxErrors) {
|
| 1030 |
+
this.errors.shift();
|
| 1031 |
}
|
| 1032 |
+
|
| 1033 |
+
// Send to analytics service if available
|
| 1034 |
+
this.sendToAnalytics(error);
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
sendToAnalytics(error) {
|
| 1038 |
+
// Placeholder for analytics integration
|
| 1039 |
+
// In production, you might send to services like Sentry, LogRocket, etc.
|
| 1040 |
+
if (window.gtag) {
|
| 1041 |
+
window.gtag('event', 'exception', {
|
| 1042 |
+
description: error.message,
|
| 1043 |
+
fatal: false
|
| 1044 |
+
});
|
| 1045 |
+
}
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
getRecentErrors() {
|
| 1049 |
+
return this.errors.slice(-10); // Return last 10 errors
|
| 1050 |
}
|
| 1051 |
+
}
|
| 1052 |
+
|
| 1053 |
+
// Initialize error reporter
|
| 1054 |
+
const errorReporter = new ErrorReporter();
|
| 1055 |
+
|
| 1056 |
+
// Accessibility helpers
|
| 1057 |
+
class AccessibilityManager {
|
| 1058 |
+
constructor() {
|
| 1059 |
+
this.setupKeyboardNavigation();
|
| 1060 |
+
this.setupScreenReaderSupport();
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
setupKeyboardNavigation() {
|
| 1064 |
+
document.addEventListener('keydown', (event) => {
|
| 1065 |
+
// Handle global keyboard shortcuts
|
| 1066 |
+
if (event.ctrlKey || event.metaKey) {
|
| 1067 |
+
switch (event.key) {
|
| 1068 |
+
case 'n':
|
| 1069 |
+
event.preventDefault();
|
| 1070 |
+
chatApp.createNewConversation();
|
| 1071 |
+
break;
|
| 1072 |
+
case '/':
|
| 1073 |
+
event.preventDefault();
|
| 1074 |
+
chatApp.elements.composer?.focus();
|
| 1075 |
+
break;
|
| 1076 |
+
}
|
| 1077 |
+
}
|
| 1078 |
+
|
| 1079 |
+
// Escape key to cancel current operation
|
| 1080 |
+
if (event.key === 'Escape') {
|
| 1081 |
+
chatApp.cancelCurrentMessage();
|
| 1082 |
+
}
|
| 1083 |
+
});
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
setupScreenReaderSupport() {
|
| 1087 |
+
// Announce new messages to screen readers
|
| 1088 |
+
const announcer = document.createElement('div');
|
| 1089 |
+
announcer.setAttribute('aria-live', 'polite');
|
| 1090 |
+
announcer.setAttribute('aria-atomic', 'true');
|
| 1091 |
+
announcer.className = 'sr-only';
|
| 1092 |
+
document.body.appendChild(announcer);
|
| 1093 |
+
|
| 1094 |
+
this.announcer = announcer;
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
announceMessage(message) {
|
| 1098 |
+
if (this.announcer) {
|
| 1099 |
+
this.announcer.textContent = `${message.role === 'user' ? 'You' : 'Assistant'}: ${message.content}`;
|
| 1100 |
+
}
|
| 1101 |
+
}
|
| 1102 |
+
}
|
| 1103 |
+
|
| 1104 |
+
// Initialize accessibility manager
|
| 1105 |
+
const accessibilityManager = new AccessibilityManager();
|
| 1106 |
|
| 1107 |
+
// Export for debugging and testing
|
| 1108 |
+
window.chatDebug = {
|
| 1109 |
+
chatApp,
|
| 1110 |
+
chatState,
|
| 1111 |
+
apiManager,
|
| 1112 |
+
messageRenderer,
|
| 1113 |
+
performanceMonitor,
|
| 1114 |
+
errorReporter,
|
| 1115 |
+
accessibilityManager,
|
| 1116 |
+
SecurityUtils
|
| 1117 |
+
};
|
public/i18n.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Enhanced Internationalization (i18n) utility for the AI Chat application
|
| 2 |
+
|
| 3 |
+
class I18n {
|
| 4 |
+
constructor() {
|
| 5 |
+
this.currentLocale = 'en';
|
| 6 |
+
this.translations = {};
|
| 7 |
+
this.fallbackLocale = 'en';
|
| 8 |
+
this.loadedLocales = new Set();
|
| 9 |
+
this.cache = new Map();
|
| 10 |
+
this.observers = [];
|
| 11 |
+
this.pluralRules = new Map();
|
| 12 |
+
this.dateTimeFormats = new Map();
|
| 13 |
+
this.numberFormats = new Map();
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// Load translations for a specific locale with caching
|
| 17 |
+
async loadLocale(locale) {
|
| 18 |
+
if (this.loadedLocales.has(locale)) {
|
| 19 |
+
return this.translations[locale];
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
try {
|
| 23 |
+
const response = await fetch(`../locales/${locale}.json`);
|
| 24 |
+
if (!response.ok) {
|
| 25 |
+
throw new Error(`Failed to load locale ${locale}: ${response.status}`);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
const translations = await response.json();
|
| 29 |
+
this.translations[locale] = translations;
|
| 30 |
+
this.loadedLocales.add(locale);
|
| 31 |
+
|
| 32 |
+
// Cache commonly used translations
|
| 33 |
+
this.cacheTranslations(locale, translations);
|
| 34 |
+
|
| 35 |
+
// Setup locale-specific formatters
|
| 36 |
+
this.setupLocaleFormatters(locale);
|
| 37 |
+
|
| 38 |
+
return translations;
|
| 39 |
+
} catch (error) {
|
| 40 |
+
console.warn(`Could not load locale ${locale}:`, error);
|
| 41 |
+
if (locale !== this.fallbackLocale) {
|
| 42 |
+
return await this.loadLocale(this.fallbackLocale);
|
| 43 |
+
}
|
| 44 |
+
throw error;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Cache frequently used translations
|
| 49 |
+
cacheTranslations(locale, translations) {
|
| 50 |
+
const commonKeys = [
|
| 51 |
+
'app.title',
|
| 52 |
+
'chat.placeholder',
|
| 53 |
+
'chat.send',
|
| 54 |
+
'ui.loading',
|
| 55 |
+
'ui.error',
|
| 56 |
+
'ui.retry'
|
| 57 |
+
];
|
| 58 |
+
|
| 59 |
+
commonKeys.forEach(key => {
|
| 60 |
+
const value = this.getNestedValue(translations, key);
|
| 61 |
+
if (value !== undefined) {
|
| 62 |
+
this.cache.set(`${locale}:${key}`, value);
|
| 63 |
+
}
|
| 64 |
+
});
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Setup locale-specific formatters
|
| 68 |
+
setupLocaleFormatters(locale) {
|
| 69 |
+
try {
|
| 70 |
+
// Date/time formatters
|
| 71 |
+
this.dateTimeFormats.set(locale, {
|
| 72 |
+
short: new Intl.DateTimeFormat(locale, {
|
| 73 |
+
timeStyle: 'short'
|
| 74 |
+
}),
|
| 75 |
+
medium: new Intl.DateTimeFormat(locale, {
|
| 76 |
+
dateStyle: 'medium',
|
| 77 |
+
timeStyle: 'short'
|
| 78 |
+
}),
|
| 79 |
+
relative: new Intl.RelativeTimeFormat(locale, {
|
| 80 |
+
numeric: 'auto'
|
| 81 |
+
})
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
// Number formatters
|
| 85 |
+
this.numberFormats.set(locale, {
|
| 86 |
+
decimal: new Intl.NumberFormat(locale),
|
| 87 |
+
percent: new Intl.NumberFormat(locale, {
|
| 88 |
+
style: 'percent'
|
| 89 |
+
}),
|
| 90 |
+
currency: new Intl.NumberFormat(locale, {
|
| 91 |
+
style: 'currency',
|
| 92 |
+
currency: 'USD'
|
| 93 |
+
})
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
// Plural rules
|
| 97 |
+
this.pluralRules.set(locale, new Intl.PluralRules(locale));
|
| 98 |
+
|
| 99 |
+
} catch (error) {
|
| 100 |
+
console.warn(`Failed to setup formatters for ${locale}:`, error);
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// Set the current locale with enhanced features
|
| 105 |
+
async setLocale(locale) {
|
| 106 |
+
const previousLocale = this.currentLocale;
|
| 107 |
+
|
| 108 |
+
if (!this.translations[locale]) {
|
| 109 |
+
await this.loadLocale(locale);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
this.currentLocale = locale;
|
| 113 |
+
|
| 114 |
+
// Update document language
|
| 115 |
+
document.documentElement.lang = locale;
|
| 116 |
+
|
| 117 |
+
// Update direction for RTL languages
|
| 118 |
+
const rtlLanguages = ['ar', 'he', 'fa', 'ur'];
|
| 119 |
+
document.documentElement.dir = rtlLanguages.includes(locale) ? 'rtl' : 'ltr';
|
| 120 |
+
|
| 121 |
+
// Store preference
|
| 122 |
+
localStorage.setItem('i18n-locale', locale);
|
| 123 |
+
|
| 124 |
+
// Notify observers
|
| 125 |
+
this.notifyObservers(locale, previousLocale);
|
| 126 |
+
|
| 127 |
+
// Trigger global locale change event
|
| 128 |
+
document.dispatchEvent(new CustomEvent('localeChanged', {
|
| 129 |
+
detail: {
|
| 130 |
+
locale: this.currentLocale,
|
| 131 |
+
previousLocale
|
| 132 |
+
}
|
| 133 |
+
}));
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// Enhanced translation with context and pluralization
|
| 137 |
+
t(key, params = {}) {
|
| 138 |
+
const cacheKey = `${this.currentLocale}:${key}`;
|
| 139 |
+
|
| 140 |
+
// Check cache first
|
| 141 |
+
if (this.cache.has(cacheKey) && Object.keys(params).length === 0) {
|
| 142 |
+
return this.cache.get(cacheKey);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
let translation = this.getNestedValue(
|
| 146 |
+
this.translations[this.currentLocale],
|
| 147 |
+
key
|
| 148 |
+
);
|
| 149 |
+
|
| 150 |
+
if (translation === undefined) {
|
| 151 |
+
// Fallback to default locale
|
| 152 |
+
translation = this.getNestedValue(
|
| 153 |
+
this.translations[this.fallbackLocale],
|
| 154 |
+
key
|
| 155 |
+
);
|
| 156 |
+
|
| 157 |
+
if (translation === undefined) {
|
| 158 |
+
console.warn(`Translation missing for key: ${key}`);
|
| 159 |
+
return key;
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// Handle pluralization
|
| 164 |
+
if (typeof translation === 'object' && params.count !== undefined) {
|
| 165 |
+
translation = this.handlePluralization(translation, params.count);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
// Handle interpolation
|
| 169 |
+
const result = this.interpolate(translation, params);
|
| 170 |
+
|
| 171 |
+
// Cache result if no parameters
|
| 172 |
+
if (Object.keys(params).length === 0) {
|
| 173 |
+
this.cache.set(cacheKey, result);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
return result;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// Handle pluralization rules
|
| 180 |
+
handlePluralization(translations, count) {
|
| 181 |
+
const rules = this.pluralRules.get(this.currentLocale);
|
| 182 |
+
if (!rules) return translations.other || '';
|
| 183 |
+
|
| 184 |
+
const rule = rules.select(count);
|
| 185 |
+
|
| 186 |
+
return translations[rule] ||
|
| 187 |
+
translations.other ||
|
| 188 |
+
translations.one ||
|
| 189 |
+
'';
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// Enhanced interpolation with formatting
|
| 193 |
+
interpolate(text, params) {
|
| 194 |
+
if (typeof text !== 'string') return text;
|
| 195 |
+
|
| 196 |
+
return text.replace(/\{\{(\w+)(?::(\w+))?\}\}/g, (match, key, format) => {
|
| 197 |
+
const value = params[key];
|
| 198 |
+
|
| 199 |
+
if (value === undefined) return match;
|
| 200 |
+
|
| 201 |
+
// Apply formatting if specified
|
| 202 |
+
if (format) {
|
| 203 |
+
return this.formatValue(value, format);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
return value;
|
| 207 |
+
});
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// Format values based on type
|
| 211 |
+
formatValue(value, format) {
|
| 212 |
+
const formatters = this.numberFormats.get(this.currentLocale);
|
| 213 |
+
const dateFormatters = this.dateTimeFormats.get(this.currentLocale);
|
| 214 |
+
|
| 215 |
+
switch (format) {
|
| 216 |
+
case 'number':
|
| 217 |
+
return formatters?.decimal.format(value) || value;
|
| 218 |
+
case 'percent':
|
| 219 |
+
return formatters?.percent.format(value) || value;
|
| 220 |
+
case 'currency':
|
| 221 |
+
return formatters?.currency.format(value) || value;
|
| 222 |
+
case 'date':
|
| 223 |
+
return dateFormatters?.medium.format(new Date(value)) || value;
|
| 224 |
+
case 'time':
|
| 225 |
+
return dateFormatters?.short.format(new Date(value)) || value;
|
| 226 |
+
case 'relative':
|
| 227 |
+
const now = Date.now();
|
| 228 |
+
const diff = Math.floor((value - now) / 1000);
|
| 229 |
+
return dateFormatters?.relative.format(diff, 'second') || value;
|
| 230 |
+
case 'uppercase':
|
| 231 |
+
return String(value).toUpperCase();
|
| 232 |
+
case 'lowercase':
|
| 233 |
+
return String(value).toLowerCase();
|
| 234 |
+
case 'capitalize':
|
| 235 |
+
return String(value).charAt(0).toUpperCase() + String(value).slice(1);
|
| 236 |
+
default:
|
| 237 |
+
return value;
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
// Get nested value from object using dot notation
|
| 242 |
+
getNestedValue(obj, path) {
|
| 243 |
+
return path.split('.').reduce((current, key) => {
|
| 244 |
+
return current && current[key] !== undefined ? current[key] : undefined;
|
| 245 |
+
}, obj);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
// Add observer for locale changes
|
| 249 |
+
addObserver(callback) {
|
| 250 |
+
this.observers.push(callback);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
// Remove observer
|
| 254 |
+
removeObserver(callback) {
|
| 255 |
+
const index = this.observers.indexOf(callback);
|
| 256 |
+
if (index > -1) {
|
| 257 |
+
this.observers.splice(index, 1);
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
// Notify all observers of locale change
|
| 262 |
+
notifyObservers(newLocale, oldLocale) {
|
| 263 |
+
this.observers.forEach(callback => {
|
| 264 |
+
try {
|
| 265 |
+
callback(newLocale, oldLocale);
|
| 266 |
+
} catch (error) {
|
| 267 |
+
console.error('Error in i18n observer:', error);
|
| 268 |
+
}
|
| 269 |
+
});
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
// Get current locale
|
| 273 |
+
getCurrentLocale() {
|
| 274 |
+
return this.currentLocale;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
// Get available locales
|
| 278 |
+
getAvailableLocales() {
|
| 279 |
+
return Array.from(this.loadedLocales);
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
// Get all loaded translations (for debugging)
|
| 283 |
+
getAllTranslations() {
|
| 284 |
+
return this.translations;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// Initialize the i18n system with enhanced detection
|
| 288 |
+
async init(locale = null) {
|
| 289 |
+
try {
|
| 290 |
+
// Detect locale in order of preference
|
| 291 |
+
const detectedLocale = locale ||
|
| 292 |
+
localStorage.getItem('i18n-locale') ||
|
| 293 |
+
this.detectBrowserLocale() ||
|
| 294 |
+
this.fallbackLocale;
|
| 295 |
+
|
| 296 |
+
await this.loadLocale(detectedLocale);
|
| 297 |
+
await this.setLocale(detectedLocale);
|
| 298 |
+
|
| 299 |
+
console.log(`i18n initialized with locale: ${detectedLocale}`);
|
| 300 |
+
return true;
|
| 301 |
+
} catch (error) {
|
| 302 |
+
console.error('Failed to initialize i18n:', error);
|
| 303 |
+
return false;
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// Detect browser locale
|
| 308 |
+
detectBrowserLocale() {
|
| 309 |
+
if (navigator.languages && navigator.languages.length) {
|
| 310 |
+
// Check each preferred language
|
| 311 |
+
for (const lang of navigator.languages) {
|
| 312 |
+
const shortLang = lang.split('-')[0];
|
| 313 |
+
// Return first supported language
|
| 314 |
+
const supportedLocales = ['en', 'es', 'fr', 'de', 'cs'];
|
| 315 |
+
if (supportedLocales.includes(shortLang)) {
|
| 316 |
+
return shortLang;
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
return navigator.language?.split('-')[0] || 'en';
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
// Preload multiple locales
|
| 325 |
+
async preloadLocales(locales) {
|
| 326 |
+
const promises = locales.map(locale => this.loadLocale(locale));
|
| 327 |
+
await Promise.allSettled(promises);
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
// Clear cache
|
| 331 |
+
clearCache() {
|
| 332 |
+
this.cache.clear();
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
// Get memory usage statistics
|
| 336 |
+
getStats() {
|
| 337 |
+
return {
|
| 338 |
+
loadedLocales: this.loadedLocales.size,
|
| 339 |
+
cachedTranslations: this.cache.size,
|
| 340 |
+
observers: this.observers.length,
|
| 341 |
+
currentLocale: this.currentLocale,
|
| 342 |
+
memoryUsage: JSON.stringify(this.translations).length
|
| 343 |
+
};
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
// Enhanced translation helper functions
|
| 348 |
+
class TranslationHelpers {
|
| 349 |
+
static createKeyExtractor(prefix = '') {
|
| 350 |
+
return (key) => prefix ? `${prefix}.${key}` : key;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
static createScopedTranslator(i18n, scope) {
|
| 354 |
+
return (key, params) => i18n.t(`${scope}.${key}`, params);
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
static validateTranslations(translations, requiredKeys) {
|
| 358 |
+
const missing = [];
|
| 359 |
+
requiredKeys.forEach(key => {
|
| 360 |
+
if (!this.hasKey(translations, key)) {
|
| 361 |
+
missing.push(key);
|
| 362 |
+
}
|
| 363 |
+
});
|
| 364 |
+
return missing;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
static hasKey(obj, path) {
|
| 368 |
+
return path.split('.').reduce((current, key) => {
|
| 369 |
+
return current && current[key] !== undefined ? current[key] : undefined;
|
| 370 |
+
}, obj) !== undefined;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
static flattenTranslations(obj, prefix = '') {
|
| 374 |
+
const flattened = {};
|
| 375 |
+
for (const key in obj) {
|
| 376 |
+
const newKey = prefix ? `${prefix}.${key}` : key;
|
| 377 |
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
| 378 |
+
Object.assign(flattened, this.flattenTranslations(obj[key], newKey));
|
| 379 |
+
} else {
|
| 380 |
+
flattened[newKey] = obj[key];
|
| 381 |
+
}
|
| 382 |
+
}
|
| 383 |
+
return flattened;
|
| 384 |
+
}
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
// Create global instance
|
| 388 |
+
const i18n = new I18n();
|
| 389 |
+
|
| 390 |
+
// Helper function for easy access with enhanced features
|
| 391 |
+
function t(key, params = {}) {
|
| 392 |
+
return i18n.t(key, params);
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
// Additional helper functions
|
| 396 |
+
function tc(key, count, params = {}) {
|
| 397 |
+
return i18n.t(key, { ...params, count });
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
function td(key, date, params = {}) {
|
| 401 |
+
return i18n.t(key, { ...params, date });
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
function tn(key, number, params = {}) {
|
| 405 |
+
return i18n.t(key, { ...params, number });
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
// Export for use in other modules
|
| 409 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 410 |
+
module.exports = {
|
| 411 |
+
I18n,
|
| 412 |
+
i18n,
|
| 413 |
+
t,
|
| 414 |
+
tc,
|
| 415 |
+
td,
|
| 416 |
+
tn,
|
| 417 |
+
TranslationHelpers
|
| 418 |
+
};
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// Export to global scope for debugging
|
| 422 |
+
window.i18nDebug = {
|
| 423 |
+
i18n,
|
| 424 |
+
TranslationHelpers,
|
| 425 |
+
getStats: () => i18n.getStats(),
|
| 426 |
+
clearCache: () => i18n.clearCache(),
|
| 427 |
+
getAllTranslations: () => i18n.getAllTranslations()
|
| 428 |
+
};
|
public/index.html
CHANGED
|
@@ -1,27 +1,396 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
<head>
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
</head>
|
| 9 |
-
<body>
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
</div>
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
</div>
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
</div>
|
|
|
|
| 23 |
</div>
|
|
|
|
|
|
|
| 24 |
</div>
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
</body>
|
| 27 |
-
</html>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" class="h-full">
|
| 3 |
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>AI Chat — Tailwind (Dribbble-inspired)</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script>
|
| 9 |
+
tailwind.config = {
|
| 10 |
+
darkMode: 'class',
|
| 11 |
+
theme: {
|
| 12 |
+
extend: {
|
| 13 |
+
colors: {
|
| 14 |
+
border: 'hsl(240 5% 84%)',
|
| 15 |
+
background: 'hsl(0 0% 100%)',
|
| 16 |
+
foreground: 'hsl(222 47% 11%)',
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
</script>
|
| 22 |
+
<style>
|
| 23 |
+
.scrollbar-thin{scrollbar-width:thin} .scrollbar-thin::-webkit-scrollbar{height:8px;width:8px} .scrollbar-thin::-webkit-scrollbar-thumb{border-radius:9999px;background-color:#c7c7d0} .scrollbar-thin::-webkit-scrollbar-track{background-color:transparent}
|
| 24 |
+
</style>
|
| 25 |
</head>
|
| 26 |
+
<body class="h-full bg-white text-slate-900 antialiased dark:bg-zinc-900 dark:text-zinc-100">
|
| 27 |
+
<div id="app" class="flex h-dvh w-full flex-col">
|
| 28 |
+
<!-- Top Bar -->
|
| 29 |
+
<header class="flex items-center gap-2 border-b border-zinc-200 px-3 py-2 dark:border-zinc-800">
|
| 30 |
+
<button id="btn-open-left" class="lg:hidden inline-flex h-9 w-9 items-center justify-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800" aria-label="Open left panel">
|
| 31 |
+
<!-- panel-left -->
|
| 32 |
+
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4v16"/><path d="M7 4v16"/><path d="M11 6h10"/><path d="M11 12h10"/><path d="M11 18h10"/></svg>
|
| 33 |
+
</button>
|
| 34 |
+
<div class="flex items-center gap-2">
|
| 35 |
+
<div class="h-7 w-7 rounded-xl bg-gradient-to-tr from-indigo-500/80 to-indigo-600"></div>
|
| 36 |
+
<div class="text-sm font-semibold tracking-tight">Nova AI</div>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="mx-2 hidden h-6 w-px bg-zinc-200 lg:block dark:bg-zinc-800"></div>
|
| 39 |
+
<!-- Search -->
|
| 40 |
+
<div class="relative hidden max-w-lg flex-1 lg:block">
|
| 41 |
+
<svg class="pointer-events-none absolute left-3 top-2.5 h-4 w-4 text-zinc-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
| 42 |
+
<input type="text" placeholder="Search messages, files, prompts…" class="w-full rounded-md border border-zinc-200 bg-white pl-9 pr-3 py-2 text-sm outline-none ring-indigo-500 transition focus:border-indigo-500 dark:border-zinc-800 dark:bg-zinc-900" />
|
| 43 |
+
</div>
|
| 44 |
+
<div class="ml-auto flex items-center gap-1">
|
| 45 |
+
<!-- Model Dropdown -->
|
| 46 |
+
<div class="relative" id="model-dd">
|
| 47 |
+
<button class="inline-flex items-center gap-2 rounded-md border border-zinc-200 px-3 py-1.5 text-sm hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800" data-dd-trigger>
|
| 48 |
+
Qwen 3 Coder
|
| 49 |
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
| 50 |
+
</button>
|
| 51 |
+
<div class="absolute right-0 z-40 mt-2 hidden w-56 overflow-hidden rounded-md border border-zinc-200 bg-white p-1 text-sm shadow-lg dark:border-zinc-800 dark:bg-zinc-900" data-dd-menu>
|
| 52 |
+
<div class="px-2 py-1 text-xs font-medium text-zinc-500">Models</div>
|
| 53 |
+
<button class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800">GPT-4.1</button>
|
| 54 |
+
<button class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800">Claude 3.5 Sonnet</button>
|
| 55 |
+
<button class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800">Qwen 3 Coder</button>
|
| 56 |
+
<div class="my-1 h-px bg-zinc-200 dark:bg-zinc-800"></div>
|
| 57 |
+
<button class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
| 58 |
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13v-2"/><path d="M14 13v-2"/><path d="M7 22v-4a4 4 0 0 1 4-4h2a4 4 0 0 1 4 4v4"/><circle cx="12" cy="7" r="4"/></svg>
|
| 59 |
+
Manage keys
|
| 60 |
+
</button>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
<!-- Theme toggle -->
|
| 64 |
+
<button id="btn-theme" class="inline-flex h-9 w-9 items-center justify-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800" aria-label="Toggle theme">
|
| 65 |
+
<span class="sr-only">Toggle theme</span>
|
| 66 |
+
<svg id="icon-sun" class="h-5 w-5 hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
|
| 67 |
+
<svg id="icon-moon" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
| 68 |
+
</button>
|
| 69 |
+
<!-- Right panel button (mobile) -->
|
| 70 |
+
<button id="btn-open-right" class="lg:hidden inline-flex h-9 w-9 items-center justify-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800" aria-label="Open right panel">
|
| 71 |
+
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M15 3v18"/></svg>
|
| 72 |
+
</button>
|
| 73 |
+
<button class="hidden sm:inline-flex items-center gap-2 rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-700"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>New</button>
|
| 74 |
+
<button class="hidden sm:inline-flex items-center gap-2 rounded-md border border-zinc-200 px-3 py-1.5 text-sm hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v2"/><path d="M12 19v2"/><rect x="4" y="7" width="16" height="10" rx="2"/></svg>Settings</button>
|
| 75 |
+
</div>
|
| 76 |
+
</header>
|
| 77 |
+
|
| 78 |
+
<div class="flex min-h-0 flex-1">
|
| 79 |
+
<!-- Left Sidebar (desktop) -->
|
| 80 |
+
<aside class="hidden w-72 flex-col border-r border-zinc-200 lg:flex dark:border-zinc-800" id="left-desktop">
|
| 81 |
+
<div class="p-3">
|
| 82 |
+
<button class="inline-flex w-full items-center gap-2 rounded-md border border-zinc-200 px-3 py-2 text-sm hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>New chat</button>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="px-3 pb-2">
|
| 85 |
+
<div class="relative">
|
| 86 |
+
<svg class="absolute left-2 top-2.5 h-4 w-4 text-zinc-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
| 87 |
+
<input class="w-full rounded-md border border-zinc-200 bg-white pl-8 pr-3 py-2 text-sm outline-none ring-indigo-500 transition focus:border-indigo-500 dark:border-zinc-800 dark:bg-zinc-900" placeholder="Search chats" />
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="h-px bg-zinc-200 dark:bg-zinc-800"></div>
|
| 91 |
+
<div class="flex-1 overflow-y-auto p-2 scrollbar-thin">
|
| 92 |
+
<!-- chat list -->
|
| 93 |
+
<button class="group flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
| 94 |
+
<svg class="h-4 w-4 text-zinc-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12c2 0 4 1 5 3-1 2-3 3-5 3s-4-1-5-3c1-2 3-3 5-3Z"/><circle cx="12" cy="8" r="3"/></svg>
|
| 95 |
+
<div class="min-w-0 flex-1"><div class="truncate text-sm font-medium">Build Tailwind Navbar</div><div class="truncate text-xs text-zinc-500">Today</div></div>
|
| 96 |
+
<svg class="h-4 w-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
|
| 97 |
+
</button>
|
| 98 |
+
<button class="group flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
| 99 |
+
<svg class="h-4 w-4 text-zinc-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12c2 0 4 1 5 3-1 2-3 3-5 3s-4-1-5-3c1-2 3-3 5-3Z"/><circle cx="12" cy="8" r="3"/></svg>
|
| 100 |
+
<div class="min-w-0 flex-1"><div class="truncate text-sm font-medium">Optimize React hooks</div><div class="truncate text-xs text-zinc-500">Today</div></div>
|
| 101 |
+
<svg class="h-4 w-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
|
| 102 |
+
</button>
|
| 103 |
+
<button class="group flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
| 104 |
+
<svg class="h-4 w-4 text-zinc-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12c2 0 4 1 5 3-1 2-3 3-5 3s-4-1-5-3c1-2 3-3 5-3Z"/><circle cx="12" cy="8" r="3"/></svg>
|
| 105 |
+
<div class="min-w-0 flex-1"><div class="truncate text-sm font-medium">Write SQL for analytics</div><div class="truncate text-xs text-zinc-500">Yesterday</div></div>
|
| 106 |
+
<svg class="h-4 w-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
|
| 107 |
+
</button>
|
| 108 |
+
</div>
|
| 109 |
+
<div class="h-px bg-zinc-200 dark:bg-zinc-800"></div>
|
| 110 |
+
<div class="flex items-center justify-between p-3">
|
| 111 |
+
<div class="flex items-center gap-2">
|
| 112 |
+
<div class="h-8 w-8 overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-700"></div>
|
| 113 |
+
<div>
|
| 114 |
+
<div class="text-sm font-medium">You</div>
|
| 115 |
+
<div class="text-xs text-zinc-500">Free plan</div>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
<button class="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800" aria-label="Account">
|
| 119 |
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v2"/><path d="M12 19v2"/><rect x="4" y="7" width="16" height="10" rx="2"/></svg>
|
| 120 |
+
</button>
|
| 121 |
+
</div>
|
| 122 |
+
</aside>
|
| 123 |
+
|
| 124 |
+
<!-- Mobile Left Sheet -->
|
| 125 |
+
<aside id="left-sheet" class="fixed inset-y-0 left-0 z-50 w-72 -translate-x-full transform border-r border-zinc-200 bg-white transition-transform duration-200 ease-out dark:border-zinc-800 dark:bg-zinc-900 lg:hidden">
|
| 126 |
+
<div class="flex h-full flex-col">
|
| 127 |
+
<div class="flex items-center justify-between border-b border-zinc-200 p-3 dark:border-zinc-800">
|
| 128 |
+
<div class="text-sm font-semibold">Conversations</div>
|
| 129 |
+
<button id="btn-close-left" class="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800" aria-label="Close">
|
| 130 |
+
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
| 131 |
+
</button>
|
| 132 |
+
</div>
|
| 133 |
+
<div class="flex-1 overflow-y-auto p-2 scrollbar-thin">
|
| 134 |
+
<!-- reuse simple list -->
|
| 135 |
+
<button class="group flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
| 136 |
+
<svg class="h-4 w-4 text-zinc-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12c2 0 4 1 5 3-1 2-3 3-5 3s-4-1-5-3c1-2 3-3 5-3Z"/><circle cx="12" cy="8" r="3"/></svg>
|
| 137 |
+
<div class="min-w-0 flex-1"><div class="truncate text-sm font-medium">Design prompt library</div><div class="truncate text-xs text-zinc-500">Today</div></div>
|
| 138 |
+
<svg class="h-4 w-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
|
| 139 |
+
</button>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
</aside>
|
| 143 |
+
<div id="left-overlay" class="fixed inset-0 z-40 hidden bg-black/40 opacity-0 transition-opacity lg:hidden"></div>
|
| 144 |
+
|
| 145 |
+
<!-- Main Column -->
|
| 146 |
+
<main class="flex min-w-0 flex-1 flex-col">
|
| 147 |
+
<!-- Chat header -->
|
| 148 |
+
<div class="flex items-center gap-3 border-b border-zinc-200 px-4 py-3 sm:px-6 dark:border-zinc-800">
|
| 149 |
+
<div class="flex h-9 w-9 items-center justify-center rounded-xl bg-indigo-600/10 text-indigo-600 dark:text-indigo-400">
|
| 150 |
+
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12c2 0 4 1 5 3-1 2-3 3-5 3s-4-1-5-3c1-2 3-3 5-3Z"/><circle cx="12" cy="8" r="3"/></svg>
|
| 151 |
+
</div>
|
| 152 |
+
<div class="min-w-0">
|
| 153 |
+
<div class="truncate text-sm font-semibold">Design prompt library</div>
|
| 154 |
+
<div class="truncate text-xs text-zinc-500 dark:text-zinc-400">Using Qwen 3 Coder • Web + Files enabled</div>
|
| 155 |
+
</div>
|
| 156 |
+
<div class="ml-auto flex items-center gap-2">
|
| 157 |
+
<span class="hidden rounded-md border border-zinc-200 px-2 py-1 text-xs sm:inline-flex dark:border-zinc-800">/public</span>
|
| 158 |
+
<button class="inline-flex items-center gap-2 rounded-md border border-zinc-200 px-3 py-1.5 text-sm hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v7a1 1 0 0 0 1 1h7"/><path d="m21 3-9 9"/><path d="M15 3h6v6"/></svg>Share</button>
|
| 159 |
+
<div class="relative" id="chat-more">
|
| 160 |
+
<button class="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800" data-dd-trigger aria-label="More"><svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg></button>
|
| 161 |
+
<div class="absolute right-0 z-30 mt-2 hidden w-40 overflow-hidden rounded-md border border-zinc-200 bg-white p-1 text-sm shadow-lg dark:border-zinc-800 dark:bg-zinc-900" data-dd-menu>
|
| 162 |
+
<button class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17l-5-5 5-5"/><path d="M19 17l-5-5 5-5"/></svg>Pin</button>
|
| 163 |
+
<button class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 hover:bg-zinc-100 text-red-600 dark:hover:bg-zinc-800 dark:text-red-400"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>Delete</button>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<!-- Messages -->
|
| 170 |
+
<div id="msg-scroll" class="flex-1 overflow-y-auto">
|
| 171 |
+
<div id="messages" class="mx-auto w-full max-w-3xl">
|
| 172 |
+
<!-- assistant message 1 -->
|
| 173 |
+
<div class="relative flex items-start gap-3 px-4 py-5 sm:px-6">
|
| 174 |
+
<div class="mt-1 h-8 w-8 shrink-0 overflow-hidden rounded-full bg-zinc-200"></div>
|
| 175 |
+
<div class="min-w-0 flex-1">
|
| 176 |
+
<div class="mb-1 flex items-baseline gap-2"><div class="text-sm font-medium">Ava</div><div class="text-xs text-zinc-500">19:04</div></div>
|
| 177 |
+
<div class="prose prose-sm max-w-none rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:prose-invert dark:border-zinc-800 dark:bg-zinc-800/60">
|
| 178 |
+
<p>Ahoj! 👋 Jsem tvůj AI asistent. Jaký úkol dnes řešíš?</p>
|
| 179 |
+
<div class="mt-3 flex flex-wrap gap-2">
|
| 180 |
+
<span class="inline-flex cursor-pointer select-none items-center rounded-md bg-zinc-100 px-2.5 py-1 text-xs ring-1 ring-inset ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700" data-suggest>Refactor code</span>
|
| 181 |
+
<span class="inline-flex cursor-pointer select-none items-center rounded-md bg-zinc-100 px-2.5 py-1 text-xs ring-1 ring-inset ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700" data-suggest>Generate unit tests</span>
|
| 182 |
+
<span class="inline-flex cursor-pointer select-none items-center rounded-md bg-zinc-100 px-2.5 py-1 text-xs ring-1 ring-inset ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700" data-suggest>Explain this snippet</span>
|
| 183 |
+
<span class="inline-flex cursor-pointer select-none items-center rounded-md bg-zinc-100 px-2.5 py-1 text-xs ring-1 ring-inset ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700" data-suggest>Create README</span>
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
</div>
|
| 188 |
+
<!-- user message -->
|
| 189 |
+
<div class="relative flex items-start gap-3 px-4 py-5 sm:px-6">
|
| 190 |
+
<div class="mt-1 h-8 w-8 shrink-0 overflow-hidden rounded-full bg-zinc-300 grid place-items-center text-[10px] font-medium">YOU</div>
|
| 191 |
+
<div class="min-w-0 flex-1">
|
| 192 |
+
<div class="mb-1 flex items-baseline gap-2"><div class="text-sm font-medium">You</div><div class="text-xs text-zinc-500">19:05</div></div>
|
| 193 |
+
<div class="prose prose-sm max-w-none rounded-2xl border border-zinc-200 bg-white p-4 dark:prose-invert dark:border-zinc-800 dark:bg-zinc-900">Potřebuji minimalistické UI pro chat (jako ChatGPT), prosím v Tailwindu.</div>
|
| 194 |
+
</div>
|
| 195 |
</div>
|
| 196 |
+
|
| 197 |
+
<!-- assistant message 2 (card demo) -->
|
| 198 |
+
<div class="relative flex items-start gap-3 px-4 py-5 sm:px-6">
|
| 199 |
+
<div class="mt-1 h-8 w-8 shrink-0 overflow-hidden rounded-full bg-zinc-200"></div>
|
| 200 |
+
<div class="min-w-0 flex-1">
|
| 201 |
+
<div class="mb-1 flex items-baseline gap-2"><div class="text-sm font-medium">Ava</div><div class="text-xs text-zinc-500">19:05</div></div>
|
| 202 |
+
<div class="prose prose-sm max-w-none rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:prose-invert dark:border-zinc-800 dark:bg-zinc-800/60">
|
| 203 |
+
<p>Jasně! Níže je kostra komponenty. Zahrnuje hlavičku, seznam zpráv a composer.</p>
|
| 204 |
+
<div class="mt-3 overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-800">
|
| 205 |
+
<div class="border-b border-zinc-200 px-4 py-2 text-xs text-zinc-500 dark:border-zinc-800">Example component</div>
|
| 206 |
+
<pre class="max-h-80 overflow-auto p-4 text-xs scrollbar-thin"><code>export default function Chat(){/* ... */}</code></pre>
|
| 207 |
+
<div class="flex items-center justify-between gap-2 border-t border-zinc-200 px-4 py-2 text-xs text-zinc-500 dark:border-zinc-800">
|
| 208 |
+
<div class="flex items-center gap-2"><span class="rounded border px-1.5 py-0.5">TypeScript</span><span>•</span><span>Copy & adapt</span></div>
|
| 209 |
+
<button class="inline-flex items-center gap-2 rounded-md border border-zinc-200 px-2 py-1 text-xs hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800"><svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>Copy</button>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
<div class="mt-3 flex gap-2 text-xs text-zinc-500"><span class="rounded border px-1.5 py-0.5">#ui</span><span class="rounded border px-1.5 py-0.5">#tailwind</span><span class="rounded border px-1.5 py-0.5">#shadcn</span></div>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
</div>
|
| 216 |
+
|
| 217 |
+
<!-- assistant message 3 (quick actions) -->
|
| 218 |
+
<div class="relative flex items-start gap-3 px-4 py-5 sm:px-6">
|
| 219 |
+
<div class="mt-1 h-8 w-8 shrink-0 overflow-hidden rounded-full bg-zinc-200"></div>
|
| 220 |
+
<div class="min-w-0 flex-1">
|
| 221 |
+
<div class="mb-1 flex items-baseline gap-2"><div class="text-sm font-medium">Ava</div><div class="text-xs text-zinc-500">19:06</div></div>
|
| 222 |
+
<div class="rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-800/60">
|
| 223 |
+
<p class="text-sm">Mohu také vytvořit sadu rychlých příkazů. Vyber jeden z nich:</p>
|
| 224 |
+
<div class="mt-3 grid gap-2 sm:grid-cols-2">
|
| 225 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600"><svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20"/><path d="M5 7h14"/></svg>Summarize this page</button>
|
| 226 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600"><svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16v4H4z"/><path d="M4 12h16v8H4z"/></svg>Draft an email</button>
|
| 227 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600"><svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>Explain like I'm 5</button>
|
| 228 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600"><svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M3 12h18"/><path d="M3 18h18"/></svg>Create tasks</button>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
<!-- Composer -->
|
| 237 |
+
<div class="border-t border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
|
| 238 |
+
<div class="mx-auto max-w-3xl px-4 py-4 sm:px-6">
|
| 239 |
+
<div class="rounded-2xl border border-zinc-200 bg-white p-3 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
| 240 |
+
<div class="flex items-center gap-2 px-1">
|
| 241 |
+
<button class="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800" title="Attach file"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05 12 20.5a6 6 0 1 1-8.49-8.49l9.19-9.19A4 4 0 0 1 19.86 8l-9.19 9.19a2 2 0 1 1-2.83-2.83L16.34 6.66"/></svg></button>
|
| 242 |
+
<button class="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800" title="Insert image"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L7 20"/></svg></button>
|
| 243 |
+
<button class="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800" title="Voice"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1v11a3 3 0 0 1-6 0V1"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><path d="M12 19v4"/></svg></button>
|
| 244 |
+
<div class="ml-auto flex items-center gap-2 text-xs text-zinc-500">
|
| 245 |
+
<span class="rounded bg-zinc-100 px-2 py-0.5 dark:bg-zinc-800">Web</span>
|
| 246 |
+
<span class="rounded border px-2 py-0.5 dark:border-zinc-700">Files</span>
|
| 247 |
+
<span class="rounded border px-2 py-0.5 dark:border-zinc-700">Code</span>
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
<textarea id="composer" placeholder="Ask anything…" class="mt-2 w-full min-h-[96px] max-h-40 resize-y border-0 bg-transparent p-0 text-sm outline-none focus:ring-0"></textarea>
|
| 251 |
+
<div class="mt-2 flex items-center justify-between">
|
| 252 |
+
<div class="text-xs text-zinc-500">Shift+Enter = new line</div>
|
| 253 |
+
<div class="flex items-center gap-2">
|
| 254 |
+
<button class="inline-flex items-center gap-2 rounded-md border border-zinc-200 px-3 py-1.5 text-sm hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2"/><path d="M12 8v8"/><path d="M8 12h8"/></svg>Stop</button>
|
| 255 |
+
<button id="btn-send" class="inline-flex items-center gap-2 rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2 11 13"/><path d="M22 2 15 22 11 13 2 9 22 2z"/></svg>Send</button>
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
<div class="mt-2 flex flex-wrap gap-2 text-xs text-zinc-500"><span class="cursor-pointer rounded border px-2 py-0.5 dark:border-zinc-700">/summarize</span><span class="cursor-pointer rounded border px-2 py-0.5 dark:border-zinc-700">/translate</span><span class="cursor-pointer rounded border px-2 py-0.5 dark:border-zinc-700">/fix</span></div>
|
| 260 |
+
</div>
|
| 261 |
+
</div>
|
| 262 |
+
</main>
|
| 263 |
+
|
| 264 |
+
<!-- Right Panel (desktop) -->
|
| 265 |
+
<aside class="hidden w-80 flex-col border-l border-zinc-200 lg:flex dark:border-zinc-800">
|
| 266 |
+
<div class="p-4">
|
| 267 |
+
<div class="mb-2 text-sm font-semibold">Quick actions</div>
|
| 268 |
+
<div class="grid gap-2">
|
| 269 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600"><svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20"/><path d="M5 7h14"/></svg>Summarize this page</button>
|
| 270 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600"><svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16v4H4z"/><path d="M4 12h16v8H4z"/></svg>Draft an email</button>
|
| 271 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600"><svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>Explain like I'm 5</button>
|
| 272 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600"><svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M3 12h18"/><path d="M3 18h18"/></svg>Create tasks</button>
|
| 273 |
+
</div>
|
| 274 |
+
</div>
|
| 275 |
+
<div class="h-px bg-zinc-200 dark:bg-zinc-800"></div>
|
| 276 |
+
<div class="p-4">
|
| 277 |
+
<div class="mb-2 text-sm font-semibold">Tools</div>
|
| 278 |
+
<div class="space-y-3 text-sm">
|
| 279 |
+
<div class="flex items-center justify-between"><div class="flex items-center gap-2"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>Web browsing</div><label class="relative inline-flex cursor-pointer items-center"><input type="checkbox" class="peer sr-only" checked><div class="peer h-5 w-9 rounded-full bg-zinc-300 after:absolute after:left-0.5 after:top-0.5 after:h-4 after:w-4 after:rounded-full after:bg-white after:transition peer-checked:bg-indigo-600 peer-checked:after:translate-x-4"></div></label></div>
|
| 280 |
+
<div class="flex items-center justify-between"><div class="flex items-center gap-2"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22h16"/><path d="M4 2h16"/><path d="M4 2v20"/><path d="M20 2v20"/><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h10"/></svg>Files</div><label class="relative inline-flex cursor-pointer items-center"><input type="checkbox" class="peer sr-only"><div class="peer h-5 w-9 rounded-full bg-zinc-300 after:absolute after:left-0.5 after:top-0.5 after:h-4 after:w-4 after:rounded-full after:bg-white after:transition peer-checked:bg-indigo-600 peer-checked:after:translate-x-4"></div></label></div>
|
| 281 |
+
<div class="flex items-center justify-between"><div class="flex items-center gap-2"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M7 15h0"/><path d="M2 9h20"/></svg>Code interpreter</div><label class="relative inline-flex cursor-pointer items-center"><input type="checkbox" class="peer sr-only"><div class="peer h-5 w-9 rounded-full bg-zinc-300 after:absolute after:left-0.5 after:top-0.5 after:h-4 after:w-4 after:rounded-full after:bg-white after:transition peer-checked:bg-indigo-600 peer-checked:after:translate-x-4"></div></label></div>
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
<div class="h-px bg-zinc-200 dark:bg-zinc-800"></div>
|
| 285 |
+
<div class="p-4">
|
| 286 |
+
<div class="mb-2 text-sm font-semibold">Shortcuts</div>
|
| 287 |
+
<div class="flex flex-wrap gap-2 text-xs text-zinc-500"><span class="rounded border px-2 py-0.5 dark:border-zinc-700">⌘K</span><span class="rounded border px-2 py-0.5 dark:border-zinc-700">/help</span><span class="rounded border px-2 py-0.5 dark:border-zinc-700">⌘/</span></div>
|
| 288 |
+
</div>
|
| 289 |
+
</aside>
|
| 290 |
+
|
| 291 |
+
<!-- Right sheet (mobile) -->
|
| 292 |
+
<aside id="right-sheet" class="fixed inset-y-0 right-0 z-50 w-80 translate-x-full transform border-l border-zinc-200 bg-white transition-transform duration-200 ease-out dark:border-zinc-800 dark:bg-zinc-900 lg:hidden">
|
| 293 |
+
<div class="flex items-center justify-between border-b border-zinc-200 p-3 dark:border-zinc-800"><div class="text-sm font-semibold">Actions & tools</div><button id="btn-close-right" class="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800" aria-label="Close"><svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button></div>
|
| 294 |
+
<div class="p-4 space-y-4">
|
| 295 |
+
<div>
|
| 296 |
+
<div class="mb-2 text-sm font-semibold">Quick actions</div>
|
| 297 |
+
<div class="grid gap-2">
|
| 298 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600">Summarize this page</button>
|
| 299 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600">Draft an email</button>
|
| 300 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600">Explain like I'm 5</button>
|
| 301 |
+
<button class="inline-flex items-center rounded-md bg-zinc-100 px-3 py-2 text-sm hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600">Create tasks</button>
|
| 302 |
</div>
|
| 303 |
+
</div>
|
| 304 |
</div>
|
| 305 |
+
</aside>
|
| 306 |
+
<div id="right-overlay" class="fixed inset-0 z-40 hidden bg-black/40 opacity-0 transition-opacity lg:hidden"></div>
|
| 307 |
</div>
|
| 308 |
+
</div>
|
| 309 |
+
|
| 310 |
+
<!-- Enhanced JavaScript for AI Chat Application -->
|
| 311 |
+
<script src="utils.js"></script>
|
| 312 |
+
<script src="i18n.js"></script>
|
| 313 |
+
<script src="app.js"></script>
|
| 314 |
+
<script>
|
| 315 |
+
// Theme toggle
|
| 316 |
+
const btnTheme = document.getElementById('btn-theme');
|
| 317 |
+
const iconSun = document.getElementById('icon-sun');
|
| 318 |
+
const iconMoon = document.getElementById('icon-moon');
|
| 319 |
+
function setDark(on){
|
| 320 |
+
document.documentElement.classList.toggle('dark', on);
|
| 321 |
+
iconSun.classList.toggle('hidden', !on);
|
| 322 |
+
iconMoon.classList.toggle('hidden', on);
|
| 323 |
+
}
|
| 324 |
+
setDark(document.documentElement.classList.contains('dark'))
|
| 325 |
+
btnTheme.addEventListener('click', ()=> setDark(!document.documentElement.classList.contains('dark')));
|
| 326 |
+
|
| 327 |
+
// Simple dropdowns (model + chat more)
|
| 328 |
+
document.querySelectorAll('[data-dd-trigger]').forEach(btn=>{
|
| 329 |
+
const root = btn.closest('div');
|
| 330 |
+
const menu = root.querySelector('[data-dd-menu]');
|
| 331 |
+
btn.addEventListener('click', (e)=>{ e.stopPropagation(); menu.classList.toggle('hidden'); });
|
| 332 |
+
document.addEventListener('click', ()=> menu.classList.add('hidden'));
|
| 333 |
+
});
|
| 334 |
+
|
| 335 |
+
// Left sheet
|
| 336 |
+
const leftSheet = document.getElementById('left-sheet');
|
| 337 |
+
const leftOverlay = document.getElementById('left-overlay');
|
| 338 |
+
document.getElementById('btn-open-left').addEventListener('click', ()=>{
|
| 339 |
+
leftSheet.classList.remove('-translate-x-full'); leftOverlay.classList.remove('hidden'); requestAnimationFrame(()=>leftOverlay.classList.add('opacity-100'));
|
| 340 |
+
});
|
| 341 |
+
document.getElementById('btn-close-left').addEventListener('click', closeLeft);
|
| 342 |
+
leftOverlay.addEventListener('click', closeLeft);
|
| 343 |
+
function closeLeft(){ leftSheet.classList.add('-translate-x-full'); leftOverlay.classList.remove('opacity-100'); setTimeout(()=>leftOverlay.classList.add('hidden'),150); }
|
| 344 |
+
|
| 345 |
+
// Right sheet
|
| 346 |
+
const rightSheet = document.getElementById('right-sheet');
|
| 347 |
+
const rightOverlay = document.getElementById('right-overlay');
|
| 348 |
+
document.getElementById('btn-open-right').addEventListener('click', ()=>{
|
| 349 |
+
rightSheet.classList.remove('translate-x-full'); rightOverlay.classList.remove('hidden'); requestAnimationFrame(()=>rightOverlay.classList.add('opacity-100'));
|
| 350 |
+
});
|
| 351 |
+
document.getElementById('btn-close-right').addEventListener('click', closeRight);
|
| 352 |
+
rightOverlay.addEventListener('click', closeRight);
|
| 353 |
+
function closeRight(){ rightSheet.classList.add('translate-x-full'); rightOverlay.classList.remove('opacity-100'); setTimeout(()=>rightOverlay.classList.add('hidden'),150); }
|
| 354 |
+
|
| 355 |
+
// Composer enable/disable and send
|
| 356 |
+
const ta = document.getElementById('composer');
|
| 357 |
+
const send = document.getElementById('btn-send');
|
| 358 |
+
const messages = document.getElementById('messages');
|
| 359 |
+
const scroller = document.getElementById('msg-scroll');
|
| 360 |
+
function autoResize(){ ta.style.height='auto'; ta.style.height=Math.min(ta.scrollHeight,160)+'px'; }
|
| 361 |
+
function sync(){ send.disabled = ta.value.trim().length===0; }
|
| 362 |
+
ta.addEventListener('input', ()=>{ autoResize(); sync(); });
|
| 363 |
+
ta.addEventListener('keydown', (e)=>{ if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); if(!send.disabled) doSend(); }});
|
| 364 |
+
send.addEventListener('click', doSend);
|
| 365 |
+
function bubble(role, text){
|
| 366 |
+
const wrap = document.createElement('div');
|
| 367 |
+
wrap.className = 'relative flex items-start gap-3 px-4 py-5 sm:px-6';
|
| 368 |
+
const avatar = document.createElement('div');
|
| 369 |
+
avatar.className = 'mt-1 h-8 w-8 shrink-0 overflow-hidden rounded-full '+(role==='user'?'bg-zinc-300 grid place-items-center text-[10px] font-medium':'bg-zinc-200');
|
| 370 |
+
if(role==='user') avatar.textContent='YOU';
|
| 371 |
+
const body = document.createElement('div'); body.className='min-w-0 flex-1';
|
| 372 |
+
const meta = document.createElement('div'); meta.className='mb-1 flex items-baseline gap-2'; meta.innerHTML = `<div class="text-sm font-medium">${role==='user'?'You':'Ava'}</div><div class="text-xs text-zinc-500">${new Date().toLocaleTimeString().slice(0,5)}</div>`;
|
| 373 |
+
const bubble = document.createElement('div');
|
| 374 |
+
bubble.className = (role==='user'?
|
| 375 |
+
'prose prose-sm max-w-none rounded-2xl border border-zinc-200 bg-white p-4 dark:prose-invert dark:border-zinc-800 dark:bg-zinc-900'
|
| 376 |
+
:'prose prose-sm max-w-none rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:prose-invert dark:border-zinc-800 dark:bg-zinc-800/60');
|
| 377 |
+
bubble.textContent = text;
|
| 378 |
+
body.appendChild(meta); body.appendChild(bubble);
|
| 379 |
+
wrap.appendChild(avatar); wrap.appendChild(body);
|
| 380 |
+
messages.appendChild(wrap);
|
| 381 |
+
scroller.scrollTop = scroller.scrollHeight;
|
| 382 |
+
}
|
| 383 |
+
function doSend(){
|
| 384 |
+
const text = ta.value.trim(); if(!text) return;
|
| 385 |
+
bubble('user', text); ta.value=''; autoResize(); sync();
|
| 386 |
+
setTimeout(()=> bubble('assistant','🔧 Stubbed response. Sem přijde odpověď modelu.'), 400);
|
| 387 |
+
}
|
| 388 |
+
autoResize(); sync();
|
| 389 |
+
|
| 390 |
+
// Welcome pills → insert into composer
|
| 391 |
+
document.querySelectorAll('[data-suggest]').forEach(el=>{
|
| 392 |
+
el.addEventListener('click', ()=>{ ta.value = el.textContent.trim(); autoResize(); sync(); ta.focus(); });
|
| 393 |
+
});
|
| 394 |
+
</script>
|
| 395 |
</body>
|
| 396 |
+
</html>
|
public/styles.css
CHANGED
|
@@ -1,201 +1,326 @@
|
|
| 1 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
/* Base styles */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
body {
|
| 5 |
-
font-family: 'Inter', sans-serif;
|
| 6 |
-
background-color: #f8f9fa;
|
| 7 |
-
color: #333;
|
| 8 |
margin: 0;
|
| 9 |
padding: 0;
|
| 10 |
-
|
|
|
|
| 11 |
}
|
| 12 |
|
| 13 |
-
/*
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
background-color: #1a202c;
|
| 17 |
-
color: #e2e8f0;
|
| 18 |
-
}
|
| 19 |
}
|
| 20 |
|
| 21 |
-
/*
|
| 22 |
-
.
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
padding: 20px;
|
| 26 |
-
display: flex;
|
| 27 |
-
flex-direction: column;
|
| 28 |
-
height: 100vh;
|
| 29 |
}
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
text-align: center;
|
| 34 |
-
margin-bottom: 20px;
|
| 35 |
}
|
| 36 |
|
| 37 |
-
.
|
| 38 |
-
|
| 39 |
-
font-weight: 700;
|
| 40 |
-
margin-bottom: 10px;
|
| 41 |
-
background: linear-gradient(135deg, #007cf0, #00dfd8);
|
| 42 |
-
-webkit-background-clip: text;
|
| 43 |
-
-webkit-text-fill-color: transparent;
|
| 44 |
-
background-clip: text;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
.
|
| 48 |
-
|
| 49 |
-
|
| 50 |
}
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
color: #a0aec0;
|
| 55 |
-
}
|
| 56 |
}
|
| 57 |
|
| 58 |
-
/*
|
| 59 |
-
.
|
| 60 |
-
|
| 61 |
-
overflow-y: auto;
|
| 62 |
-
padding: 20px;
|
| 63 |
-
background-color: #fff;
|
| 64 |
-
border-radius: 10px;
|
| 65 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 66 |
-
margin-bottom: 20px;
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
@media (prefers-color-scheme: dark) {
|
| 70 |
-
.chat-messages {
|
| 71 |
-
background-color: #2d3748;
|
| 72 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
| 73 |
-
}
|
| 74 |
}
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
margin-bottom: 15px;
|
| 79 |
-
padding: 10px;
|
| 80 |
-
border-radius: 8px;
|
| 81 |
-
animation: fadeIn 0.3s ease-in;
|
| 82 |
}
|
| 83 |
|
| 84 |
-
.
|
| 85 |
-
background-color: #
|
| 86 |
-
margin-left: 20%;
|
| 87 |
}
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
}
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
}
|
| 98 |
|
| 99 |
-
.
|
| 100 |
-
|
| 101 |
}
|
| 102 |
}
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
}
|
| 108 |
|
| 109 |
-
.
|
| 110 |
-
color:
|
| 111 |
}
|
| 112 |
|
| 113 |
-
.message.
|
| 114 |
-
color:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
-
/*
|
| 118 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
display: flex;
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
flex: 1;
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
resize: none;
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
}
|
| 133 |
|
| 134 |
-
@
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
| 139 |
}
|
| 140 |
}
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
border: none;
|
| 147 |
-
border-radius: 8px;
|
| 148 |
-
font-size: 1rem;
|
| 149 |
-
font-weight: 600;
|
| 150 |
cursor: pointer;
|
| 151 |
-
transition:
|
| 152 |
-
|
|
|
|
|
|
|
| 153 |
}
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
box-shadow: 0 6px 8px rgba(0, 124, 240, 0.4);
|
| 158 |
}
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
}
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
border-
|
| 171 |
-
border-top-color: #007cf0;
|
| 172 |
-
animation: spin 1s ease-in-out infinite;
|
| 173 |
}
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
|
|
|
| 177 |
}
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
}
|
| 183 |
|
| 184 |
/* Responsive design */
|
| 185 |
@media (max-width: 768px) {
|
| 186 |
-
.
|
| 187 |
-
padding:
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
.chat-header h1 {
|
| 191 |
-
font-size: 1.5rem;
|
| 192 |
}
|
| 193 |
|
| 194 |
-
.
|
| 195 |
-
|
|
|
|
| 196 |
}
|
| 197 |
|
| 198 |
-
.
|
| 199 |
-
|
| 200 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
}
|
|
|
|
| 1 |
+
/* ChatGPT-style CSS for AI Chat Application */
|
| 2 |
+
|
| 3 |
+
/* Import Inter font for better typography */
|
| 4 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
| 5 |
+
|
| 6 |
+
/* Root variables for consistent theming */
|
| 7 |
+
:root {
|
| 8 |
+
--chat-gray: #f7f7f8;
|
| 9 |
+
--chat-dark: #343541;
|
| 10 |
+
--sidebar-light: #ffffff;
|
| 11 |
+
--sidebar-dark: #202123;
|
| 12 |
+
--user-msg: #f7f7f8;
|
| 13 |
+
--assistant-msg: #ffffff;
|
| 14 |
+
--border-light: #e5e7eb;
|
| 15 |
+
--border-dark: #4b5563;
|
| 16 |
+
}
|
| 17 |
|
| 18 |
/* Base styles */
|
| 19 |
+
* {
|
| 20 |
+
box-sizing: border-box;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
body {
|
| 24 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
|
|
|
|
|
| 25 |
margin: 0;
|
| 26 |
padding: 0;
|
| 27 |
+
height: 100vh;
|
| 28 |
+
overflow: hidden;
|
| 29 |
}
|
| 30 |
|
| 31 |
+
/* Smooth transitions */
|
| 32 |
+
.transition-all {
|
| 33 |
+
transition: all 0.2s ease-in-out;
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
+
/* Custom scrollbar styling */
|
| 37 |
+
.custom-scrollbar {
|
| 38 |
+
scrollbar-width: thin;
|
| 39 |
+
scrollbar-color: #cbd5e1 transparent;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|
| 41 |
|
| 42 |
+
.custom-scrollbar::-webkit-scrollbar {
|
| 43 |
+
width: 6px;
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
+
.custom-scrollbar::-webkit-scrollbar-track {
|
| 47 |
+
background: transparent;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
| 51 |
+
background-color: #cbd5e1;
|
| 52 |
+
border-radius: 3px;
|
| 53 |
}
|
| 54 |
|
| 55 |
+
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
| 56 |
+
background-color: #94a3b8;
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
+
/* Dark mode scrollbar */
|
| 60 |
+
.dark .custom-scrollbar {
|
| 61 |
+
scrollbar-color: #4b5563 transparent;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
|
| 64 |
+
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
| 65 |
+
background-color: #4b5563;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
}
|
| 67 |
|
| 68 |
+
.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
| 69 |
+
background-color: #6b7280;
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
+
/* Sidebar styling */
|
| 73 |
+
#sidebar {
|
| 74 |
+
z-index: 50;
|
| 75 |
}
|
| 76 |
|
| 77 |
+
/* Mobile sidebar overlay */
|
| 78 |
+
@media (max-width: 768px) {
|
| 79 |
+
#sidebar {
|
| 80 |
+
position: fixed;
|
| 81 |
+
left: 0;
|
| 82 |
+
top: 0;
|
| 83 |
+
height: 100vh;
|
| 84 |
+
transform: translateX(-100%);
|
| 85 |
+
z-index: 50;
|
| 86 |
}
|
| 87 |
|
| 88 |
+
#sidebar.open {
|
| 89 |
+
transform: translateX(0);
|
| 90 |
}
|
| 91 |
}
|
| 92 |
|
| 93 |
+
/* Message styling */
|
| 94 |
+
.message {
|
| 95 |
+
display: flex;
|
| 96 |
+
padding: 1.5rem 1rem;
|
| 97 |
+
gap: 1rem;
|
| 98 |
+
border-bottom: 1px solid var(--border-light);
|
| 99 |
+
transition: background-color 0.1s ease;
|
| 100 |
}
|
| 101 |
|
| 102 |
+
.dark .message {
|
| 103 |
+
border-bottom-color: var(--border-dark);
|
| 104 |
}
|
| 105 |
|
| 106 |
+
.message.user {
|
| 107 |
+
background-color: var(--user-msg);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.dark .message.user {
|
| 111 |
+
background-color: #40414f;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.message.assistant {
|
| 115 |
+
background-color: var(--assistant-msg);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.dark .message.assistant {
|
| 119 |
+
background-color: var(--chat-dark);
|
| 120 |
}
|
| 121 |
|
| 122 |
+
/* Avatar styling */
|
| 123 |
+
.avatar {
|
| 124 |
+
width: 2rem;
|
| 125 |
+
height: 2rem;
|
| 126 |
+
border-radius: 0.125rem;
|
| 127 |
+
flex-shrink: 0;
|
| 128 |
display: flex;
|
| 129 |
+
align-items: center;
|
| 130 |
+
justify-content: center;
|
| 131 |
+
font-weight: 600;
|
| 132 |
+
font-size: 0.875rem;
|
| 133 |
}
|
| 134 |
|
| 135 |
+
.avatar.user {
|
| 136 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 137 |
+
color: white;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.avatar.assistant {
|
| 141 |
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
| 142 |
+
color: white;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/* Message content */
|
| 146 |
+
.message-content {
|
| 147 |
flex: 1;
|
| 148 |
+
min-width: 0;
|
| 149 |
+
line-height: 1.6;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.message-content pre {
|
| 153 |
+
background-color: #f3f4f6;
|
| 154 |
+
border-radius: 0.375rem;
|
| 155 |
+
padding: 0.75rem;
|
| 156 |
+
overflow-x: auto;
|
| 157 |
+
margin: 0.5rem 0;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.dark .message-content pre {
|
| 161 |
+
background-color: #374151;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.message-content code {
|
| 165 |
+
background-color: #f3f4f6;
|
| 166 |
+
padding: 0.125rem 0.25rem;
|
| 167 |
+
border-radius: 0.25rem;
|
| 168 |
+
font-size: 0.875rem;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.dark .message-content code {
|
| 172 |
+
background-color: #374151;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* Auto-resize textarea */
|
| 176 |
+
#user-input {
|
| 177 |
resize: none;
|
| 178 |
+
transition: border-color 0.2s ease;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
#user-input:focus {
|
| 182 |
+
border-color: #3b82f6 !important;
|
| 183 |
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/* Send button states */
|
| 187 |
+
#send-button:disabled {
|
| 188 |
+
opacity: 0.5;
|
| 189 |
+
cursor: not-allowed;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
#send-button:not(:disabled):hover {
|
| 193 |
+
background-color: #2563eb;
|
| 194 |
+
transform: translateY(-1px);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
/* Loading animation */
|
| 198 |
+
.typing-indicator {
|
| 199 |
+
display: flex;
|
| 200 |
+
align-items: center;
|
| 201 |
+
gap: 0.25rem;
|
| 202 |
+
color: #6b7280;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.typing-dot {
|
| 206 |
+
width: 0.5rem;
|
| 207 |
+
height: 0.5rem;
|
| 208 |
+
background-color: currentColor;
|
| 209 |
+
border-radius: 50%;
|
| 210 |
+
animation: typing 1.4s infinite ease-in-out;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.typing-dot:nth-child(1) {
|
| 214 |
+
animation-delay: -0.32s;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.typing-dot:nth-child(2) {
|
| 218 |
+
animation-delay: -0.16s;
|
| 219 |
}
|
| 220 |
|
| 221 |
+
@keyframes typing {
|
| 222 |
+
0%, 80%, 100% {
|
| 223 |
+
transform: scale(0.8);
|
| 224 |
+
opacity: 0.5;
|
| 225 |
+
}
|
| 226 |
+
40% {
|
| 227 |
+
transform: scale(1);
|
| 228 |
+
opacity: 1;
|
| 229 |
}
|
| 230 |
}
|
| 231 |
|
| 232 |
+
/* Chat history items */
|
| 233 |
+
.chat-history-item {
|
| 234 |
+
padding: 0.75rem;
|
| 235 |
+
border-radius: 0.5rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
cursor: pointer;
|
| 237 |
+
transition: background-color 0.1s ease;
|
| 238 |
+
font-size: 0.875rem;
|
| 239 |
+
color: #374151;
|
| 240 |
+
border: 1px solid transparent;
|
| 241 |
}
|
| 242 |
|
| 243 |
+
.dark .chat-history-item {
|
| 244 |
+
color: #d1d5db;
|
|
|
|
| 245 |
}
|
| 246 |
|
| 247 |
+
.chat-history-item:hover {
|
| 248 |
+
background-color: #f3f4f6;
|
| 249 |
}
|
| 250 |
|
| 251 |
+
.dark .chat-history-item:hover {
|
| 252 |
+
background-color: #374151;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.chat-history-item.active {
|
| 256 |
+
background-color: #e5e7eb;
|
| 257 |
+
border-color: #d1d5db;
|
|
|
|
|
|
|
| 258 |
}
|
| 259 |
|
| 260 |
+
.dark .chat-history-item.active {
|
| 261 |
+
background-color: #4b5563;
|
| 262 |
+
border-color: #6b7280;
|
| 263 |
}
|
| 264 |
|
| 265 |
+
/* Welcome screen */
|
| 266 |
+
.welcome-screen {
|
| 267 |
+
display: flex;
|
| 268 |
+
flex-direction: column;
|
| 269 |
+
align-items: center;
|
| 270 |
+
justify-content: center;
|
| 271 |
+
height: 100%;
|
| 272 |
+
padding: 2rem;
|
| 273 |
+
text-align: center;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.welcome-title {
|
| 277 |
+
font-size: 2rem;
|
| 278 |
+
font-weight: 600;
|
| 279 |
+
margin-bottom: 1rem;
|
| 280 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 281 |
+
-webkit-background-clip: text;
|
| 282 |
+
-webkit-text-fill-color: transparent;
|
| 283 |
+
background-clip: text;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.welcome-subtitle {
|
| 287 |
+
color: #6b7280;
|
| 288 |
+
margin-bottom: 2rem;
|
| 289 |
+
max-width: 32rem;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.dark .welcome-subtitle {
|
| 293 |
+
color: #9ca3af;
|
| 294 |
}
|
| 295 |
|
| 296 |
/* Responsive design */
|
| 297 |
@media (max-width: 768px) {
|
| 298 |
+
.message {
|
| 299 |
+
padding: 1rem 0.75rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
}
|
| 301 |
|
| 302 |
+
.avatar {
|
| 303 |
+
width: 1.75rem;
|
| 304 |
+
height: 1.75rem;
|
| 305 |
}
|
| 306 |
|
| 307 |
+
.welcome-title {
|
| 308 |
+
font-size: 1.5rem;
|
| 309 |
}
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
/* Focus ring removal for better custom styling */
|
| 313 |
+
.focus\:outline-none:focus {
|
| 314 |
+
outline: none;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
/* Custom button hover effects */
|
| 318 |
+
.hover-lift:hover {
|
| 319 |
+
transform: translateY(-1px);
|
| 320 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
/* Dark mode toggle improvements */
|
| 324 |
+
.dark-mode-transition {
|
| 325 |
+
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
| 326 |
}
|
public/utils.js
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Utility functions for the AI Chat Application
|
| 3 |
+
*
|
| 4 |
+
* This module provides various utility functions for:
|
| 5 |
+
* - Text processing and formatting
|
| 6 |
+
* - Date/time operations
|
| 7 |
+
* - Local storage management
|
| 8 |
+
* - Event handling
|
| 9 |
+
* - Performance monitoring
|
| 10 |
+
* - Debouncing and throttling
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
// Text Processing Utilities
|
| 14 |
+
class TextUtils {
|
| 15 |
+
// Truncate text to specified length
|
| 16 |
+
static truncate(text, maxLength, suffix = '...') {
|
| 17 |
+
if (!text || text.length <= maxLength) return text;
|
| 18 |
+
return text.substring(0, maxLength - suffix.length) + suffix;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// Count words in text
|
| 22 |
+
static wordCount(text) {
|
| 23 |
+
if (!text) return 0;
|
| 24 |
+
return text.trim().split(/\s+/).filter(word => word.length > 0).length;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// Estimate reading time (assuming 200 words per minute)
|
| 28 |
+
static estimateReadingTime(text) {
|
| 29 |
+
const words = this.wordCount(text);
|
| 30 |
+
const minutes = Math.ceil(words / 200);
|
| 31 |
+
return minutes;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Clean and normalize text
|
| 35 |
+
static normalize(text) {
|
| 36 |
+
if (!text) return '';
|
| 37 |
+
return text
|
| 38 |
+
.trim()
|
| 39 |
+
.replace(/\s+/g, ' ')
|
| 40 |
+
.replace(/[^\w\s.,!?;:-]/g, '');
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// Extract hashtags from text
|
| 44 |
+
static extractHashtags(text) {
|
| 45 |
+
const hashtagRegex = /#[\w]+/g;
|
| 46 |
+
return text.match(hashtagRegex) || [];
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Extract mentions from text
|
| 50 |
+
static extractMentions(text) {
|
| 51 |
+
const mentionRegex = /@[\w]+/g;
|
| 52 |
+
return text.match(mentionRegex) || [];
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Convert text to slug format
|
| 56 |
+
static slugify(text) {
|
| 57 |
+
return text
|
| 58 |
+
.toLowerCase()
|
| 59 |
+
.trim()
|
| 60 |
+
.replace(/[^\w\s-]/g, '')
|
| 61 |
+
.replace(/[\s_-]+/g, '-')
|
| 62 |
+
.replace(/^-+|-+$/g, '');
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// Highlight search terms in text
|
| 66 |
+
static highlight(text, searchTerm, className = 'highlight') {
|
| 67 |
+
if (!searchTerm) return text;
|
| 68 |
+
const regex = new RegExp(`(${searchTerm})`, 'gi');
|
| 69 |
+
return text.replace(regex, `<span class="${className}">$1</span>`);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Strip HTML tags from text
|
| 73 |
+
static stripHtml(html) {
|
| 74 |
+
const div = document.createElement('div');
|
| 75 |
+
div.innerHTML = html;
|
| 76 |
+
return div.textContent || div.innerText || '';
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Convert markdown-like syntax to HTML
|
| 80 |
+
static simpleMarkdown(text) {
|
| 81 |
+
return text
|
| 82 |
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
| 83 |
+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
| 84 |
+
.replace(/`(.*?)`/g, '<code>$1</code>')
|
| 85 |
+
.replace(/\n/g, '<br>');
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// Date and Time Utilities
|
| 90 |
+
class DateUtils {
|
| 91 |
+
// Format date relative to now (e.g., "2 minutes ago")
|
| 92 |
+
static formatRelative(date) {
|
| 93 |
+
const now = new Date();
|
| 94 |
+
const diffInSeconds = Math.floor((now - date) / 1000);
|
| 95 |
+
|
| 96 |
+
if (diffInSeconds < 60) return 'just now';
|
| 97 |
+
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
|
| 98 |
+
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
|
| 99 |
+
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`;
|
| 100 |
+
|
| 101 |
+
return date.toLocaleDateString();
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// Format date for display
|
| 105 |
+
static formatDisplay(date, options = {}) {
|
| 106 |
+
const defaultOptions = {
|
| 107 |
+
year: 'numeric',
|
| 108 |
+
month: 'short',
|
| 109 |
+
day: 'numeric',
|
| 110 |
+
hour: '2-digit',
|
| 111 |
+
minute: '2-digit'
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
return date.toLocaleDateString(undefined, { ...defaultOptions, ...options });
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// Get start of day
|
| 118 |
+
static startOfDay(date = new Date()) {
|
| 119 |
+
const start = new Date(date);
|
| 120 |
+
start.setHours(0, 0, 0, 0);
|
| 121 |
+
return start;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
// Get end of day
|
| 125 |
+
static endOfDay(date = new Date()) {
|
| 126 |
+
const end = new Date(date);
|
| 127 |
+
end.setHours(23, 59, 59, 999);
|
| 128 |
+
return end;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Check if date is today
|
| 132 |
+
static isToday(date) {
|
| 133 |
+
const today = new Date();
|
| 134 |
+
return this.startOfDay(date).getTime() === this.startOfDay(today).getTime();
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Check if date is yesterday
|
| 138 |
+
static isYesterday(date) {
|
| 139 |
+
const yesterday = new Date();
|
| 140 |
+
yesterday.setDate(yesterday.getDate() - 1);
|
| 141 |
+
return this.startOfDay(date).getTime() === this.startOfDay(yesterday).getTime();
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// Get time zone offset
|
| 145 |
+
static getTimezoneOffset() {
|
| 146 |
+
return new Date().getTimezoneOffset();
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
// Parse various date formats
|
| 150 |
+
static parseDate(dateString) {
|
| 151 |
+
// Try different date formats
|
| 152 |
+
const formats = [
|
| 153 |
+
/^\d{4}-\d{2}-\d{2}$/, // YYYY-MM-DD
|
| 154 |
+
/^\d{2}\/\d{2}\/\d{4}$/, // MM/DD/YYYY
|
| 155 |
+
/^\d{2}-\d{2}-\d{4}$/, // MM-DD-YYYY
|
| 156 |
+
];
|
| 157 |
+
|
| 158 |
+
for (const format of formats) {
|
| 159 |
+
if (format.test(dateString)) {
|
| 160 |
+
const date = new Date(dateString);
|
| 161 |
+
if (!isNaN(date.getTime())) {
|
| 162 |
+
return date;
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
return null;
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// Local Storage Utilities
|
| 172 |
+
class StorageUtils {
|
| 173 |
+
// Safe JSON parse
|
| 174 |
+
static safeJsonParse(str, fallback = null) {
|
| 175 |
+
try {
|
| 176 |
+
return JSON.parse(str);
|
| 177 |
+
} catch (error) {
|
| 178 |
+
console.warn('Failed to parse JSON:', error);
|
| 179 |
+
return fallback;
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
// Safe JSON stringify
|
| 184 |
+
static safeJsonStringify(obj, fallback = '{}') {
|
| 185 |
+
try {
|
| 186 |
+
return JSON.stringify(obj);
|
| 187 |
+
} catch (error) {
|
| 188 |
+
console.warn('Failed to stringify JSON:', error);
|
| 189 |
+
return fallback;
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
// Get item from localStorage with fallback
|
| 194 |
+
static getItem(key, fallback = null) {
|
| 195 |
+
try {
|
| 196 |
+
const item = localStorage.getItem(key);
|
| 197 |
+
return item !== null ? this.safeJsonParse(item, fallback) : fallback;
|
| 198 |
+
} catch (error) {
|
| 199 |
+
console.warn('Failed to get localStorage item:', error);
|
| 200 |
+
return fallback;
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
// Set item in localStorage
|
| 205 |
+
static setItem(key, value) {
|
| 206 |
+
try {
|
| 207 |
+
localStorage.setItem(key, this.safeJsonStringify(value));
|
| 208 |
+
return true;
|
| 209 |
+
} catch (error) {
|
| 210 |
+
console.warn('Failed to set localStorage item:', error);
|
| 211 |
+
return false;
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// Remove item from localStorage
|
| 216 |
+
static removeItem(key) {
|
| 217 |
+
try {
|
| 218 |
+
localStorage.removeItem(key);
|
| 219 |
+
return true;
|
| 220 |
+
} catch (error) {
|
| 221 |
+
console.warn('Failed to remove localStorage item:', error);
|
| 222 |
+
return false;
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
// Clear all localStorage data
|
| 227 |
+
static clear() {
|
| 228 |
+
try {
|
| 229 |
+
localStorage.clear();
|
| 230 |
+
return true;
|
| 231 |
+
} catch (error) {
|
| 232 |
+
console.warn('Failed to clear localStorage:', error);
|
| 233 |
+
return false;
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// Get storage usage
|
| 238 |
+
static getStorageUsage() {
|
| 239 |
+
let total = 0;
|
| 240 |
+
for (let key in localStorage) {
|
| 241 |
+
if (localStorage.hasOwnProperty(key)) {
|
| 242 |
+
total += localStorage[key].length + key.length;
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
return total;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
// Check if storage is available
|
| 249 |
+
static isAvailable() {
|
| 250 |
+
try {
|
| 251 |
+
const test = '__storage_test__';
|
| 252 |
+
localStorage.setItem(test, test);
|
| 253 |
+
localStorage.removeItem(test);
|
| 254 |
+
return true;
|
| 255 |
+
} catch (error) {
|
| 256 |
+
return false;
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
// Event Handling Utilities
|
| 262 |
+
class EventUtils {
|
| 263 |
+
// Debounce function calls
|
| 264 |
+
static debounce(func, wait, immediate = false) {
|
| 265 |
+
let timeout;
|
| 266 |
+
return function executedFunction(...args) {
|
| 267 |
+
const later = () => {
|
| 268 |
+
timeout = null;
|
| 269 |
+
if (!immediate) func.apply(this, args);
|
| 270 |
+
};
|
| 271 |
+
const callNow = immediate && !timeout;
|
| 272 |
+
clearTimeout(timeout);
|
| 273 |
+
timeout = setTimeout(later, wait);
|
| 274 |
+
if (callNow) func.apply(this, args);
|
| 275 |
+
};
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
// Throttle function calls
|
| 279 |
+
static throttle(func, limit) {
|
| 280 |
+
let inThrottle;
|
| 281 |
+
return function executedFunction(...args) {
|
| 282 |
+
if (!inThrottle) {
|
| 283 |
+
func.apply(this, args);
|
| 284 |
+
inThrottle = true;
|
| 285 |
+
setTimeout(() => inThrottle = false, limit);
|
| 286 |
+
}
|
| 287 |
+
};
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
// Add event listener with cleanup
|
| 291 |
+
static addListener(element, event, handler, options = {}) {
|
| 292 |
+
element.addEventListener(event, handler, options);
|
| 293 |
+
return () => element.removeEventListener(event, handler, options);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
// Create event emitter
|
| 297 |
+
static createEmitter() {
|
| 298 |
+
const listeners = new Map();
|
| 299 |
+
|
| 300 |
+
return {
|
| 301 |
+
on(event, callback) {
|
| 302 |
+
if (!listeners.has(event)) {
|
| 303 |
+
listeners.set(event, []);
|
| 304 |
+
}
|
| 305 |
+
listeners.get(event).push(callback);
|
| 306 |
+
},
|
| 307 |
+
|
| 308 |
+
off(event, callback) {
|
| 309 |
+
const eventListeners = listeners.get(event);
|
| 310 |
+
if (eventListeners) {
|
| 311 |
+
const index = eventListeners.indexOf(callback);
|
| 312 |
+
if (index > -1) {
|
| 313 |
+
eventListeners.splice(index, 1);
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
},
|
| 317 |
+
|
| 318 |
+
emit(event, data) {
|
| 319 |
+
const eventListeners = listeners.get(event);
|
| 320 |
+
if (eventListeners) {
|
| 321 |
+
eventListeners.forEach(callback => callback(data));
|
| 322 |
+
}
|
| 323 |
+
},
|
| 324 |
+
|
| 325 |
+
once(event, callback) {
|
| 326 |
+
const onceWrapper = (data) => {
|
| 327 |
+
callback(data);
|
| 328 |
+
this.off(event, onceWrapper);
|
| 329 |
+
};
|
| 330 |
+
this.on(event, onceWrapper);
|
| 331 |
+
}
|
| 332 |
+
};
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
// Check if element is in viewport
|
| 336 |
+
static isInViewport(element) {
|
| 337 |
+
const rect = element.getBoundingClientRect();
|
| 338 |
+
return (
|
| 339 |
+
rect.top >= 0 &&
|
| 340 |
+
rect.left >= 0 &&
|
| 341 |
+
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
| 342 |
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
| 343 |
+
);
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
// Smooth scroll to element
|
| 347 |
+
static scrollToElement(element, options = {}) {
|
| 348 |
+
const defaultOptions = {
|
| 349 |
+
behavior: 'smooth',
|
| 350 |
+
block: 'start',
|
| 351 |
+
inline: 'nearest'
|
| 352 |
+
};
|
| 353 |
+
|
| 354 |
+
element.scrollIntoView({ ...defaultOptions, ...options });
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
// Performance Utilities
|
| 359 |
+
class PerformanceUtils {
|
| 360 |
+
// Measure function execution time
|
| 361 |
+
static measureTime(func, ...args) {
|
| 362 |
+
const start = performance.now();
|
| 363 |
+
const result = func.apply(this, args);
|
| 364 |
+
const end = performance.now();
|
| 365 |
+
|
| 366 |
+
console.log(`Function executed in ${end - start} milliseconds`);
|
| 367 |
+
return result;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
// Measure async function execution time
|
| 371 |
+
static async measureAsyncTime(func, ...args) {
|
| 372 |
+
const start = performance.now();
|
| 373 |
+
const result = await func.apply(this, args);
|
| 374 |
+
const end = performance.now();
|
| 375 |
+
|
| 376 |
+
console.log(`Async function executed in ${end - start} milliseconds`);
|
| 377 |
+
return result;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
// Create performance observer
|
| 381 |
+
static observePerformance(callback) {
|
| 382 |
+
if ('PerformanceObserver' in window) {
|
| 383 |
+
const observer = new PerformanceObserver(callback);
|
| 384 |
+
observer.observe({ entryTypes: ['measure', 'navigation', 'resource'] });
|
| 385 |
+
return observer;
|
| 386 |
+
}
|
| 387 |
+
return null;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
// Memory usage information
|
| 391 |
+
static getMemoryInfo() {
|
| 392 |
+
if (performance.memory) {
|
| 393 |
+
return {
|
| 394 |
+
used: Math.round(performance.memory.usedJSHeapSize / 1048576),
|
| 395 |
+
total: Math.round(performance.memory.totalJSHeapSize / 1048576),
|
| 396 |
+
limit: Math.round(performance.memory.jsHeapSizeLimit / 1048576)
|
| 397 |
+
};
|
| 398 |
+
}
|
| 399 |
+
return null;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
// Request idle callback wrapper
|
| 403 |
+
static onIdle(callback, options = {}) {
|
| 404 |
+
if ('requestIdleCallback' in window) {
|
| 405 |
+
return requestIdleCallback(callback, options);
|
| 406 |
+
} else {
|
| 407 |
+
return setTimeout(callback, 0);
|
| 408 |
+
}
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
// Cancel idle callback
|
| 412 |
+
static cancelIdle(id) {
|
| 413 |
+
if ('cancelIdleCallback' in window) {
|
| 414 |
+
cancelIdleCallback(id);
|
| 415 |
+
} else {
|
| 416 |
+
clearTimeout(id);
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// Validation Utilities
|
| 422 |
+
class ValidationUtils {
|
| 423 |
+
// Validate email format
|
| 424 |
+
static isValidEmail(email) {
|
| 425 |
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
| 426 |
+
return emailRegex.test(email);
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
// Validate URL format
|
| 430 |
+
static isValidUrl(url) {
|
| 431 |
+
try {
|
| 432 |
+
new URL(url);
|
| 433 |
+
return true;
|
| 434 |
+
} catch {
|
| 435 |
+
return false;
|
| 436 |
+
}
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
// Check if string is empty or whitespace
|
| 440 |
+
static isEmpty(str) {
|
| 441 |
+
return !str || str.trim().length === 0;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
// Validate message content
|
| 445 |
+
static isValidMessage(content) {
|
| 446 |
+
if (typeof content !== 'string') return false;
|
| 447 |
+
if (this.isEmpty(content)) return false;
|
| 448 |
+
if (content.length > 10000) return false; // Max length check
|
| 449 |
+
return true;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
// Check for profanity (basic implementation)
|
| 453 |
+
static containsProfanity(text) {
|
| 454 |
+
const profanityList = ['spam', 'scam']; // Add more as needed
|
| 455 |
+
const lowerText = text.toLowerCase();
|
| 456 |
+
return profanityList.some(word => lowerText.includes(word));
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
// Validate JSON structure
|
| 460 |
+
static isValidJson(str) {
|
| 461 |
+
try {
|
| 462 |
+
JSON.parse(str);
|
| 463 |
+
return true;
|
| 464 |
+
} catch {
|
| 465 |
+
return false;
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
// Random Utilities
|
| 471 |
+
class RandomUtils {
|
| 472 |
+
// Generate random ID
|
| 473 |
+
static generateId(length = 8) {
|
| 474 |
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
| 475 |
+
let result = '';
|
| 476 |
+
for (let i = 0; i < length; i++) {
|
| 477 |
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
| 478 |
+
}
|
| 479 |
+
return result;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
// Generate UUID v4
|
| 483 |
+
static generateUuid() {
|
| 484 |
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
| 485 |
+
const r = Math.random() * 16 | 0;
|
| 486 |
+
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
| 487 |
+
return v.toString(16);
|
| 488 |
+
});
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
// Get random element from array
|
| 492 |
+
static randomElement(array) {
|
| 493 |
+
return array[Math.floor(Math.random() * array.length)];
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
// Shuffle array
|
| 497 |
+
static shuffle(array) {
|
| 498 |
+
const shuffled = [...array];
|
| 499 |
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
| 500 |
+
const j = Math.floor(Math.random() * (i + 1));
|
| 501 |
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
| 502 |
+
}
|
| 503 |
+
return shuffled;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
// Generate random color
|
| 507 |
+
static randomColor() {
|
| 508 |
+
return `#${Math.floor(Math.random()*16777215).toString(16)}`;
|
| 509 |
+
}
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
// Export utilities
|
| 513 |
+
const Utils = {
|
| 514 |
+
Text: TextUtils,
|
| 515 |
+
Date: DateUtils,
|
| 516 |
+
Storage: StorageUtils,
|
| 517 |
+
Event: EventUtils,
|
| 518 |
+
Performance: PerformanceUtils,
|
| 519 |
+
Validation: ValidationUtils,
|
| 520 |
+
Random: RandomUtils
|
| 521 |
+
};
|
| 522 |
+
|
| 523 |
+
// Make available globally
|
| 524 |
+
window.Utils = Utils;
|
| 525 |
+
|
| 526 |
+
// Export for module usage
|
| 527 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 528 |
+
module.exports = Utils;
|
| 529 |
+
}
|
requirements.txt
CHANGED
|
@@ -5,4 +5,5 @@ fastapi>=0.68.0
|
|
| 5 |
uvicorn>=0.15.0
|
| 6 |
redis>=3.5.0
|
| 7 |
aiohttp>=3.7.0
|
| 8 |
-
pydantic>=1.8.0
|
|
|
|
|
|
| 5 |
uvicorn>=0.15.0
|
| 6 |
redis>=3.5.0
|
| 7 |
aiohttp>=3.7.0
|
| 8 |
+
pydantic>=1.8.0
|
| 9 |
+
accelerate>=0.20.0
|
utils/model_utils.py
CHANGED
|
@@ -23,9 +23,18 @@ class ModelManager:
|
|
| 23 |
def __init__(self):
|
| 24 |
self.model = None
|
| 25 |
self.tokenizer = None
|
| 26 |
-
self.device =
|
| 27 |
self.load_model()
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
def load_model(self) -> None:
|
| 30 |
"""Load the Qwen model"""
|
| 31 |
try:
|
|
@@ -33,7 +42,7 @@ class ModelManager:
|
|
| 33 |
self.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
| 34 |
self.model = AutoModelForCausalLM.from_pretrained(
|
| 35 |
MODEL_NAME,
|
| 36 |
-
torch_dtype=torch.float16 if self.device
|
| 37 |
low_cpu_mem_usage=True,
|
| 38 |
device_map="auto"
|
| 39 |
)
|
|
|
|
| 23 |
def __init__(self):
|
| 24 |
self.model = None
|
| 25 |
self.tokenizer = None
|
| 26 |
+
self.device = self._get_device()
|
| 27 |
self.load_model()
|
| 28 |
|
| 29 |
+
def _get_device(self) -> str:
|
| 30 |
+
"""Determine the best available device"""
|
| 31 |
+
if torch.cuda.is_available():
|
| 32 |
+
return "cuda"
|
| 33 |
+
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
|
| 34 |
+
return "mps"
|
| 35 |
+
else:
|
| 36 |
+
return "cpu"
|
| 37 |
+
|
| 38 |
def load_model(self) -> None:
|
| 39 |
"""Load the Qwen model"""
|
| 40 |
try:
|
|
|
|
| 42 |
self.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
| 43 |
self.model = AutoModelForCausalLM.from_pretrained(
|
| 44 |
MODEL_NAME,
|
| 45 |
+
torch_dtype=torch.float16 if self.device in ["cuda", "mps"] else torch.float32,
|
| 46 |
low_cpu_mem_usage=True,
|
| 47 |
device_map="auto"
|
| 48 |
)
|