llms-py 3.0.0b1__py3-none-any.whl → 3.0.0b2__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.
- llms/__pycache__/__init__.cpython-312.pyc +0 -0
- llms/__pycache__/__init__.cpython-313.pyc +0 -0
- llms/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/__pycache__/__main__.cpython-312.pyc +0 -0
- llms/__pycache__/__main__.cpython-314.pyc +0 -0
- llms/__pycache__/llms.cpython-312.pyc +0 -0
- llms/__pycache__/main.cpython-312.pyc +0 -0
- llms/__pycache__/main.cpython-313.pyc +0 -0
- llms/__pycache__/main.cpython-314.pyc +0 -0
- llms/__pycache__/plugins.cpython-314.pyc +0 -0
- llms/index.html +25 -56
- llms/llms.json +2 -2
- llms/main.py +452 -93
- llms/providers.json +1 -1
- llms/ui/App.mjs +25 -4
- llms/ui/Avatar.mjs +3 -2
- llms/ui/ChatPrompt.mjs +43 -52
- llms/ui/Main.mjs +87 -98
- llms/ui/OAuthSignIn.mjs +2 -33
- llms/ui/ProviderStatus.mjs +7 -8
- llms/ui/Recents.mjs +10 -9
- llms/ui/Sidebar.mjs +2 -1
- llms/ui/SignIn.mjs +7 -6
- llms/ui/ai.mjs +9 -41
- llms/ui/app.css +137 -138
- llms/ui/index.mjs +213 -0
- llms/ui/{ModelSelector.mjs → model-selector.mjs} +193 -200
- llms/ui/tailwind.input.css +441 -79
- llms/ui/threadStore.mjs +17 -6
- llms/ui/utils.mjs +1 -0
- {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/METADATA +1 -1
- llms_py-3.0.0b2.dist-info/RECORD +58 -0
- llms/ui/SystemPromptEditor.mjs +0 -31
- llms/ui/SystemPromptSelector.mjs +0 -56
- llms_py-3.0.0b1.dist-info/RECORD +0 -49
- {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/WHEEL +0 -0
- {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ref, computed, watch,
|
|
2
|
-
import { useRouter, useRoute } from "vue-router"
|
|
1
|
+
import { ref, computed, watch, inject, onMounted } from "vue"
|
|
3
2
|
import ProviderStatus from "./ProviderStatus.mjs"
|
|
4
3
|
import ProviderIcon from "./ProviderIcon.mjs"
|
|
4
|
+
import { storageObject } from "./utils.mjs"
|
|
5
5
|
|
|
6
6
|
const SORT_OPTIONS = [
|
|
7
7
|
{ id: 'name', label: 'Name' },
|
|
@@ -13,90 +13,62 @@ const SORT_OPTIONS = [
|
|
|
13
13
|
{ id: 'context', label: 'Context Limit' },
|
|
14
14
|
]
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
const I = x => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${x}</svg>`
|
|
17
|
+
const modalityIcons = {
|
|
18
|
+
text: I(`<polyline points="4,7 4,4 20,4 20,7"></polyline><line x1="9" y1="20" x2="15" y2="20"></line><line x1="12" y1="4" x2="12" y2="20"></line>`),
|
|
19
|
+
image: I(`<rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><circle cx="9" cy="9" r="2"></circle><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>`),
|
|
20
|
+
audio: I(`<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="m19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>`),
|
|
21
|
+
video: I(`<path d="m22 8-6 4 6 4V8Z"></path><rect width="14" height="12" x="2" y="6" rx="2" ry="2"></rect>`),
|
|
22
|
+
pdf: I(`<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14,2 14,8 20,8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10,9 9,9 8,9"></polyline>`),
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const STORAGE_KEY = 'llms.modelSelector'
|
|
25
26
|
const FAVORITES_KEY = 'llms.favorites'
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// Formatting helpers
|
|
29
|
+
const numFmt = new Intl.NumberFormat()
|
|
30
|
+
const currFmt = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
|
|
31
|
+
|
|
32
|
+
function formatCost(cost) {
|
|
33
|
+
if (cost == null) return '-'
|
|
34
|
+
const val = parseFloat(cost)
|
|
35
|
+
if (val === 0) return 'Free'
|
|
36
|
+
return currFmt.format(val)
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
function
|
|
36
|
-
|
|
39
|
+
function formatNumber(num) {
|
|
40
|
+
if (num == null) return '-'
|
|
41
|
+
return numFmt.format(num)
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
|
|
44
|
+
function formatShortNumber(num) {
|
|
45
|
+
if (num == null) return '-'
|
|
46
|
+
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
|
|
47
|
+
if (num >= 1000) return (num / 1000).toFixed(0) + 'K'
|
|
48
|
+
return numFmt.format(num)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getModelModalities(model) {
|
|
52
|
+
const mods = new Set()
|
|
53
|
+
const input = model.modalities?.input || []
|
|
54
|
+
const output = model.modalities?.output || []
|
|
55
|
+
|
|
56
|
+
// Collect all modalities
|
|
57
|
+
input.forEach(m => mods.add(m))
|
|
58
|
+
output.forEach(m => mods.add(m))
|
|
59
|
+
|
|
60
|
+
// Filter out text and ensure we only show known icons
|
|
61
|
+
const allowed = ['image', 'audio', 'video', 'pdf']
|
|
62
|
+
return Array.from(mods).filter(m => m !== 'text' && allowed.includes(m)).sort()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ModelSelectorModal = {
|
|
40
66
|
components: {
|
|
41
|
-
ProviderStatus,
|
|
42
67
|
ProviderIcon,
|
|
43
68
|
},
|
|
44
69
|
template: `
|
|
45
|
-
<!-- Model Selector Button -->
|
|
46
|
-
<div class="pl-1 flex space-x-2">
|
|
47
|
-
<button type="button" @click="openDialog"
|
|
48
|
-
class="flex items-center space-x-2 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800 text-sm text-gray-700 dark:text-gray-300 transition-colors min-w-48 max-w-96"
|
|
49
|
-
@mouseenter="showTooltip = true"
|
|
50
|
-
@mouseleave="showTooltip = false">
|
|
51
|
-
<ProviderIcon v-if="selectedModelObj?.provider" :provider="selectedModelObj.provider" class="size-5 flex-shrink-0" />
|
|
52
|
-
<span class="truncate flex-1 text-left">{{ selectedModelObj?.name || 'Select Model...' }}</span>
|
|
53
|
-
<svg class="size-4 flex-shrink-0 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
54
|
-
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
|
|
55
|
-
</svg>
|
|
56
|
-
</button>
|
|
57
|
-
|
|
58
|
-
<!-- Info Tooltip (on hover) -->
|
|
59
|
-
<div v-if="showTooltip && selectedModelObj"
|
|
60
|
-
class="absolute z-50 mt-12 ml-0 p-3 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 text-sm w-72">
|
|
61
|
-
<div class="font-semibold text-gray-900 dark:text-gray-100 mb-2">{{ selectedModelObj.name }}</div>
|
|
62
|
-
<div class="text-xs text-gray-500 dark:text-gray-400 mb-2">{{ selectedModelObj.provider }}</div>
|
|
63
|
-
|
|
64
|
-
<div v-if="selectedModelObj.cost" class="mb-2">
|
|
65
|
-
<div class="text-xs font-medium text-gray-700 dark:text-gray-300">Cost per 1M tokens:</div>
|
|
66
|
-
<div class="text-xs text-gray-600 dark:text-gray-400 ml-2">
|
|
67
|
-
Input: {{ formatCost(selectedModelObj.cost.input) }} · Output: {{ formatCost(selectedModelObj.cost.output) }}
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
<div v-if="selectedModelObj.limit" class="mb-2">
|
|
72
|
-
<div class="text-xs font-medium text-gray-700 dark:text-gray-300">Limits:</div>
|
|
73
|
-
<div class="text-xs text-gray-600 dark:text-gray-400 ml-2">
|
|
74
|
-
Context: {{ formatNumber(selectedModelObj.limit.context) }} · Output: {{ formatNumber(selectedModelObj.limit.output) }}
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
<div v-if="selectedModelObj.knowledge" class="text-xs text-gray-600 dark:text-gray-400">
|
|
79
|
-
Knowledge: {{ selectedModelObj.knowledge }}
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
<div class="flex flex-wrap gap-1 mt-2">
|
|
83
|
-
<span v-if="selectedModelObj.reasoning" class="px-1.5 py-0.5 text-xs rounded bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300">reasoning</span>
|
|
84
|
-
<span v-if="selectedModelObj.tool_call" class="px-1.5 py-0.5 text-xs rounded bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300">tools</span>
|
|
85
|
-
|
|
86
|
-
<!-- Modality Icons -->
|
|
87
|
-
<span v-for="mod in getModelModalities(selectedModelObj)" :key="mod"
|
|
88
|
-
class="inline-flex items-center justify-center p-0.5 text-gray-400 dark:text-gray-500"
|
|
89
|
-
:title="mod"
|
|
90
|
-
v-html="modalityIcons[mod]">
|
|
91
|
-
</span>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
<ProviderStatus @updated="$emit('updated', $event)" />
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
70
|
<!-- Dialog Overlay -->
|
|
99
|
-
<div
|
|
71
|
+
<div class="fixed inset-0 z-50 overflow-hidden" @keydown.escape="closeDialog">
|
|
100
72
|
<!-- Backdrop -->
|
|
101
73
|
<div class="fixed inset-0 bg-black/50 transition-opacity" @click="closeDialog"></div>
|
|
102
74
|
|
|
@@ -311,29 +283,21 @@ export default {
|
|
|
311
283
|
</div>
|
|
312
284
|
</div>
|
|
313
285
|
</div>
|
|
314
|
-
</div>
|
|
286
|
+
</div>
|
|
315
287
|
`,
|
|
316
|
-
emits: ['
|
|
317
|
-
props: {
|
|
318
|
-
models: Array,
|
|
319
|
-
modelValue: String,
|
|
320
|
-
},
|
|
288
|
+
emits: ['done'],
|
|
321
289
|
setup(props, { emit }) {
|
|
322
|
-
const
|
|
323
|
-
const route = useRoute()
|
|
324
|
-
|
|
325
|
-
const isOpen = ref(false)
|
|
326
|
-
const showTooltip = ref(false)
|
|
290
|
+
const ctx = inject('ctx')
|
|
327
291
|
const searchQuery = ref('')
|
|
328
292
|
const searchInput = ref(null)
|
|
329
293
|
|
|
330
294
|
// Load preferences
|
|
331
|
-
const prefs =
|
|
295
|
+
const prefs = storageObject(STORAGE_KEY)
|
|
332
296
|
const sortBy = ref(prefs.sortBy || 'name')
|
|
333
297
|
const sortAsc = ref(prefs.sortAsc !== false)
|
|
334
298
|
const selectedProviders = ref(new Set())
|
|
335
299
|
const selectedModalities = ref(new Set())
|
|
336
|
-
const
|
|
300
|
+
const models = computed(() => ctx.state.models || [])
|
|
337
301
|
|
|
338
302
|
// Favorites State
|
|
339
303
|
const favorites = ref([])
|
|
@@ -347,24 +311,18 @@ export default {
|
|
|
347
311
|
|
|
348
312
|
const sortOptions = SORT_OPTIONS
|
|
349
313
|
|
|
350
|
-
// Get selected model object
|
|
351
|
-
const selectedModelObj = computed(() => {
|
|
352
|
-
if (!props.modelValue || !props.models) return null
|
|
353
|
-
return props.models.find(m => m.name === props.modelValue) || props.models.find(m => m.id === props.modelValue)
|
|
354
|
-
})
|
|
355
|
-
|
|
356
314
|
// Get unique providers
|
|
357
315
|
const uniqueProviders = computed(() => {
|
|
358
|
-
if (!
|
|
359
|
-
const providers = [...new Set(
|
|
316
|
+
if (!models.value) return []
|
|
317
|
+
const providers = [...new Set(models.value.map(m => m.provider))].filter(Boolean)
|
|
360
318
|
return providers.sort()
|
|
361
319
|
})
|
|
362
320
|
|
|
363
321
|
// Provider counts
|
|
364
322
|
const providerCounts = computed(() => {
|
|
365
|
-
if (!
|
|
323
|
+
if (!models.value) return {}
|
|
366
324
|
const counts = {}
|
|
367
|
-
|
|
325
|
+
models.value.forEach(m => {
|
|
368
326
|
if (m.provider) {
|
|
369
327
|
counts[m.provider] = (counts[m.provider] || 0) + 1
|
|
370
328
|
}
|
|
@@ -384,8 +342,8 @@ export default {
|
|
|
384
342
|
|
|
385
343
|
// Unavailable favorites (provider disabled or model removed)
|
|
386
344
|
const unavailableFavorites = computed(() => {
|
|
387
|
-
if (!
|
|
388
|
-
const availableKeys = new Set(
|
|
345
|
+
if (!models.value) return []
|
|
346
|
+
const availableKeys = new Set(models.value.map(getModelKey))
|
|
389
347
|
const missingKeys = favorites.value.filter(key => !availableKeys.has(key))
|
|
390
348
|
|
|
391
349
|
return missingKeys.map(key => {
|
|
@@ -403,9 +361,9 @@ export default {
|
|
|
403
361
|
|
|
404
362
|
// Filter and sort models
|
|
405
363
|
const filteredModels = computed(() => {
|
|
406
|
-
if (!
|
|
364
|
+
if (!models.value) return []
|
|
407
365
|
|
|
408
|
-
let result = [...
|
|
366
|
+
let result = [...models.value]
|
|
409
367
|
|
|
410
368
|
// Filter by Tab
|
|
411
369
|
if (activeTab.value === 'favorites') {
|
|
@@ -492,63 +450,17 @@ export default {
|
|
|
492
450
|
return result
|
|
493
451
|
})
|
|
494
452
|
|
|
495
|
-
// Formatting helpers
|
|
496
|
-
const numFmt = new Intl.NumberFormat()
|
|
497
|
-
const currFmt = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
|
|
498
|
-
|
|
499
|
-
function formatCost(cost) {
|
|
500
|
-
if (cost == null) return '-'
|
|
501
|
-
const val = parseFloat(cost)
|
|
502
|
-
if (val === 0) return 'Free'
|
|
503
|
-
return currFmt.format(val)
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function formatNumber(num) {
|
|
507
|
-
if (num == null) return '-'
|
|
508
|
-
return numFmt.format(num)
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function formatShortNumber(num) {
|
|
512
|
-
if (num == null) return '-'
|
|
513
|
-
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
|
|
514
|
-
if (num >= 1000) return (num / 1000).toFixed(0) + 'K'
|
|
515
|
-
return numFmt.format(num)
|
|
516
|
-
}
|
|
517
|
-
|
|
518
453
|
function isFreeModel(model) {
|
|
519
454
|
return model.cost && parseFloat(model.cost.input) === 0 && parseFloat(model.cost.output) === 0
|
|
520
455
|
}
|
|
521
456
|
|
|
522
|
-
function
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const output = model.modalities?.output || []
|
|
526
|
-
|
|
527
|
-
// Collect all modalities
|
|
528
|
-
input.forEach(m => mods.add(m))
|
|
529
|
-
output.forEach(m => mods.add(m))
|
|
530
|
-
|
|
531
|
-
// Filter out text and ensure we only show known icons
|
|
532
|
-
const allowed = ['image', 'audio', 'video', 'pdf']
|
|
533
|
-
return Array.from(mods).filter(m => m !== 'text' && allowed.includes(m)).sort()
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Actions
|
|
537
|
-
function openDialog() {
|
|
538
|
-
isOpen.value = true
|
|
539
|
-
searchQuery.value = ''
|
|
540
|
-
setTimeout(() => {
|
|
541
|
-
searchInput.value?.focus()
|
|
542
|
-
}, 100)
|
|
457
|
+
function selectModel(model) {
|
|
458
|
+
ctx.setState({ selectedModel: model.name })
|
|
459
|
+
closeDialog()
|
|
543
460
|
}
|
|
544
461
|
|
|
545
462
|
function closeDialog() {
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function selectModel(model) {
|
|
550
|
-
emit('update:modelValue', model.name)
|
|
551
|
-
closeDialog()
|
|
463
|
+
emit('done')
|
|
552
464
|
}
|
|
553
465
|
|
|
554
466
|
function setActiveTab(tab, provider) {
|
|
@@ -590,11 +502,7 @@ export default {
|
|
|
590
502
|
} else {
|
|
591
503
|
favorites.value.splice(idx, 1)
|
|
592
504
|
}
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function saveFavorites() {
|
|
597
|
-
localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites.value))
|
|
505
|
+
storageObject(FAVORITES_KEY, favorites.value)
|
|
598
506
|
}
|
|
599
507
|
|
|
600
508
|
function toggleSortDirection() {
|
|
@@ -603,67 +511,28 @@ export default {
|
|
|
603
511
|
|
|
604
512
|
// Save preferences when sort changes
|
|
605
513
|
watch([sortBy, sortAsc], () => {
|
|
606
|
-
|
|
514
|
+
storageObject(STORAGE_KEY, {
|
|
607
515
|
sortBy: sortBy.value,
|
|
608
516
|
sortAsc: sortAsc.value
|
|
609
517
|
})
|
|
610
518
|
})
|
|
611
519
|
|
|
612
|
-
// Handle escape key
|
|
613
|
-
function handleKeydown(e) {
|
|
614
|
-
if (e.key === 'Escape' && isOpen.value) {
|
|
615
|
-
closeDialog()
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
520
|
// Deep link logic with Vue Router
|
|
620
521
|
onMounted(() => {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
isOpen.value = true
|
|
626
|
-
}
|
|
627
|
-
})
|
|
628
|
-
|
|
629
|
-
// Watch route to open/close
|
|
630
|
-
watch(() => route.query.show, (newVal) => {
|
|
631
|
-
if (newVal === 'models') {
|
|
632
|
-
isOpen.value = true
|
|
633
|
-
} else if (isOpen.value) {
|
|
634
|
-
isOpen.value = false
|
|
635
|
-
}
|
|
636
|
-
})
|
|
637
|
-
|
|
638
|
-
// Sync state to URL
|
|
639
|
-
watch(isOpen, (newVal) => {
|
|
640
|
-
if (newVal) {
|
|
641
|
-
if (route.query.show !== 'models') {
|
|
642
|
-
router.push({ query: { ...route.query, show: 'models' } })
|
|
643
|
-
}
|
|
644
|
-
} else {
|
|
645
|
-
if (route.query.show === 'models') {
|
|
646
|
-
const newQuery = { ...route.query }
|
|
647
|
-
delete newQuery.show
|
|
648
|
-
router.push({ query: newQuery })
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
})
|
|
652
|
-
|
|
653
|
-
onUnmounted(() => {
|
|
654
|
-
document.removeEventListener('keydown', handleKeydown)
|
|
522
|
+
searchQuery.value = ''
|
|
523
|
+
setTimeout(() => {
|
|
524
|
+
searchInput.value?.focus()
|
|
525
|
+
}, 100)
|
|
655
526
|
})
|
|
656
527
|
|
|
657
528
|
return {
|
|
658
|
-
|
|
659
|
-
showTooltip,
|
|
529
|
+
models,
|
|
660
530
|
searchQuery,
|
|
661
531
|
searchInput,
|
|
662
532
|
sortBy,
|
|
663
533
|
sortAsc,
|
|
664
534
|
sortOptions,
|
|
665
535
|
selectedProviders,
|
|
666
|
-
selectedModelObj,
|
|
667
536
|
uniqueProviders,
|
|
668
537
|
providerCounts,
|
|
669
538
|
filteredModels,
|
|
@@ -672,7 +541,6 @@ export default {
|
|
|
672
541
|
formatShortNumber,
|
|
673
542
|
isFreeModel,
|
|
674
543
|
|
|
675
|
-
openDialog,
|
|
676
544
|
closeDialog,
|
|
677
545
|
selectModel,
|
|
678
546
|
toggleProvider,
|
|
@@ -691,3 +559,128 @@ export default {
|
|
|
691
559
|
}
|
|
692
560
|
}
|
|
693
561
|
}
|
|
562
|
+
|
|
563
|
+
const ModelTooltip = {
|
|
564
|
+
template: `
|
|
565
|
+
<div v-if="model"
|
|
566
|
+
class="absolute z-50 mt-10 ml-0 p-3 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 text-sm w-72">
|
|
567
|
+
<div class="font-semibold text-gray-900 dark:text-gray-100 mb-2">{{ model.name }}</div>
|
|
568
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 mb-2">{{ model.provider }}</div>
|
|
569
|
+
|
|
570
|
+
<div v-if="model.cost" class="mb-2">
|
|
571
|
+
<div class="text-xs font-medium text-gray-700 dark:text-gray-300">Cost per 1M tokens:</div>
|
|
572
|
+
<div class="text-xs text-gray-600 dark:text-gray-400 ml-2">
|
|
573
|
+
Input: {{ formatCost(model.cost.input) }} · Output: {{ formatCost(model.cost.output) }}
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
<div v-if="model.limit" class="mb-2">
|
|
578
|
+
<div class="text-xs font-medium text-gray-700 dark:text-gray-300">Limits:</div>
|
|
579
|
+
<div class="text-xs text-gray-600 dark:text-gray-400 ml-2">
|
|
580
|
+
Context: {{ formatNumber(model.limit.context) }} · Output: {{ formatNumber(model.limit.output) }}
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
<div v-if="model.knowledge" class="text-xs text-gray-600 dark:text-gray-400">
|
|
585
|
+
Knowledge: {{ model.knowledge }}
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
<div class="flex flex-wrap gap-1 mt-2">
|
|
589
|
+
<span v-if="model.reasoning" class="px-1.5 py-0.5 text-xs rounded bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300">reasoning</span>
|
|
590
|
+
<span v-if="model.tool_call" class="px-1.5 py-0.5 text-xs rounded bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300">tools</span>
|
|
591
|
+
|
|
592
|
+
<!-- Modality Icons -->
|
|
593
|
+
<span v-for="mod in getModelModalities(model)" :key="mod"
|
|
594
|
+
class="inline-flex items-center justify-center p-0.5 text-gray-400 dark:text-gray-500"
|
|
595
|
+
:title="mod"
|
|
596
|
+
v-html="modalityIcons[mod]">
|
|
597
|
+
</span>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
`,
|
|
601
|
+
props: {
|
|
602
|
+
model: Object,
|
|
603
|
+
},
|
|
604
|
+
setup(props) {
|
|
605
|
+
return {
|
|
606
|
+
formatCost,
|
|
607
|
+
formatNumber,
|
|
608
|
+
getModelModalities,
|
|
609
|
+
modalityIcons,
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const ModelSelector = {
|
|
615
|
+
components: {
|
|
616
|
+
ProviderStatus,
|
|
617
|
+
ProviderIcon,
|
|
618
|
+
ModelSelectorModal,
|
|
619
|
+
ModelTooltip,
|
|
620
|
+
},
|
|
621
|
+
template: `
|
|
622
|
+
<!-- Model Selector Button -->
|
|
623
|
+
<div class="pl-1 flex space-x-2">
|
|
624
|
+
<button type="button" @click="openDialog"
|
|
625
|
+
class="flex items-center space-x-2 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800 text-sm text-gray-700 dark:text-gray-300 transition-colors min-w-48 max-w-96"
|
|
626
|
+
@mouseenter="showTooltip = true"
|
|
627
|
+
@mouseleave="showTooltip = false">
|
|
628
|
+
<ProviderIcon v-if="selectedModel?.provider" :provider="selectedModel.provider" class="size-5 flex-shrink-0" />
|
|
629
|
+
<span class="truncate flex-1 text-left">{{ selectedModel?.name || 'Select Model...' }}</span>
|
|
630
|
+
<svg class="size-4 flex-shrink-0 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
631
|
+
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
|
|
632
|
+
</svg>
|
|
633
|
+
</button>
|
|
634
|
+
|
|
635
|
+
<!-- Info Tooltip (on hover) -->
|
|
636
|
+
<ModelTooltip v-if="showTooltip" :model="selectedModel" />
|
|
637
|
+
|
|
638
|
+
<ProviderStatus @updated="$emit('updated', $event)" />
|
|
639
|
+
</div>
|
|
640
|
+
`,
|
|
641
|
+
emits: ['updated', 'update:modelValue'],
|
|
642
|
+
props: {
|
|
643
|
+
models: Array,
|
|
644
|
+
modelValue: String,
|
|
645
|
+
},
|
|
646
|
+
setup(props, { emit }) {
|
|
647
|
+
const ctx = inject('ctx')
|
|
648
|
+
const showTooltip = ref(false)
|
|
649
|
+
|
|
650
|
+
// Get selected model object
|
|
651
|
+
const selectedModel = computed(() => {
|
|
652
|
+
if (!props.modelValue || !props.models) return null
|
|
653
|
+
return props.models.find(m => m.name === props.modelValue) || props.models.find(m => m.id === props.modelValue)
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
function openDialog() {
|
|
657
|
+
ctx.state.models = props.models
|
|
658
|
+
ctx.openModal('models')
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
watch(() => ctx.state.selectedModel, (newVal) => {
|
|
662
|
+
emit('update:modelValue', newVal)
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
onMounted(() => {
|
|
666
|
+
ctx.state.models = props.models
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
showTooltip,
|
|
671
|
+
openDialog,
|
|
672
|
+
selectedModel,
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export default {
|
|
678
|
+
install(ctx) {
|
|
679
|
+
ctx.components({
|
|
680
|
+
ModelSelector,
|
|
681
|
+
})
|
|
682
|
+
ctx.modals({
|
|
683
|
+
'models': ModelSelectorModal,
|
|
684
|
+
})
|
|
685
|
+
}
|
|
686
|
+
}
|