llms-py 3.0.0b7__py3-none-any.whl → 3.0.0b8__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 (157) hide show
  1. llms/__pycache__/main.cpython-314.pyc +0 -0
  2. llms/extensions/analytics/ui/index.mjs +51 -162
  3. llms/extensions/app/__init__.py +519 -0
  4. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  5. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  6. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  7. llms/extensions/app/db.py +641 -0
  8. llms/extensions/app/db_manager.py +195 -0
  9. llms/extensions/app/requests.json +9073 -0
  10. llms/extensions/app/threads.json +15290 -0
  11. llms/{ui/modules/threads → extensions/app/ui}/Recents.mjs +82 -55
  12. llms/{ui/modules/threads → extensions/app/ui}/index.mjs +78 -9
  13. llms/extensions/app/ui/threadStore.mjs +407 -0
  14. llms/extensions/core_tools/__init__.py +272 -32
  15. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  16. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  17. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  18. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  19. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  20. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  22. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  23. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  24. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  25. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  26. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  27. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +344 -0
  28. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +9884 -0
  29. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  30. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  31. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  32. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  33. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  34. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  35. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  36. llms/extensions/core_tools/ui/index.mjs +650 -0
  37. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  38. llms/extensions/gallery/db.py +4 -4
  39. llms/extensions/gallery/ui/index.mjs +2 -1
  40. llms/extensions/katex/__init__.py +6 -0
  41. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  42. llms/extensions/katex/ui/README.md +125 -0
  43. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  44. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  45. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  46. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  47. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  48. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  49. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  50. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  51. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  52. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  53. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  54. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  55. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  56. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  57. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  58. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  59. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  60. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  61. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  62. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  63. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  118. llms/extensions/katex/ui/index.mjs +92 -0
  119. llms/extensions/katex/ui/katex-swap.css +1230 -0
  120. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  121. llms/extensions/katex/ui/katex.css +1230 -0
  122. llms/extensions/katex/ui/katex.js +19080 -0
  123. llms/extensions/katex/ui/katex.min.css +1 -0
  124. llms/extensions/katex/ui/katex.min.js +1 -0
  125. llms/extensions/katex/ui/katex.min.mjs +1 -0
  126. llms/extensions/katex/ui/katex.mjs +18547 -0
  127. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  128. llms/extensions/providers/anthropic.py +44 -1
  129. llms/extensions/system_prompts/ui/index.mjs +2 -1
  130. llms/extensions/tools/__init__.py +5 -0
  131. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  132. llms/extensions/tools/ui/index.mjs +8 -8
  133. llms/index.html +26 -38
  134. llms/llms.json +4 -1
  135. llms/main.py +492 -103
  136. llms/ui/App.mjs +2 -3
  137. llms/ui/ai.mjs +29 -13
  138. llms/ui/app.css +250 -398
  139. llms/ui/ctx.mjs +84 -6
  140. llms/ui/index.mjs +4 -6
  141. llms/ui/lib/vue.min.mjs +10 -9
  142. llms/ui/lib/vue.mjs +1796 -1635
  143. llms/ui/markdown.mjs +4 -2
  144. llms/ui/modules/chat/ChatBody.mjs +90 -86
  145. llms/ui/modules/chat/HomeTools.mjs +0 -242
  146. llms/ui/modules/chat/index.mjs +103 -170
  147. llms/ui/modules/model-selector.mjs +2 -2
  148. llms/ui/tailwind.input.css +35 -1
  149. llms/ui/utils.mjs +12 -0
  150. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b8.dist-info}/METADATA +1 -1
  151. llms_py-3.0.0b8.dist-info/RECORD +198 -0
  152. llms/ui/modules/threads/threadStore.mjs +0 -640
  153. llms_py-3.0.0b7.dist-info/RECORD +0 -80
  154. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b8.dist-info}/WHEEL +0 -0
  155. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b8.dist-info}/entry_points.txt +0 -0
  156. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b8.dist-info}/licenses/LICENSE +0 -0
  157. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b8.dist-info}/top_level.txt +0 -0
