llms-py 2.0.9__py3-none-any.whl → 2.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 (43) hide show
  1. llms.py +14 -7
  2. llms_py-2.0.10.data/data/index.html +80 -0
  3. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/llms.json +1 -4
  4. llms_py-2.0.10.data/data/ui/Avatar.mjs +28 -0
  5. llms_py-2.0.10.data/data/ui/Brand.mjs +23 -0
  6. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/ChatPrompt.mjs +101 -69
  7. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/Main.mjs +43 -183
  8. llms_py-2.0.10.data/data/ui/ModelSelector.mjs +29 -0
  9. llms_py-2.0.10.data/data/ui/ProviderStatus.mjs +105 -0
  10. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/Recents.mjs +2 -1
  11. llms_py-2.0.10.data/data/ui/SettingsDialog.mjs +374 -0
  12. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/Sidebar.mjs +11 -27
  13. llms_py-2.0.10.data/data/ui/SignIn.mjs +64 -0
  14. llms_py-2.0.10.data/data/ui/SystemPromptEditor.mjs +31 -0
  15. llms_py-2.0.10.data/data/ui/SystemPromptSelector.mjs +36 -0
  16. llms_py-2.0.10.data/data/ui/Welcome.mjs +8 -0
  17. llms_py-2.0.10.data/data/ui/ai.mjs +80 -0
  18. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/app.css +76 -10
  19. llms_py-2.0.10.data/data/ui/lib/servicestack-vue.mjs +37 -0
  20. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/markdown.mjs +9 -2
  21. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/tailwind.input.css +13 -4
  22. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/threadStore.mjs +2 -2
  23. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/typography.css +109 -1
  24. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/utils.mjs +8 -2
  25. {llms_py-2.0.9.dist-info → llms_py-2.0.10.dist-info}/METADATA +1 -1
  26. llms_py-2.0.10.dist-info/RECORD +40 -0
  27. llms_py-2.0.9.data/data/index.html +0 -64
  28. llms_py-2.0.9.data/data/ui/lib/servicestack-vue.min.mjs +0 -37
  29. llms_py-2.0.9.dist-info/RECORD +0 -30
  30. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/requirements.txt +0 -0
  31. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/App.mjs +0 -0
  32. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/fav.svg +0 -0
  33. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/lib/highlight.min.mjs +0 -0
  34. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/lib/idb.min.mjs +0 -0
  35. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/lib/marked.min.mjs +0 -0
  36. /llms_py-2.0.9.data/data/ui/lib/servicestack-client.min.mjs → /llms_py-2.0.10.data/data/ui/lib/servicestack-client.mjs +0 -0
  37. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/lib/vue-router.min.mjs +0 -0
  38. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui/lib/vue.min.mjs +0 -0
  39. {llms_py-2.0.9.data → llms_py-2.0.10.data}/data/ui.json +0 -0
  40. {llms_py-2.0.9.dist-info → llms_py-2.0.10.dist-info}/WHEEL +0 -0
  41. {llms_py-2.0.9.dist-info → llms_py-2.0.10.dist-info}/entry_points.txt +0 -0
  42. {llms_py-2.0.9.dist-info → llms_py-2.0.10.dist-info}/licenses/LICENSE +0 -0
  43. {llms_py-2.0.9.dist-info → llms_py-2.0.10.dist-info}/top_level.txt +0 -0
@@ -1,197 +1,54 @@
1
- import { ref, computed, nextTick, watch, onMounted, onUnmounted, provide, inject } from 'vue'
1
+ import { ref, computed, nextTick, watch, onMounted, provide, inject } from 'vue'
2
2
  import { useRouter, useRoute } from 'vue-router'
3
3
  import { useThreadStore } from './threadStore.mjs'
4
4
  import { storageObject, addCopyButtons } from './utils.mjs'
5
5
  import { renderMarkdown } from './markdown.mjs'
6
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
- }
7
+ import SignIn from './SignIn.mjs'
8
+ import Avatar from './Avatar.mjs'
9
+ import ModelSelector from './ModelSelector.mjs'
10
+ import SystemPromptSelector from './SystemPromptSelector.mjs'
11
+ import SystemPromptEditor from './SystemPromptEditor.mjs'
12
+ import { useSettings } from "./SettingsDialog.mjs"
13
+ import Welcome from './Welcome.mjs'
112
14
 
