stratifyai 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
api/static/index.html ADDED
@@ -0,0 +1,1126 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>StratifyAI - Multi-Provider LLM Interface</title>
7
+ <!-- Markdown rendering -->
8
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
+ <!-- Syntax highlighting -->
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
12
+ <style>
13
+ * {
14
+ margin: 0;
15
+ padding: 0;
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ body {
20
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
21
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
22
+ min-height: 100vh;
23
+ padding: 20px;
24
+ }
25
+
26
+ .container {
27
+ max-width: 1200px;
28
+ margin: 0 auto;
29
+ }
30
+
31
+ .header {
32
+ background: white;
33
+ padding: 20px 30px;
34
+ border-radius: 10px;
35
+ margin-bottom: 20px;
36
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
37
+ }
38
+
39
+ .header h1 {
40
+ color: #667eea;
41
+ margin-bottom: 5px;
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 15px;
45
+ }
46
+
47
+ .header h1 img {
48
+ height: 50px;
49
+ width: auto;
50
+ }
51
+
52
+ .header p {
53
+ color: #666;
54
+ }
55
+
56
+ .main-grid {
57
+ display: grid;
58
+ grid-template-columns: 300px 1fr;
59
+ gap: 20px;
60
+ }
61
+
62
+ .sidebar {
63
+ background: white;
64
+ padding: 20px;
65
+ border-radius: 10px;
66
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
67
+ height: fit-content;
68
+ }
69
+
70
+ .sidebar h3 {
71
+ color: #333;
72
+ margin-bottom: 15px;
73
+ }
74
+
75
+ .form-group {
76
+ margin-bottom: 15px;
77
+ }
78
+
79
+ label {
80
+ display: block;
81
+ color: #666;
82
+ font-size: 14px;
83
+ margin-bottom: 5px;
84
+ }
85
+
86
+ select, textarea, input, button {
87
+ width: 100%;
88
+ padding: 10px;
89
+ border: 1px solid #ddd;
90
+ border-radius: 5px;
91
+ font-size: 14px;
92
+ }
93
+
94
+ textarea {
95
+ min-height: 100px;
96
+ resize: vertical;
97
+ font-family: inherit;
98
+ }
99
+
100
+ button {
101
+ background: #667eea;
102
+ color: white;
103
+ border: none;
104
+ cursor: pointer;
105
+ font-weight: 500;
106
+ transition: background 0.3s;
107
+ }
108
+
109
+ button:hover {
110
+ background: #5568d3;
111
+ }
112
+
113
+ button:disabled {
114
+ background: #ccc;
115
+ cursor: not-allowed;
116
+ }
117
+
118
+ .main-content {
119
+ background: white;
120
+ padding: 20px;
121
+ border-radius: 10px;
122
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
123
+ }
124
+
125
+ .chat-container {
126
+ border: 1px solid #ddd;
127
+ border-radius: 5px;
128
+ min-height: 400px;
129
+ max-height: 600px;
130
+ overflow-y: auto;
131
+ padding: 15px;
132
+ background: #f9f9f9;
133
+ margin-bottom: 15px;
134
+ }
135
+
136
+ .message {
137
+ margin-bottom: 15px;
138
+ padding: 10px 15px;
139
+ border-radius: 8px;
140
+ max-width: 80%;
141
+ }
142
+
143
+ .message.user {
144
+ background: #667eea;
145
+ color: white;
146
+ margin-left: auto;
147
+ }
148
+
149
+ .message.assistant {
150
+ background: white;
151
+ border: 1px solid #ddd;
152
+ }
153
+
154
+ .message.system {
155
+ background: #f0f0f0;
156
+ font-style: italic;
157
+ max-width: 100%;
158
+ }
159
+
160
+ .message.error {
161
+ background: #fee;
162
+ border: 1px solid #fcc;
163
+ color: #c33;
164
+ max-width: 100%;
165
+ }
166
+
167
+ /* Markdown content styling */
168
+ .message.assistant .markdown-content {
169
+ line-height: 1.6;
170
+ }
171
+
172
+ .message.assistant .markdown-content p {
173
+ margin: 0 0 10px 0;
174
+ }
175
+
176
+ .message.assistant .markdown-content p:last-child {
177
+ margin-bottom: 0;
178
+ }
179
+
180
+ .message.assistant .markdown-content pre {
181
+ background: #f6f8fa;
182
+ border: 1px solid #e1e4e8;
183
+ border-radius: 6px;
184
+ padding: 12px;
185
+ overflow-x: auto;
186
+ margin: 10px 0;
187
+ }
188
+
189
+ .message.assistant .markdown-content code {
190
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
191
+ font-size: 13px;
192
+ }
193
+
194
+ .message.assistant .markdown-content :not(pre) > code {
195
+ background: #f0f0f0;
196
+ padding: 2px 6px;
197
+ border-radius: 4px;
198
+ font-size: 13px;
199
+ }
200
+
201
+ .message.assistant .markdown-content ul,
202
+ .message.assistant .markdown-content ol {
203
+ margin: 10px 0;
204
+ padding-left: 24px;
205
+ }
206
+
207
+ .message.assistant .markdown-content li {
208
+ margin: 4px 0;
209
+ }
210
+
211
+ .message.assistant .markdown-content h1,
212
+ .message.assistant .markdown-content h2,
213
+ .message.assistant .markdown-content h3,
214
+ .message.assistant .markdown-content h4 {
215
+ margin: 16px 0 8px 0;
216
+ font-weight: 600;
217
+ }
218
+
219
+ .message.assistant .markdown-content h1 { font-size: 1.4em; }
220
+ .message.assistant .markdown-content h2 { font-size: 1.25em; }
221
+ .message.assistant .markdown-content h3 { font-size: 1.1em; }
222
+
223
+ .message.assistant .markdown-content blockquote {
224
+ border-left: 4px solid #ddd;
225
+ margin: 10px 0;
226
+ padding: 8px 16px;
227
+ color: #666;
228
+ background: #f9f9f9;
229
+ }
230
+
231
+ .message.assistant .markdown-content table {
232
+ border-collapse: collapse;
233
+ margin: 10px 0;
234
+ width: 100%;
235
+ }
236
+
237
+ .message.assistant .markdown-content th,
238
+ .message.assistant .markdown-content td {
239
+ border: 1px solid #ddd;
240
+ padding: 8px 12px;
241
+ text-align: left;
242
+ }
243
+
244
+ .message.assistant .markdown-content th {
245
+ background: #f6f8fa;
246
+ font-weight: 600;
247
+ }
248
+
249
+ .message.assistant .markdown-content hr {
250
+ border: none;
251
+ border-top: 1px solid #ddd;
252
+ margin: 16px 0;
253
+ }
254
+
255
+ .message.assistant .markdown-content a {
256
+ color: #667eea;
257
+ text-decoration: none;
258
+ }
259
+
260
+ .message.assistant .markdown-content a:hover {
261
+ text-decoration: underline;
262
+ }
263
+
264
+ .error-details {
265
+ font-size: 12px;
266
+ margin-top: 8px;
267
+ padding: 8px;
268
+ background: #fff;
269
+ border-radius: 4px;
270
+ font-family: monospace;
271
+ white-space: pre-wrap;
272
+ word-break: break-word;
273
+ }
274
+
275
+ .cost-tracker {
276
+ background: #f9f9f9;
277
+ padding: 15px;
278
+ border-radius: 5px;
279
+ margin-top: 15px;
280
+ }
281
+
282
+ .cost-tracker h4 {
283
+ color: #333;
284
+ margin-bottom: 10px;
285
+ }
286
+
287
+ .validation-status {
288
+ font-size: 13px;
289
+ margin-top: 8px;
290
+ padding: 6px 10px;
291
+ border-radius: 4px;
292
+ background: #f0f9ff;
293
+ border-left: 3px solid #3b82f6;
294
+ }
295
+
296
+ .validation-status.success {
297
+ background: #f0fdf4;
298
+ border-left-color: #22c55e;
299
+ color: #166534;
300
+ }
301
+
302
+ .validation-status.warning {
303
+ background: #fffbeb;
304
+ border-left-color: #f59e0b;
305
+ color: #92400e;
306
+ }
307
+
308
+ .validation-status.loading {
309
+ background: #f3f4f6;
310
+ border-left-color: #6b7280;
311
+ color: #374151;
312
+ }
313
+
314
+ .cost-row {
315
+ display: flex;
316
+ justify-content: space-between;
317
+ padding: 5px 0;
318
+ border-bottom: 1px solid #ddd;
319
+ }
320
+
321
+ .cost-row:last-child {
322
+ border-bottom: none;
323
+ font-weight: bold;
324
+ }
325
+
326
+ .loading {
327
+ display: inline-block;
328
+ width: 20px;
329
+ height: 20px;
330
+ border: 3px solid #f3f3f3;
331
+ border-top: 3px solid #667eea;
332
+ border-radius: 50%;
333
+ animation: spin 1s linear infinite;
334
+ }
335
+
336
+ @keyframes spin {
337
+ 0% { transform: rotate(0deg); }
338
+ 100% { transform: rotate(360deg); }
339
+ }
340
+ </style>
341
+ </head>
342
+ <body>
343
+ <div class="container">
344
+ <div class="header">
345
+ <img src="/static/stratifyai_wide_logo.png" alt="StratifyAI Logo">
346
+ </div>
347
+
348
+ <div class="main-grid">
349
+ <div class="sidebar">
350
+ <h3>Model Configuration</h3>
351
+
352
+ <div class="form-group">
353
+ <label for="provider">Provider</label>
354
+ <select id="provider">
355
+ <option value="">Select Provider...</option>
356
+ </select>
357
+ </div>
358
+
359
+ <div class="form-group">
360
+ <label for="model">Model</label>
361
+ <select id="model">
362
+ <option value="">Select Model...</option>
363
+ </select>
364
+ <div id="validation-status" class="validation-status" style="display: none;"></div>
365
+ </div>
366
+
367
+ <div class="form-group">
368
+ <label for="temperature">Temperature: <span id="temp-value">0.7</span></label>
369
+ <input type="range" id="temperature" min="0" max="2" step="0.1" value="0.7">
370
+ </div>
371
+
372
+ <div class="form-group">
373
+ <label for="max-tokens">Max Tokens (optional)</label>
374
+ <input type="number" id="max-tokens" placeholder="Leave empty for default">
375
+ </div>
376
+
377
+ <div class="form-group">
378
+ <button id="reset-chat">Reset Chat</button>
379
+ </div>
380
+
381
+ <div class="cost-tracker">
382
+ <h4>Model Cost Tracking</h4>
383
+ <div class="cost-row">
384
+ <span>Context:</span>
385
+ <span id="context-window">-</span>
386
+ </div>
387
+ <div class="cost-row">
388
+ <span>Calls:</span>
389
+ <span id="cost-calls">0</span>
390
+ </div>
391
+ <div class="cost-row">
392
+ <span>Tokens:</span>
393
+ <span id="cost-tokens">0 (In: 0, Out: 0)</span>
394
+ </div>
395
+ <div class="cost-row">
396
+ <span>Total Cost:</span>
397
+ <span id="cost-total">$0.0000</span>
398
+ </div>
399
+ <div class="form-group" style="margin-top: 15px; margin-bottom: 0;">
400
+ <a href="/models" class="view-models-btn" style="display: block; text-align: center; background: #667eea; color: white; padding: 10px; border-radius: 5px; text-decoration: none; font-weight: 500; transition: background 0.3s;">📋 View Model Catalog</a>
401
+ </div>
402
+ </div>
403
+ </div>
404
+
405
+ <div class="main-content">
406
+ <h3>Chat Interface</h3>
407
+ <div class="chat-container" id="chat-container"></div>
408
+
409
+ <div class="form-group">
410
+ <textarea id="user-input" placeholder="Type your message here..."></textarea>
411
+ </div>
412
+
413
+ <div class="form-group">
414
+ <label for="file-input" id="file-input-label">Attach File (optional) - Text files only</label>
415
+ <input type="file" id="file-input" accept=".txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h">
416
+ <div id="file-status" style="margin-top: 5px; font-size: 13px; color: #666;"></div>
417
+ </div>
418
+
419
+ <div class="form-group" style="display: flex; gap: 10px; align-items: center;">
420
+ <input type="checkbox" id="chunked" style="width: auto;">
421
+ <label for="chunked" style="margin: 0; cursor: pointer;">Enable Smart Chunking (reduces token usage for large files)</label>
422
+ </div>
423
+
424
+ <div class="form-group" id="chunk-size-group" style="display: none;">
425
+ <label for="chunk-size">Chunk Size (characters): <span id="chunk-size-value">50000</span></label>
426
+ <input type="range" id="chunk-size" min="10000" max="100000" step="10000" value="50000">
427
+ <div style="font-size: 12px; color: #666; margin-top: 5px;">💡 Smaller chunks = more summaries, larger chunks = fewer summaries</div>
428
+ </div>
429
+
430
+ <div class="form-group">
431
+ <button id="send-btn">Send Message</button>
432
+ </div>
433
+
434
+ <div class="message-history" id="message-history" style="margin-top: 20px; display: none;">
435
+ <h4 style="color: #666; margin-bottom: 10px; font-size: 14px;">📜 Recent Messages (Last 10)</h4>
436
+ <div id="history-list" style="max-height: 200px; overflow-y: auto; border: 1px solid #eee; border-radius: 5px; padding: 10px; background: #fafafa;"></div>
437
+ </div>
438
+ </div>
439
+ </div>
440
+ </div>
441
+
442
+ <script>
443
+ const API_BASE = 'http://localhost:8080';
444
+ let messages = [];
445
+ let totalCost = 0;
446
+ let totalCalls = 0;
447
+ let totalTokens = 0;
448
+ let totalPromptTokens = 0;
449
+ let totalCompletionTokens = 0;
450
+
451
+ // Load providers on startup
452
+ async function loadProviders() {
453
+ const response = await fetch(`${API_BASE}/api/providers`);
454
+ const providers = await response.json();
455
+
456
+ const select = document.getElementById('provider');
457
+ providers.forEach(provider => {
458
+ const option = document.createElement('option');
459
+ option.value = provider;
460
+ option.textContent = provider.charAt(0).toUpperCase() + provider.slice(1);
461
+ select.appendChild(option);
462
+ });
463
+ }
464
+
465
+ // List of reasoning models that don't support temperature
466
+ const REASONING_MODELS = [
467
+ 'o1', 'o1-mini', 'o1-preview', 'o3-mini',
468
+ 'o1-2024-12-17', 'o1-mini-2024-09-12',
469
+ 'deepseek-reasoner', 'gpt-5'
470
+ ];
471
+
472
+ // Check if model is a reasoning model
473
+ function isReasoningModel(model) {
474
+ if (!model) return false;
475
+ const modelLower = model.toLowerCase();
476
+ // Check explicit list or patterns
477
+ return REASONING_MODELS.some(rm => modelLower.includes(rm.toLowerCase())) ||
478
+ modelLower.startsWith('o1') ||
479
+ modelLower.startsWith('o3') ||
480
+ modelLower.startsWith('gpt-5') ||
481
+ modelLower.includes('reasoner') ||
482
+ modelLower.includes('reasoning');
483
+ }
484
+
485
+ // Update temperature slider state based on model
486
+ async function updateTemperatureState() {
487
+ const provider = document.getElementById('provider').value;
488
+ const model = document.getElementById('model').value;
489
+ const tempSlider = document.getElementById('temperature');
490
+ const tempLabel = tempSlider.previousElementSibling;
491
+
492
+ if (!provider || !model) return;
493
+
494
+ try {
495
+ // Fetch model metadata from API
496
+ const response = await fetch(`${API_BASE}/api/model-info/${provider}/${model}`);
497
+ if (!response.ok) {
498
+ // Fallback to pattern matching if API fails
499
+ if (isReasoningModel(model)) {
500
+ tempSlider.disabled = true;
501
+ tempSlider.value = 1.0;
502
+ document.getElementById('temp-value').textContent = '1.0 (fixed)';
503
+ tempLabel.style.opacity = '0.5';
504
+ } else {
505
+ tempSlider.disabled = false;
506
+ tempLabel.style.opacity = '1';
507
+ document.getElementById('temp-value').textContent = tempSlider.value;
508
+ }
509
+ return;
510
+ }
511
+
512
+ const modelInfo = await response.json();
513
+
514
+ // Check if model has fixed temperature
515
+ if (modelInfo.fixed_temperature !== null && modelInfo.fixed_temperature !== undefined) {
516
+ tempSlider.disabled = true;
517
+ tempSlider.value = modelInfo.fixed_temperature;
518
+ document.getElementById('temp-value').textContent = `${modelInfo.fixed_temperature} (fixed)`;
519
+ tempLabel.style.opacity = '0.5';
520
+ } else {
521
+ tempSlider.disabled = false;
522
+ tempLabel.style.opacity = '1';
523
+ document.getElementById('temp-value').textContent = tempSlider.value;
524
+ }
525
+ } catch (error) {
526
+ console.error('Error fetching model info:', error);
527
+ // Fallback to pattern matching
528
+ if (isReasoningModel(model)) {
529
+ tempSlider.disabled = true;
530
+ tempSlider.value = 1.0;
531
+ document.getElementById('temp-value').textContent = '1.0 (fixed)';
532
+ tempLabel.style.opacity = '0.5';
533
+ } else {
534
+ tempSlider.disabled = false;
535
+ tempLabel.style.opacity = '1';
536
+ document.getElementById('temp-value').textContent = tempSlider.value;
537
+ }
538
+ }
539
+ }
540
+
541
+ // Load models for selected provider
542
+ document.getElementById('provider').addEventListener('change', async (e) => {
543
+ const provider = e.target.value;
544
+ const validationStatus = document.getElementById('validation-status');
545
+
546
+ if (!provider) {
547
+ validationStatus.style.display = 'none';
548
+ return;
549
+ }
550
+
551
+ // Show loading status
552
+ validationStatus.style.display = 'block';
553
+ validationStatus.className = 'validation-status loading';
554
+ validationStatus.textContent = `🔄 Validating ${provider} models...`;
555
+
556
+ try {
557
+ const response = await fetch(`${API_BASE}/api/models/${provider}`);
558
+ const data = await response.json();
559
+
560
+ const models = data.models || data; // Support both new and old format
561
+ const validation = data.validation;
562
+
563
+ const select = document.getElementById('model');
564
+ select.innerHTML = '<option value="">Select Model...</option>';
565
+
566
+ // Check if we have models
567
+ if (models.length === 0) {
568
+ // No models available
569
+ const option = document.createElement('option');
570
+ option.value = '';
571
+ option.textContent = 'No validated models available';
572
+ option.disabled = true;
573
+ select.appendChild(option);
574
+
575
+ validationStatus.className = 'validation-status warning';
576
+ if (validation && validation.error) {
577
+ validationStatus.textContent = `⚠️ ${validation.error}`;
578
+ } else {
579
+ validationStatus.textContent = `⚠️ No models could be validated. Check API key configuration.`;
580
+ }
581
+ } else {
582
+ // Group models by category
583
+ const groupedModels = {};
584
+ models.forEach(model => {
585
+ // Handle both old format (string) and new format (object)
586
+ const modelId = typeof model === 'string' ? model : model.id;
587
+ const displayName = typeof model === 'string' ? model : model.display_name;
588
+ const description = typeof model === 'string' ? '' : model.description;
589
+ const category = typeof model === 'string' ? '' : model.category;
590
+ const supportsVision = typeof model === 'string' ? false : model.supports_vision;
591
+ const reasoningModel = typeof model === 'string' ? false : model.reasoning_model;
592
+
593
+ if (!groupedModels[category]) {
594
+ groupedModels[category] = [];
595
+ }
596
+ groupedModels[category].push({ modelId, displayName, description, supportsVision, reasoningModel });
597
+ });
598
+
599
+ // Add models to dropdown with categories
600
+ Object.keys(groupedModels).forEach(category => {
601
+ // Add category header if not empty
602
+ if (category) {
603
+ const categoryOption = document.createElement('option');
604
+ categoryOption.disabled = true;
605
+ categoryOption.textContent = `── ${category} ──`;
606
+ categoryOption.style.fontWeight = 'bold';
607
+ categoryOption.style.color = '#666';
608
+ select.appendChild(categoryOption);
609
+ }
610
+
611
+ // Add models in this category
612
+ groupedModels[category].forEach(({ modelId, displayName, description, supportsVision, reasoningModel }) => {
613
+ const option = document.createElement('option');
614
+ option.value = modelId;
615
+
616
+ // Build display text with labels
617
+ let text = displayName;
618
+ if (description) {
619
+ text += ` - ${description}`;
620
+ }
621
+
622
+ option.textContent = text;
623
+ option.title = description; // Tooltip
624
+ select.appendChild(option);
625
+ });
626
+ });
627
+
628
+ // Display validation feedback
629
+ if (validation) {
630
+ if (validation.error) {
631
+ validationStatus.className = 'validation-status warning';
632
+ validationStatus.textContent = `⚠️ Default models displayed. Could not validate models.`;
633
+ } else {
634
+ validationStatus.className = 'validation-status success';
635
+ validationStatus.textContent = `✓ Validated ${models.length} models (${validation.validation_time_ms}ms)`;
636
+ }
637
+ } else {
638
+ // Fallback if no validation data
639
+ validationStatus.style.display = 'none';
640
+ }
641
+ }
642
+ } catch (error) {
643
+ console.error('Error loading models:', error);
644
+ validationStatus.className = 'validation-status warning';
645
+ validationStatus.textContent = `⚠️ Error loading models: ${error.message}`;
646
+ }
647
+
648
+ // Reset temperature state when provider changes
649
+ updateTemperatureState();
650
+ });
651
+
652
+ // Update temperature state and context window when model changes
653
+ document.getElementById('model').addEventListener('change', async () => {
654
+ await updateTemperatureState();
655
+ await updateContextWindow();
656
+ });
657
+
658
+ // Update context window display and file input capabilities
659
+ async function updateContextWindow() {
660
+ const provider = document.getElementById('provider').value;
661
+ const model = document.getElementById('model').value;
662
+ const contextDisplay = document.getElementById('context-window');
663
+ const fileInput = document.getElementById('file-input');
664
+ const fileLabel = document.getElementById('file-input-label');
665
+
666
+ if (!provider || !model) {
667
+ contextDisplay.textContent = '-';
668
+ // Reset to text files only
669
+ fileInput.accept = '.txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h';
670
+ fileLabel.textContent = 'Attach File (optional) - Text files only';
671
+ return;
672
+ }
673
+
674
+ try {
675
+ const response = await fetch(`${API_BASE}/api/model-info/${provider}/${model}`);
676
+ if (response.ok) {
677
+ const modelInfo = await response.json();
678
+
679
+ // Update context window
680
+ const context = modelInfo.context;
681
+ if (context && typeof context === 'number' && context > 0) {
682
+ contextDisplay.textContent = `${context.toLocaleString()} tokens`;
683
+ } else {
684
+ contextDisplay.textContent = 'N/A';
685
+ }
686
+
687
+ // Update file input based on vision support
688
+ if (modelInfo.supports_vision) {
689
+ fileInput.accept = '.txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h,.jpg,.jpeg,.png,.gif,.webp';
690
+ fileLabel.textContent = 'Attach File (optional) - Text files and images (vision enabled)';
691
+ } else {
692
+ fileInput.accept = '.txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h';
693
+ fileLabel.textContent = 'Attach File (optional) - Text files only';
694
+ }
695
+ } else {
696
+ contextDisplay.textContent = '-';
697
+ // Reset to text files only on error
698
+ fileInput.accept = '.txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h';
699
+ fileLabel.textContent = 'Attach File (optional) - Text files only';
700
+ }
701
+ } catch (error) {
702
+ console.error('Error fetching model info:', error);
703
+ contextDisplay.textContent = '-';
704
+ // Reset to text files only on error
705
+ fileInput.accept = '.txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h';
706
+ fileLabel.textContent = 'Attach File (optional) - Text files only';
707
+ }
708
+ }
709
+
710
+ // Update temperature display
711
+ document.getElementById('temperature').addEventListener('input', (e) => {
712
+ document.getElementById('temp-value').textContent = e.target.value;
713
+ });
714
+
715
+ // Handle chunking checkbox toggle
716
+ document.getElementById('chunked').addEventListener('change', (e) => {
717
+ const chunkSizeGroup = document.getElementById('chunk-size-group');
718
+ chunkSizeGroup.style.display = e.target.checked ? 'block' : 'none';
719
+ });
720
+
721
+ // Update chunk size display
722
+ document.getElementById('chunk-size').addEventListener('input', (e) => {
723
+ document.getElementById('chunk-size-value').textContent = parseInt(e.target.value).toLocaleString();
724
+ });
725
+
726
+ // Configure marked for code highlighting
727
+ marked.setOptions({
728
+ highlight: function(code, lang) {
729
+ if (lang && hljs.getLanguage(lang)) {
730
+ return hljs.highlight(code, { language: lang }).value;
731
+ }
732
+ return hljs.highlightAuto(code).value;
733
+ },
734
+ breaks: true,
735
+ gfm: true
736
+ });
737
+
738
+ // Update message history display
739
+ function updateMessageHistory() {
740
+ const historyContainer = document.getElementById('message-history');
741
+ const historyList = document.getElementById('history-list');
742
+
743
+ // Filter out system messages and get last 10
744
+ const recentMessages = messages.slice(-10);
745
+
746
+ if (recentMessages.length === 0) {
747
+ historyContainer.style.display = 'none';
748
+ return;
749
+ }
750
+
751
+ historyContainer.style.display = 'block';
752
+
753
+ let html = '';
754
+ for (const msg of recentMessages) {
755
+ const roleIcon = msg.role === 'user' ? '👤' : '🤖';
756
+ const roleColor = msg.role === 'user' ? '#667eea' : '#22c55e';
757
+ // Truncate long messages
758
+ const truncated = msg.content.length > 100 ? msg.content.substring(0, 100) + '...' : msg.content;
759
+ // Escape HTML
760
+ const escaped = truncated.replace(/</g, '&lt;').replace(/>/g, '&gt;');
761
+ html += `<div style="padding: 6px 0; border-bottom: 1px solid #eee; font-size: 13px;">
762
+ <span style="color: ${roleColor}; font-weight: 500;">${roleIcon} ${msg.role}:</span>
763
+ <span style="color: #444;">${escaped}</span>
764
+ </div>`;
765
+ }
766
+
767
+ historyList.innerHTML = html;
768
+ }
769
+
770
+ // Add message to chat
771
+ function addMessage(role, content, details = null) {
772
+ const container = document.getElementById('chat-container');
773
+ const messageDiv = document.createElement('div');
774
+ messageDiv.className = `message ${role}`;
775
+
776
+ // Add error details if provided
777
+ if (details && role === 'error') {
778
+ // Create main message text node
779
+ const mainText = document.createTextNode(content);
780
+ messageDiv.appendChild(mainText);
781
+
782
+ // Create details section
783
+ const detailsDiv = document.createElement('div');
784
+ detailsDiv.className = 'error-details';
785
+ detailsDiv.textContent = details;
786
+ messageDiv.appendChild(detailsDiv);
787
+ } else if (role === 'assistant') {
788
+ // Render markdown for assistant messages
789
+ const markdownDiv = document.createElement('div');
790
+ markdownDiv.className = 'markdown-content';
791
+ markdownDiv.innerHTML = marked.parse(content);
792
+ messageDiv.appendChild(markdownDiv);
793
+ } else {
794
+ // For user/system messages, just set text content
795
+ messageDiv.textContent = content;
796
+ }
797
+
798
+ container.appendChild(messageDiv);
799
+ container.scrollTop = container.scrollHeight;
800
+
801
+ // Don't add error messages to conversation history
802
+ if (role !== 'error' && role !== 'system') {
803
+ messages.push({role, content});
804
+ updateMessageHistory();
805
+ }
806
+ }
807
+
808
+ // Handle file selection
809
+ let selectedFile = null;
810
+ let fileContent = null;
811
+
812
+ document.getElementById('file-input').addEventListener('change', async (e) => {
813
+ const file = e.target.files[0];
814
+ const fileStatus = document.getElementById('file-status');
815
+
816
+ if (!file) {
817
+ selectedFile = null;
818
+ fileContent = null;
819
+ fileStatus.textContent = '';
820
+ return;
821
+ }
822
+
823
+ // Check file size (5MB limit)
824
+ const maxSize = 5 * 1024 * 1024; // 5MB
825
+ if (file.size > maxSize) {
826
+ fileStatus.textContent = `⚠️ File too large (${(file.size / 1024 / 1024).toFixed(2)} MB). Max: 5 MB`;
827
+ fileStatus.style.color = '#c33';
828
+ e.target.value = ''; // Clear selection
829
+ selectedFile = null;
830
+ fileContent = null;
831
+ return;
832
+ }
833
+
834
+ // Read file content - handle text and image files differently
835
+ try {
836
+ const fileType = file.type;
837
+ const isImage = fileType.startsWith('image/');
838
+
839
+ if (isImage) {
840
+ // Read image as base64 data URL
841
+ const reader = new FileReader();
842
+ await new Promise((resolve, reject) => {
843
+ reader.onload = (e) => {
844
+ fileContent = e.target.result; // base64 data URL
845
+ resolve();
846
+ };
847
+ reader.onerror = reject;
848
+ reader.readAsDataURL(file);
849
+ });
850
+ selectedFile = file;
851
+ const sizeStr = file.size < 1024 ?
852
+ `${file.size} bytes` :
853
+ file.size < 1024 * 1024 ?
854
+ `${(file.size / 1024).toFixed(1)} KB` :
855
+ `${(file.size / 1024 / 1024).toFixed(2)} MB`;
856
+ fileStatus.textContent = `✓ ${file.name} (${sizeStr}, image)`;
857
+ fileStatus.style.color = '#22c55e';
858
+ } else {
859
+ // Read text file
860
+ fileContent = await file.text();
861
+ selectedFile = file;
862
+ const sizeStr = file.size < 1024 ?
863
+ `${file.size} bytes` :
864
+ file.size < 1024 * 1024 ?
865
+ `${(file.size / 1024).toFixed(1)} KB` :
866
+ `${(file.size / 1024 / 1024).toFixed(2)} MB`;
867
+ fileStatus.textContent = `✓ ${file.name} (${sizeStr}, ${fileContent.length.toLocaleString()} chars)`;
868
+ fileStatus.style.color = '#22c55e';
869
+ }
870
+ } catch (error) {
871
+ fileStatus.textContent = `⚠️ Error reading file: ${error.message}`;
872
+ fileStatus.style.color = '#c33';
873
+ e.target.value = '';
874
+ selectedFile = null;
875
+ fileContent = null;
876
+ }
877
+ });
878
+
879
+ // Send message
880
+ document.getElementById('send-btn').addEventListener('click', async () => {
881
+ const provider = document.getElementById('provider').value;
882
+ const model = document.getElementById('model').value;
883
+ const input = document.getElementById('user-input');
884
+ const userMessage = input.value.trim();
885
+ const fileInput = document.getElementById('file-input');
886
+ const fileStatus = document.getElementById('file-status');
887
+
888
+ if (!provider || !model) {
889
+ alert('Please select provider and model');
890
+ return;
891
+ }
892
+
893
+ if (!userMessage && !fileContent) {
894
+ alert('Please enter a message or attach a file');
895
+ return;
896
+ }
897
+
898
+ // Validate that vision models are used for image files
899
+ if (fileContent && selectedFile) {
900
+ const isImage = selectedFile.type.startsWith('image/');
901
+ if (isImage) {
902
+ // Check if model supports vision
903
+ try {
904
+ const response = await fetch(`${API_BASE}/api/model-info/${provider}/${model}`);
905
+ if (response.ok) {
906
+ const modelInfo = await response.json();
907
+ if (!modelInfo.supports_vision) {
908
+ alert(`❌ Vision Not Supported\n\nThe model "${model}" cannot process image files.\n\nPlease either:\n• Select a vision-capable model (e.g., GPT-4 Vision, Claude 3), or\n• Remove the image attachment`);
909
+ return;
910
+ }
911
+ }
912
+ } catch (error) {
913
+ console.error('Error checking vision support:', error);
914
+ // Continue anyway - let the API handle it
915
+ }
916
+ }
917
+ }
918
+
919
+ // Build display message and actual content
920
+ let displayMessage = userMessage;
921
+ let actualContent = userMessage;
922
+ let apiFileContent = null;
923
+ let apiFileName = null;
924
+
925
+ if (fileContent) {
926
+ const isImage = selectedFile.type.startsWith('image/');
927
+
928
+ if (isImage) {
929
+ // For images, show indicator in display
930
+ displayMessage = displayMessage ?
931
+ `${displayMessage}\n\n🖼️ Image attached: ${selectedFile.name}` :
932
+ `🖼️ Image attached: ${selectedFile.name}`;
933
+
934
+ // Extract base64 data from data URL (format: data:image/jpeg;base64,<data>)
935
+ const base64Data = fileContent.split(',')[1]; // Get part after comma
936
+ const mimeType = fileContent.split(';')[0].split(':')[1]; // Extract mime type
937
+
938
+ // Format for vision models: [IMAGE:mime_type]\nbase64_data
939
+ const imageContent = `[IMAGE:${mimeType}]\n${base64Data}`;
940
+
941
+ actualContent = actualContent ?
942
+ `${actualContent}\n\n${imageContent}` :
943
+ imageContent;
944
+ } else {
945
+ // For text files, set API file parameters for chunking support
946
+ displayMessage = displayMessage ?
947
+ `${displayMessage}\n\n📎 Attached: ${selectedFile.name}` :
948
+ `📎 Attached: ${selectedFile.name}`;
949
+
950
+ // Check if chunking is enabled
951
+ const chunked = document.getElementById('chunked').checked;
952
+ if (chunked) {
953
+ // Pass file to API for chunking
954
+ apiFileContent = fileContent;
955
+ apiFileName = selectedFile.name;
956
+ displayMessage += ` (chunking enabled)`;
957
+ } else {
958
+ // Combine message with file content directly
959
+ actualContent = actualContent ?
960
+ `${actualContent}\n\n[File: ${selectedFile.name}]\n\n${fileContent}` :
961
+ `[File: ${selectedFile.name}]\n\n${fileContent}`;
962
+ }
963
+ }
964
+ }
965
+
966
+ // Add user message to chat display
967
+ const container = document.getElementById('chat-container');
968
+ const messageDiv = document.createElement('div');
969
+ messageDiv.className = 'message user';
970
+ messageDiv.textContent = displayMessage;
971
+ container.appendChild(messageDiv);
972
+ container.scrollTop = container.scrollHeight;
973
+
974
+ // Add actual content (with file) to conversation history
975
+ messages.push({role: 'user', content: actualContent});
976
+
977
+ input.value = '';
978
+
979
+ // Clear file attachment
980
+ if (fileContent) {
981
+ fileInput.value = '';
982
+ fileStatus.textContent = '';
983
+ selectedFile = null;
984
+ fileContent = null;
985
+ }
986
+
987
+ // Disable send button
988
+ const sendBtn = document.getElementById('send-btn');
989
+ sendBtn.disabled = true;
990
+ sendBtn.innerHTML = '<span class="loading"></span> Sending...';
991
+
992
+ try {
993
+ // Prepare request
994
+ let temperature = parseFloat(document.getElementById('temperature').value);
995
+ const maxTokensInput = document.getElementById('max-tokens').value;
996
+ const maxTokens = maxTokensInput ? parseInt(maxTokensInput) : null;
997
+
998
+ // Force temperature to 1.0 for reasoning models
999
+ if (isReasoningModel(model)) {
1000
+ temperature = 1.0;
1001
+ }
1002
+
1003
+ // Build request body with optional chunking parameters
1004
+ const requestBody = {
1005
+ provider,
1006
+ model,
1007
+ messages: messages,
1008
+ temperature,
1009
+ max_tokens: maxTokens,
1010
+ };
1011
+
1012
+ // Add file and chunking parameters if file is being chunked
1013
+ if (apiFileContent && apiFileName) {
1014
+ requestBody.file_content = apiFileContent;
1015
+ requestBody.file_name = apiFileName;
1016
+ requestBody.chunked = true;
1017
+ requestBody.chunk_size = parseInt(document.getElementById('chunk-size').value);
1018
+ }
1019
+
1020
+ const response = await fetch(`${API_BASE}/api/chat`, {
1021
+ method: 'POST',
1022
+ headers: {'Content-Type': 'application/json'},
1023
+ body: JSON.stringify(requestBody)
1024
+ });
1025
+
1026
+ const data = await response.json();
1027
+
1028
+ // Check for API errors
1029
+ if (!response.ok) {
1030
+ // Handle structured error response (object) or simple error (string)
1031
+ let errorMsg;
1032
+ let errorData = null;
1033
+
1034
+ if (typeof data.detail === 'object' && data.detail !== null) {
1035
+ // Structured error response from our API
1036
+ errorData = data.detail;
1037
+ errorMsg = errorData.detail || errorData.message || JSON.stringify(data.detail);
1038
+ } else {
1039
+ // Simple string error
1040
+ errorMsg = data.detail || data.error || 'Unknown error occurred';
1041
+ }
1042
+
1043
+ let userFriendlyMsg = 'API Error';
1044
+ let details = errorMsg;
1045
+
1046
+ // Parse common error patterns
1047
+ if (errorData && errorData.error === 'input_too_long') {
1048
+ // Enhanced token limit error with suggestions
1049
+ userFriendlyMsg = '📊 Input Too Large';
1050
+
1051
+ const msg = errorData.message || errorMsg;
1052
+ const suggestion = errorData.suggestion || '';
1053
+ const tokens = errorData.estimated_tokens;
1054
+ const limit = errorData.api_limit || errorData.model_limit;
1055
+ const chunkingEnabled = errorData.chunking_enabled;
1056
+
1057
+ details = `${msg}\n\n`;
1058
+
1059
+ if (suggestion) {
1060
+ details += `💡 Suggestions:\n${suggestion}\n\n`;
1061
+ }
1062
+
1063
+ if (!chunkingEnabled) {
1064
+ details += `⚠️ TIP: Enable the 'Smart Chunking' checkbox below the file upload to automatically reduce token usage by 40-90%.`;
1065
+ }
1066
+ } else if (errorData && errorData.error === 'content_too_large') {
1067
+ // File exceeds system maximum
1068
+ userFriendlyMsg = '🚫 File Too Large';
1069
+ details = `${errorData.message}\n\n${errorData.suggestion || ''}`;
1070
+ } else if (errorMsg.includes('temperature') && errorMsg.includes('not support')) {
1071
+ const selectedTemp = parseFloat(document.getElementById('temperature').value);
1072
+ userFriendlyMsg = `${model} does not support temperature ${selectedTemp}. The default value is 1.0.`;
1073
+ details = errorMsg;
1074
+ } else if (errorMsg.includes('authentication') || errorMsg.includes('api key')) {
1075
+ userFriendlyMsg = '🔑 Authentication Error';
1076
+ details = `API key is missing or invalid for ${provider}.\n\nError: ${errorMsg}\n\nPlease check your .env file.`;
1077
+ } else if (errorMsg.includes('rate limit')) {
1078
+ userFriendlyMsg = '⏱️ Rate Limit Exceeded';
1079
+ details = `Too many requests to ${provider}.\n\nError: ${errorMsg}\n\nPlease wait a moment and try again.`;
1080
+ } else if (errorMsg.includes('model') && errorMsg.includes('not found')) {
1081
+ userFriendlyMsg = '🔍 Model Not Found';
1082
+ details = `The model \"${model}\" is not available.\n\nError: ${errorMsg}`;
1083
+ }
1084
+
1085
+ addMessage('error', userFriendlyMsg, details);
1086
+ return;
1087
+ }
1088
+
1089
+ // Add assistant response
1090
+ addMessage('assistant', data.content);
1091
+
1092
+ // Update cost tracking
1093
+ totalCalls++;
1094
+ totalTokens += data.usage?.total_tokens || 0;
1095
+ totalPromptTokens += data.usage?.prompt_tokens || 0;
1096
+ totalCompletionTokens += data.usage?.completion_tokens || 0;
1097
+ totalCost += data.cost_usd || 0;
1098
+
1099
+ document.getElementById('cost-calls').textContent = totalCalls;
1100
+ document.getElementById('cost-tokens').textContent =
1101
+ `${totalTokens.toLocaleString()} (In: ${totalPromptTokens.toLocaleString()}, Out: ${totalCompletionTokens.toLocaleString()})`;
1102
+ document.getElementById('cost-total').textContent = `$${totalCost.toFixed(4)}`;
1103
+
1104
+ } catch (error) {
1105
+ // Network or parsing errors
1106
+ addMessage('error', '🌐 Connection Error', `Failed to communicate with the API.\n\nError: ${error.message}\n\nPlease check if the server is running.`);
1107
+ } finally {
1108
+ sendBtn.disabled = false;
1109
+ sendBtn.textContent = 'Send Message';
1110
+ }
1111
+ });
1112
+
1113
+ // Reset chat
1114
+ document.getElementById('reset-chat').addEventListener('click', () => {
1115
+ messages = [];
1116
+ document.getElementById('chat-container').innerHTML = '';
1117
+ updateMessageHistory();
1118
+ addMessage('system', 'Chat reset. Start a new conversation.');
1119
+ });
1120
+
1121
+ // Initialize
1122
+ loadProviders();
1123
+ addMessage('system', 'Welcome to StratifyAI! Select a provider and model to start chatting.');
1124
+ </script>
1125
+ </body>
1126
+ </html>