llms/ui/markdown.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Marked } from "marked"
2
2
  import hljs from "highlight.js"
3
3
 
4
- export const marked = (() => {
4
+ export function createMarked() {
5
5
  const aliases = {
6
6
  vue: 'html',
7
7
  }
@@ -21,7 +21,9 @@ export const marked = (() => {
21
21
  ret.use({ extensions: [thinkTag()] })
22
22
  //ret.use({ extensions: [divExtension()] })
23
23
  return ret
24
- })();
24
+ }
25
+
26
+ export const marked = createMarked();
25
27
 
26
28
  export function renderMarkdown(content) {
27
29
  if (Array.isArray(content)) {
@@ -1,7 +1,29 @@
1
1
  import { ref, computed, nextTick, watch, onMounted, onUnmounted, inject } from 'vue'
2
2
  import { useRouter, useRoute } from 'vue-router'
3
3
 
4
+ const MessageUsage = {
5
+ template: `
6
+ <div class="mt-2 text-xs opacity-70">
7
+ <span v-if="message.model" @click="$chat.setSelectedModel({ name: message.model })" title="Select model"><span class="cursor-pointer hover:underline">{{ message.model }}</span> &#8226; </span>
8
+ <span>{{ $fmt.time(message.timestamp) }}</span>
9
+ <span v-if="usage" :title="$fmt.tokensTitle(usage)">
10
+ &#8226;
11
+ {{ $fmt.humanifyNumber(usage.tokens) }} tokens
12
+ <span v-if="usage.cost">&#183; {{ $fmt.tokenCostLong(usage.cost) }}</span>
13
+ <span v-if="usage.duration"> in {{ $fmt.humanifyMs(usage.duration) }}</span>
14
+ </span>
15
+ </div>
16
+ `,
17
+ props: {
18
+ usage: Object,
19
+ message: Object,
20
+ }
21
+ }
22
+
4
23
  export default {
24
+ components: {
25
+ MessageUsage,
26
+ },
5
27
  template: `
6
28
  <div class="flex flex-col h-full">
7
29
  <!-- Messages Area -->
@@ -18,7 +40,7 @@ export default {
18
40
  </div>
19
41
 
20
42
  <!-- Messages -->
21
- <div v-else class="space-y-2">
43
+ <div v-else-if="currentThread?.messages?.length" class="space-y-2">
22
44
  <div v-if="currentThread?.messages.length && currentThread?.model" class="flex items-center justify-center select-none">
23
45
  <span @click="$chat.setSelectedModel({ name: currentThread.model})"
24
46
  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">
@@ -27,7 +49,7 @@ export default {
27
49
  </span>
28
50
  </div>
29
51
  <div
30
- v-for="message in currentThread.messages"
52
+ v-for="message in currentThread.messages.filter(x => x.role !== 'system')"
31
53
  :key="message.id"
32
54
  v-show="!(message.role === 'tool' && isToolLinked(message))"
33
55
  class="flex items-start space-x-3 group"
@@ -50,7 +72,7 @@ export default {
50
72
  </div>
51
73
 
52
74
  <!-- Delete button (shown on hover) -->
53
- <button type="button" @click.stop="threads.deleteMessageFromThread(currentThread.id, message.id)"
75
+ <button type="button" @click.stop="$threads.deleteMessageFromThread(currentThread.id, message.id)"
54
76
  class="mx-auto opacity-0 group-hover:opacity-100 mt-2 rounded text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-all"
55
77
  title="Delete message">
56
78
  <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -212,16 +234,7 @@ export default {
212
234
  </div>
213
235
  </div>
214
236
 
215
- <div class="mt-2 text-xs opacity-70">
216
- <span v-if="message.model" @click="$chat.setSelectedModel({ name: message.model })" title="Select model"><span class="cursor-pointer hover:underline">{{ message.model }}</span> &#8226; </span>
217
- <span>{{ $fmt.time(message.timestamp) }}</span>
218
- <span v-if="message.usage" :title="tokensTitle(message.usage)">
219
- &#8226;
220
- {{ $fmt.humanifyNumber(message.usage.tokens) }} tokens
221
- <span v-if="message.usage.cost">&#183; {{ message.usage.cost }}</span>
222
- <span v-if="message.usage.duration"> in {{ $fmt.humanifyMs(message.usage.duration) }}</span>
223
- </span>
224
- </div>
237
+ <MessageUsage :message="message" :usage="getMessageUsage(message)" />
225
238
  </div>
226
239
 
227
240
  <!-- Edit and Redo buttons (shown on hover for user messages, outside bubble) -->
@@ -252,7 +265,7 @@ export default {
252
265
  </div>
253
266
 
254
267
  <!-- Loading indicator -->
255
- <div v-if="isGenerating" class="flex items-start space-x-3 group">
268
+ <div v-if="$threads.watchingThread" class="flex items-start space-x-3 group">
256
269
  <!-- Avatar outside the bubble -->
257
270
  <div class="flex-shrink-0">
258
271
  <div class="w-8 h-8 rounded-full bg-gray-600 dark:bg-gray-500 text-white flex items-center justify-center text-sm font-medium">
@@ -270,18 +283,36 @@ export default {
270
283
  </div>
271
284
 
272
285
  <!-- Cancel button -->
273
- <button type="button" @click="cancelRequest"
286
+ <button type="button" @click="$threads.cancelThread()"
274
287
  class="px-3 py-1 rounded text-sm text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 border border-transparent hover:border-red-300 dark:hover:border-red-600 transition-all"
275
288
  title="Cancel request">
276
289
  cancel
277
290
  </button>
278
291
  </div>
279
292
 
293
+ <!-- Thread error message bubble -->
294
+ <div v-if="currentThread?.error" class="mt-8 flex items-center space-x-3">
295
+ <!-- Avatar outside the bubble -->
296
+ <div class="flex-shrink-0">
297
+ <div class="size-8 rounded-full bg-red-600 dark:bg-red-500 text-white flex items-center justify-center text-lg font-bold">
298
+ !
299
+ </div>
300
+ </div>
301
+ <!-- Error bubble -->
302
+ <div class="max-w-[85%] rounded-lg px-3 py-1 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 shadow-sm">
303
+ <div class="flex items-start space-x-2">
304
+ <div class="flex-1 min-w-0">
305
+ <div v-if="currentThread.error" class="text-base mb-1">{{ currentThread.error }}</div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </div>
310
+
280
311
  <!-- Error message bubble -->
281
- <div v-if="errorStatus" class="flex items-start space-x-3">
312
+ <div v-if="$state.error" class="mt-8 flex items-start space-x-3">
282
313
  <!-- Avatar outside the bubble -->
283
314
  <div class="flex-shrink-0">
284
- <div class="w-8 h-8 rounded-full bg-red-600 dark:bg-red-500 text-white flex items-center justify-center text-sm font-medium">
315
+ <div class="size-8 rounded-full bg-red-600 dark:bg-red-500 text-white flex items-center justify-center text-lg font-bold">
285
316
  !
286
317
  </div>
287
318
  </div>
@@ -290,20 +321,20 @@ export default {
290
321
  <div class="max-w-[85%] rounded-lg px-4 py-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 shadow-sm">
291
322
  <div class="flex items-start space-x-2">
292
323
  <div class="flex-1 min-w-0">
293
- <div class="text-base font-medium mb-1">{{ errorStatus?.errorCode || 'Error' }}</div>
294
- <div v-if="errorStatus?.message" class="text-base mb-1">{{ errorStatus.message }}</div>
295
- <div v-if="errorStatus?.stackTrace" class="text-sm whitespace-pre-wrap break-words max-h-80 overflow-y-auto font-mono p-2 rounded bg-red-100 dark:bg-red-950/50">
296
- {{ errorStatus.stackTrace }}
324
+ <div class="flex justify-between items-start">
325
+ <div class="text-base font-medium mb-1">{{ $state.error?.errorCode || 'Error' }}</div>
326
+ <button type="button" @click="$ctx.clearError()" title="Clear Error"
327
+ class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0">
328
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
329
+ <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>
330
+ </svg>
331
+ </button>
332
+ </div>
333
+ <div v-if="$state.error?.message" class="text-base mb-1">{{ $state.error.message }}</div>
334
+ <div v-if="$state.error?.stackTrace" class="mt-2 text-sm whitespace-pre-wrap break-words max-h-80 overflow-y-auto font-mono p-2 border border-red-200/70 dark:border-red-800/70">
335
+ {{ $state.error.stackTrace }}
297
336
  </div>
298
337
  </div>
299
- <button type="button"
300
- @click="errorStatus = null"
301
- class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0"
302
- >
303
- <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
304
- <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>
305
- </svg>
306
- </button>
307
338
  </div>
308
339
  </div>
309
340
  </div>
@@ -340,7 +371,6 @@ export default {
340
371
  const threads = ctx.threads
341
372
  const chatPrompt = ctx.chat
342
373
  const { currentThread } = threads
343
- const { errorStatus, isGenerating } = ctx.chat
344
374
 
345
375
  const router = useRouter()
346
376
  const route = useRoute()
@@ -383,12 +413,9 @@ export default {
383
413
 
384
414
  // Watch for route changes and load the appropriate thread
385
415
  watch(() => route.params.id, async (newId) => {
386
- const thread = await threads.setCurrentThreadFromRoute(newId, router)
387
-
388
- // If the selected thread specifies a model and it's available, switch to it
389
- if (thread?.model && Array.isArray(models) && models.includes(thread.model)) {
390
- selectedModel.value = thread.model
391
- }
416
+ // console.debug('watch route.params.id', newId)
417
+ ctx.clearError()
418
+ threads.setCurrentThreadFromRoute(newId, router)
392
419
 
393
420
  if (!newId) {
394
421
  chatPrompt.reset()
@@ -516,37 +543,18 @@ export default {
516
543
  const redoMessage = async (message) => {
517
544
  if (!currentThread.value || message.role !== 'user') return
518
545
 
519
- try {
520
- const threadId = currentThread.value.id
521
-
522
- // Clear all messages after this one
523
- await threads.redoMessageFromThread(threadId, message.id)
524
-
525
- const state = await extractMessageState(message)
546
+ const threadId = currentThread.value.id
526
547
 
527
- // Set the message text in the chat prompt
528
- chatPrompt.messageText.value = state.text
548
+ // Clear all messages after this one
549
+ await threads.redoMessageFromThread(threadId, message.timestamp)
529
550
 
530
- // Restore attached files
531
- chatPrompt.attachedFiles.value = state.files
551
+ const state = await extractMessageState(message)
532
552
 
533
- // Trigger send by simulating the send action
534
- // We'll use a small delay to ensure the UI updates
535
- await nextTick()
553
+ // Set the message text in the chat prompt
554
+ chatPrompt.messageText.value = state.text
536
555
 
537
- // Find the send button and click it
538
- const sendButton = document.querySelector('button[title*="Send"]')
539
- if (sendButton && !sendButton.disabled) {
540
- sendButton.click()
541
- }
542
- } catch (error) {
543
- console.error('Failed to redo message:', error)
544
- errorStatus.value = {
545
- errorCode: 'Error',
546
- message: 'Failed to redo message: ' + error.message,
547
- stackTrace: null
548
- }
549
- }
556
+ // Restore attached files
557
+ chatPrompt.attachedFiles.value = state.files
550
558
  }
551
559
 
552
560
  // Edit a user message
@@ -557,7 +565,7 @@ export default {
557
565
  const state = await extractMessageState(message)
558
566
  chatPrompt.messageText.value = state.text
559
567
  chatPrompt.attachedFiles.value = state.files
560
- chatPrompt.editingMessageId.value = message.id
568
+ chatPrompt.editingMessage.value = message.timestamp
561
569
 
562
570
  // Focus the textarea
563
571
  nextTick(() => {
@@ -570,23 +578,6 @@ export default {
570
578
  })
571
579
  }
572
580
 
573
- // Cancel pending request
574
- const cancelRequest = () => {
575
- chatPrompt.cancel()
576
- }
577
-
578
- function tokensTitle(usage) {
579
- let title = []
580
- if (usage.tokens && usage.price) {
581
- const msg = parseFloat(usage.price) > 0
582
- ? `${usage.tokens} tokens @ ${usage.price} = ${ctx.fmt.tokenCostLong(usage.price, usage.tokens)}`
583
- : `${usage.tokens} tokens`
584
- const duration = usage.duration ? ` in ${usage.duration}ms` : ''
585
- title.push(msg + duration)
586
- }
587
- return title.join('\n')
588
- }
589
-
590
581
  let sub
591
582
  onMounted(() => {
592
583
  sub = ctx.events.subscribe(`keydown:Escape`, closeLightbox)
@@ -598,6 +589,20 @@ export default {
598
589
  return currentThread.value?.messages?.find(m => m.role === 'tool' && m.tool_call_id === toolCallId)
599
590
  }
600
591
 
592
+ const getMessageUsage = (message) => {
593
+ if (message.usage) return message.usage
594
+ if (message.tool_calls?.length) {
595
+ const toolUsages = message.tool_calls.map(tc => getToolOutput(tc.id)?.usage)
596
+ const agg = {
597
+ tokens: toolUsages.reduce((a, b) => a + (b?.tokens || 0), 0),
598
+ cost: toolUsages.reduce((a, b) => a + (b?.cost || 0), 0),
599
+ duration: toolUsages.reduce((a, b) => a + (b?.duration || 0), 0)
600
+ }
601
+ return agg
602
+ }
603
+ return null
604
+ }
605
+
601
606
  const isToolLinked = (message) => {
602
607
  if (message.role !== 'tool') return false
603
608
  return currentThread.value?.messages?.some(m => m.role === 'assistant' && m.tool_calls?.some(tc => tc.id === message.tool_call_id))
@@ -625,6 +630,9 @@ export default {
625
630
  if (tag == 'th') {
626
631
  cls += ' lowercase'
627
632
  }
633
+ if (tag == 'td') {
634
+ cls += ' whitespace-pre-wrap'
635
+ }
628
636
  return cls
629
637
  }
630
638
 
@@ -638,13 +646,10 @@ export default {
638
646
  setPrefs,
639
647
  config,
640
648
  models,
641
- threads,
642
- isGenerating,
643
649
  currentThread,
644
650
  selectedModel,
645
651
  selectedModelObj,
646
652
  messagesContainer,
647
- errorStatus,
648
653
  copying,
649
654
  isReasoningExpanded,
650
655
  toggleReasoning,
@@ -652,15 +657,14 @@ export default {
652
657
  copyMessageContent,
653
658
  redoMessage,
654
659
  editMessage,
655
- cancelRequest,
656
660
  configUpdated,
657
- tokensTitle,
658
661
  getAttachments,
659
662
  hasAttachments,
660
663
  lightboxUrl,
661
664
  openLightbox,
662
665
  closeLightbox,
663
666
  resolveUrl,
667
+ getMessageUsage,
664
668
  getToolOutput,
665
669
  isToolLinked,
666
670
  tryParseJson,
@@ -2,253 +2,11 @@ import { ref, inject } from 'vue'
2
2
 
3
3
  export default {
4
4
  template: `
5
- <!-- Export/Import buttons -->
6
5
  <div class="mt-4 flex space-x-3 justify-center items-center">
7
- <button type="button"
8
- @click="(e) => e.altKey ? exportRequests() : exportThreads()"
9
- :disabled="isExporting"
10
- :title="'Export ' + threads?.threads?.value?.length + ' conversations'"
11
- class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
12
- >
13
- <svg v-if="!isExporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
14
- <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>
15
- </svg>
16
- <svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
17
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
18
- <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>
19
- </svg>
20
- {{ isExporting ? 'Exporting...' : 'Export' }}
21
- </button>
22
-
23
- <button type="button"
24
- @click="triggerImport"
25
- :disabled="isImporting"
26
- title="Import conversations from JSON file"
27
- class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
28
- >
29
- <svg v-if="!isImporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
30
- <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"/>
31
- </svg>
32
- <svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
33
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
34
- <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>
35
- </svg>
36
- {{ isImporting ? 'Importing...' : 'Import' }}
37
- </button>
38
-
39
- <!-- Hidden file input for import -->
40
- <input
41
- ref="fileInput"
42
- type="file"
43
- accept=".json"
44
- @change="handleFileImport"
45
- class="hidden"
46
- />
47
-
48
6
  <DarkModeToggle />
49
7
  </div>
50
8
 
51
9
  `,
52
10
  setup() {
53
- const ctx = inject('ctx')
54
- const threads = ctx.threads
55
-
56
- const isExporting = ref(false)
57
- const isImporting = ref(false)
58
- const fileInput = ref(null)
59
-
60
- async function exportThreads() {
61
- if (isExporting.value) return
62
-
63
- isExporting.value = true
64
- try {
65
- // Load all threads from IndexedDB
66
- await threads.loadThreads()
67
- const allThreads = threads.threads.value
68
-
69
- // Create export data with metadata
70
- const exportData = {
71
- exportedAt: new Date().toISOString(),
72
- version: '1.0',
73
- source: 'llmspy',
74
- threadCount: allThreads.length,
75
- threads: allThreads
76
- }
77
-
78
- // Create and download JSON file
79
- const jsonString = JSON.stringify(exportData, null, 2)
80
- const blob = new Blob([jsonString], { type: 'application/json' })
81
- const url = URL.createObjectURL(blob)
82
-
83
- const link = document.createElement('a')
84
- link.href = url
85
- link.download = `llmsthreads-export-${new Date().toISOString().split('T')[0]}.json`
86
- document.body.appendChild(link)
87
- link.click()
88
- document.body.removeChild(link)
89
- URL.revokeObjectURL(url)
90
-
91
- } catch (error) {
92
- console.error('Failed to export threads:', error)
93
- alert('Failed to export threads: ' + error.message)
94
- } finally {
95
- isExporting.value = false
96
- }
97
- }
98
-
99
- async function exportRequests() {
100
- if (isExporting.value) return
101
-
102
- isExporting.value = true
103
- try {
104
- // Load all threads from IndexedDB
105
- const allRequests = await threads.getAllRequests()
106
-
107
- // Create export data with metadata
108
- const exportData = {
109
- exportedAt: new Date().toISOString(),
110
- version: '1.0',
111
- source: 'llmspy',
112
- requestsCount: allRequests.length,
113
- requests: allRequests
114
- }
115
-
116
- // Create and download JSON file
117
- const jsonString = JSON.stringify(exportData, null, 2)
118
- const blob = new Blob([jsonString], { type: 'application/json' })
119
- const url = URL.createObjectURL(blob)
120
-
121
- const link = document.createElement('a')
122
- link.href = url
123
- link.download = `llmsrequests-export-${new Date().toISOString().split('T')[0]}.json`
124
- document.body.appendChild(link)
125
- link.click()
126
- document.body.removeChild(link)
127
- URL.revokeObjectURL(url)
128
-
129
- } catch (error) {
130
- console.error('Failed to export requests:', error)
131
- alert('Failed to export requests: ' + error.message)
132
- } finally {
133
- isExporting.value = false
134
- }
135
- }
136
-
137
- function triggerImport() {
138
- if (isImporting.value) return
139
- fileInput.value?.click()
140
- }
141
-
142
- async function handleFileImport(event) {
143
- const file = event.target.files?.[0]
144
- if (!file) return
145
-
146
- isImporting.value = true
147
- var importType = 'threads'
148
- try {
149
- const text = await file.text()
150
- const importData = JSON.parse(text)
151
- importType = importData.threads
152
- ? 'threads'
153
- : importData.requests
154
- ? 'requests'
155
- : 'unknown'
156
-
157
- // Import threads one by one
158
- let importedCount = 0
159
- let existingCount = 0
160
-
161
- const db = await threads.initDB()
162
-
163
- if (importData.threads) {
164
- if (!Array.isArray(importData.threads)) {
165
- throw new Error('Invalid import file: missing or invalid threads array')
166
- }
167
-
168
- const threadIds = new Set(await threads.getAllThreadIds())
169
-
170
- for (const threadData of importData.threads) {
171
- if (!threadData.id) {
172
- console.warn('Skipping thread without ID:', threadData)
173
- continue
174
- }
175
-
176
- try {
177
- // Check if thread already exists
178
- const existingThread = threadIds.has(threadData.id)
179
- if (existingThread) {
180
- existingCount++
181
- } else {
182
- // Add new thread directly to IndexedDB
183
- const tx = db.transaction(['threads'], 'readwrite')
184
- await tx.objectStore('threads').add(threadData)
185
- await tx.complete
186
- importedCount++
187
- }
188
- } catch (error) {
189
- console.error('Failed to import thread:', threadData.id, error)
190
- }
191
- }
192
-
193
- // Reload threads to reflect changes
194
- await threads.loadThreads()
195
-
196
- alert(`Import completed!\nNew threads: ${importedCount}\nExisting threads: ${existingCount}`)
197
- }
198
- if (importData.requests) {
199
- if (!Array.isArray(importData.requests)) {
200
- throw new Error('Invalid import file: missing or invalid requests array')
201
- }
202
-
203
- const requestIds = new Set(await threads.getAllRequestIds())
204
-
205
- for (const requestData of importData.requests) {
206
- if (!requestData.id) {
207
- console.warn('Skipping request without ID:', requestData)
208
- continue
209
- }
210
-
211
- try {
212
- // Check if request already exists
213
- const existingRequest = requestIds.has(requestData.id)
214
- if (existingRequest) {
215
- existingCount++
216
- } else {
217
- // Add new request directly to IndexedDB
218
- const db = await threads.initDB()
219
- const tx = db.transaction(['requests'], 'readwrite')
220
- await tx.objectStore('requests').add(requestData)
221
- await tx.complete
222
- importedCount++
223
- }
224
- } catch (error) {
225
- console.error('Failed to import request:', requestData.id, error)
226
- }
227
- }
228
-
229
- alert(`Import completed!\nNew requests: ${importedCount}\nExisting requests: ${existingCount}`)
230
- }
231
-
232
- } catch (error) {
233
- console.error('Failed to import ' + importType + ':', error)
234
- alert('Failed to import ' + importType + ': ' + error.message)
235
- } finally {
236
- isImporting.value = false
237
- // Clear the file input
238
- if (fileInput.value) {
239
- fileInput.value.value = ''
240
- }
241
- }
242
- }
243
-
244
- return {
245
- exportThreads,
246
- exportRequests,
247
- isExporting,
248
- triggerImport,
249
- handleFileImport,
250
- isImporting,
251
- fileInput,
252
- }
253
11
  }
254
12
  }