113
15
  export default {
114
16
  components: {
17
+ ModelSelector,
18
+ SystemPromptSelector,
19
+ SystemPromptEditor,
115
20
  ChatPrompt,
116
- ProviderStatus,
21
+ SignIn,
22
+ Avatar,
23
+ Welcome,
117
24
  },
118
25
  template: `
119
26
  <div class="flex flex-col h-full w-full">
120
27
  <!-- Header with model and prompt selectors -->
121
28
  <div class="border-b border-gray-200 bg-white px-2 py-2 w-full min-h-16">
122
29
  <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>
30
+ <ModelSelector :models="models" v-model="selectedModel" @updated="configUpdated" />
135
31
 
136
- <!-- System Prompt Selector -->
137
32
  <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>
33
+ <SystemPromptSelector :prompts="prompts" v-model="selectedPrompt"
34
+ :show="showSystemPrompt" @toggle="showSystemPrompt = !showSystemPrompt" />
35
+ <Avatar />
161
36
  </div>
162
37
  </div>
163
38
  </div>
164
39
 
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>
40
+ <SystemPromptEditor v-if="showSystemPrompt"
41
+ v-model="currentSystemPrompt" :prompts="prompts" :selected="selectedPrompt" />
185
42
 
186
43
  <!-- Messages Area -->
187
44
  <div class="flex-1 overflow-y-auto" ref="messagesContainer">
188
45
  <div class="mx-auto max-w-6xl px-4 py-6">
46
+ <div v-if="$ai.requiresAuth && !$ai.auth">
47
+ <SignIn @done="$ai.signIn($event)" />
48
+ </div>
189
49
  <!-- 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>
50
+ <div v-else-if="!currentThread" class="text-center py-12">
51
+ <Welcome />
195
52
 
196
53
  <!-- Chat input for new conversation -->
197
54
  <div class="max-w-2xl mx-auto">
@@ -338,7 +195,7 @@ export default {
338
195
  </div>
339
196
 
340
197
  <!-- Error message bubble -->
341
- <div v-if="errorMessage" class="flex items-start space-x-3">
198
+ <div v-if="errorStatus" class="flex items-start space-x-3">
342
199
  <!-- Avatar outside the bubble -->
343
200
  <div class="flex-shrink-0">
344
201
  <div class="w-8 h-8 rounded-full bg-red-600 text-white flex items-center justify-center text-sm font-medium">
@@ -350,13 +207,14 @@ export default {
350
207
  <div class="max-w-[85%] rounded-lg px-4 py-3 bg-red-50 border border-red-200 text-red-800 shadow-sm">
351
208
  <div class="flex items-start space-x-2">
352
209
  <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>
210
+ <div class="text-base font-medium mb-1">{{ errorStatus?.errorCode || 'Error' }}</div>
211
+ <div v-if="errorStatus?.message" class="text-base mb-1">{{ errorStatus.message }}</div>
212
+ <div v-if="errorStatus?.stackTrace" class="text-sm whitespace-pre-wrap break-words max-h-80 overflow-y-auto font-mono p-2 rounded">
213
+ {{ errorStatus.stackTrace }}
214
+ </div>
357
215
  </div>
358
216
  <button type="button"
359
- @click="errorMessage = null"
217
+ @click="errorStatus = null"
360
218
  class="text-red-400 hover:text-red-600 flex-shrink-0"
361
219
  >
362
220
  <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
@@ -379,22 +237,24 @@ export default {
379
237
  props: {
380
238
  },
381
239
  setup(props) {
240
+ const ai = inject('ai')
382
241
  const router = useRouter()
383
242
  const route = useRoute()
384
243
  const threads = useThreadStore()
385
244
  const { currentThread } = threads
386
245
  const chatPrompt = useChatPrompt()
246
+ const chatSettings = useSettings()
387
247
  const {
388
248
  errorStatus,
389
- errorMessage,
390
249
  isGenerating,
391
250
  } = chatPrompt
392
251
  provide('threads', threads)
393
252
  provide('chatPrompt', chatPrompt)
253
+ provide('chatSettings', chatSettings)
394
254
  const models = inject('models')
395
255
  const config = inject('config')
396
256
 
397
- const prefs = storageObject('prefs')
257
+ const prefs = storageObject(ai.prefsKey)
398
258
 
399
259
  const customPromptValue = ref('')
400
260
  const customPrompt = {
@@ -406,7 +266,7 @@ export default {
406
266
  const prompts = computed(() => [customPrompt, ...config.prompts])
407
267
 
408
268
  const selectedModel = ref(prefs.model || config.defaults.text.model || '')
409
- const selectedPrompt = ref(prefs.systemPrompt || '')
269
+ const selectedPrompt = ref(prefs.systemPrompt || null)
410
270
  const currentSystemPrompt = ref('')
411
271
  const showSystemPrompt = ref(false)
412
272
  const messagesContainer = ref(null)
@@ -448,8 +308,9 @@ export default {
448
308
  currentSystemPrompt.value = thread.systemPrompt
449
309
  }
450
310
  } else {
451
- selectedPrompt.value = null
452
- currentSystemPrompt.value = ''
311
+ // Preserve existing selected prompt
312
+ // selectedPrompt.value = null
313
+ // currentSystemPrompt.value = ''
453
314
  }
454
315
  }
455
316
 
@@ -468,7 +329,7 @@ export default {
468
329
  }, { immediate: true })
469
330
 
470
331
  watch(() => [selectedModel.value, selectedPrompt.value], () => {
471
- localStorage.setItem('prefs', JSON.stringify({
332
+ localStorage.setItem(ai.prefsKey, JSON.stringify({
472
333
  model: selectedModel.value,
473
334
  systemPrompt: selectedPrompt.value
474
335
  }))
@@ -487,7 +348,7 @@ export default {
487
348
  const exportData = {
488
349
  exportedAt: new Date().toISOString(),
489
350
  version: '1.0',
490
- source: 'llms.py',
351
+ source: 'ServiceStack.AI.Chat',
491
352
  threadCount: allThreads.length,
492
353
  threads: allThreads
493
354
  }
@@ -499,7 +360,7 @@ export default {
499
360
 
500
361
  const link = document.createElement('a')
501
362
  link.href = url
502
- link.download = `llms-threads-export-${new Date().toISOString().split('T')[0]}.json`
363
+ link.download = `aichat-threads-export-${new Date().toISOString().split('T')[0]}.json`
503
364
  document.body.appendChild(link)
504
365
  link.click()
505
366
  document.body.removeChild(link)
@@ -661,7 +522,6 @@ export default {
661
522
  showSystemPrompt,
662
523
  messagesContainer,
663
524
  errorStatus,
664
- errorMessage,
665
525
  formatTime,
666
526
  renderMarkdown,
667
527
  isReasoningExpanded,
@@ -0,0 +1,29 @@
1
+ import ProviderStatus from "./ProviderStatus.mjs";
2
+
3
+ export default {
4
+ components: {
5
+ ProviderStatus,
6
+ },
7
+ template:`
8
+ <!-- Model Selector -->
9
+ <div class="pl-1 flex space-x-2">
10
+ <Autocomplete id="model" :options="models" label=""
11
+ :modelValue="modelValue" @update:modelValue="$emit('update:modelValue', $event)"
12
+ class="w-72 xl:w-84"
13
+ :match="(x, value) => x.toLowerCase().includes(value.toLowerCase())"
14
+ placeholder="Select Model...">
15
+ <template #item="{ value }">
16
+ <div class="truncate max-w-72" :title="value">{{value}}</div>
17
+ </template>
18
+ </Autocomplete>
19
+ <ProviderStatus @updated="$emit('updated', $event)" />
20
+ </div>
21
+ `,
22
+ emits: ['updated', 'update:modelValue'],
23
+ props: {
24
+ models: Array,
25
+ modelValue: String,
26
+ },
27
+ setup() {
28
+ }
29
+ }
@@ -0,0 +1,105 @@
1
+ import { ref, computed, inject, onMounted, onUnmounted } from "vue"
2
+
3
+ export default {
4
+ template:`
5
+ <div v-if="$ai.isAdmin" ref="triggerRef" class="relative" :key="renderKey">
6
+ <button type="button" @click="togglePopover"
7
+ 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">
8
+ <span class="text-gray-600" :title="models.length + ' models from ' + (config.status.enabled||[]).length + ' enabled providers'">{{models.length}}</span>
9
+ <div class="cursor-pointer flex items-center" :title="'Enabled:\\n' + (config.status.enabled||[]).map(x => ' ' + x).join('\\n')">
10
+ <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>
11
+ <span class="text-green-700">{{(config.status.enabled||[]).length}}</span>
12
+ </div>
13
+ <div class="cursor-pointer flex items-center" :title="'Disabled:\\n' + (config.status.disabled||[]).map(x => ' ' + x).join('\\n')">
14
+ <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>
15
+ <span class="text-red-700">{{(config.status.disabled||[]).length}}</span>
16
+ </div>
17
+ </button>
18
+ <div v-if="showPopover" ref="popoverRef" class="absolute right-0 mt-2 w-72 max-h-120 overflow-y-auto bg-white border border-gray-200 rounded-md shadow-lg z-10">
19
+ <div class="divide-y divide-gray-100">
20
+ <div v-for="p in allProviders" :key="p" class="flex items-center justify-between px-3 py-2">
21
+ <label :for="'chk_' + p" class="cursor-pointer text-sm text-gray-900 truncate mr-2" :title="p">{{ p }}</label>
22
+ <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">
23
+ <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" />
24
+ <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" />
25
+ <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" />
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ `,
32
+ emits: ['updated'],
33
+ setup(props, { emit }) {
34
+ const ai = inject('ai')
35
+ const config = inject('config')
36
+ const models = inject('models')
37
+ const showPopover = ref(false)
38
+ const triggerRef = ref(null)
39
+ const popoverRef = ref(null)
40
+ const pending = ref({})
41
+ const renderKey = ref(0)
42
+ const allProviders = computed(() => config.status?.all)
43
+ const isEnabled = (p) => config.status.enabled.includes(p)
44
+ const togglePopover = () => showPopover.value = !showPopover.value
45
+
46
+ const onToggle = async (provider, enable) => {
47
+ pending.value = { ...pending.value, [provider]: true }
48
+ try {
49
+ const res = await ai.post(`/providers/${encodeURIComponent(provider)}`, {
50
+ body: JSON.stringify(enable ? { enable: true } : { disable: true })
51
+ })
52
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
53
+ const json = await res.json()
54
+ config.status.enabled = json.enabled || []
55
+ config.status.disabled = json.disabled || []
56
+ if (json.feedback) {
57
+ alert(json.feedback)
58
+ }
59
+
60
+ try {
61
+ const [configRes, modelsRes] = await Promise.all([
62
+ ai.getConfig(),
63
+ ai.getModels(),
64
+ ])
65
+ const newConfig = await configRes.json()
66
+ const newModels = await modelsRes.json()
67
+ Object.assign(config, newConfig)
68
+ models.length = 0
69
+ newModels.forEach(m => models.push(m))
70
+ emit('updated')
71
+ renderKey.value++
72
+ } catch (e) {
73
+ alert(`Failed to reload config: ${e.message}`)
74
+ }
75
+
76
+ } catch (e) {
77
+ alert(`Failed to ${enable ? 'enable' : 'disable'} ${provider}: ${e.message}`)
78
+ } finally {
79
+ pending.value = { ...pending.value, [provider]: false }
80
+ }
81
+ }
82
+
83
+ const onDocClick = (e) => {
84
+ const t = e.target
85
+ if (triggerRef.value?.contains(t)) return
86
+ if (popoverRef.value?.contains(t)) return
87
+ showPopover.value = false
88
+ }
89
+ onMounted(() => document.addEventListener('click', onDocClick))
90
+ onUnmounted(() => document.removeEventListener('click', onDocClick))
91
+ return {
92
+ renderKey,
93
+ config,
94
+ models,
95
+ showPopover,
96
+ triggerRef,
97
+ popoverRef,
98
+ allProviders,
99
+ isEnabled,
100
+ togglePopover,
101
+ onToggle,
102
+ pending,
103
+ }
104
+ }
105
+ }
@@ -46,6 +46,7 @@ const RecentResults = {
46
46
  q: String
47
47
  },
48
48
  setup(props) {
49
+ const ai = inject('ai')
49
50
  const router = useRouter()
50
51
  const config = inject('config')
51
52
  const { threads, loadThreads } = useThreadStore()
@@ -127,7 +128,7 @@ const RecentResults = {
127
128
  return ''
128
129
  }
129
130
 
130
- const open = (id) => router.push(`/c/${id}`)
131
+ const open = (id) => router.push(`${ai.base}/c/${id}`)
131
132
  const formatDate = (iso) => new Date(iso).toLocaleString()
132
133
 
133
134
  return {