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.
Files changed (194) hide show
  1. llms/__init__.py +4 -0
  2. llms/__main__.py +9 -0
  3. llms/db.py +359 -0
  4. llms/extensions/analytics/ui/index.mjs +1444 -0
  5. llms/extensions/app/README.md +20 -0
  6. llms/extensions/app/__init__.py +589 -0
  7. llms/extensions/app/db.py +536 -0
  8. {llms_py-2.0.9.data/data → llms/extensions/app}/ui/Recents.mjs +100 -73
  9. llms_py-2.0.9.data/data/ui/Sidebar.mjs → llms/extensions/app/ui/index.mjs +150 -79
  10. llms/extensions/app/ui/threadStore.mjs +433 -0
  11. llms/extensions/core_tools/CALCULATOR.md +32 -0
  12. llms/extensions/core_tools/__init__.py +637 -0
  13. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  14. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  15. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  16. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  17. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  18. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  19. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  20. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  22. llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
  23. llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
  24. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  25. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  26. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  27. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  28. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  29. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  30. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  31. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  32. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  33. llms/extensions/core_tools/ui/index.mjs +650 -0
  34. llms/extensions/gallery/README.md +61 -0
  35. llms/extensions/gallery/__init__.py +63 -0
  36. llms/extensions/gallery/db.py +243 -0
  37. llms/extensions/gallery/ui/index.mjs +482 -0
  38. llms/extensions/katex/README.md +39 -0
  39. llms/extensions/katex/__init__.py +6 -0
  40. llms/extensions/katex/ui/README.md +125 -0
  41. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  42. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  43. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  44. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  45. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  46. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  47. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  48. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  49. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  50. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  51. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  52. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  53. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  54. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  55. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  56. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  57. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  58. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  59. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  60. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  61. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  62. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  63. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  116. llms/extensions/katex/ui/index.mjs +92 -0
  117. llms/extensions/katex/ui/katex-swap.css +1230 -0
  118. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  119. llms/extensions/katex/ui/katex.css +1230 -0
  120. llms/extensions/katex/ui/katex.js +19080 -0
  121. llms/extensions/katex/ui/katex.min.css +1 -0
  122. llms/extensions/katex/ui/katex.min.js +1 -0
  123. llms/extensions/katex/ui/katex.min.mjs +1 -0
  124. llms/extensions/katex/ui/katex.mjs +18547 -0
  125. llms/extensions/providers/__init__.py +22 -0
  126. llms/extensions/providers/anthropic.py +233 -0
  127. llms/extensions/providers/cerebras.py +37 -0
  128. llms/extensions/providers/chutes.py +153 -0
  129. llms/extensions/providers/google.py +481 -0
  130. llms/extensions/providers/nvidia.py +103 -0
  131. llms/extensions/providers/openai.py +154 -0
  132. llms/extensions/providers/openrouter.py +74 -0
  133. llms/extensions/providers/zai.py +182 -0
  134. llms/extensions/system_prompts/README.md +22 -0
  135. llms/extensions/system_prompts/__init__.py +45 -0
  136. llms/extensions/system_prompts/ui/index.mjs +280 -0
  137. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  138. llms/extensions/tools/__init__.py +144 -0
  139. llms/extensions/tools/ui/index.mjs +706 -0
  140. llms/index.html +58 -0
  141. llms/llms.json +400 -0
  142. llms/main.py +4407 -0
  143. llms/providers-extra.json +394 -0
  144. llms/providers.json +1 -0
  145. llms/ui/App.mjs +188 -0
  146. llms/ui/ai.mjs +217 -0
  147. llms/ui/app.css +7081 -0
  148. llms/ui/ctx.mjs +412 -0
  149. llms/ui/index.mjs +131 -0
  150. llms/ui/lib/chart.js +14 -0
  151. llms/ui/lib/charts.mjs +16 -0
  152. llms/ui/lib/color.js +14 -0
  153. llms/ui/lib/servicestack-vue.mjs +37 -0
  154. llms/ui/lib/vue.min.mjs +13 -0
  155. llms/ui/lib/vue.mjs +18530 -0
  156. {llms_py-2.0.9.data/data → llms}/ui/markdown.mjs +33 -15
  157. llms/ui/modules/chat/ChatBody.mjs +976 -0
  158. llms/ui/modules/chat/SettingsDialog.mjs +374 -0
  159. llms/ui/modules/chat/index.mjs +991 -0
  160. llms/ui/modules/icons.mjs +46 -0
  161. llms/ui/modules/layout.mjs +271 -0
  162. llms/ui/modules/model-selector.mjs +811 -0
  163. llms/ui/tailwind.input.css +742 -0
  164. {llms_py-2.0.9.data/data → llms}/ui/typography.css +133 -7
  165. llms/ui/utils.mjs +261 -0
  166. llms_py-3.0.10.dist-info/METADATA +49 -0
  167. llms_py-3.0.10.dist-info/RECORD +177 -0
  168. llms_py-3.0.10.dist-info/entry_points.txt +2 -0
  169. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
  170. llms.py +0 -1402
  171. llms_py-2.0.9.data/data/index.html +0 -64
  172. llms_py-2.0.9.data/data/llms.json +0 -447
  173. llms_py-2.0.9.data/data/requirements.txt +0 -1
  174. llms_py-2.0.9.data/data/ui/App.mjs +0 -20
  175. llms_py-2.0.9.data/data/ui/ChatPrompt.mjs +0 -389
  176. llms_py-2.0.9.data/data/ui/Main.mjs +0 -680
  177. llms_py-2.0.9.data/data/ui/app.css +0 -3951
  178. llms_py-2.0.9.data/data/ui/lib/servicestack-vue.min.mjs +0 -37
  179. llms_py-2.0.9.data/data/ui/lib/vue.min.mjs +0 -12
  180. llms_py-2.0.9.data/data/ui/tailwind.input.css +0 -261
  181. llms_py-2.0.9.data/data/ui/threadStore.mjs +0 -273
  182. llms_py-2.0.9.data/data/ui/utils.mjs +0 -114
  183. llms_py-2.0.9.data/data/ui.json +0 -1069
  184. llms_py-2.0.9.dist-info/METADATA +0 -941
  185. llms_py-2.0.9.dist-info/RECORD +0 -30
  186. llms_py-2.0.9.dist-info/entry_points.txt +0 -2
  187. {llms_py-2.0.9.data/data → llms}/ui/fav.svg +0 -0
  188. {llms_py-2.0.9.data/data → llms}/ui/lib/highlight.min.mjs +0 -0
  189. {llms_py-2.0.9.data/data → llms}/ui/lib/idb.min.mjs +0 -0
  190. {llms_py-2.0.9.data/data → llms}/ui/lib/marked.min.mjs +0 -0
  191. /llms_py-2.0.9.data/data/ui/lib/servicestack-client.min.mjs → /llms/ui/lib/servicestack-client.mjs +0 -0
  192. {llms_py-2.0.9.data/data → llms}/ui/lib/vue-router.min.mjs +0 -0
  193. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
  194. {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
- }