llms-py 3.0.0b1__py3-none-any.whl → 3.0.0b3__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 (63) hide show
  1. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  2. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  3. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  4. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  5. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  6. llms/__pycache__/llms.cpython-312.pyc +0 -0
  7. llms/__pycache__/main.cpython-312.pyc +0 -0
  8. llms/__pycache__/main.cpython-313.pyc +0 -0
  9. llms/__pycache__/main.cpython-314.pyc +0 -0
  10. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  11. llms/index.html +27 -57
  12. llms/llms.json +48 -15
  13. llms/main.py +923 -624
  14. llms/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  15. llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  16. llms/providers/__pycache__/google.cpython-314.pyc +0 -0
  17. llms/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  18. llms/providers/__pycache__/openai.cpython-314.pyc +0 -0
  19. llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  20. llms/providers/anthropic.py +189 -0
  21. llms/providers/chutes.py +152 -0
  22. llms/providers/google.py +306 -0
  23. llms/providers/nvidia.py +107 -0
  24. llms/providers/openai.py +159 -0
  25. llms/providers/openrouter.py +70 -0
  26. llms/providers-extra.json +356 -0
  27. llms/providers.json +1 -1
  28. llms/ui/App.mjs +150 -57
  29. llms/ui/ai.mjs +84 -50
  30. llms/ui/app.css +1 -4963
  31. llms/ui/ctx.mjs +196 -0
  32. llms/ui/index.mjs +117 -0
  33. llms/ui/lib/charts.mjs +9 -13
  34. llms/ui/markdown.mjs +6 -0
  35. llms/ui/{Analytics.mjs → modules/analytics.mjs} +76 -64
  36. llms/ui/{Main.mjs → modules/chat/ChatBody.mjs} +91 -179
  37. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +8 -8
  38. llms/ui/{ChatPrompt.mjs → modules/chat/index.mjs} +281 -96
  39. llms/ui/modules/layout.mjs +267 -0
  40. llms/ui/modules/model-selector.mjs +851 -0
  41. llms/ui/{Recents.mjs → modules/threads/Recents.mjs} +10 -11
  42. llms/ui/{Sidebar.mjs → modules/threads/index.mjs} +48 -45
  43. llms/ui/{threadStore.mjs → modules/threads/threadStore.mjs} +21 -7
  44. llms/ui/tailwind.input.css +441 -79
  45. llms/ui/utils.mjs +83 -123
  46. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/METADATA +1 -1
  47. llms_py-3.0.0b3.dist-info/RECORD +65 -0
  48. llms/ui/Avatar.mjs +0 -85
  49. llms/ui/Brand.mjs +0 -52
  50. llms/ui/ModelSelector.mjs +0 -693
  51. llms/ui/OAuthSignIn.mjs +0 -92
  52. llms/ui/ProviderIcon.mjs +0 -36
  53. llms/ui/ProviderStatus.mjs +0 -105
  54. llms/ui/SignIn.mjs +0 -64
  55. llms/ui/SystemPromptEditor.mjs +0 -31
  56. llms/ui/SystemPromptSelector.mjs +0 -56
  57. llms/ui/Welcome.mjs +0 -8
  58. llms/ui.json +0 -1069
  59. llms_py-3.0.0b1.dist-info/RECORD +0 -49
  60. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/WHEEL +0 -0
  61. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/entry_points.txt +0 -0
  62. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/licenses/LICENSE +0 -0
  63. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/top_level.txt +0 -0
@@ -1,56 +1,13 @@
1
- import { ref, computed, nextTick, watch, onMounted, provide, inject } from 'vue'
1
+ import { ref, computed, nextTick, watch, onMounted, onUnmounted, inject } from 'vue'
2
2
  import { useRouter, useRoute } from 'vue-router'
3
- import { useFormatters } from '@servicestack/vue'
4
- import { useThreadStore } from './threadStore.mjs'
5
- import { storageObject, addCopyButtons, formatCost, statsTitle, fetchCacheInfos } from './utils.mjs'
6
- import { renderMarkdown } from './markdown.mjs'
7
- import ChatPrompt, { useChatPrompt } from './ChatPrompt.mjs'
8
- import SignIn from './SignIn.mjs'
9
- import OAuthSignIn from './OAuthSignIn.mjs'
10
- import Avatar from './Avatar.mjs'
11
- import ModelSelector from './ModelSelector.mjs'
12
- import SystemPromptSelector from './SystemPromptSelector.mjs'
13
- import SystemPromptEditor from './SystemPromptEditor.mjs'
14
- import { useSettings } from "./SettingsDialog.mjs"
15
- import Welcome from './Welcome.mjs'
16
-
17
- const { humanifyMs, humanifyNumber } = useFormatters()
18
3
 
