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
|
@@ -1,680 +0,0 @@
|
|
|
1
|
-
import { ref, computed, nextTick, watch, onMounted, onUnmounted, provide, inject } from 'vue'
|
|
2
|
-
import { useRouter, useRoute } from 'vue-router'
|
|
3
|
-
import { useThreadStore } from './threadStore.mjs'
|
|
4
|
-
import { storageObject, addCopyButtons } from './utils.mjs'
|
|
5
|
-
import { renderMarkdown } from './markdown.mjs'
|
|
6
|
-
import ChatPrompt, { useChatPrompt } from './ChatPrompt.mjs'
|
|
7
|
-
|
|
8
|
-
const ProviderStatus = {
|
|
9
|
-
template:`
|
|
10
|
-
<div ref="triggerRef" class="relative" :key="renderKey">
|
|
11
|
-
<button type="button" @click="togglePopover"
|
|
12
|
-
class="mt-1 flex space-x-2 items-center text-sm font-semibold select-none rounded-sm py-2 px-3 border border-transparent hover:bg-gray-50 hover:shadow hover:border-gray-200">
|
|
13
|
-
<span class="text-gray-600" :title="models.length + ' models from ' + (config.status.enabled||[]).length + ' enabled providers'">{{models.length}}</span>
|
|
14
|
-
<div class="cursor-pointer flex items-center" :title="'Enabled:\\n' + (config.status.enabled||[]).map(x => ' ' + x).join('\\n')">
|
|
15
|
-
<svg class="size-4 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="currentColor"/></svg>
|
|
16
|
-
<span class="text-green-700">{{(config.status.enabled||[]).length}}</span>
|
|
17
|
-
</div>
|
|
18
|
-
<div class="cursor-pointer flex items-center" :title="'Disabled:\\n' + (config.status.disabled||[]).map(x => ' ' + x).join('\\n')">
|
|
19
|
-
<svg class="size-4 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="currentColor"/></svg>
|
|
20
|
-
<span class="text-red-700">{{(config.status.disabled||[]).length}}</span>
|
|
21
|
-
</div>
|
|
22
|
-
</button>
|
|
23
|
-
<div v-if="showPopover" ref="popoverRef" class="absolute right-0 mt-2 w-72 max-h-116 overflow-y-auto bg-white border border-gray-200 rounded-md shadow-lg z-10">
|
|
24
|
-
<div class="divide-y divide-gray-100">
|
|
25
|
-
<div v-for="p in allProviders" :key="p" class="flex items-center justify-between px-3 py-2">
|
|
26
|
-
<label :for="'chk_' + p" class="cursor-pointer text-sm text-gray-900 truncate mr-2" :title="p">{{ p }}</label>
|
|
27
|
-
<div @click="onToggle(p, !isEnabled(p))" class="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">
|
|
28
|
-
<span class="absolute mx-auto h-4 w-9 rounded-full bg-gray-200 inset-ring inset-ring-gray-900/5 transition-colors duration-200 ease-in-out group-has-checked:bg-green-600" />
|
|
29
|
-
<span class="absolute left-0 size-5 rounded-full border border-gray-300 bg-white shadow-xs transition-transform duration-200 ease-in-out group-has-checked:translate-x-5" />
|
|
30
|
-
<input :id="'chk_' + p" type="checkbox" :checked="isEnabled(p)" class="cursor-pointer absolute inset-0 appearance-none focus:outline-hidden" aria-label="Use setting" name="setting" />
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
`,
|
|
37
|
-
emits: ['updated'],
|
|
38
|
-
setup(props, { emit }) {
|
|
39
|
-
const config = inject('config')
|
|
40
|
-
const models = inject('models')
|
|
41
|
-
const showPopover = ref(false)
|
|
42
|
-
const triggerRef = ref(null)
|
|
43
|
-
const popoverRef = ref(null)
|
|
44
|
-
const pending = ref({})
|
|
45
|
-
const renderKey = ref(0)
|
|
46
|
-
const allProviders = computed(() => config.status?.all)
|
|
47
|
-
const isEnabled = (p) => config.status.enabled.includes(p)
|
|
48
|
-
const togglePopover = () => showPopover.value = !showPopover.value
|
|
49
|
-
|
|
50
|
-
const onToggle = async (provider, enable) => {
|
|
51
|
-
pending.value = { ...pending.value, [provider]: true }
|
|
52
|
-
try {
|
|
53
|
-
const res = await fetch(`/providers/${encodeURIComponent(provider)}`, {
|
|
54
|
-
method: 'POST',
|
|
55
|
-
headers: { 'Content-Type': 'application/json' },
|
|
56
|
-
body: JSON.stringify(enable ? { enable: true } : { disable: true })
|
|
57
|
-
})
|
|
58
|
-
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
59
|
-
const json = await res.json()
|
|
60
|
-
config.status.enabled = json.enabled || []
|
|
61
|
-
config.status.disabled = json.disabled || []
|
|
62
|
-
if (json.feedback) {
|
|
63
|
-
alert(json.feedback)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const [configRes, modelsRes] = await Promise.all([
|
|
68
|
-
fetch('/ui.json'),
|
|
69
|
-
fetch('/models'),
|
|
70
|
-
])
|
|
71
|
-
const newConfig = await configRes.json()
|
|
72
|
-
const newModels = await modelsRes.json()
|
|
73
|
-
Object.assign(config, newConfig)
|
|
74
|
-
models.length = 0
|
|
75
|
-
newModels.forEach(m => models.push(m))
|
|
76
|
-
emit('updated')
|
|
77
|
-
renderKey.value++
|
|
78
|
-
} catch (e) {
|
|
79
|
-
alert(`Failed to reload config: ${e.message}`)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
} catch (e) {
|
|
83
|
-
alert(`Failed to ${enable ? 'enable' : 'disable'} ${provider}: ${e.message}`)
|
|
84
|
-
} finally {
|
|
85
|
-
pending.value = { ...pending.value, [provider]: false }
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const onDocClick = (e) => {
|
|
90
|
-
const t = e.target
|
|
91
|
-
if (triggerRef.value?.contains(t)) return
|
|
92
|
-
if (popoverRef.value?.contains(t)) return
|
|
93
|
-
showPopover.value = false
|
|
94
|
-
}
|
|
95
|
-
onMounted(() => document.addEventListener('click', onDocClick))
|
|
96
|
-
onUnmounted(() => document.removeEventListener('click', onDocClick))
|
|
97
|
-
return {
|
|
98
|
-
renderKey,
|
|
99
|
-
config,
|
|
100
|
-
models,
|
|
101
|
-
showPopover,
|
|
102
|
-
triggerRef,
|
|
103
|
-
popoverRef,
|
|
104
|
-
allProviders,
|
|
105
|
-
isEnabled,
|
|
106
|
-
togglePopover,
|
|
107
|
-
onToggle,
|
|
108
|
-
pending,
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export default {
|
|
114
|
-
components: {
|
|
115
|
-
ChatPrompt,
|
|
116
|
-
ProviderStatus,
|
|
117
|
-
},
|
|
118
|
-
template: `
|
|
119
|
-
<div class="flex flex-col h-full w-full">
|
|
120
|
-
<!-- Header with model and prompt selectors -->
|
|
121
|
-
<div class="border-b border-gray-200 bg-white px-2 py-2 w-full min-h-16">
|
|
122
|
-
<div class="flex items-center justify-between w-full">
|
|
123
|
-
<!-- Model Selector -->
|
|
124
|
-
<div class="pl-1 flex space-x-2">
|
|
125
|
-
<Autocomplete id="model" :options="models" v-model="selectedModel" label=""
|
|
126
|
-
class="w-72 xl:w-84"
|
|
127
|
-
:match="(x, value) => x.toLowerCase().includes(value.toLowerCase())"
|
|
128
|
-
placeholder="Select Model...">
|
|
129
|
-
<template #item="name">
|
|
130
|
-
<div class="truncate max-w-72" :title="name">{{name}}</div>
|
|
131
|
-
</template>
|
|
132
|
-
</Autocomplete>
|
|
133
|
-
<ProviderStatus @updated="configUpdated" />
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
<!-- System Prompt Selector -->
|
|
137
|
-
<div class="flex items-center space-x-2">
|
|
138
|
-
<button v-if="selectedPrompt" type="button" title="Clear System Prompt" @click="selectedPrompt = null">
|
|
139
|
-
<svg class="size-4 text-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><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"/></svg>
|
|
140
|
-
</button>
|
|
141
|
-
|
|
142
|
-
<Autocomplete id="prompt" :options="prompts" v-model="selectedPrompt" label=""
|
|
143
|
-
class="w-72 xl:w-84"
|
|
144
|
-
:match="(x, value) => x.name.toLowerCase().includes(value.toLowerCase())"
|
|
145
|
-
placeholder="Select a System Prompt...">
|
|
146
|
-
<template #item="{ name }">
|
|
147
|
-
<div class="truncate max-w-72" :title="name">{{name}}</div>
|
|
148
|
-
</template>
|
|
149
|
-
</Autocomplete>
|
|
150
|
-
|
|
151
|
-
<!-- Toggle System Prompt Visibility -->
|
|
152
|
-
<button
|
|
153
|
-
@click="showSystemPrompt = !showSystemPrompt"
|
|
154
|
-
:class="showSystemPrompt ? 'text-blue-700' : 'text-gray-600'"
|
|
155
|
-
class="p-1 rounded-md hover:bg-blue-100 hover:text-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
156
|
-
:title="showSystemPrompt ? 'Hide system prompt' : 'Show system prompt'"
|
|
157
|
-
>
|
|
158
|
-
<svg v-if="!showSystemPrompt" class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="currentColor" d="M33.62 17.53c-3.37-6.23-9.28-10-15.82-10S5.34 11.3 2 17.53l-.28.47l.26.48c3.37 6.23 9.28 10 15.82 10s12.46-3.72 15.82-10l.26-.48Zm-15.82 8.9C12.17 26.43 7 23.29 4 18c3-5.29 8.17-8.43 13.8-8.43S28.54 12.72 31.59 18c-3.05 5.29-8.17 8.43-13.79 8.43"/><path fill="currentColor" d="M18.09 11.17A6.86 6.86 0 1 0 25 18a6.86 6.86 0 0 0-6.91-6.83m0 11.72A4.86 4.86 0 1 1 23 18a4.87 4.87 0 0 1-4.91 4.89"/><path fill="none" d="M0 0h36v36H0z"/></svg>
|
|
159
|
-
<svg v-else class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="currentColor" d="M25.19 20.4a6.8 6.8 0 0 0 .43-2.4a6.86 6.86 0 0 0-6.86-6.86a6.8 6.8 0 0 0-2.37.43L18 13.23a5 5 0 0 1 .74-.06A4.87 4.87 0 0 1 23.62 18a5 5 0 0 1-.06.74Z" class="clr-i-outline clr-i-outline-path-1"/><path fill="currentColor" d="M34.29 17.53c-3.37-6.23-9.28-10-15.82-10a16.8 16.8 0 0 0-5.24.85L14.84 10a14.8 14.8 0 0 1 3.63-.47c5.63 0 10.75 3.14 13.8 8.43a17.8 17.8 0 0 1-4.37 5.1l1.42 1.42a19.9 19.9 0 0 0 5-6l.26-.48Z"/><path fill="currentColor" d="m4.87 5.78l4.46 4.46a19.5 19.5 0 0 0-6.69 7.29l-.26.47l.26.48c3.37 6.23 9.28 10 15.82 10a16.9 16.9 0 0 0 7.37-1.69l5 5l1.75-1.5l-26-26Zm9.75 9.75l6.65 6.65a4.8 4.8 0 0 1-2.5.72A4.87 4.87 0 0 1 13.9 18a4.8 4.8 0 0 1 .72-2.47m-1.45-1.45a6.85 6.85 0 0 0 9.55 9.55l1.6 1.6a14.9 14.9 0 0 1-5.86 1.2c-5.63 0-10.75-3.14-13.8-8.43a17.3 17.3 0 0 1 6.12-6.3Z"/><path fill="none" d="M0 0h36v36H0z"/></svg>
|
|
160
|
-
</button>
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
<!-- System Prompt Editor -->
|
|
166
|
-
<div v-if="showSystemPrompt" class="border-b border-gray-200 bg-gray-50 px-6 py-4">
|
|
167
|
-
<div class="max-w-6xl mx-auto">
|
|
168
|
-
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
169
|
-
System Prompt
|
|
170
|
-
<span v-if="selectedPrompt" class="text-gray-500 font-normal">
|
|
171
|
-
({{ prompts.find(p => p.id === selectedPrompt.id)?.name || 'Custom' }})
|
|
172
|
-
</span>
|
|
173
|
-
</label>
|
|
174
|
-
<textarea
|
|
175
|
-
v-model="currentSystemPrompt"
|
|
176
|
-
placeholder="Enter a system prompt to guide AI's behavior..."
|
|
177
|
-
rows="6"
|
|
178
|
-
class="block w-full resize-vertical rounded-md border border-gray-300 px-3 py-2 text-sm placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
179
|
-
></textarea>
|
|
180
|
-
<div class="mt-2 text-xs text-gray-500">
|
|
181
|
-
You can modify this system prompt before sending messages. Changes will only apply to new conversations.
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
</div>
|
|
185
|
-
|
|
186
|
-
<!-- Messages Area -->
|
|
187
|
-
<div class="flex-1 overflow-y-auto" ref="messagesContainer">
|
|
188
|
-
<div class="mx-auto max-w-6xl px-4 py-6">
|
|
189
|
-
<!-- Welcome message when no thread is selected -->
|
|
190
|
-
<div v-if="!currentThread" class="text-center py-12">
|
|
191
|
-
<div class="mb-2 flex justify-center">
|
|
192
|
-
<svg class="size-20 text-gray-700" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M8 2.19c3.13 0 5.68 2.25 5.68 5s-2.55 5-5.68 5a5.7 5.7 0 0 1-1.89-.29l-.75-.26l-.56.56a14 14 0 0 1-2 1.55a.13.13 0 0 1-.07 0v-.06a6.58 6.58 0 0 0 .15-4.29a5.25 5.25 0 0 1-.55-2.16c0-2.77 2.55-5 5.68-5M8 .94c-3.83 0-6.93 2.81-6.93 6.27a6.4 6.4 0 0 0 .64 2.64a5.53 5.53 0 0 1-.18 3.48a1.32 1.32 0 0 0 2 1.5a15 15 0 0 0 2.16-1.71a6.8 6.8 0 0 0 2.31.36c3.83 0 6.93-2.81 6.93-6.27S11.83.94 8 .94"/><ellipse cx="5.2" cy="7.7" fill="currentColor" rx=".8" ry=".75"/><ellipse cx="8" cy="7.7" fill="currentColor" rx=".8" ry=".75"/><ellipse cx="10.8" cy="7.7" fill="currentColor" rx=".8" ry=".75"/></svg>
|
|
193
|
-
</div>
|
|
194
|
-
<h2 class="text-2xl font-semibold text-gray-900 mb-2">Welcome to llms.py</h2>
|
|
195
|
-
|
|
196
|
-
<!-- Chat input for new conversation -->
|
|
197
|
-
<div class="max-w-2xl mx-auto">
|
|
198
|
-
<ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
<!-- Export/Import buttons -->
|
|
202
|
-
<div class="mt-2 flex space-x-3 justify-center">
|
|
203
|
-
<button type="button"
|
|
204
|
-
@click="exportThreads"
|
|
205
|
-
:disabled="isExporting"
|
|
206
|
-
:title="'Export ' + threads?.threads?.value?.length + ' conversations'"
|
|
207
|
-
class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
208
|
-
>
|
|
209
|
-
<svg v-if="!isExporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
210
|
-
<path fill="currentColor" d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z"></path>
|
|
211
|
-
</svg>
|
|
212
|
-
<svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
213
|
-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
214
|
-
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
215
|
-
</svg>
|
|
216
|
-
{{ isExporting ? 'Exporting...' : 'Export' }}
|
|
217
|
-
</button>
|
|
218
|
-
|
|
219
|
-
<button type="button"
|
|
220
|
-
@click="triggerImport"
|
|
221
|
-
:disabled="isImporting"
|
|
222
|
-
title="Import conversations from JSON file"
|
|
223
|
-
class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
224
|
-
>
|
|
225
|
-
<svg v-if="!isImporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
226
|
-
<path fill="currentColor" d="m14 12l-4-4v3H2v2h8v3m10 2V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v3h2V6h12v12H6v-3H4v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2"/>
|
|
227
|
-
</svg>
|
|
228
|
-
<svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
229
|
-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
230
|
-
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
231
|
-
</svg>
|
|
232
|
-
{{ isImporting ? 'Importing...' : 'Import' }}
|
|
233
|
-
</button>
|
|
234
|
-
|
|
235
|
-
<!-- Hidden file input for import -->
|
|
236
|
-
<input
|
|
237
|
-
ref="fileInput"
|
|
238
|
-
type="file"
|
|
239
|
-
accept=".json"
|
|
240
|
-
@change="handleFileImport"
|
|
241
|
-
class="hidden"
|
|
242
|
-
/>
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
</div>
|
|
246
|
-
|
|
247
|
-
<!-- Messages -->
|
|
248
|
-
<div v-else class="space-y-6">
|
|
249
|
-
<div
|
|
250
|
-
v-for="message in currentThread.messages"
|
|
251
|
-
:key="message.id"
|
|
252
|
-
class="flex items-start space-x-3 group"
|
|
253
|
-
:class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
|
|
254
|
-
>
|
|
255
|
-
<!-- Avatar outside the bubble -->
|
|
256
|
-
<div class="flex-shrink-0 flex flex-col justify-center">
|
|
257
|
-
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
|
|
258
|
-
:class="message.role === 'user'
|
|
259
|
-
? 'bg-blue-600 text-white'
|
|
260
|
-
: 'bg-gray-600 text-white'"
|
|
261
|
-
>
|
|
262
|
-
{{ message.role === 'user' ? 'U' : 'AI' }}
|
|
263
|
-
</div>
|
|
264
|
-
|
|
265
|
-
<!-- Delete button (shown on hover) -->
|
|
266
|
-
<button type="button" @click.stop="threads.deleteMessageFromThread(currentThread.id, message.id)"
|
|
267
|
-
class="mx-auto opacity-0 group-hover:opacity-100 mt-2 rounded text-gray-400 hover:text-red-600 hover:bg-red-50 transition-all"
|
|
268
|
-
title="Delete message">
|
|
269
|
-
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
270
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
271
|
-
</svg>
|
|
272
|
-
</button>
|
|
273
|
-
</div>
|
|
274
|
-
|
|
275
|
-
<!-- Message bubble -->
|
|
276
|
-
<div
|
|
277
|
-
class="message rounded-lg px-4 py-3 relative group"
|
|
278
|
-
:class="message.role === 'user'
|
|
279
|
-
? 'bg-blue-600 text-white'
|
|
280
|
-
: 'bg-gray-100 text-gray-900 border border-gray-200'"
|
|
281
|
-
>
|
|
282
|
-
<!-- Copy button in top right corner -->
|
|
283
|
-
<button
|
|
284
|
-
type="button"
|
|
285
|
-
@click="copyMessageContent(message)"
|
|
286
|
-
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 p-1 rounded hover:bg-black/10 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
287
|
-
:class="message.role === 'user' ? 'text-white/70 hover:text-white hover:bg-white/20' : 'text-gray-500 hover:text-gray-700'"
|
|
288
|
-
title="Copy message content"
|
|
289
|
-
>
|
|
290
|
-
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
291
|
-
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
|
|
292
|
-
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
|
|
293
|
-
</svg>
|
|
294
|
-
</button>
|
|
295
|
-
|
|
296
|
-
<div
|
|
297
|
-
v-if="message.role === 'assistant'"
|
|
298
|
-
v-html="renderMarkdown(message.content)"
|
|
299
|
-
class="prose prose-sm max-w-none"
|
|
300
|
-
></div>
|
|
301
|
-
|
|
302
|
-
<!-- Collapsible reasoning section -->
|
|
303
|
-
<div v-if="message.role === 'assistant' && message.reasoning" class="mt-2">
|
|
304
|
-
<button type="button" @click="toggleReasoning(message.id)" class="text-xs text-gray-600 hover:text-gray-800 flex items-center space-x-1">
|
|
305
|
-
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" :class="isReasoningExpanded(message.id) ? 'transform rotate-90' : ''"><path fill="currentColor" d="M7 5l6 5l-6 5z"/></svg>
|
|
306
|
-
<span>{{ isReasoningExpanded(message.id) ? 'Hide reasoning' : 'Show reasoning' }}</span>
|
|
307
|
-
</button>
|
|
308
|
-
<div v-if="isReasoningExpanded(message.id)" class="mt-2 rounded border border-gray-200 bg-gray-50 p-2">
|
|
309
|
-
<div v-if="typeof message.reasoning === 'string'" v-html="renderMarkdown(message.reasoning)" class="prose prose-xs max-w-none"></div>
|
|
310
|
-
<pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto">{{ formatReasoning(message.reasoning) }}</pre>
|
|
311
|
-
</div>
|
|
312
|
-
</div>
|
|
313
|
-
|
|
314
|
-
<div v-if="message.role !== 'assistant'" class="whitespace-pre-wrap">{{ message.content }}</div>
|
|
315
|
-
<div class="mt-2 text-xs opacity-70">
|
|
316
|
-
{{ formatTime(message.timestamp) }}
|
|
317
|
-
</div>
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
320
|
-
|
|
321
|
-
<!-- Loading indicator -->
|
|
322
|
-
<div v-if="isGenerating" class="flex items-start space-x-3">
|
|
323
|
-
<!-- Avatar outside the bubble -->
|
|
324
|
-
<div class="flex-shrink-0">
|
|
325
|
-
<div class="w-8 h-8 rounded-full bg-gray-600 text-white flex items-center justify-center text-sm font-medium">
|
|
326
|
-
AI
|
|
327
|
-
</div>
|
|
328
|
-
</div>
|
|
329
|
-
|
|
330
|
-
<!-- Loading bubble -->
|
|
331
|
-
<div class="rounded-lg px-4 py-3 bg-gray-100 border border-gray-200">
|
|
332
|
-
<div class="flex space-x-1">
|
|
333
|
-
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
|
334
|
-
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
|
|
335
|
-
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
|
336
|
-
</div>
|
|
337
|
-
</div>
|
|
338
|
-
</div>
|
|
339
|
-
|
|
340
|
-
<!-- Error message bubble -->
|
|
341
|
-
<div v-if="errorMessage" class="flex items-start space-x-3">
|
|
342
|
-
<!-- Avatar outside the bubble -->
|
|
343
|
-
<div class="flex-shrink-0">
|
|
344
|
-
<div class="w-8 h-8 rounded-full bg-red-600 text-white flex items-center justify-center text-sm font-medium">
|
|
345
|
-
!
|
|
346
|
-
</div>
|
|
347
|
-
</div>
|
|
348
|
-
|
|
349
|
-
<!-- Error bubble -->
|
|
350
|
-
<div class="max-w-[85%] rounded-lg px-4 py-3 bg-red-50 border border-red-200 text-red-800 shadow-sm">
|
|
351
|
-
<div class="flex items-start space-x-2">
|
|
352
|
-
<div class="flex-1 min-w-0">
|
|
353
|
-
<div class="text-base font-medium mb-1">{{ errorStatus || 'Error' }}</div>
|
|
354
|
-
<div class="text-sm whitespace-pre-wrap break-words max-h-80 overflow-y-auto font-mono p-2 rounded">
|
|
355
|
-
{{ errorMessage }}
|
|
356
|
-
</div>
|
|
357
|
-
</div>
|
|
358
|
-
<button type="button"
|
|
359
|
-
@click="errorMessage = null"
|
|
360
|
-
class="text-red-400 hover:text-red-600 flex-shrink-0"
|
|
361
|
-
>
|
|
362
|
-
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
363
|
-
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
364
|
-
</svg>
|
|
365
|
-
</button>
|
|
366
|
-
</div>
|
|
367
|
-
</div>
|
|
368
|
-
</div>
|
|
369
|
-
</div>
|
|
370
|
-
</div>
|
|
371
|
-
</div>
|
|
372
|
-
|
|
373
|
-
<!-- Input Area - only show when thread is selected -->
|
|
374
|
-
<div v-if="currentThread" class="flex-shrink-0 border-t border-gray-200 bg-white px-6 py-4">
|
|
375
|
-
<ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
|
|
376
|
-
</div>
|
|
377
|
-
</div>
|
|
378
|
-
`,
|
|
379
|
-
props: {
|
|
380
|
-
},
|
|
381
|
-
setup(props) {
|
|
382
|
-
const router = useRouter()
|
|
383
|
-
const route = useRoute()
|
|
384
|
-
const threads = useThreadStore()
|
|
385
|
-
const { currentThread } = threads
|
|
386
|
-
const chatPrompt = useChatPrompt()
|
|
387
|
-
const {
|
|
388
|
-
errorStatus,
|
|
389
|
-
errorMessage,
|
|
390
|
-
isGenerating,
|
|
391
|
-
} = chatPrompt
|
|
392
|
-
provide('threads', threads)
|
|
393
|
-
provide('chatPrompt', chatPrompt)
|
|
394
|
-
const models = inject('models')
|
|
395
|
-
const config = inject('config')
|
|
396
|
-
|
|
397
|
-
const prefs = storageObject('prefs')
|
|
398
|
-
|
|
399
|
-
const customPromptValue = ref('')
|
|
400
|
-
const customPrompt = {
|
|
401
|
-
id: '_custom_',
|
|
402
|
-
name: 'Custom...',
|
|
403
|
-
value: ''
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const prompts = computed(() => [customPrompt, ...config.prompts])
|
|
407
|
-
|
|
408
|
-
const selectedModel = ref(prefs.model || config.defaults.text.model || '')
|
|
409
|
-
const selectedPrompt = ref(prefs.systemPrompt || '')
|
|
410
|
-
const currentSystemPrompt = ref('')
|
|
411
|
-
const showSystemPrompt = ref(false)
|
|
412
|
-
const messagesContainer = ref(null)
|
|
413
|
-
const isExporting = ref(false)
|
|
414
|
-
const isImporting = ref(false)
|
|
415
|
-
const fileInput = ref(null)
|
|
416
|
-
|
|
417
|
-
// Auto-scroll to bottom when new messages arrive
|
|
418
|
-
const scrollToBottom = async () => {
|
|
419
|
-
await nextTick()
|
|
420
|
-
if (messagesContainer.value) {
|
|
421
|
-
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Watch for new messages and scroll
|
|
426
|
-
watch(() => currentThread.value?.messages?.length, scrollToBottom)
|
|
427
|
-
|
|
428
|
-
// Watch for route changes and load the appropriate thread
|
|
429
|
-
watch(() => route.params.id, async (newId) => {
|
|
430
|
-
const thread = await threads.setCurrentThreadFromRoute(newId, router)
|
|
431
|
-
|
|
432
|
-
// If the selected thread specifies a model and it's available, switch to it
|
|
433
|
-
if (thread?.model && Array.isArray(models) && models.includes(thread.model)) {
|
|
434
|
-
selectedModel.value = thread.model
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Sync System Prompt selection from thread
|
|
438
|
-
if (thread) {
|
|
439
|
-
const norm = s => (s || '').replace(/\s+/g, ' ').trim()
|
|
440
|
-
const tsp = norm(thread.systemPrompt || '')
|
|
441
|
-
if (tsp) {
|
|
442
|
-
const match = config.prompts.find(p => norm(p.value) === tsp)
|
|
443
|
-
if (match) {
|
|
444
|
-
selectedPrompt.value = match
|
|
445
|
-
currentSystemPrompt.value = match.value.replace(/\n/g, ' ')
|
|
446
|
-
} else {
|
|
447
|
-
selectedPrompt.value = customPrompt
|
|
448
|
-
currentSystemPrompt.value = thread.systemPrompt
|
|
449
|
-
}
|
|
450
|
-
} else {
|
|
451
|
-
selectedPrompt.value = null
|
|
452
|
-
currentSystemPrompt.value = ''
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (!newId) {
|
|
457
|
-
chatPrompt.reset()
|
|
458
|
-
}
|
|
459
|
-
nextTick(addCopyButtons)
|
|
460
|
-
}, { immediate: true })
|
|
461
|
-
|
|
462
|
-
// Watch selectedPrompt and update currentSystemPrompt
|
|
463
|
-
watch(selectedPrompt, (newPrompt) => {
|
|
464
|
-
// If using a custom prompt, keep whatever is already in currentSystemPrompt
|
|
465
|
-
if (newPrompt && newPrompt.id === '_custom_') return
|
|
466
|
-
const prompt = newPrompt && config.prompts.find(p => p.id === newPrompt.id)
|
|
467
|
-
currentSystemPrompt.value = prompt ? prompt.value.replace(/\n/g,' ') : ''
|
|
468
|
-
}, { immediate: true })
|
|
469
|
-
|
|
470
|
-
watch(() => [selectedModel.value, selectedPrompt.value], () => {
|
|
471
|
-
localStorage.setItem('prefs', JSON.stringify({
|
|
472
|
-
model: selectedModel.value,
|
|
473
|
-
systemPrompt: selectedPrompt.value
|
|
474
|
-
}))
|
|
475
|
-
})
|
|
476
|
-
|
|
477
|
-
async function exportThreads() {
|
|
478
|
-
if (isExporting.value) return
|
|
479
|
-
|
|
480
|
-
isExporting.value = true
|
|
481
|
-
try {
|
|
482
|
-
// Load all threads from IndexedDB
|
|
483
|
-
await threads.loadThreads()
|
|
484
|
-
const allThreads = threads.threads.value
|
|
485
|
-
|
|
486
|
-
// Create export data with metadata
|
|
487
|
-
const exportData = {
|
|
488
|
-
exportedAt: new Date().toISOString(),
|
|
489
|
-
version: '1.0',
|
|
490
|
-
source: 'llms.py',
|
|
491
|
-
threadCount: allThreads.length,
|
|
492
|
-
threads: allThreads
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Create and download JSON file
|
|
496
|
-
const jsonString = JSON.stringify(exportData, null, 2)
|
|
497
|
-
const blob = new Blob([jsonString], { type: 'application/json' })
|
|
498
|
-
const url = URL.createObjectURL(blob)
|
|
499
|
-
|
|
500
|
-
const link = document.createElement('a')
|
|
501
|
-
link.href = url
|
|
502
|
-
link.download = `llms-threads-export-${new Date().toISOString().split('T')[0]}.json`
|
|
503
|
-
document.body.appendChild(link)
|
|
504
|
-
link.click()
|
|
505
|
-
document.body.removeChild(link)
|
|
506
|
-
URL.revokeObjectURL(url)
|
|
507
|
-
|
|
508
|
-
} catch (error) {
|
|
509
|
-
console.error('Failed to export threads:', error)
|
|
510
|
-
alert('Failed to export threads: ' + error.message)
|
|
511
|
-
} finally {
|
|
512
|
-
isExporting.value = false
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function triggerImport() {
|
|
517
|
-
if (isImporting.value) return
|
|
518
|
-
fileInput.value?.click()
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
async function handleFileImport(event) {
|
|
522
|
-
const file = event.target.files?.[0]
|
|
523
|
-
if (!file) return
|
|
524
|
-
|
|
525
|
-
isImporting.value = true
|
|
526
|
-
try {
|
|
527
|
-
const text = await file.text()
|
|
528
|
-
const importData = JSON.parse(text)
|
|
529
|
-
|
|
530
|
-
// Validate import data structure
|
|
531
|
-
if (!importData.threads || !Array.isArray(importData.threads)) {
|
|
532
|
-
throw new Error('Invalid import file: missing or invalid threads array')
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Import threads one by one
|
|
536
|
-
let importedCount = 0
|
|
537
|
-
let updatedCount = 0
|
|
538
|
-
|
|
539
|
-
for (const threadData of importData.threads) {
|
|
540
|
-
if (!threadData.id) {
|
|
541
|
-
console.warn('Skipping thread without ID:', threadData)
|
|
542
|
-
continue
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
// Check if thread already exists
|
|
547
|
-
const existingThread = await threads.getThread(threadData.id)
|
|
548
|
-
|
|
549
|
-
if (existingThread) {
|
|
550
|
-
// Update existing thread
|
|
551
|
-
await threads.updateThread(threadData.id, {
|
|
552
|
-
title: threadData.title,
|
|
553
|
-
model: threadData.model,
|
|
554
|
-
systemPrompt: threadData.systemPrompt,
|
|
555
|
-
messages: threadData.messages || [],
|
|
556
|
-
createdAt: threadData.createdAt,
|
|
557
|
-
// Keep the existing updatedAt or use imported one
|
|
558
|
-
updatedAt: threadData.updatedAt || existingThread.updatedAt
|
|
559
|
-
})
|
|
560
|
-
updatedCount++
|
|
561
|
-
} else {
|
|
562
|
-
// Add new thread directly to IndexedDB
|
|
563
|
-
await threads.initDB()
|
|
564
|
-
const db = await threads.initDB()
|
|
565
|
-
const tx = db.transaction(['threads'], 'readwrite')
|
|
566
|
-
await tx.objectStore('threads').add({
|
|
567
|
-
id: threadData.id,
|
|
568
|
-
title: threadData.title || 'Imported Chat',
|
|
569
|
-
model: threadData.model || '',
|
|
570
|
-
systemPrompt: threadData.systemPrompt || '',
|
|
571
|
-
messages: threadData.messages || [],
|
|
572
|
-
createdAt: threadData.createdAt || new Date().toISOString(),
|
|
573
|
-
updatedAt: threadData.updatedAt || new Date().toISOString()
|
|
574
|
-
})
|
|
575
|
-
await tx.complete
|
|
576
|
-
importedCount++
|
|
577
|
-
}
|
|
578
|
-
} catch (error) {
|
|
579
|
-
console.error('Failed to import thread:', threadData.id, error)
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Reload threads to reflect changes
|
|
584
|
-
await threads.loadThreads()
|
|
585
|
-
|
|
586
|
-
alert(`Import completed!\nNew threads: ${importedCount}\nUpdated threads: ${updatedCount}`)
|
|
587
|
-
|
|
588
|
-
} catch (error) {
|
|
589
|
-
console.error('Failed to import threads:', error)
|
|
590
|
-
alert('Failed to import threads: ' + error.message)
|
|
591
|
-
} finally {
|
|
592
|
-
isImporting.value = false
|
|
593
|
-
// Clear the file input
|
|
594
|
-
if (fileInput.value) {
|
|
595
|
-
fileInput.value.value = ''
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function configUpdated() {
|
|
601
|
-
console.log('configUpdated', selectedModel.value, models.length, models.includes(selectedModel.value))
|
|
602
|
-
if (selectedModel.value && !models.includes(selectedModel.value)) {
|
|
603
|
-
selectedModel.value = config.defaults.text.model || ''
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Format timestamp
|
|
608
|
-
const formatTime = (timestamp) => {
|
|
609
|
-
return new Date(timestamp).toLocaleTimeString([], {
|
|
610
|
-
hour: '2-digit',
|
|
611
|
-
minute: '2-digit'
|
|
612
|
-
})
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Reasoning collapse state and helpers
|
|
616
|
-
const expandedReasoning = ref(new Set())
|
|
617
|
-
const isReasoningExpanded = (id) => expandedReasoning.value.has(id)
|
|
618
|
-
const toggleReasoning = (id) => {
|
|
619
|
-
const s = new Set(expandedReasoning.value)
|
|
620
|
-
if (s.has(id)) {
|
|
621
|
-
s.delete(id)
|
|
622
|
-
} else {
|
|
623
|
-
s.add(id)
|
|
624
|
-
}
|
|
625
|
-
expandedReasoning.value = s
|
|
626
|
-
}
|
|
627
|
-
const formatReasoning = (r) => typeof r === 'string' ? r : JSON.stringify(r, null, 2)
|
|
628
|
-
|
|
629
|
-
// Copy message content to clipboard
|
|
630
|
-
const copyMessageContent = async (message) => {
|
|
631
|
-
try {
|
|
632
|
-
await navigator.clipboard.writeText(message.content)
|
|
633
|
-
// Could add a toast notification here if desired
|
|
634
|
-
} catch (err) {
|
|
635
|
-
console.error('Failed to copy message content:', err)
|
|
636
|
-
// Fallback for older browsers
|
|
637
|
-
const textArea = document.createElement('textarea')
|
|
638
|
-
textArea.value = message.content
|
|
639
|
-
document.body.appendChild(textArea)
|
|
640
|
-
textArea.select()
|
|
641
|
-
document.execCommand('copy')
|
|
642
|
-
document.body.removeChild(textArea)
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
onMounted(() => {
|
|
647
|
-
setTimeout(addCopyButtons, 1)
|
|
648
|
-
})
|
|
649
|
-
|
|
650
|
-
return {
|
|
651
|
-
config,
|
|
652
|
-
models,
|
|
653
|
-
threads,
|
|
654
|
-
prompts,
|
|
655
|
-
isGenerating,
|
|
656
|
-
customPromptValue,
|
|
657
|
-
currentThread,
|
|
658
|
-
selectedModel,
|
|
659
|
-
selectedPrompt,
|
|
660
|
-
currentSystemPrompt,
|
|
661
|
-
showSystemPrompt,
|
|
662
|
-
messagesContainer,
|
|
663
|
-
errorStatus,
|
|
664
|
-
errorMessage,
|
|
665
|
-
formatTime,
|
|
666
|
-
renderMarkdown,
|
|
667
|
-
isReasoningExpanded,
|
|
668
|
-
toggleReasoning,
|
|
669
|
-
formatReasoning,
|
|
670
|
-
copyMessageContent,
|
|
671
|
-
configUpdated,
|
|
672
|
-
exportThreads,
|
|
673
|
-
isExporting,
|
|
674
|
-
triggerImport,
|
|
675
|
-
handleFileImport,
|
|
676
|
-
isImporting,
|
|
677
|
-
fileInput,
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|