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.
Files changed (39) hide show
  1. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  2. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  3. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  4. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  5. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  6. llms/__pycache__/llms.cpython-312.pyc +0 -0
  7. llms/__pycache__/main.cpython-312.pyc +0 -0
  8. llms/__pycache__/main.cpython-313.pyc +0 -0
  9. llms/__pycache__/main.cpython-314.pyc +0 -0
  10. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  11. llms/index.html +25 -56
  12. llms/llms.json +2 -2
  13. llms/main.py +452 -93
  14. llms/providers.json +1 -1
  15. llms/ui/App.mjs +25 -4
  16. llms/ui/Avatar.mjs +3 -2
  17. llms/ui/ChatPrompt.mjs +43 -52
  18. llms/ui/Main.mjs +87 -98
  19. llms/ui/OAuthSignIn.mjs +2 -33
  20. llms/ui/ProviderStatus.mjs +7 -8
  21. llms/ui/Recents.mjs +10 -9
  22. llms/ui/Sidebar.mjs +2 -1
  23. llms/ui/SignIn.mjs +7 -6
  24. llms/ui/ai.mjs +9 -41
  25. llms/ui/app.css +137 -138
  26. llms/ui/index.mjs +213 -0
  27. llms/ui/{ModelSelector.mjs → model-selector.mjs} +193 -200
  28. llms/ui/tailwind.input.css +441 -79
  29. llms/ui/threadStore.mjs +17 -6
  30. llms/ui/utils.mjs +1 -0
  31. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/METADATA +1 -1
  32. llms_py-3.0.0b2.dist-info/RECORD +58 -0
  33. llms/ui/SystemPromptEditor.mjs +0 -31
  34. llms/ui/SystemPromptSelector.mjs +0 -56
  35. llms_py-3.0.0b1.dist-info/RECORD +0 -49
  36. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/WHEEL +0 -0
  37. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/entry_points.txt +0 -0
  38. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
  39. {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, onMounted, onUnmounted } from "vue"
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 MODALITY_ICONS = {
17
- text: `<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"><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></svg>`,
18
- image: `<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"><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></svg>`,
19
- audio: `<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"><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></svg>`,
20
- video: `<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"><path d="m22 8-6 4 6 4V8Z"></path><rect width="14" height="12" x="2" y="6" rx="2" ry="2"></rect></svg>`,
21
- pdf: `<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"><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></svg>`,
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
- function loadPrefs() {
28
- try {
29
- return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}
30
- } catch {
31
- return {}
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 savePrefs(prefs) {
36
- localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs))
39
+ function formatNumber(num) {
40
+ if (num == null) return '-'
41
+ return numFmt.format(num)
37
42
  }
38
43
 
39
- export default {
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 v-if="isOpen" class="fixed inset-0 z-50 overflow-hidden" @keydown.escape="closeDialog">
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: ['updated', 'update:modelValue'],
317
- props: {
318
- models: Array,
319
- modelValue: String,
320
- },
288
+ emits: ['done'],
321
289
  setup(props, { emit }) {
322
- const router = useRouter()
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 = loadPrefs()
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 modalityIcons = MODALITY_ICONS
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 (!props.models) return []
359
- const providers = [...new Set(props.models.map(m => m.provider))].filter(Boolean)
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 (!props.models) return {}
323
+ if (!models.value) return {}
366
324
  const counts = {}
367
- props.models.forEach(m => {
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 (!props.models) return []
388
- const availableKeys = new Set(props.models.map(getModelKey))
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 (!props.models) return []
364
+ if (!models.value) return []
407
365
 
408
- let result = [...props.models]
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 getModelModalities(model) {
523
- const mods = new Set()
524
- const input = model.modalities?.input || []
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
- isOpen.value = false
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
- saveFavorites()
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
- savePrefs({
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
- document.addEventListener('keydown', handleKeydown)
622
-
623
- // Initial check
624
- if (route.query.show === 'models') {
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
- isOpen,
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
+ }