llms-py 2.0.9__py3-none-any.whl → 3.0.10__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/__init__.py +4 -0
- llms/__main__.py +9 -0
- llms/db.py +359 -0
- llms/extensions/analytics/ui/index.mjs +1444 -0
- llms/extensions/app/README.md +20 -0
- llms/extensions/app/__init__.py +589 -0
- llms/extensions/app/db.py +536 -0
- {llms_py-2.0.9.data/data → llms/extensions/app}/ui/Recents.mjs +100 -73
- llms_py-2.0.9.data/data/ui/Sidebar.mjs → llms/extensions/app/ui/index.mjs +150 -79
- llms/extensions/app/ui/threadStore.mjs +433 -0
- llms/extensions/core_tools/CALCULATOR.md +32 -0
- llms/extensions/core_tools/__init__.py +637 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
- llms/extensions/core_tools/ui/index.mjs +650 -0
- llms/extensions/gallery/README.md +61 -0
- llms/extensions/gallery/__init__.py +63 -0
- llms/extensions/gallery/db.py +243 -0
- llms/extensions/gallery/ui/index.mjs +482 -0
- llms/extensions/katex/README.md +39 -0
- llms/extensions/katex/__init__.py +6 -0
- llms/extensions/katex/ui/README.md +125 -0
- llms/extensions/katex/ui/contrib/auto-render.js +338 -0
- llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
- llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
- llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
- llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
- llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
- llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
- llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
- llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- llms/extensions/katex/ui/index.mjs +92 -0
- llms/extensions/katex/ui/katex-swap.css +1230 -0
- llms/extensions/katex/ui/katex-swap.min.css +1 -0
- llms/extensions/katex/ui/katex.css +1230 -0
- llms/extensions/katex/ui/katex.js +19080 -0
- llms/extensions/katex/ui/katex.min.css +1 -0
- llms/extensions/katex/ui/katex.min.js +1 -0
- llms/extensions/katex/ui/katex.min.mjs +1 -0
- llms/extensions/katex/ui/katex.mjs +18547 -0
- llms/extensions/providers/__init__.py +22 -0
- llms/extensions/providers/anthropic.py +233 -0
- llms/extensions/providers/cerebras.py +37 -0
- llms/extensions/providers/chutes.py +153 -0
- llms/extensions/providers/google.py +481 -0
- llms/extensions/providers/nvidia.py +103 -0
- llms/extensions/providers/openai.py +154 -0
- llms/extensions/providers/openrouter.py +74 -0
- llms/extensions/providers/zai.py +182 -0
- llms/extensions/system_prompts/README.md +22 -0
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/ui/index.mjs +280 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +144 -0
- llms/extensions/tools/ui/index.mjs +706 -0
- llms/index.html +58 -0
- llms/llms.json +400 -0
- llms/main.py +4407 -0
- llms/providers-extra.json +394 -0
- llms/providers.json +1 -0
- llms/ui/App.mjs +188 -0
- llms/ui/ai.mjs +217 -0
- llms/ui/app.css +7081 -0
- llms/ui/ctx.mjs +412 -0
- llms/ui/index.mjs +131 -0
- llms/ui/lib/chart.js +14 -0
- llms/ui/lib/charts.mjs +16 -0
- llms/ui/lib/color.js +14 -0
- llms/ui/lib/servicestack-vue.mjs +37 -0
- llms/ui/lib/vue.min.mjs +13 -0
- llms/ui/lib/vue.mjs +18530 -0
- {llms_py-2.0.9.data/data → llms}/ui/markdown.mjs +33 -15
- llms/ui/modules/chat/ChatBody.mjs +976 -0
- llms/ui/modules/chat/SettingsDialog.mjs +374 -0
- llms/ui/modules/chat/index.mjs +991 -0
- llms/ui/modules/icons.mjs +46 -0
- llms/ui/modules/layout.mjs +271 -0
- llms/ui/modules/model-selector.mjs +811 -0
- llms/ui/tailwind.input.css +742 -0
- {llms_py-2.0.9.data/data → llms}/ui/typography.css +133 -7
- llms/ui/utils.mjs +261 -0
- llms_py-3.0.10.dist-info/METADATA +49 -0
- llms_py-3.0.10.dist-info/RECORD +177 -0
- llms_py-3.0.10.dist-info/entry_points.txt +2 -0
- {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
- llms.py +0 -1402
- llms_py-2.0.9.data/data/index.html +0 -64
- llms_py-2.0.9.data/data/llms.json +0 -447
- llms_py-2.0.9.data/data/requirements.txt +0 -1
- llms_py-2.0.9.data/data/ui/App.mjs +0 -20
- llms_py-2.0.9.data/data/ui/ChatPrompt.mjs +0 -389
- llms_py-2.0.9.data/data/ui/Main.mjs +0 -680
- llms_py-2.0.9.data/data/ui/app.css +0 -3951
- llms_py-2.0.9.data/data/ui/lib/servicestack-vue.min.mjs +0 -37
- llms_py-2.0.9.data/data/ui/lib/vue.min.mjs +0 -12
- llms_py-2.0.9.data/data/ui/tailwind.input.css +0 -261
- llms_py-2.0.9.data/data/ui/threadStore.mjs +0 -273
- llms_py-2.0.9.data/data/ui/utils.mjs +0 -114
- llms_py-2.0.9.data/data/ui.json +0 -1069
- llms_py-2.0.9.dist-info/METADATA +0 -941
- llms_py-2.0.9.dist-info/RECORD +0 -30
- llms_py-2.0.9.dist-info/entry_points.txt +0 -2
- {llms_py-2.0.9.data/data → llms}/ui/fav.svg +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/highlight.min.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/idb.min.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/marked.min.mjs +0 -0
- /llms_py-2.0.9.data/data/ui/lib/servicestack-client.min.mjs → /llms/ui/lib/servicestack-client.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/vue-router.min.mjs +0 -0
- {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
- {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import { ref, computed, watch, inject, onMounted, onUnmounted } from "vue"
|
|
2
|
+
|
|
3
|
+
const SORT_OPTIONS = [
|
|
4
|
+
{ id: 'name', label: 'Name' },
|
|
5
|
+
{ id: 'knowledge', label: 'Knowledge Cutoff' },
|
|
6
|
+
{ id: 'release_date', label: 'Release Date' },
|
|
7
|
+
{ id: 'last_updated', label: 'Last Updated' },
|
|
8
|
+
{ id: 'cost_input', label: 'Cost (Input)' },
|
|
9
|
+
{ id: 'cost_output', label: 'Cost (Output)' },
|
|
10
|
+
{ id: 'context', label: 'Context Limit' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
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>`
|
|
14
|
+
const modalityIcons = {
|
|
15
|
+
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>`),
|
|
16
|
+
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>`),
|
|
17
|
+
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>`),
|
|
18
|
+
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>`),
|
|
19
|
+
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>`),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Formatting helpers
|
|
23
|
+
const numFmt = new Intl.NumberFormat()
|
|
24
|
+
const currFmt = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
|
|
25
|
+
|
|
26
|
+
function formatCost(cost) {
|
|
27
|
+
if (cost == null) return '-'
|
|
28
|
+
const val = parseFloat(cost)
|
|
29
|
+
if (val === 0) return 'Free'
|
|
30
|
+
return currFmt.format(val)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatNumber(num) {
|
|
34
|
+
if (num == null) return '-'
|
|
35
|
+
return numFmt.format(num)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatShortNumber(num) {
|
|
39
|
+
if (num == null) return '-'
|
|
40
|
+
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
|
|
41
|
+
if (num >= 1000) return (num / 1000).toFixed(0) + 'K'
|
|
42
|
+
return numFmt.format(num)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getInputModalities(model) {
|
|
46
|
+
const mods = new Set()
|
|
47
|
+
const input = model.modalities?.input || []
|
|
48
|
+
|
|
49
|
+
// Collect input modalities
|
|
50
|
+
input.forEach(m => mods.add(m))
|
|
51
|
+
|
|
52
|
+
// Filter out text and ensure we only show known icons for inputs
|
|
53
|
+
const allowed = ['image', 'audio', 'video', 'pdf']
|
|
54
|
+
return Array.from(mods).filter(m => m !== 'text' && allowed.includes(m)).sort()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getOutputModalities(model) {
|
|
58
|
+
const mods = new Set()
|
|
59
|
+
const output = model.modalities?.output || []
|
|
60
|
+
|
|
61
|
+
// Collect output modalities
|
|
62
|
+
output.forEach(m => mods.add(m))
|
|
63
|
+
|
|
64
|
+
// Filter out text (we show tags for other output types like audio/image generation)
|
|
65
|
+
return Array.from(mods).filter(m => m !== 'text').sort()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const ProviderStatus = {
|
|
69
|
+
template: `
|
|
70
|
+
<div v-if="$ai.isAdmin" ref="triggerRef" class="relative" :key="renderKey">
|
|
71
|
+
<button type="button" @click="togglePopover"
|
|
72
|
+
class="flex space-x-2 items-center text-sm font-semibold select-none rounded-md py-2 px-3 border border-transparent hover:border-gray-300 dark:hover:border-gray-600 bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 transition-colors">
|
|
73
|
+
<span class="text-gray-600 dark:text-gray-400" :title="$state.models.length + ' models from ' + ($state.config.status.enabled||[]).length + ' enabled providers'">{{$state.models.length}}</span>
|
|
74
|
+
<div class="cursor-pointer flex items-center" :title="'Enabled:\\n' + ($state.config.status.enabled||[]).map(x => ' ' + x).join('\\n')">
|
|
75
|
+
<svg class="size-4 text-green-400 dark:text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="currentColor"/></svg>
|
|
76
|
+
<span class="text-green-700 dark:text-green-400">{{($state.config.status.enabled||[]).length}}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="cursor-pointer flex items-center" :title="'Disabled:\\n' + ($state.config.status.disabled||[]).map(x => ' ' + x).join('\\n')">
|
|
79
|
+
<svg class="size-4 text-red-400 dark:text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="currentColor"/></svg>
|
|
80
|
+
<span class="text-red-700 dark:text-red-400">{{($state.config.status.disabled||[]).length}}</span>
|
|
81
|
+
</div>
|
|
82
|
+
</button>
|
|
83
|
+
<div v-if="showPopover" ref="popoverRef" class="absolute right-0 mt-2 w-72 overflow-y-auto bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg z-10">
|
|
84
|
+
<div class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
85
|
+
<div v-for="p in allProviders" :key="p" class="flex items-center justify-between px-3 py-2">
|
|
86
|
+
<label :for="'chk_' + p" class="cursor-pointer text-sm text-gray-900 dark:text-gray-100 truncate mr-2" :title="p">{{ p }}</label>
|
|
87
|
+
<div @click="onToggle(p, !isEnabled(p))" class="cursor-pointer group relative inline-flex h-5 w-10 shrink-0 items-center justify-center rounded-full outline-offset-2 outline-green-600 has-focus-visible:outline-2">
|
|
88
|
+
<span class="absolute mx-auto h-4 w-9 rounded-full bg-gray-200 dark:bg-gray-700 inset-ring inset-ring-gray-900/5 dark:inset-ring-gray-100/5 transition-colors duration-200 ease-in-out group-has-checked:bg-green-600 dark:group-has-checked:bg-green-500" />
|
|
89
|
+
<span class="absolute left-0 size-5 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-200 shadow-xs transition-transform duration-200 ease-in-out group-has-checked:translate-x-5" />
|
|
90
|
+
<input :id="'chk_' + p" type="checkbox" :checked="isEnabled(p)" class="switch cursor-pointer absolute inset-0 appearance-none focus:outline-hidden" aria-label="Use setting" name="setting" />
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
`,
|
|
97
|
+
emits: ['updated'],
|
|
98
|
+
setup(props, { emit }) {
|
|
99
|
+
const ctx = inject('ctx')
|
|
100
|
+
const showPopover = ref(false)
|
|
101
|
+
const triggerRef = ref(null)
|
|
102
|
+
const popoverRef = ref(null)
|
|
103
|
+
const pending = ref({})
|
|
104
|
+
const renderKey = ref(0)
|
|
105
|
+
const allProviders = computed(() => ctx.state.config.status?.all)
|
|
106
|
+
const isEnabled = (p) => ctx.state.config.status.enabled.includes(p)
|
|
107
|
+
const togglePopover = () => showPopover.value = !showPopover.value
|
|
108
|
+
|
|
109
|
+
const onToggle = async (provider, enable) => {
|
|
110
|
+
pending.value = { ...pending.value, [provider]: true }
|
|
111
|
+
try {
|
|
112
|
+
const res = await ctx.post(`/providers/${encodeURIComponent(provider)}`, {
|
|
113
|
+
body: JSON.stringify(enable ? { enable: true } : { disable: true })
|
|
114
|
+
})
|
|
115
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
116
|
+
const json = await res.json()
|
|
117
|
+
ctx.state.config.status.enabled = json.enabled || []
|
|
118
|
+
ctx.state.config.status.disabled = json.disabled || []
|
|
119
|
+
if (json.feedback) {
|
|
120
|
+
alert(json.feedback)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const [configRes, modelsRes] = await Promise.all([
|
|
125
|
+
ctx.ai.getConfig(),
|
|
126
|
+
ctx.ai.getModels(),
|
|
127
|
+
])
|
|
128
|
+
const [config, models] = await Promise.all([
|
|
129
|
+
configRes.json(),
|
|
130
|
+
modelsRes.json(),
|
|
131
|
+
])
|
|
132
|
+
Object.assign(ctx.state, { config, models })
|
|
133
|
+
renderKey.value++
|
|
134
|
+
emit('updated')
|
|
135
|
+
} catch (e) {
|
|
136
|
+
alert(`Failed to reload config: ${e.message}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
} catch (e) {
|
|
140
|
+
alert(`Failed to ${enable ? 'enable' : 'disable'} ${provider}: ${e.message}`)
|
|
141
|
+
} finally {
|
|
142
|
+
pending.value = { ...pending.value, [provider]: false }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const onDocClick = (e) => {
|
|
147
|
+
const t = e.target
|
|
148
|
+
if (triggerRef.value?.contains(t)) return
|
|
149
|
+
if (popoverRef.value?.contains(t)) return
|
|
150
|
+
showPopover.value = false
|
|
151
|
+
}
|
|
152
|
+
onMounted(() => document.addEventListener('click', onDocClick))
|
|
153
|
+
onUnmounted(() => document.removeEventListener('click', onDocClick))
|
|
154
|
+
return {
|
|
155
|
+
renderKey,
|
|
156
|
+
showPopover,
|
|
157
|
+
triggerRef,
|
|
158
|
+
popoverRef,
|
|
159
|
+
allProviders,
|
|
160
|
+
isEnabled,
|
|
161
|
+
togglePopover,
|
|
162
|
+
onToggle,
|
|
163
|
+
pending,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ModelSelectorModal = {
|
|
169
|
+
template: `
|
|
170
|
+
<!-- Dialog Overlay -->
|
|
171
|
+
<div class="fixed inset-0 z-50 overflow-hidden" @keydown.escape="closeDialog">
|
|
172
|
+
<!-- Backdrop -->
|
|
173
|
+
<div class="fixed inset-0 bg-black/50 transition-opacity" @click="closeDialog"></div>
|
|
174
|
+
|
|
175
|
+
<!-- Dialog -->
|
|
176
|
+
<div class="fixed inset-4 md:inset-8 lg:inset-12 flex items-center justify-center">
|
|
177
|
+
<div class="relative bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full h-full max-w-6xl max-h-[90vh] flex flex-col overflow-hidden">
|
|
178
|
+
<!-- Header -->
|
|
179
|
+
<div class="flex-shrink-0 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
180
|
+
<div class="flex items-center justify-between mb-4">
|
|
181
|
+
<h2 class="mr-4 text-xl font-semibold text-gray-900 dark:text-gray-100">Select Model</h2>
|
|
182
|
+
<div class="flex items-center gap-4">
|
|
183
|
+
<ProviderStatus @updated="renderKey++" />
|
|
184
|
+
<button type="button" @click="closeDialog" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
|
185
|
+
<svg class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
186
|
+
<path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"/>
|
|
187
|
+
</svg>
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- Search and Controls -->
|
|
193
|
+
<div class="flex flex-col md:flex-row gap-3">
|
|
194
|
+
<!-- Search -->
|
|
195
|
+
<div class="flex-1 relative">
|
|
196
|
+
<svg class="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
197
|
+
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
|
198
|
+
</svg>
|
|
199
|
+
<input type="text" v-model="prefs.query" ref="searchInput"
|
|
200
|
+
placeholder="Search models..."
|
|
201
|
+
class="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" />
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<!-- Modality Filters -->
|
|
205
|
+
<div class="flex items-center gap-1.5">
|
|
206
|
+
<!-- Input Modalities (Exclusive) -->
|
|
207
|
+
<div class="flex items-center space-x-1">
|
|
208
|
+
<button v-for="type in inputModalityTypes" :key="type" type="button"
|
|
209
|
+
@click="toggleInputModality(type)"
|
|
210
|
+
:title="'Input: ' + type"
|
|
211
|
+
:class="[
|
|
212
|
+
'p-2 rounded-lg transition-colors border',
|
|
213
|
+
prefs.inputModality === type
|
|
214
|
+
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800'
|
|
215
|
+
: 'bg-white dark:bg-gray-800 text-gray-400 border-transparent hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-200'
|
|
216
|
+
]"
|
|
217
|
+
v-html="modalityIcons[type]">
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<!-- Divider -->
|
|
222
|
+
<div class="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1"></div>
|
|
223
|
+
|
|
224
|
+
<!-- Output Modalities (Exclusive) -->
|
|
225
|
+
<div class="flex items-center space-x-1">
|
|
226
|
+
<button v-for="type in outputModalityTypes" :key="type" type="button"
|
|
227
|
+
@click="toggleOutputModality(type)"
|
|
228
|
+
:title="'Output: ' + type"
|
|
229
|
+
:class="[
|
|
230
|
+
'p-2 rounded-lg transition-colors border',
|
|
231
|
+
prefs.outputModality === type
|
|
232
|
+
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800'
|
|
233
|
+
: 'bg-white dark:bg-gray-800 text-gray-400 border-transparent hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-200'
|
|
234
|
+
]"
|
|
235
|
+
v-html="modalityIcons[type]">
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<!-- Sort -->
|
|
241
|
+
<div class="flex items-center space-x-2">
|
|
242
|
+
<label class="text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap">Sort by:</label>
|
|
243
|
+
<select v-model="prefs.sortBy"
|
|
244
|
+
class="px-3 py-2 pr-8 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 min-w-[200px]">
|
|
245
|
+
<option v-for="opt in sortOptions" :key="opt.id" :value="opt.id">{{ opt.label }}</option>
|
|
246
|
+
</select>
|
|
247
|
+
<button type="button" @click="toggleSortDirection"
|
|
248
|
+
class="p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
249
|
+
:title="prefs.sortAsc ? 'Ascending' : 'Descending'">
|
|
250
|
+
<svg v-if="prefs.sortAsc" class="size-5 text-gray-600 dark:text-gray-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
251
|
+
<path fill="currentColor" d="M19 7h3l-4-4l-4 4h3v14h2M2 17h10v2H2M6 5v2H2V5m0 6h7v2H2z"/>
|
|
252
|
+
</svg>
|
|
253
|
+
<svg v-else class="size-5 text-gray-600 dark:text-gray-400" style="transform: scaleY(-1)" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
254
|
+
<path fill="currentColor" d="M19 7h3l-4-4l-4 4h3v14h2M2 17h10v2H2M6 5v2H2V5m0 6h7v2H2z"/>
|
|
255
|
+
</svg>
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<!-- Provider Filter -->
|
|
261
|
+
<div class="mt-3 flex flex-wrap gap-2">
|
|
262
|
+
<button type="button"
|
|
263
|
+
@click="setActiveTab('favorites')"
|
|
264
|
+
:class="[
|
|
265
|
+
'flex items-center space-x-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
|
|
266
|
+
activeTab === 'favorites'
|
|
267
|
+
? 'bg-fuchsia-600 text-white'
|
|
268
|
+
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
|
269
|
+
]">
|
|
270
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3.5">
|
|
271
|
+
<path fill-rule="evenodd" d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401z" clip-rule="evenodd" />
|
|
272
|
+
</svg>
|
|
273
|
+
<span>Favorites</span>
|
|
274
|
+
<span v-if="favorites.length > 0" class="ml-1 opacity-75">({{ favorites.length }})</span>
|
|
275
|
+
</button>
|
|
276
|
+
<div class="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1 self-center"></div>
|
|
277
|
+
<button type="button"
|
|
278
|
+
@click="setActiveTab('browse', null)"
|
|
279
|
+
:class="[
|
|
280
|
+
'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
|
|
281
|
+
activeTab === 'browse' && !prefs.provider
|
|
282
|
+
? 'bg-blue-600 text-white'
|
|
283
|
+
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
|
284
|
+
]">
|
|
285
|
+
All
|
|
286
|
+
</button>
|
|
287
|
+
<button v-for="provider in uniqueProviders" :key="provider"
|
|
288
|
+
type="button"
|
|
289
|
+
@click="setActiveTab('browse', provider)"
|
|
290
|
+
:class="[
|
|
291
|
+
'flex items-center space-x-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
|
|
292
|
+
activeTab === 'browse' && prefs.provider == provider
|
|
293
|
+
? 'bg-blue-600 text-white'
|
|
294
|
+
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
|
295
|
+
]">
|
|
296
|
+
<ProviderIcon :provider="provider" class="size-4" />
|
|
297
|
+
<span>{{ provider }}</span>
|
|
298
|
+
<span class="opacity-60">({{ providerCounts[provider] }})</span>
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<!-- Model List -->
|
|
304
|
+
<div class="flex-1 overflow-y-auto p-4">
|
|
305
|
+
<div v-if="filteredModels.length === 0 && !hasUnavailableFavorites" class="text-center py-12 text-gray-500 dark:text-gray-400">
|
|
306
|
+
No models found matching your criteria.
|
|
307
|
+
</div>
|
|
308
|
+
<div v-else class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
309
|
+
<button v-for="model in filteredModels" :key="model.id + '-' + model.provider"
|
|
310
|
+
type="button"
|
|
311
|
+
@click="selectModel(model)"
|
|
312
|
+
:class="[
|
|
313
|
+
'relative text-left p-4 rounded-lg border transition-all group',
|
|
314
|
+
$state.selectedModel === model.name
|
|
315
|
+
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 ring-2 ring-blue-500/50'
|
|
316
|
+
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
|
317
|
+
]">
|
|
318
|
+
<!-- Favorite Star -->
|
|
319
|
+
<div @click.stop="toggleFavorite(model)"
|
|
320
|
+
class="absolute top-2 right-2 p-1.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors z-10 cursor-pointer"
|
|
321
|
+
:title="isFavorite(model) ? 'Remove from favorites' : 'Add to favorites'">
|
|
322
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
|
|
323
|
+
:class="['size-4 transition-colors', isFavorite(model) ? 'text-yellow-400' : 'text-gray-300 dark:text-gray-600 group-hover:text-gray-400 dark:group-hover:text-gray-500']">
|
|
324
|
+
<path fill-rule="evenodd" d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401z" clip-rule="evenodd" />
|
|
325
|
+
</svg>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div class="flex items-start justify-between mb-2 pr-6">
|
|
329
|
+
<div class="flex items-center space-x-2 min-w-0">
|
|
330
|
+
<ProviderIcon :provider="model.provider" class="size-5 flex-shrink-0" />
|
|
331
|
+
<span class="font-medium text-gray-900 dark:text-gray-100 truncate">{{ model.name }}</span>
|
|
332
|
+
</div>
|
|
333
|
+
<div v-if="isFreeModel(model)" class="flex-shrink-0 ml-2">
|
|
334
|
+
<span class="px-1.5 py-0.5 text-xs font-semibold rounded bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300">FREE</span>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 mb-2 truncate" :title="model.id">{{ model.id }}</div>
|
|
339
|
+
|
|
340
|
+
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-600 dark:text-gray-400">
|
|
341
|
+
<span v-if="model.cost && !isFreeModel(model)" :title="'Input: ' + model.cost.input + ' / Output: ' + model.cost.output + ' per 1M tokens'">
|
|
342
|
+
💰 {{ formatCost(model.cost.input) }} / {{ formatCost(model.cost.output) }}
|
|
343
|
+
</span>
|
|
344
|
+
<span v-if="model.limit?.context" :title="'Context window: ' + formatNumber(model.limit.context) + ' tokens'">
|
|
345
|
+
📏 {{ formatShortNumber(model.limit.context) }}
|
|
346
|
+
</span>
|
|
347
|
+
<span v-if="model.knowledge" :title="'Knowledge cutoff: ' + model.knowledge">
|
|
348
|
+
📅 {{ model.knowledge }}
|
|
349
|
+
</span>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div class="flex flex-wrap gap-1 mt-2">
|
|
353
|
+
<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>
|
|
354
|
+
<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>
|
|
355
|
+
|
|
356
|
+
<!-- Modality Icons (Input) -->
|
|
357
|
+
<span v-for="mod in getInputModalities(model)" :key="mod"
|
|
358
|
+
class="inline-flex items-center justify-center p-0.5 text-gray-400 dark:text-gray-500"
|
|
359
|
+
:title="'Input: ' + mod"
|
|
360
|
+
v-html="modalityIcons[mod]">
|
|
361
|
+
</span>
|
|
362
|
+
|
|
363
|
+
<!-- Modality Tags (Output) -->
|
|
364
|
+
<span v-for="mod in getOutputModalities(model)" :key="mod"
|
|
365
|
+
class="px-1.5 py-0.5 text-xs rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
|
|
366
|
+
:title="'Output: ' + mod">
|
|
367
|
+
{{ mod }}
|
|
368
|
+
</span>
|
|
369
|
+
</div>
|
|
370
|
+
</button>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
<!-- Unavailable Favorites -->
|
|
375
|
+
<div v-if="activeTab === 'favorites' && unavailableFavorites.length > 0" class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
376
|
+
<div class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-3 ml-1">Unavailable</div>
|
|
377
|
+
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 opacity-60 grayscale">
|
|
378
|
+
<div v-for="model in unavailableFavorites" :key="model.id"
|
|
379
|
+
class="relative text-left p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 cursor-not-allowed">
|
|
380
|
+
|
|
381
|
+
<!-- Remove from favorites button -->
|
|
382
|
+
<div @click.stop="toggleFavorite(model)"
|
|
383
|
+
class="absolute top-2 right-2 p-1.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors z-10 cursor-pointer"
|
|
384
|
+
title="Remove from favorites">
|
|
385
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4 text-yellow-400">
|
|
386
|
+
<path fill-rule="evenodd" d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401z" clip-rule="evenodd" />
|
|
387
|
+
</svg>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div class="flex items-start justify-between mb-2 pr-6">
|
|
391
|
+
<div class="flex items-center space-x-2 min-w-0">
|
|
392
|
+
<ProviderIcon v-if="model.provider" :provider="model.provider" class="size-5 flex-shrink-0" />
|
|
393
|
+
<span class="font-medium text-gray-900 dark:text-gray-100 truncate">{{ model.name || model.id }}</span>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 truncate">{{ model.id }}</div>
|
|
397
|
+
<div class="mt-2 text-xs italic text-gray-400">Provider unavailable</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<!-- Footer -->
|
|
404
|
+
<div class="flex-shrink-0 px-6 py-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
|
|
405
|
+
<div class="flex items-center justify-between">
|
|
406
|
+
<span class="text-sm text-gray-600 dark:text-gray-400">
|
|
407
|
+
{{ filteredModels.length }} of {{ models.length }} models
|
|
408
|
+
</span>
|
|
409
|
+
<button type="button" @click="closeDialog"
|
|
410
|
+
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 transition-colors">
|
|
411
|
+
Close
|
|
412
|
+
</button>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
`,
|
|
419
|
+
emits: ['done'],
|
|
420
|
+
setup(props, { emit }) {
|
|
421
|
+
const ctx = inject('ctx')
|
|
422
|
+
const searchInput = ref(null)
|
|
423
|
+
|
|
424
|
+
// Load preferences
|
|
425
|
+
const renderKey = ref(0)
|
|
426
|
+
const ext = ctx.scope('model-selector')
|
|
427
|
+
const prefs = ref(ext.getPrefs())
|
|
428
|
+
|
|
429
|
+
const inputModalityTypes = ['image', 'audio', 'video', 'pdf']
|
|
430
|
+
const outputModalityTypes = ['image', 'audio']
|
|
431
|
+
|
|
432
|
+
const models = computed(() => ctx.state.models || [])
|
|
433
|
+
|
|
434
|
+
// Favorites State
|
|
435
|
+
const favorites = computed(() => prefs.value.favorites || [])
|
|
436
|
+
|
|
437
|
+
const activeTab = computed(() => prefs.value.activeTab || (favorites.value.length > 0 ? 'favorites' : 'browse'))
|
|
438
|
+
|
|
439
|
+
const sortOptions = SORT_OPTIONS
|
|
440
|
+
|
|
441
|
+
// Get unique providers
|
|
442
|
+
const uniqueProviders = computed(() => {
|
|
443
|
+
if (!models.value) return []
|
|
444
|
+
const providers = [...new Set(models.value.map(m => m.provider))].filter(Boolean)
|
|
445
|
+
return providers.sort()
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
// Provider counts
|
|
449
|
+
const providerCounts = computed(() => {
|
|
450
|
+
if (!models.value) return {}
|
|
451
|
+
const counts = {}
|
|
452
|
+
models.value.forEach(m => {
|
|
453
|
+
if (m.provider) {
|
|
454
|
+
counts[m.provider] = (counts[m.provider] || 0) + 1
|
|
455
|
+
}
|
|
456
|
+
})
|
|
457
|
+
return counts
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
// Filter and sort helpers
|
|
461
|
+
function getModelKey(model) {
|
|
462
|
+
return `${model.provider}:${model.id}`
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function isFavorite(model) {
|
|
466
|
+
const key = getModelKey(model)
|
|
467
|
+
return favorites.value.includes(key)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Unavailable favorites (provider disabled or model removed)
|
|
471
|
+
const unavailableFavorites = computed(() => {
|
|
472
|
+
if (!models.value) return []
|
|
473
|
+
const availableKeys = new Set(models.value.map(getModelKey))
|
|
474
|
+
const missingKeys = favorites.value.filter(key => !availableKeys.has(key))
|
|
475
|
+
|
|
476
|
+
return missingKeys.map(key => {
|
|
477
|
+
const [provider, ...idParts] = key.split(':')
|
|
478
|
+
const id = idParts.join(':')
|
|
479
|
+
return {
|
|
480
|
+
id,
|
|
481
|
+
provider,
|
|
482
|
+
name: id // Fallback
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
const hasUnavailableFavorites = computed(() => unavailableFavorites.value.length > 0)
|
|
488
|
+
|
|
489
|
+
// Filter and sort models
|
|
490
|
+
const filteredModels = computed(() => {
|
|
491
|
+
if (!models.value) return []
|
|
492
|
+
|
|
493
|
+
let result = [...models.value]
|
|
494
|
+
|
|
495
|
+
// Filter by Tab
|
|
496
|
+
if (activeTab.value === 'favorites') {
|
|
497
|
+
result = result.filter(isFavorite)
|
|
498
|
+
} else {
|
|
499
|
+
// Browse Tab - Filter by provider
|
|
500
|
+
if (prefs.value.provider) {
|
|
501
|
+
result = result.filter(m => m.provider == prefs.value.provider)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Filter by Modalities (Input)
|
|
506
|
+
if (prefs.value.inputModality) {
|
|
507
|
+
result = result.filter(m => {
|
|
508
|
+
const mods = m.modalities || {}
|
|
509
|
+
const inputMods = mods.input || []
|
|
510
|
+
return inputMods.includes(prefs.value.inputModality)
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Filter by Modalities (Output)
|
|
515
|
+
if (prefs.value.outputModality) {
|
|
516
|
+
result = result.filter(m => {
|
|
517
|
+
const mods = m.modalities || {}
|
|
518
|
+
const outputMods = mods.output || []
|
|
519
|
+
return outputMods.includes(prefs.value.outputModality)
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Filter by search query
|
|
524
|
+
if (prefs.value.query?.trim()) {
|
|
525
|
+
const query = prefs.value.query.toLowerCase()
|
|
526
|
+
result = result.filter(m =>
|
|
527
|
+
m.name?.toLowerCase().includes(query) ||
|
|
528
|
+
m.id?.toLowerCase().includes(query) ||
|
|
529
|
+
m.provider?.toLowerCase().includes(query)
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Sort
|
|
534
|
+
result.sort((a, b) => {
|
|
535
|
+
let cmp = 0
|
|
536
|
+
switch (prefs.value.sortBy) {
|
|
537
|
+
case 'name':
|
|
538
|
+
cmp = (a.name || '').localeCompare(b.name || '')
|
|
539
|
+
break
|
|
540
|
+
case 'knowledge':
|
|
541
|
+
cmp = (a.knowledge || '').localeCompare(b.knowledge || '')
|
|
542
|
+
break
|
|
543
|
+
case 'release_date':
|
|
544
|
+
cmp = (a.release_date || '').localeCompare(b.release_date || '')
|
|
545
|
+
break
|
|
546
|
+
case 'last_updated':
|
|
547
|
+
cmp = (a.last_updated || '').localeCompare(b.last_updated || '')
|
|
548
|
+
break
|
|
549
|
+
case 'cost_input':
|
|
550
|
+
cmp = (parseFloat(a.cost?.input) || 0) - (parseFloat(b.cost?.input) || 0)
|
|
551
|
+
break
|
|
552
|
+
case 'cost_output':
|
|
553
|
+
cmp = (parseFloat(a.cost?.output) || 0) - (parseFloat(b.cost?.output) || 0)
|
|
554
|
+
break
|
|
555
|
+
case 'context':
|
|
556
|
+
cmp = (a.limit?.context || 0) - (b.limit?.context || 0)
|
|
557
|
+
break
|
|
558
|
+
default:
|
|
559
|
+
cmp = 0
|
|
560
|
+
}
|
|
561
|
+
return prefs.value.sortAsc ? cmp : -cmp
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
return result
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
function isFreeModel(model) {
|
|
568
|
+
return model.cost && parseFloat(model.cost.input) === 0 && parseFloat(model.cost.output) === 0
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function selectModel(model) {
|
|
572
|
+
ctx.setState({ selectedModel: model.name })
|
|
573
|
+
closeDialog()
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function closeDialog() {
|
|
577
|
+
emit('done')
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function setActiveTab(tab, provider) {
|
|
581
|
+
prefs.value.activeTab = tab
|
|
582
|
+
ext.setPrefs(prefs.value)
|
|
583
|
+
if (tab === 'browse') {
|
|
584
|
+
toggleProvider(provider)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function toggleProvider(provider) {
|
|
589
|
+
prefs.value.provider = provider == prefs.value.provider ? '' : provider
|
|
590
|
+
ext.setPrefs(prefs.value)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function toggleInputModality(modality) {
|
|
594
|
+
setPrefs({
|
|
595
|
+
inputModality: prefs.value.inputModality === modality ? null : modality
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function toggleOutputModality(modality) {
|
|
600
|
+
setPrefs({
|
|
601
|
+
outputModality: prefs.value.outputModality === modality ? null : modality
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function toggleFavorite(model) {
|
|
606
|
+
const key = getModelKey(model)
|
|
607
|
+
const favorites = prefs.value.favorites || (prefs.value.favorites = [])
|
|
608
|
+
const idx = favorites.indexOf(key)
|
|
609
|
+
if (idx === -1) {
|
|
610
|
+
favorites.push(key)
|
|
611
|
+
} else {
|
|
612
|
+
favorites.splice(idx, 1)
|
|
613
|
+
}
|
|
614
|
+
setPrefs({ favorites })
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function toggleSortDirection() {
|
|
618
|
+
setPrefs({
|
|
619
|
+
sortAsc: !prefs.value.sortAsc
|
|
620
|
+
})
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Save preferences when sort changes
|
|
624
|
+
watch(() => [prefs.value.query], () => {
|
|
625
|
+
console.log('setPrefs', prefs.value.query)
|
|
626
|
+
setPrefs({
|
|
627
|
+
query: prefs.value.query,
|
|
628
|
+
})
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
function setPrefs(o) {
|
|
632
|
+
Object.assign(prefs.value, o)
|
|
633
|
+
ext.setPrefs(prefs.value)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Deep link logic with Vue Router
|
|
637
|
+
onMounted(() => {
|
|
638
|
+
if (!prefs.value.query) {
|
|
639
|
+
prefs.value.query = ''
|
|
640
|
+
}
|
|
641
|
+
if (!prefs.value.sortBy) {
|
|
642
|
+
prefs.value.sortBy = 'name'
|
|
643
|
+
}
|
|
644
|
+
setTimeout(() => {
|
|
645
|
+
searchInput.value?.focus()
|
|
646
|
+
}, 100)
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
renderKey,
|
|
651
|
+
prefs,
|
|
652
|
+
models,
|
|
653
|
+
searchInput,
|
|
654
|
+
sortOptions,
|
|
655
|
+
uniqueProviders,
|
|
656
|
+
providerCounts,
|
|
657
|
+
filteredModels,
|
|
658
|
+
formatCost,
|
|
659
|
+
formatNumber,
|
|
660
|
+
formatShortNumber,
|
|
661
|
+
isFreeModel,
|
|
662
|
+
|
|
663
|
+
closeDialog,
|
|
664
|
+
selectModel,
|
|
665
|
+
toggleProvider,
|
|
666
|
+
toggleSortDirection,
|
|
667
|
+
favorites,
|
|
668
|
+
activeTab,
|
|
669
|
+
setActiveTab,
|
|
670
|
+
toggleFavorite,
|
|
671
|
+
isFavorite,
|
|
672
|
+
unavailableFavorites,
|
|
673
|
+
hasUnavailableFavorites,
|
|
674
|
+
modalityIcons,
|
|
675
|
+
inputModalityTypes,
|
|
676
|
+
outputModalityTypes,
|
|
677
|
+
toggleInputModality,
|
|
678
|
+
toggleOutputModality,
|
|
679
|
+
getInputModalities,
|
|
680
|
+
getOutputModalities,
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const ModelTooltip = {
|
|
686
|
+
template: `
|
|
687
|
+
<div v-if="model"
|
|
688
|
+
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">
|
|
689
|
+
<div class="font-semibold text-gray-900 dark:text-gray-100 mb-2">{{ model.name }}</div>
|
|
690
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 mb-2">{{ model.provider }}</div>
|
|
691
|
+
|
|
692
|
+
<div v-if="model.cost" class="mb-2">
|
|
693
|
+
<div class="text-xs font-medium text-gray-700 dark:text-gray-300">Cost per 1M tokens:</div>
|
|
694
|
+
<div class="text-xs text-gray-600 dark:text-gray-400 ml-2">
|
|
695
|
+
Input: {{ formatCost(model.cost.input) }} · Output: {{ formatCost(model.cost.output) }}
|
|
696
|
+
</div>
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
<div v-if="model.limit" class="mb-2">
|
|
700
|
+
<div class="text-xs font-medium text-gray-700 dark:text-gray-300">Limits:</div>
|
|
701
|
+
<div class="text-xs text-gray-600 dark:text-gray-400 ml-2">
|
|
702
|
+
Context: {{ formatNumber(model.limit.context) }} · Output: {{ formatNumber(model.limit.output) }}
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
<div v-if="model.knowledge" class="text-xs text-gray-600 dark:text-gray-400">
|
|
707
|
+
Knowledge: {{ model.knowledge }}
|
|
708
|
+
</div>
|
|
709
|
+
|
|
710
|
+
<div class="flex flex-wrap gap-1 mt-2">
|
|
711
|
+
<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>
|
|
712
|
+
<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>
|
|
713
|
+
|
|
714
|
+
<!-- Modality Icons (Input) -->
|
|
715
|
+
<span v-for="mod in getInputModalities(model)" :key="mod"
|
|
716
|
+
class="inline-flex items-center justify-center p-0.5 text-gray-400 dark:text-gray-500"
|
|
717
|
+
:title="'Input: ' + mod"
|
|
718
|
+
v-html="modalityIcons[mod]">
|
|
719
|
+
</span>
|
|
720
|
+
|
|
721
|
+
<!-- Modality Tags (Output) -->
|
|
722
|
+
<span v-for="mod in getOutputModalities(model)" :key="mod"
|
|
723
|
+
class="px-1.5 py-0.5 text-xs rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 capitalize"
|
|
724
|
+
:title="'Output: ' + mod">
|
|
725
|
+
{{ mod }}
|
|
726
|
+
</span>
|
|
727
|
+
</div>
|
|
728
|
+
</div>
|
|
729
|
+
`,
|
|
730
|
+
props: {
|
|
731
|
+
model: Object,
|
|
732
|
+
},
|
|
733
|
+
setup(props) {
|
|
734
|
+
return {
|
|
735
|
+
formatCost,
|
|
736
|
+
formatNumber,
|
|
737
|
+
getInputModalities,
|
|
738
|
+
getOutputModalities,
|
|
739
|
+
modalityIcons,
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const ModelSelector = {
|
|
745
|
+
template: `
|
|
746
|
+
<!-- Model Selector Button -->
|
|
747
|
+
<div class="pl-1.5 flex space-x-2">
|
|
748
|
+
<button type="button" @click="openDialog"
|
|
749
|
+
class="select-none 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 w-full md:w-auto md:min-w-48 max-w-96"
|
|
750
|
+
@mouseenter="showTooltip = true"
|
|
751
|
+
@mouseleave="showTooltip = false">
|
|
752
|
+
<ProviderIcon v-if="selectedModel?.provider" :provider="selectedModel.provider" class="size-5 flex-shrink-0" />
|
|
753
|
+
<span class="truncate flex-1 text-left">{{ selectedModel?.name || 'Select Model...' }}</span>
|
|
754
|
+
<svg class="size-4 flex-shrink-0 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
755
|
+
<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" />
|
|
756
|
+
</svg>
|
|
757
|
+
</button>
|
|
758
|
+
|
|
759
|
+
<!-- Info Tooltip (on hover) -->
|
|
760
|
+
<ModelTooltip v-if="showTooltip" :model="selectedModel" />
|
|
761
|
+
|
|
762
|
+
</div>
|
|
763
|
+
`,
|
|
764
|
+
emits: ['updated', 'update:modelValue'],
|
|
765
|
+
props: {
|
|
766
|
+
models: Array,
|
|
767
|
+
modelValue: String,
|
|
768
|
+
},
|
|
769
|
+
setup(props, { emit }) {
|
|
770
|
+
const ctx = inject('ctx')
|
|
771
|
+
const showTooltip = ref(false)
|
|
772
|
+
|
|
773
|
+
// Get selected model object
|
|
774
|
+
const selectedModel = computed(() => {
|
|
775
|
+
if (!props.modelValue || !props.models) return null
|
|
776
|
+
return props.models.find(m => m.name === props.modelValue) || props.models.find(m => m.id === props.modelValue)
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
function openDialog() {
|
|
780
|
+
ctx.state.models = props.models
|
|
781
|
+
ctx.openModal('models')
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
watch(() => ctx.state.selectedModel, (newVal) => {
|
|
785
|
+
emit('update:modelValue', newVal)
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
onMounted(() => {
|
|
789
|
+
ctx.state.models = props.models
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
return {
|
|
793
|
+
showTooltip,
|
|
794
|
+
openDialog,
|
|
795
|
+
selectedModel,
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export default {
|
|
801
|
+
install(ctx) {
|
|
802
|
+
ctx.components({
|
|
803
|
+
ProviderStatus,
|
|
804
|
+
ModelSelector,
|
|
805
|
+
ModelTooltip,
|
|
806
|
+
})
|
|
807
|
+
ctx.modals({
|
|
808
|
+
'models': ModelSelectorModal,
|
|
809
|
+
})
|
|
810
|
+
}
|
|
811
|
+
}
|