llms-py 3.0.0b6__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 (181) hide show
  1. llms/__pycache__/main.cpython-314.pyc +0 -0
  2. llms/{ui/modules/analytics.mjs → extensions/analytics/ui/index.mjs} +55 -164
  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 +83 -20
  13. llms/extensions/app/ui/threadStore.mjs +407 -0
  14. llms/extensions/core_tools/__init__.py +598 -0
  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/__init__.py +61 -0
  38. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  39. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  40. llms/extensions/gallery/db.py +298 -0
  41. llms/extensions/gallery/ui/index.mjs +481 -0
  42. llms/extensions/katex/__init__.py +6 -0
  43. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  44. llms/extensions/katex/ui/README.md +125 -0
  45. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  46. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  47. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  48. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  49. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  50. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  51. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  52. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  53. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  54. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  55. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  56. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  57. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  58. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  59. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  60. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  61. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  62. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  63. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  120. llms/extensions/katex/ui/index.mjs +92 -0
  121. llms/extensions/katex/ui/katex-swap.css +1230 -0
  122. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  123. llms/extensions/katex/ui/katex.css +1230 -0
  124. llms/extensions/katex/ui/katex.js +19080 -0
  125. llms/extensions/katex/ui/katex.min.css +1 -0
  126. llms/extensions/katex/ui/katex.min.js +1 -0
  127. llms/extensions/katex/ui/katex.min.mjs +1 -0
  128. llms/extensions/katex/ui/katex.mjs +18547 -0
  129. llms/extensions/providers/__init__.py +18 -0
  130. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  131. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  132. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  133. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  134. llms/{providers → extensions/providers}/__pycache__/nvidia.cpython-314.pyc +0 -0
  135. llms/{providers → extensions/providers}/__pycache__/openai.cpython-314.pyc +0 -0
  136. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  137. llms/{providers → extensions/providers}/anthropic.py +45 -5
  138. llms/{providers → extensions/providers}/chutes.py +21 -18
  139. llms/{providers → extensions/providers}/google.py +99 -27
  140. llms/{providers → extensions/providers}/nvidia.py +6 -8
  141. llms/{providers → extensions/providers}/openai.py +3 -6
  142. llms/{providers → extensions/providers}/openrouter.py +12 -10
  143. llms/extensions/system_prompts/__init__.py +45 -0
  144. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  145. llms/extensions/system_prompts/ui/index.mjs +285 -0
  146. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  147. llms/extensions/tools/__init__.py +5 -0
  148. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  149. llms/{ui/modules/tools.mjs → extensions/tools/ui/index.mjs} +12 -10
  150. llms/index.html +26 -38
  151. llms/llms.json +20 -1
  152. llms/main.py +845 -245
  153. llms/providers-extra.json +0 -32
  154. llms/ui/App.mjs +18 -20
  155. llms/ui/ai.mjs +38 -15
  156. llms/ui/app.css +1440 -59
  157. llms/ui/ctx.mjs +154 -18
  158. llms/ui/index.mjs +17 -14
  159. llms/ui/lib/vue.min.mjs +10 -9
  160. llms/ui/lib/vue.mjs +1796 -1635
  161. llms/ui/markdown.mjs +4 -2
  162. llms/ui/modules/chat/ChatBody.mjs +101 -334
  163. llms/ui/modules/chat/HomeTools.mjs +12 -0
  164. llms/ui/modules/chat/SettingsDialog.mjs +1 -1
  165. llms/ui/modules/chat/index.mjs +351 -314
  166. llms/ui/modules/layout.mjs +2 -26
  167. llms/ui/modules/model-selector.mjs +3 -3
  168. llms/ui/tailwind.input.css +35 -1
  169. llms/ui/utils.mjs +33 -3
  170. {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/METADATA +1 -1
  171. llms_py-3.0.0b8.dist-info/RECORD +198 -0
  172. llms/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  173. llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  174. llms/providers/__pycache__/google.cpython-314.pyc +0 -0
  175. llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  176. llms/ui/modules/threads/threadStore.mjs +0 -586
  177. llms_py-3.0.0b6.dist-info/RECORD +0 -66
  178. {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/WHEEL +0 -0
  179. {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/entry_points.txt +0 -0
  180. {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/licenses/LICENSE +0 -0
  181. {llms_py-3.0.0b6.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 -->
@@ -14,62 +36,11 @@ export default {
14
36
  <!-- Welcome message when no thread is selected -->
15
37
  <div v-else-if="!currentThread" class="text-center py-12">
16
38
  <Welcome />
17
-
18
- <!-- Chat input for new conversation -->
19
- <!-- Moved to bottom input area -->
20
- <div class="h-2"></div>
21
-
22
- <!-- Export/Import buttons -->
23
- <div class="mt-2 flex space-x-3 justify-center items-center">
24
- <button type="button"
25
- @click="(e) => e.altKey ? exportRequests() : exportThreads()"
26
- :disabled="isExporting"
27
- :title="'Export ' + threads?.threads?.value?.length + ' conversations'"
28
- 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"
29
- >
30
- <svg v-if="!isExporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
31
- <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>
32
- </svg>
33
- <svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
34
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
35
- <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>
36
- </svg>
37
- {{ isExporting ? 'Exporting...' : 'Export' }}
38
- </button>
39
-
40
- <button type="button"
41
- @click="triggerImport"
42
- :disabled="isImporting"
43
- title="Import conversations from JSON file"
44
- 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"
45
- >
46
- <svg v-if="!isImporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
47
- <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"/>
48
- </svg>
49
- <svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
50
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
51
- <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>
52
- </svg>
53
- {{ isImporting ? 'Importing...' : 'Import' }}
54
- </button>
55
-
56
- <!-- Hidden file input for import -->
57
- <input
58
- ref="fileInput"
59
- type="file"
60
- accept=".json"
61
- @change="handleFileImport"
62
- class="hidden"
63
- />
64
-
65
- <DarkModeToggle />
66
-
67
- </div>
68
-
39
+ <HomeTools />
69
40
  </div>
70
41
 
71
42
  <!-- Messages -->
72
- <div v-else class="space-y-2">
43
+ <div v-else-if="currentThread?.messages?.length" class="space-y-2">
73
44
  <div v-if="currentThread?.messages.length && currentThread?.model" class="flex items-center justify-center select-none">
74
45
  <span @click="$chat.setSelectedModel({ name: currentThread.model})"
75
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">
@@ -78,7 +49,7 @@ export default {
78
49
  </span>
79
50
  </div>
80
51
  <div
81
- v-for="message in currentThread.messages"
52
+ v-for="message in currentThread.messages.filter(x => x.role !== 'system')"
82
53
  :key="message.id"
83
54
  v-show="!(message.role === 'tool' && isToolLinked(message))"
84
55
  class="flex items-start space-x-3 group"
@@ -101,7 +72,7 @@ export default {
101
72
  </div>
102
73
 
103
74
  <!-- Delete button (shown on hover) -->
104
- <button type="button" @click.stop="threads.deleteMessageFromThread(currentThread.id, message.id)"
75
+ <button type="button" @click.stop="$threads.deleteMessageFromThread(currentThread.id, message.id)"
105
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"
106
77
  title="Delete message">
107
78
  <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -228,6 +199,15 @@ export default {
228
199
  </template>
229
200
  </div>
230
201
 
202
+ <!-- Assistant Audios -->
203
+ <div v-if="message.audios && message.audios.length > 0" class="mt-2 flex flex-wrap gap-2">
204
+ <template v-for="(audio, i) in message.audios" :key="i">
205
+ <div v-if="audio.type === 'audio_url'" 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">
206
+ <audio controls :src="resolveUrl(audio.audio_url.url)" class="h-8 w-64"></audio>
207
+ </div>
208
+ </template>
209
+ </div>
210
+
231
211
  <!-- User Message with separate attachments -->
232
212
  <div v-else-if="message.role !== 'assistant' && message.role !== 'tool'">
233
213
  <div v-html="$fmt.markdown(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
@@ -254,16 +234,7 @@ export default {
254
234
  </div>
255
235
  </div>
256
236
 
257
- <div class="mt-2 text-xs opacity-70">
258
- <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>
259
- <span>{{ $fmt.time(message.timestamp) }}</span>
260
- <span v-if="message.usage" :title="tokensTitle(message.usage)">
261
- &#8226;
262
- {{ $fmt.humanifyNumber(message.usage.tokens) }} tokens
263
- <span v-if="message.usage.cost">&#183; {{ message.usage.cost }}</span>
264
- <span v-if="message.usage.duration"> in {{ $fmt.humanifyMs(message.usage.duration) }}</span>
265
- </span>
266
- </div>
237
+ <MessageUsage :message="message" :usage="getMessageUsage(message)" />
267
238
  </div>
268
239
 
269
240
  <!-- Edit and Redo buttons (shown on hover for user messages, outside bubble) -->
@@ -294,7 +265,7 @@ export default {
294
265
  </div>
295
266
 
296
267
  <!-- Loading indicator -->
297
- <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">
298
269
  <!-- Avatar outside the bubble -->
299
270
  <div class="flex-shrink-0">
300
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">
@@ -312,18 +283,36 @@ export default {
312
283
  </div>
313
284
 
314
285
  <!-- Cancel button -->
315
- <button type="button" @click="cancelRequest"
286
+ <button type="button" @click="$threads.cancelThread()"
316
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"
317
288
  title="Cancel request">
318
289
  cancel
319
290
  </button>
320
291
  </div>
321
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
+
322
311
  <!-- Error message bubble -->
323
- <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">
324
313
  <!-- Avatar outside the bubble -->
325
314
  <div class="flex-shrink-0">
326
- <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">
327
316
  !
328
317
  </div>
329
318
  </div>
@@ -332,20 +321,20 @@ export default {
332
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">
333
322
  <div class="flex items-start space-x-2">
334
323
  <div class="flex-1 min-w-0">
335
- <div class="text-base font-medium mb-1">{{ errorStatus?.errorCode || 'Error' }}</div>
336
- <div v-if="errorStatus?.message" class="text-base mb-1">{{ errorStatus.message }}</div>
337
- <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">
338
- {{ 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 }}
339
336
  </div>
340
337
  </div>
341
- <button type="button"
342
- @click="errorStatus = null"
343
- class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0"
344
- >
345
- <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
346
- <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>
347
- </svg>
348
- </button>
349
338
  </div>
350
339
  </div>
351
340
  </div>
@@ -355,7 +344,7 @@ export default {
355
344
  </div>
356
345
 
357
346
  <!-- Input Area -->
358
- <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">
347
+ <div v-if="$ai.hasAccess" :class="$ctx.cls('chat-input', 'flex-shrink-0 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6 py-4')">
359
348
  <ChatPrompt :model="$chat.getSelectedModel()" />
360
349
  </div>
361
350
 
@@ -382,7 +371,6 @@ export default {
382
371
  const threads = ctx.threads
383
372
  const chatPrompt = ctx.chat
384
373
  const { currentThread } = threads
385
- const { errorStatus, isGenerating } = ctx.chat
386
374
 
387
375
  const router = useRouter()
388
376
  const route = useRoute()
@@ -395,9 +383,6 @@ export default {
395
383
  return models.find(m => m.name === selectedModel.value) || models.find(m => m.id === selectedModel.value)
396
384
  })
397
385
  const messagesContainer = ref(null)
398
- const isExporting = ref(false)
399
- const isImporting = ref(false)
400
- const fileInput = ref(null)
401
386
  const copying = ref(null)
402
387
  const lightboxUrl = ref(null)
403
388
 
@@ -428,12 +413,9 @@ export default {
428
413
 
429
414
  // Watch for route changes and load the appropriate thread
430
415
  watch(() => route.params.id, async (newId) => {
431
- const thread = await threads.setCurrentThreadFromRoute(newId, router)
432
-
433
- // If the selected thread specifies a model and it's available, switch to it
434
- if (thread?.model && Array.isArray(models) && models.includes(thread.model)) {
435
- selectedModel.value = thread.model
436
- }
416
+ // console.debug('watch route.params.id', newId)
417
+ ctx.clearError()
418
+ threads.setCurrentThreadFromRoute(newId, router)
437
419
 
438
420
  if (!newId) {
439
421
  chatPrompt.reset()
@@ -446,191 +428,6 @@ export default {
446
428
  model: selectedModel.value,
447
429
  })
448
430
  })
449
-
450
- async function exportThreads() {
451
- if (isExporting.value) return
452
-
453
- isExporting.value = true
454
- try {
455
- // Load all threads from IndexedDB
456
- await threads.loadThreads()
457
- const allThreads = threads.threads.value
458
-
459
- // Create export data with metadata
460
- const exportData = {
461
- exportedAt: new Date().toISOString(),
462
- version: '1.0',
463
- source: 'llmspy',
464
- threadCount: allThreads.length,
465
- threads: allThreads
466
- }
467
-
468
- // Create and download JSON file
469
- const jsonString = JSON.stringify(exportData, null, 2)
470
- const blob = new Blob([jsonString], { type: 'application/json' })
471
- const url = URL.createObjectURL(blob)
472
-
473
- const link = document.createElement('a')
474
- link.href = url
475
- link.download = `llmsthreads-export-${new Date().toISOString().split('T')[0]}.json`
476
- document.body.appendChild(link)
477
- link.click()
478
- document.body.removeChild(link)
479
- URL.revokeObjectURL(url)
480
-
481
- } catch (error) {
482
- console.error('Failed to export threads:', error)
483
- alert('Failed to export threads: ' + error.message)
484
- } finally {
485
- isExporting.value = false
486
- }
487
- }
488
-
489
- async function exportRequests() {
490
- if (isExporting.value) return
491
-
492
- isExporting.value = true
493
- try {
494
- // Load all threads from IndexedDB
495
- const allRequests = await threads.getAllRequests()
496
-
497
- // Create export data with metadata
498
- const exportData = {
499
- exportedAt: new Date().toISOString(),
500
- version: '1.0',
501
- source: 'llmspy',
502
- requestsCount: allRequests.length,
503
- requests: allRequests
504
- }
505
-
506
- // Create and download JSON file
507
- const jsonString = JSON.stringify(exportData, null, 2)
508
- const blob = new Blob([jsonString], { type: 'application/json' })
509
- const url = URL.createObjectURL(blob)
510
-
511
- const link = document.createElement('a')
512
- link.href = url
513
- link.download = `llmsrequests-export-${new Date().toISOString().split('T')[0]}.json`
514
- document.body.appendChild(link)
515
- link.click()
516
- document.body.removeChild(link)
517
- URL.revokeObjectURL(url)
518
-
519
- } catch (error) {
520
- console.error('Failed to export requests:', error)
521
- alert('Failed to export requests: ' + error.message)
522
- } finally {
523
- isExporting.value = false
524
- }
525
- }
526
-
527
- function triggerImport() {
528
- if (isImporting.value) return
529
- fileInput.value?.click()
530
- }
531
-
532
- async function handleFileImport(event) {
533
- const file = event.target.files?.[0]
534
- if (!file) return
535
-
536
- isImporting.value = true
537
- var importType = 'threads'
538
- try {
539
- const text = await file.text()
540
- const importData = JSON.parse(text)
541
- importType = importData.threads
542
- ? 'threads'
543
- : importData.requests
544
- ? 'requests'
545
- : 'unknown'
546
-
547
- // Import threads one by one
548
- let importedCount = 0
549
- let existingCount = 0
550
-
551
- const db = await threads.initDB()
552
-
553
- if (importData.threads) {
554
- if (!Array.isArray(importData.threads)) {
555
- throw new Error('Invalid import file: missing or invalid threads array')
556
- }
557
-
558
- const threadIds = new Set(await threads.getAllThreadIds())
559
-
560
- for (const threadData of importData.threads) {
561
- if (!threadData.id) {
562
- console.warn('Skipping thread without ID:', threadData)
563
- continue
564
- }
565
-
566
- try {
567
- // Check if thread already exists
568
- const existingThread = threadIds.has(threadData.id)
569
- if (existingThread) {
570
- existingCount++
571
- } else {
572
- // Add new thread directly to IndexedDB
573
- const tx = db.transaction(['threads'], 'readwrite')
574
- await tx.objectStore('threads').add(threadData)
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}\nExisting threads: ${existingCount}`)
587
- }
588
- if (importData.requests) {
589
- if (!Array.isArray(importData.requests)) {
590
- throw new Error('Invalid import file: missing or invalid requests array')
591
- }
592
-
593
- const requestIds = new Set(await threads.getAllRequestIds())
594
-
595
- for (const requestData of importData.requests) {
596
- if (!requestData.id) {
597
- console.warn('Skipping request without ID:', requestData)
598
- continue
599
- }
600
-
601
- try {
602
- // Check if request already exists
603
- const existingRequest = requestIds.has(requestData.id)
604
- if (existingRequest) {
605
- existingCount++
606
- } else {
607
- // Add new request directly to IndexedDB
608
- const db = await threads.initDB()
609
- const tx = db.transaction(['requests'], 'readwrite')
610
- await tx.objectStore('requests').add(requestData)
611
- await tx.complete
612
- importedCount++
613
- }
614
- } catch (error) {
615
- console.error('Failed to import request:', requestData.id, error)
616
- }
617
- }
618
-
619
- alert(`Import completed!\nNew requests: ${importedCount}\nExisting requests: ${existingCount}`)
620
- }
621
-
622
- } catch (error) {
623
- console.error('Failed to import ' + importType + ':', error)
624
- alert('Failed to import ' + importType + ': ' + error.message)
625
- } finally {
626
- isImporting.value = false
627
- // Clear the file input
628
- if (fileInput.value) {
629
- fileInput.value.value = ''
630
- }
631
- }
632
- }
633
-
634
431
  function configUpdated() {
635
432
  console.log('configUpdated', selectedModel.value, models.length, models.includes(selectedModel.value))
636
433
  if (selectedModel.value && !models.includes(selectedModel.value)) {
@@ -746,37 +543,18 @@ export default {
746
543
  const redoMessage = async (message) => {
747
544
  if (!currentThread.value || message.role !== 'user') return
748
545
 
749
- try {
750
- const threadId = currentThread.value.id
751
-
752
- // Clear all messages after this one
753
- await threads.redoMessageFromThread(threadId, message.id)
546
+ const threadId = currentThread.value.id
754
547
 
755
- const state = await extractMessageState(message)
548
+ // Clear all messages after this one
549
+ await threads.redoMessageFromThread(threadId, message.timestamp)
756
550
 
757
- // Set the message text in the chat prompt
758
- chatPrompt.messageText.value = state.text
759
-
760
- // Restore attached files
761
- chatPrompt.attachedFiles.value = state.files
551
+ const state = await extractMessageState(message)
762
552
 
763
- // Trigger send by simulating the send action
764
- // We'll use a small delay to ensure the UI updates
765
- await nextTick()
553
+ // Set the message text in the chat prompt
554
+ chatPrompt.messageText.value = state.text
766
555
 
767
- // Find the send button and click it
768
- const sendButton = document.querySelector('button[title*="Send"]')
769
- if (sendButton && !sendButton.disabled) {
770
- sendButton.click()
771
- }
772
- } catch (error) {
773
- console.error('Failed to redo message:', error)
774
- errorStatus.value = {
775
- errorCode: 'Error',
776
- message: 'Failed to redo message: ' + error.message,
777
- stackTrace: null
778
- }
779
- }
556
+ // Restore attached files
557
+ chatPrompt.attachedFiles.value = state.files
780
558
  }
781
559
 
782
560
  // Edit a user message
@@ -787,7 +565,7 @@ export default {
787
565
  const state = await extractMessageState(message)
788
566
  chatPrompt.messageText.value = state.text
789
567
  chatPrompt.attachedFiles.value = state.files
790
- chatPrompt.editingMessageId.value = message.id
568
+ chatPrompt.editingMessage.value = message.timestamp
791
569
 
792
570
  // Focus the textarea
793
571
  nextTick(() => {
@@ -800,23 +578,6 @@ export default {
800
578
  })
801
579
  }
802
580
 
803
- // Cancel pending request
804
- const cancelRequest = () => {
805
- chatPrompt.cancel()
806
- }
807
-
808
- function tokensTitle(usage) {
809
- let title = []
810
- if (usage.tokens && usage.price) {
811
- const msg = parseFloat(usage.price) > 0
812
- ? `${usage.tokens} tokens @ ${usage.price} = ${ctx.fmt.tokenCostLong(usage.price, usage.tokens)}`
813
- : `${usage.tokens} tokens`
814
- const duration = usage.duration ? ` in ${usage.duration}ms` : ''
815
- title.push(msg + duration)
816
- }
817
- return title.join('\n')
818
- }
819
-
820
581
  let sub
821
582
  onMounted(() => {
822
583
  sub = ctx.events.subscribe(`keydown:Escape`, closeLightbox)
@@ -828,6 +589,20 @@ export default {
828
589
  return currentThread.value?.messages?.find(m => m.role === 'tool' && m.tool_call_id === toolCallId)
829
590
  }
830
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
+
831
606
  const isToolLinked = (message) => {
832
607
  if (message.role !== 'tool') return false
833
608
  return currentThread.value?.messages?.some(m => m.role === 'assistant' && m.tool_calls?.some(tc => tc.id === message.tool_call_id))
@@ -855,6 +630,9 @@ export default {
855
630
  if (tag == 'th') {
856
631
  cls += ' lowercase'
857
632
  }
633
+ if (tag == 'td') {
634
+ cls += ' whitespace-pre-wrap'
635
+ }
858
636
  return cls
859
637
  }
860
638
 
@@ -868,13 +646,10 @@ export default {
868
646
  setPrefs,
869
647
  config,
870
648
  models,
871
- threads,
872
- isGenerating,
873
649
  currentThread,
874
650
  selectedModel,
875
651
  selectedModelObj,
876
652
  messagesContainer,
877
- errorStatus,
878
653
  copying,
879
654
  isReasoningExpanded,
880
655
  toggleReasoning,
@@ -882,22 +657,14 @@ export default {
882
657
  copyMessageContent,
883
658
  redoMessage,
884
659
  editMessage,
885
- cancelRequest,
886
660
  configUpdated,
887
- exportThreads,
888
- exportRequests,
889
- isExporting,
890
- triggerImport,
891
- handleFileImport,
892
- isImporting,
893
- fileInput,
894
- tokensTitle,
895
661
  getAttachments,
896
662
  hasAttachments,
897
663
  lightboxUrl,
898
664
  openLightbox,
899
665
  closeLightbox,
900
666
  resolveUrl,
667
+ getMessageUsage,
901
668
  getToolOutput,
902
669
  isToolLinked,
903
670
  tryParseJson,
@@ -0,0 +1,12 @@
1
+ import { ref, inject } from 'vue'
2
+
3
+ export default {
4
+ template: `
5
+ <div class="mt-4 flex space-x-3 justify-center items-center">
6
+ <DarkModeToggle />
7
+ </div>
8
+
9
+ `,
10
+ setup() {
11
+ }
12
+ }
@@ -107,7 +107,7 @@ export function useSettings() {
107
107
 
108
108
  export default {
109
109
  template: `
110
- <div v-if="isOpen" class="fixed inset-0 z-50 overflow-y-auto" @click.self="close">
110
+ <div v-if="isOpen" class="fixed inset-0 z-100 overflow-y-auto" @click.self="close">
111
111
  <div class="flex min-h-screen items-center justify-center p-4">
112
112
  <!-- Backdrop -->
113
113
  <div class="fixed inset-0 bg-black/40 transition-opacity" @click="close"></div>