19
4
  export default {
20
- components: {
21
- ModelSelector,
22
- SystemPromptSelector,
23
- SystemPromptEditor,
24
- ChatPrompt,
25
- SignIn,
26
- OAuthSignIn,
27
- Avatar,
28
- Welcome,
29
- },
30
5
  template: `
31
- <div class="flex flex-col h-full w-full">
32
- <!-- Header with model and prompt selectors (hidden when auth required and not authenticated) -->
33
- <div v-if="!($ai.requiresAuth && !$ai.auth)"
34
- :class="!$ai.isSidebarOpen ? 'pl-6' : ''"
35
- class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 py-2 w-full min-h-16">
36
- <div class="flex flex-wrap items-center justify-between w-full">
37
- <ModelSelector :models="models" v-model="selectedModel" @updated="configUpdated" />
38
-
39
- <div class="flex items-center space-x-2 pl-4">
40
- <SystemPromptSelector :prompts="prompts" v-model="selectedPrompt"
41
- :show="showSystemPrompt" @toggle="showSystemPrompt = !showSystemPrompt" />
42
- <Avatar />
43
- </div>
44
- </div>
45
- </div>
46
-
47
- <SystemPromptEditor v-if="showSystemPrompt && !($ai.requiresAuth && !$ai.auth)"
48
- v-model="currentSystemPrompt" :prompts="prompts" :selected="selectedPrompt" />
49
-
6
+ <div class="flex flex-col h-full">
50
7
  <!-- Messages Area -->
51
8
  <div class="flex-1 overflow-y-auto" ref="messagesContainer">
52
9
  <div class="mx-auto max-w-6xl px-4 py-6">
53
- <div v-if="$ai.requiresAuth && !$ai.auth">
10
+ <div v-if="!$ai.hasAccess">
54
11
  <OAuthSignIn v-if="$ai.authType === 'oauth'" @done="$ai.signIn($event)" />
55
12
  <SignIn v-else @done="$ai.signIn($event)" />
56
13
  </div>
@@ -113,6 +70,13 @@ export default {
113
70
 
114
71
  <!-- Messages -->
115
72
  <div v-else class="space-y-6">
73
+ <div v-if="currentThread.messages.length && currentThread.model" class="absolute -mt-2">
74
+ <span @click="$chat.setSelectedModel({ name: currentThread.model})"
75
+ class="flex items-center cursor-pointer px-1.5 py-0.5 text-xs rounded text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100 transition-colors border hover:border-gray-300 dark:hover:border-gray-700">
76
+ <ProviderIcon class="size-4 mr-1" :provider="$chat.getProviderForModel(currentThread.model)" />
77
+ {{currentThread.model}}
78
+ </span>
79
+ </div>
116
80
  <div
117
81
  v-for="message in currentThread.messages"
118
82
  :key="message.id"
@@ -163,7 +127,7 @@ export default {
163
127
 
164
128
  <div
165
129
  v-if="message.role === 'assistant'"
166
- v-html="renderMarkdown(message.content)"
130
+ v-html="$fmt.markdown(message.content)"
167
131
  class="prose prose-sm max-w-none dark:prose-invert"
168
132
  ></div>
169
133
 
@@ -174,43 +138,53 @@ export default {
174
138
  <span>{{ isReasoningExpanded(message.id) ? 'Hide reasoning' : 'Show reasoning' }}</span>
175
139
  </button>
176
140
  <div v-if="isReasoningExpanded(message.id)" class="mt-2 rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 p-2">
177
- <div v-if="typeof message.reasoning === 'string'" v-html="renderMarkdown(message.reasoning)" class="prose prose-xs max-w-none dark:prose-invert"></div>
141
+ <div v-if="typeof message.reasoning === 'string'" v-html="$fmt.markdown(message.reasoning)" class="prose prose-xs max-w-none dark:prose-invert"></div>
178
142
  <pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto text-gray-900 dark:text-gray-100">{{ formatReasoning(message.reasoning) }}</pre>
179
143
  </div>
180
144
  </div>
181
145
 
182
- <!-- User Message with separate attachments -->
183
- <div v-if="message.role !== 'assistant'">
184
- <div v-html="renderMarkdown(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
185
-
186
- <!-- Attachments Grid -->
187
- <div v-if="hasAttachments(message)" class="mt-2 flex flex-wrap gap-2">
188
- <template v-for="(part, i) in getAttachments(message)" :key="i">
189
- <!-- Image -->
190
- <div v-if="part.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(part.image_url.url)">
191
- <img :src="part.image_url.url" class="max-w-[400px] max-h-96 rounded-lg border border-gray-200 dark:border-gray-700 object-contain bg-gray-50 dark:bg-gray-900 shadow-sm transition-transform hover:scale-[1.02]" />
192
- </div>
193
- <!-- Audio -->
194
- <div v-else-if="part.type === 'input_audio'" class="flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
195
- <svg class="w-5 h-5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
196
- <audio controls :src="part.input_audio.data" class="h-8 w-48"></audio>
197
- </div>
198
- <!-- File -->
199
- <a v-else-if="part.type === 'file'" :href="part.file.file_data" target="_blank"
200
- class="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-sm text-blue-600 dark:text-blue-400 hover:underline">
201
- <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
202
- <span class="max-w-xs truncate">{{ part.file.filename || 'Attachment' }}</span>
203
- </a>
204
- </template>
205
- </div>
206
- </div>
146
+ <!-- Assistant Images -->
147
+ <div v-if="message.images && message.images.length > 0" class="mt-2 flex flex-wrap gap-2">
148
+ <template v-for="(img, i) in message.images" :key="i">
149
+ <div v-if="img.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(resolveUrl(img.image_url.url))">
150
+ <img :src="resolveUrl(img.image_url.url)" class="max-w-[400px] max-h-96 rounded-lg border border-gray-200 dark:border-gray-700 object-contain bg-gray-50 dark:bg-gray-900 shadow-sm transition-transform hover:scale-[1.02]" />
151
+ </div>
152
+ </template>
153
+ </div>
154
+
155
+ <!-- User Message with separate attachments -->
156
+ <div v-if="message.role !== 'assistant'">
157
+ <div v-html="$fmt.markdown(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
158
+
159
+ <!-- Attachments Grid -->
160
+ <div v-if="hasAttachments(message)" class="mt-2 flex flex-wrap gap-2">
161
+ <template v-for="(part, i) in getAttachments(message)" :key="i">
162
+ <!-- Image -->
163
+ <div v-if="part.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(part.image_url.url)">
164
+ <img :src="part.image_url.url" class="max-w-[400px] max-h-96 rounded-lg border border-gray-200 dark:border-gray-700 object-contain bg-gray-50 dark:bg-gray-900 shadow-sm transition-transform hover:scale-[1.02]" />
165
+ </div>
166
+ <!-- Audio -->
167
+ <div v-else-if="part.type === 'input_audio'" class="flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
168
+ <svg class="w-5 h-5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
169
+ <audio controls :src="part.input_audio.data" class="h-8 w-48"></audio>
170
+ </div>
171
+ <!-- File -->
172
+ <a v-else-if="part.type === 'file'" :href="part.file.file_data" target="_blank"
173
+ class="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-sm text-blue-600 dark:text-blue-400 hover:underline">
174
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
175
+ <span class="max-w-xs truncate">{{ part.file.filename || 'Attachment' }}</span>
176
+ </a>
177
+ </template>
178
+ </div>
179
+ </div>
180
+
207
181
  <div class="mt-2 text-xs opacity-70">
208
- <span>{{ formatTime(message.timestamp) }}</span>
182
+ <span>{{ $fmt.time(message.timestamp) }}</span>
209
183
  <span v-if="message.usage" :title="tokensTitle(message.usage)">
210
184
  &#8226;
211
- {{ humanifyNumber(message.usage.tokens) }} tokens
185
+ {{ $fmt.humanifyNumber(message.usage.tokens) }} tokens
212
186
  <span v-if="message.usage.cost">&#183; {{ message.usage.cost }}</span>
213
- <span v-if="message.usage.duration"> in {{ humanifyMs(message.usage.duration) }}</span>
187
+ <span v-if="message.usage.duration"> in {{ $fmt.humanifyMs(message.usage.duration) }}</span>
214
188
  </span>
215
189
  </div>
216
190
  </div>
@@ -237,8 +211,8 @@ export default {
237
211
  </div>
238
212
 
239
213
  <div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 dark:text-gray-400 text-sm">
240
- <span :title="statsTitle(currentThread.stats)">
241
- {{ currentThread.stats.cost ? formatCost(currentThread.stats.cost) + ' for ' : '' }} {{ humanifyNumber(currentThread.stats.inputTokens) }} → {{ humanifyNumber(currentThread.stats.outputTokens) }} tokens over {{ currentThread.stats.requests }} request{{currentThread.stats.requests===1?'':'s'}} in {{ humanifyMs(currentThread.stats.duration) }}
214
+ <span :title="$fmt.statsTitle(currentThread.stats)">
215
+ {{ currentThread.stats.cost ? $fmt.costLong(currentThread.stats.cost) + ' for ' : '' }} {{ $fmt.humanifyNumber(currentThread.stats.inputTokens) }} → {{ $fmt.humanifyNumber(currentThread.stats.outputTokens) }} tokens over {{ currentThread.stats.requests }} request{{currentThread.stats.requests===1?'':'s'}} in {{ $fmt.humanifyMs(currentThread.stats.duration) }}
242
216
  </span>
243
217
  </div>
244
218
 
@@ -304,64 +278,45 @@ export default {
304
278
  </div>
305
279
 
306
280
  <!-- Input Area -->
307
- <div class="flex-shrink-0 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6 py-4">
308
- <ChatPrompt :model="selectedModelObj" :systemPrompt="currentSystemPrompt" />
281
+ <div v-if="$ai.hasAccess" class="flex-shrink-0 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6 py-4">
282
+ <ChatPrompt :model="$chat.getSelectedModel()" />
309
283
  </div>
310
284
 
311
285
  <!-- Lightbox -->
312
- <div v-if="lightboxUrl" class="fixed inset-0 z-[100] bg-black/90 flex items-center justify-center p-4 cursor-pointer" @click="closeLightbox">
286
+ <div v-if="lightboxUrl" class="fixed inset-0 z-[100] bg-black/90 flex items-center justify-center p-4 cursor-pointer"
287
+ @click="closeLightbox">
288
+ <button type="button" @click="closeLightbox"
289
+ class="absolute top-4 right-4 text-white/70 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors z-[101]"
290
+ title="Close">
291
+ <svg class="size-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
292
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
293
+ </svg>
294
+ </button>
313
295
  <div class="relative max-w-full max-h-full">
314
296
  <img :src="lightboxUrl" class="max-w-full max-h-[90vh] object-contain rounded-sm shadow-2xl" @click.stop />
315
- <button type="button" @click="closeLightbox"
316
- class="absolute -top-12 right-0 text-white/70 hover:text-white p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors"
317
- title="Close">
318
- <svg class="size-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
319
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
320
- </svg>
321
- </button>
322
297
  </div>
323
298
  </div>
324
299
  </div>
325
300
  `,
326
- props: {
327
- },
328
- setup(props) {
329
- const ai = inject('ai')
301
+ setup() {
302
+ const ctx = inject('ctx')
303
+ const models = ctx.state.models
304
+ const config = ctx.state.config
305
+ const threads = ctx.threads
306
+ const chatPrompt = ctx.chat
307
+ const { currentThread } = threads
308
+ const { errorStatus, isGenerating } = ctx.chat
309
+
330
310
  const router = useRouter()
331
311
  const route = useRoute()
332
- const threads = useThreadStore()
333
- const { currentThread } = threads
334
- const chatPrompt = useChatPrompt()
335
- const chatSettings = useSettings()
336
- const {
337
- errorStatus,
338
- isGenerating,
339
- } = chatPrompt
340
- provide('threads', threads)
341
- provide('chatPrompt', chatPrompt)
342
- provide('chatSettings', chatSettings)
343
- const models = inject('models')
344
- const config = inject('config')
345
-
346
- const prefs = storageObject(ai.prefsKey)
347
-
348
- const customPromptValue = ref('')
349
- const customPrompt = {
350
- id: '_custom_',
351
- name: 'Custom...',
352
- value: ''
353
- }
354
312
 
355
- const prompts = computed(() => [customPrompt, ...config.prompts])
313
+ const prefs = ctx.getPrefs()
356
314
 
357
315
  const selectedModel = ref(prefs.model || config.defaults.text.model || '')
358
316
  const selectedModelObj = computed(() => {
359
317
  if (!selectedModel.value || !models) return null
360
318
  return models.find(m => m.name === selectedModel.value) || models.find(m => m.id === selectedModel.value)
361
319
  })
362
- const selectedPrompt = ref(prefs.systemPrompt || null)
363
- const currentSystemPrompt = ref('')
364
- const showSystemPrompt = ref(false)
365
320
  const messagesContainer = ref(null)
366
321
  const isExporting = ref(false)
367
322
  const isImporting = ref(false)
@@ -376,6 +331,13 @@ export default {
376
331
  lightboxUrl.value = null
377
332
  }
378
333
 
334
+ const resolveUrl = (url) => {
335
+ if (url && url.startsWith('~')) {
336
+ return '/' + url
337
+ }
338
+ return ctx.ai.resolveUrl(url)
339
+ }
340
+
379
341
  // Auto-scroll to bottom when new messages arrive
380
342
  const scrollToBottom = async () => {
381
343
  await nextTick()
@@ -396,45 +358,16 @@ export default {
396
358
  selectedModel.value = thread.model
397
359
  }
398
360
 
399
- // Sync System Prompt selection from thread
400
- if (thread) {
401
- const norm = s => (s || '').replace(/\s+/g, ' ').trim()
402
- const tsp = norm(thread.systemPrompt || '')
403
- if (tsp) {
404
- const match = config.prompts.find(p => norm(p.value) === tsp)
405
- if (match) {
406
- selectedPrompt.value = match
407
- currentSystemPrompt.value = match.value.replace(/\n/g, ' ')
408
- } else {
409
- selectedPrompt.value = customPrompt
410
- currentSystemPrompt.value = thread.systemPrompt
411
- }
412
- } else {
413
- // Preserve existing selected prompt
414
- // selectedPrompt.value = null
415
- // currentSystemPrompt.value = ''
416
- }
417
- }
418
-
419
361
  if (!newId) {
420
362
  chatPrompt.reset()
421
363
  }
422
- nextTick(addCopyButtons)
364
+ nextTick(ctx.chat.addCopyButtons)
423
365
  }, { immediate: true })
424
366
 
425
- // Watch selectedPrompt and update currentSystemPrompt
426
- watch(selectedPrompt, (newPrompt) => {
427
- // If using a custom prompt, keep whatever is already in currentSystemPrompt
428
- if (newPrompt && newPrompt.id === '_custom_') return
429
- const prompt = newPrompt && config.prompts.find(p => p.id === newPrompt.id)
430
- currentSystemPrompt.value = prompt ? prompt.value.replace(/\n/g, ' ') : ''
431
- }, { immediate: true })
432
-
433
- watch(() => [selectedModel.value, selectedPrompt.value], () => {
434
- localStorage.setItem(ai.prefsKey, JSON.stringify({
367
+ watch(() => [selectedModel.value], () => {
368
+ ctx.setPrefs({
435
369
  model: selectedModel.value,
436
- systemPrompt: selectedPrompt.value
437
- }))
370
+ })
438
371
  })
439
372
 
440
373
  async function exportThreads() {
@@ -628,14 +561,6 @@ export default {
628
561
  }
629
562
  }
630
563
 
631
- // Format timestamp
632
- const formatTime = (timestamp) => {
633
- return new Date(timestamp).toLocaleTimeString([], {
634
- hour: '2-digit',
635
- minute: '2-digit'
636
- })
637
- }
638
-
639
564
  // Reasoning collapse state and helpers
640
565
  const expandedReasoning = ref(new Set())
641
566
  const isReasoningExpanded = (id) => expandedReasoning.value.has(id)
@@ -727,7 +652,7 @@ export default {
727
652
  text = message.content
728
653
  }
729
654
 
730
- const infos = await fetchCacheInfos(getCacheInfos)
655
+ const infos = await ctx.ai.fetchCacheInfos(getCacheInfos)
731
656
  // replace name with info.name
732
657
  for (let i = 0; i < files.length; i++) {
733
658
  const url = files[i]?.url
@@ -807,41 +732,32 @@ export default {
807
732
  let title = []
808
733
  if (usage.tokens && usage.price) {
809
734
  const msg = parseFloat(usage.price) > 0
810
- ? `${usage.tokens} tokens @ ${usage.price} = ${tokenCost(usage.price, usage.tokens)}`
735
+ ? `${usage.tokens} tokens @ ${usage.price} = ${ctx.fmt.tokenCostLong(usage.price, usage.tokens)}`
811
736
  : `${usage.tokens} tokens`
812
737
  const duration = usage.duration ? ` in ${usage.duration}ms` : ''
813
738
  title.push(msg + duration)
814
739
  }
815
740
  return title.join('\n')
816
741
  }
817
- const numFmt = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', minimumFractionDigits: 6 })
818
- function tokenCost(price, tokens) {
819
- if (!price || !tokens) return ''
820
- return numFmt.format(parseFloat(price) * tokens)
821
- }
822
742
 
743
+ let sub
823
744
  onMounted(() => {
824
- setTimeout(addCopyButtons, 1)
745
+ sub = ctx.events.subscribe(`keydown:Escape`, closeLightbox)
746
+ setTimeout(ctx.chat.addCopyButtons, 1)
825
747
  })
748
+ onUnmounted(() => sub?.unsubscribe())
826
749
 
827
750
  return {
828
751
  config,
829
752
  models,
830
753
  threads,
831
- prompts,
832
754
  isGenerating,
833
- customPromptValue,
834
755
  currentThread,
835
756
  selectedModel,
836
757
  selectedModelObj,
837
- selectedPrompt,
838
- currentSystemPrompt,
839
- showSystemPrompt,
840
758
  messagesContainer,
841
759
  errorStatus,
842
760
  copying,
843
- formatTime,
844
- renderMarkdown,
845
761
  isReasoningExpanded,
846
762
  toggleReasoning,
847
763
  formatReasoning,
@@ -858,16 +774,12 @@ export default {
858
774
  isImporting,
859
775
  fileInput,
860
776
  tokensTitle,
861
- humanifyMs,
862
- humanifyNumber,
863
- formatCost,
864
- formatCost,
865
- statsTitle,
866
777
  getAttachments,
867
778
  hasAttachments,
868
779
  lightboxUrl,
869
780
  openLightbox,
870
781
  closeLightbox,
782
+ resolveUrl,
871
783
  }
872
784
  }
873
785
  }
@@ -1,5 +1,5 @@
1
1
  import { ref, computed, watch, inject } from 'vue'
2
- import { storageObject } from './utils.mjs'
2
+ import { storageObject } from '../../utils.mjs'
3
3
 
4
4
  const settingsKey = 'llms.settings'
5
5
 
@@ -40,7 +40,7 @@ export function useSettings() {
40
40
  ]
41
41
 
42
42
  let settings = ref(storageObject(settingsKey))
43
-
43
+
44
44
  function validSettings(localSettings) {
45
45
  const to = {}
46
46
  intFields.forEach(f => {
@@ -65,9 +65,9 @@ export function useSettings() {
65
65
  })
66
66
  listFields.forEach(f => {
67
67
  if (localSettings[f] != null && localSettings[f] !== '') {
68
- to[f] = Array.isArray(localSettings[f])
68
+ to[f] = Array.isArray(localSettings[f])
69
69
  ? localSettings[f]
70
- : typeof localSettings[f] == 'string'
70
+ : typeof localSettings[f] == 'string'
71
71
  ? localSettings[f].split(',').map(x => x.trim())
72
72
  : []
73
73
  }
@@ -88,7 +88,7 @@ export function useSettings() {
88
88
  function resetSettings() {
89
89
  return saveSettings({})
90
90
  }
91
-
91
+
92
92
  function saveSettings(localSettings) {
93
93
  // console.log('saveSettings', JSON.stringify(localSettings, undefined, 2))
94
94
  settings.value = validSettings(localSettings)
@@ -337,9 +337,9 @@ export default {
337
337
  },
338
338
  emits: ['close'],
339
339
  setup(props, { emit }) {
340
- const chatSettings = inject('chatSettings')
341
- const { settings, saveSettings, resetSettings } = chatSettings
342
-
340
+ const ctx = inject('ctx')
341
+ const { settings, saveSettings, resetSettings } = ctx.chat.settings
342
+
343
343
  // Local copy for editing
344
344
  const localSettings = ref(Object.assign({}, settings.value))
345
345