llms-py 2.0.34__py3-none-any.whl → 3.0.0__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 (207) hide show
  1. llms/__init__.py +3 -1
  2. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  3. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  4. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  5. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  6. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  7. llms/__pycache__/llms.cpython-312.pyc +0 -0
  8. llms/__pycache__/main.cpython-312.pyc +0 -0
  9. llms/__pycache__/main.cpython-313.pyc +0 -0
  10. llms/__pycache__/main.cpython-314.pyc +0 -0
  11. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  12. llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +154 -238
  13. llms/extensions/app/README.md +20 -0
  14. llms/extensions/app/__init__.py +530 -0
  15. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  16. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  17. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  18. llms/extensions/app/db.py +644 -0
  19. llms/extensions/app/db_manager.py +195 -0
  20. llms/extensions/app/requests.json +9073 -0
  21. llms/extensions/app/threads.json +15290 -0
  22. llms/{ui → extensions/app/ui}/Recents.mjs +91 -65
  23. llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +124 -58
  24. llms/extensions/app/ui/threadStore.mjs +411 -0
  25. llms/extensions/core_tools/CALCULATOR.md +32 -0
  26. llms/extensions/core_tools/__init__.py +598 -0
  27. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  28. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  29. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  30. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  31. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  32. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  33. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  34. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  35. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  36. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  37. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  38. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  39. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +344 -0
  40. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +9884 -0
  41. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  42. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  43. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  44. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  45. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  46. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  47. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  48. llms/extensions/core_tools/ui/index.mjs +650 -0
  49. llms/extensions/gallery/README.md +61 -0
  50. llms/extensions/gallery/__init__.py +61 -0
  51. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  52. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  53. llms/extensions/gallery/db.py +298 -0
  54. llms/extensions/gallery/ui/index.mjs +482 -0
  55. llms/extensions/katex/README.md +39 -0
  56. llms/extensions/katex/__init__.py +6 -0
  57. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  58. llms/extensions/katex/ui/README.md +125 -0
  59. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  60. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  61. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  62. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  63. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  64. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  65. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  66. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  67. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  68. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  69. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  70. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  71. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  72. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  73. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  124. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  125. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  126. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  127. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  128. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  129. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  130. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  131. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  132. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  133. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  134. llms/extensions/katex/ui/index.mjs +92 -0
  135. llms/extensions/katex/ui/katex-swap.css +1230 -0
  136. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  137. llms/extensions/katex/ui/katex.css +1230 -0
  138. llms/extensions/katex/ui/katex.js +19080 -0
  139. llms/extensions/katex/ui/katex.min.css +1 -0
  140. llms/extensions/katex/ui/katex.min.js +1 -0
  141. llms/extensions/katex/ui/katex.min.mjs +1 -0
  142. llms/extensions/katex/ui/katex.mjs +18547 -0
  143. llms/extensions/providers/__init__.py +18 -0
  144. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  145. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  146. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  147. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  148. llms/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  149. llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
  150. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  151. llms/extensions/providers/anthropic.py +229 -0
  152. llms/extensions/providers/chutes.py +155 -0
  153. llms/extensions/providers/google.py +378 -0
  154. llms/extensions/providers/nvidia.py +105 -0
  155. llms/extensions/providers/openai.py +156 -0
  156. llms/extensions/providers/openrouter.py +72 -0
  157. llms/extensions/system_prompts/README.md +22 -0
  158. llms/extensions/system_prompts/__init__.py +45 -0
  159. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  160. llms/extensions/system_prompts/ui/index.mjs +280 -0
  161. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  162. llms/extensions/tools/__init__.py +5 -0
  163. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  164. llms/extensions/tools/ui/index.mjs +204 -0
  165. llms/index.html +35 -77
  166. llms/llms.json +357 -1186
  167. llms/main.py +2847 -999
  168. llms/providers-extra.json +356 -0
  169. llms/providers.json +1 -0
  170. llms/ui/App.mjs +151 -60
  171. llms/ui/ai.mjs +132 -60
  172. llms/ui/app.css +2173 -161
  173. llms/ui/ctx.mjs +365 -0
  174. llms/ui/index.mjs +129 -0
  175. llms/ui/lib/charts.mjs +9 -13
  176. llms/ui/lib/servicestack-vue.mjs +3 -3
  177. llms/ui/lib/vue.min.mjs +10 -9
  178. llms/ui/lib/vue.mjs +1796 -1635
  179. llms/ui/markdown.mjs +18 -7
  180. llms/ui/modules/chat/ChatBody.mjs +691 -0
  181. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +9 -9
  182. llms/ui/modules/chat/index.mjs +828 -0
  183. llms/ui/modules/layout.mjs +243 -0
  184. llms/ui/modules/model-selector.mjs +851 -0
  185. llms/ui/tailwind.input.css +496 -80
  186. llms/ui/utils.mjs +161 -93
  187. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/METADATA +1 -1
  188. llms_py-3.0.0.dist-info/RECORD +202 -0
  189. llms/ui/Avatar.mjs +0 -85
  190. llms/ui/Brand.mjs +0 -52
  191. llms/ui/ChatPrompt.mjs +0 -590
  192. llms/ui/Main.mjs +0 -823
  193. llms/ui/ModelSelector.mjs +0 -78
  194. llms/ui/OAuthSignIn.mjs +0 -92
  195. llms/ui/ProviderIcon.mjs +0 -30
  196. llms/ui/ProviderStatus.mjs +0 -105
  197. llms/ui/SignIn.mjs +0 -64
  198. llms/ui/SystemPromptEditor.mjs +0 -31
  199. llms/ui/SystemPromptSelector.mjs +0 -56
  200. llms/ui/Welcome.mjs +0 -8
  201. llms/ui/threadStore.mjs +0 -563
  202. llms/ui.json +0 -1069
  203. llms_py-2.0.34.dist-info/RECORD +0 -48
  204. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/WHEEL +0 -0
  205. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/entry_points.txt +0 -0
  206. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/licenses/LICENSE +0 -0
  207. {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/top_level.txt +0 -0
llms/ui/Main.mjs DELETED
@@ -1,823 +0,0 @@
1
- import { ref, computed, nextTick, watch, onMounted, provide, inject } from 'vue'
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 } 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
-
19
- export default {
20
- components: {
21
- ModelSelector,
22
- SystemPromptSelector,
23
- SystemPromptEditor,
24
- ChatPrompt,
25
- SignIn,
26
- OAuthSignIn,
27
- Avatar,
28
- Welcome,
29
- },
30
- 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
-
50
- <!-- Messages Area -->
51
- <div class="flex-1 overflow-y-auto" ref="messagesContainer">
52
- <div class="mx-auto max-w-6xl px-4 py-6">
53
- <div v-if="$ai.requiresAuth && !$ai.auth">
54
- <OAuthSignIn v-if="$ai.authType === 'oauth'" @done="$ai.signIn($event)" />
55
- <SignIn v-else @done="$ai.signIn($event)" />
56
- </div>
57
- <!-- Welcome message when no thread is selected -->
58
- <div v-else-if="!currentThread" class="text-center py-12">
59
- <Welcome />
60
-
61
- <!-- Chat input for new conversation -->
62
- <div class="max-w-2xl mx-auto">
63
- <ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
64
- </div>
65
-
66
- <!-- Export/Import buttons -->
67
- <div class="mt-2 flex space-x-3 justify-center items-center">
68
- <button type="button"
69
- @click="(e) => e.altKey ? exportRequests() : exportThreads()"
70
- :disabled="isExporting"
71
- :title="'Export ' + threads?.threads?.value?.length + ' conversations'"
72
- 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"
73
- >
74
- <svg v-if="!isExporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
75
- <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>
76
- </svg>
77
- <svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
78
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
79
- <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>
80
- </svg>
81
- {{ isExporting ? 'Exporting...' : 'Export' }}
82
- </button>
83
-
84
- <button type="button"
85
- @click="triggerImport"
86
- :disabled="isImporting"
87
- title="Import conversations from JSON file"
88
- 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"
89
- >
90
- <svg v-if="!isImporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
91
- <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"/>
92
- </svg>
93
- <svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
94
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
95
- <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>
96
- </svg>
97
- {{ isImporting ? 'Importing...' : 'Import' }}
98
- </button>
99
-
100
- <!-- Hidden file input for import -->
101
- <input
102
- ref="fileInput"
103
- type="file"
104
- accept=".json"
105
- @change="handleFileImport"
106
- class="hidden"
107
- />
108
-
109
- <DarkModeToggle />
110
-
111
- </div>
112
-
113
- </div>
114
-
115
- <!-- Messages -->
116
- <div v-else class="space-y-6">
117
- <div
118
- v-for="message in currentThread.messages"
119
- :key="message.id"
120
- class="flex items-start space-x-3 group"
121
- :class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
122
- >
123
- <!-- Avatar outside the bubble -->
124
- <div class="flex-shrink-0 flex flex-col justify-center">
125
- <div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
126
- :class="message.role === 'user'
127
- ? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
128
- : 'bg-gray-600 dark:bg-gray-500 text-white'"
129
- >
130
- {{ message.role === 'user' ? 'U' : 'AI' }}
131
- </div>
132
-
133
- <!-- Delete button (shown on hover) -->
134
- <button type="button" @click.stop="threads.deleteMessageFromThread(currentThread.id, message.id)"
135
- 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"
136
- title="Delete message">
137
- <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
138
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
139
- </svg>
140
- </button>
141
- </div>
142
-
143
- <!-- Message bubble -->
144
- <div
145
- class="message rounded-lg px-4 py-3 relative group"
146
- :class="message.role === 'user'
147
- ? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
148
- : 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700'"
149
- >
150
- <!-- Copy button in top right corner -->
151
- <button
152
- type="button"
153
- @click="copyMessageContent(message)"
154
- class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 focus:outline-none focus:ring-0"
155
- :class="message.role === 'user' ? 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'"
156
- title="Copy message content"
157
- >
158
- <svg v-if="copying === message" class="size-4 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
159
- <svg v-else class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
160
- <rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
161
- <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
162
- </svg>
163
- </button>
164
-
165
- <div
166
- v-if="message.role === 'assistant'"
167
- v-html="renderMarkdown(message.content)"
168
- class="prose prose-sm max-w-none dark:prose-invert"
169
- ></div>
170
-
171
- <!-- Collapsible reasoning section -->
172
- <div v-if="message.role === 'assistant' && message.reasoning" class="mt-2">
173
- <button type="button" @click="toggleReasoning(message.id)" class="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 flex items-center space-x-1">
174
- <svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" :class="isReasoningExpanded(message.id) ? 'transform rotate-90' : ''"><path fill="currentColor" d="M7 5l6 5l-6 5z"/></svg>
175
- <span>{{ isReasoningExpanded(message.id) ? 'Hide reasoning' : 'Show reasoning' }}</span>
176
- </button>
177
- <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">
178
- <div v-if="typeof message.reasoning === 'string'" v-html="renderMarkdown(message.reasoning)" class="prose prose-xs max-w-none dark:prose-invert"></div>
179
- <pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto text-gray-900 dark:text-gray-100">{{ formatReasoning(message.reasoning) }}</pre>
180
- </div>
181
- </div>
182
-
183
- <div v-if="message.role !== 'assistant'" class="whitespace-pre-wrap">{{ message.content }}</div>
184
- <div class="mt-2 text-xs opacity-70">
185
- <span>{{ formatTime(message.timestamp) }}</span>
186
- <span v-if="message.usage" :title="tokensTitle(message.usage)">
187
- &#8226;
188
- {{ humanifyNumber(message.usage.tokens) }} tokens
189
- <span v-if="message.usage.cost">&#183; {{ message.usage.cost }}</span>
190
- <span v-if="message.usage.duration"> in {{ humanifyMs(message.usage.duration) }}</span>
191
- </span>
192
- </div>
193
- </div>
194
-
195
- <!-- Edit and Redo buttons (shown on hover for user messages, outside bubble) -->
196
- <div v-if="message.role === 'user'" class="flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity mt-1">
197
- <button type="button" @click.stop="editMessage(message)"
198
- class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 dark:text-gray-500 hover:text-green-600 dark:hover:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/30 transition-all"
199
- title="Edit message">
200
- <svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
201
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
202
- </svg>
203
- Edit
204
- </button>
205
- <button type="button" @click.stop="redoMessage(message)"
206
- class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-all"
207
- title="Redo message (clears all responses after this message and re-runs it)">
208
- <svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
209
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
210
- </svg>
211
- Redo
212
- </button>
213
- </div>
214
- </div>
215
-
216
- <div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 dark:text-gray-400 text-sm">
217
- <span :title="statsTitle(currentThread.stats)">
218
- {{ 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) }}
219
- </span>
220
- </div>
221
-
222
- <!-- Loading indicator -->
223
- <div v-if="isGenerating" class="flex items-start space-x-3 group">
224
- <!-- Avatar outside the bubble -->
225
- <div class="flex-shrink-0">
226
- <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">
227
- AI
228
- </div>
229
- </div>
230
-
231
- <!-- Loading bubble -->
232
- <div class="rounded-lg px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
233
- <div class="flex space-x-1">
234
- <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"></div>
235
- <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
236
- <div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
237
- </div>
238
- </div>
239
-
240
- <!-- Cancel button -->
241
- <button type="button" @click="cancelRequest"
242
- 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"
243
- title="Cancel request">
244
- cancel
245
- </button>
246
- </div>
247
-
248
- <!-- Error message bubble -->
249
- <div v-if="errorStatus" class="flex items-start space-x-3">
250
- <!-- Avatar outside the bubble -->
251
- <div class="flex-shrink-0">
252
- <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">
253
- !
254
- </div>
255
- </div>
256
-
257
- <!-- Error bubble -->
258
- <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">
259
- <div class="flex items-start space-x-2">
260
- <div class="flex-1 min-w-0">
261
- <div class="text-base font-medium mb-1">{{ errorStatus?.errorCode || 'Error' }}</div>
262
- <div v-if="errorStatus?.message" class="text-base mb-1">{{ errorStatus.message }}</div>
263
- <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">
264
- {{ errorStatus.stackTrace }}
265
- </div>
266
- </div>
267
- <button type="button"
268
- @click="errorStatus = null"
269
- class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0"
270
- >
271
- <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
272
- <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>
273
- </svg>
274
- </button>
275
- </div>
276
- </div>
277
- </div>
278
- </div>
279
- </div>
280
-
281
- <!-- Edit message modal -->
282
- <div v-if="editingMessageId" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
283
- <div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 max-w-2xl w-full mx-4">
284
- <CloseButton @click="cancelEdit" class="" />
285
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Edit Message</h3>
286
- <textarea
287
- v-model="editingMessageContent"
288
- class="w-full h-40 px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
289
- placeholder="Edit your message..."
290
- ></textarea>
291
- <div class="mt-4 flex gap-2 justify-end">
292
- <button type="button" @click="cancelEdit"
293
- class="px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-all">
294
- Cancel
295
- </button>
296
- <button type="button" @click="saveEditedMessage"
297
- class="px-4 py-2 rounded-md bg-blue-600 dark:bg-blue-500 text-white hover:bg-blue-700 dark:hover:bg-blue-600 transition-all">
298
- Save
299
- </button>
300
- </div>
301
- </div>
302
- </div>
303
- </div>
304
-
305
- <!-- Input Area - only show when thread is selected -->
306
- <div v-if="currentThread" class="flex-shrink-0 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6 py-4">
307
- <ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
308
- </div>
309
- </div>
310
- `,
311
- props: {
312
- },
313
- setup(props) {
314
- const ai = inject('ai')
315
- const router = useRouter()
316
- const route = useRoute()
317
- const threads = useThreadStore()
318
- const { currentThread } = threads
319
- const chatPrompt = useChatPrompt()
320
- const chatSettings = useSettings()
321
- const {
322
- errorStatus,
323
- isGenerating,
324
- } = chatPrompt
325
- provide('threads', threads)
326
- provide('chatPrompt', chatPrompt)
327
- provide('chatSettings', chatSettings)
328
- const models = inject('models')
329
- const config = inject('config')
330
-
331
- const prefs = storageObject(ai.prefsKey)
332
-
333
- const customPromptValue = ref('')
334
- const customPrompt = {
335
- id: '_custom_',
336
- name: 'Custom...',
337
- value: ''
338
- }
339
-
340
- const prompts = computed(() => [customPrompt, ...config.prompts])
341
-
342
- const selectedModel = ref(prefs.model || config.defaults.text.model || '')
343
- const selectedPrompt = ref(prefs.systemPrompt || null)
344
- const currentSystemPrompt = ref('')
345
- const showSystemPrompt = ref(false)
346
- const messagesContainer = ref(null)
347
- const isExporting = ref(false)
348
- const isImporting = ref(false)
349
- const fileInput = ref(null)
350
- const copying = ref(null)
351
- const editingMessageId = ref(null)
352
- const editingMessageContent = ref('')
353
- const editingMessage = ref(null)
354
-
355
- // Auto-scroll to bottom when new messages arrive
356
- const scrollToBottom = async () => {
357
- await nextTick()
358
- if (messagesContainer.value) {
359
- messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
360
- }
361
- }
362
-
363
- // Watch for new messages and scroll
364
- watch(() => currentThread.value?.messages?.length, scrollToBottom)
365
-
366
- // Watch for route changes and load the appropriate thread
367
- watch(() => route.params.id, async (newId) => {
368
- const thread = await threads.setCurrentThreadFromRoute(newId, router)
369
-
370
- // If the selected thread specifies a model and it's available, switch to it
371
- if (thread?.model && Array.isArray(models) && models.includes(thread.model)) {
372
- selectedModel.value = thread.model
373
- }
374
-
375
- // Sync System Prompt selection from thread
376
- if (thread) {
377
- const norm = s => (s || '').replace(/\s+/g, ' ').trim()
378
- const tsp = norm(thread.systemPrompt || '')
379
- if (tsp) {
380
- const match = config.prompts.find(p => norm(p.value) === tsp)
381
- if (match) {
382
- selectedPrompt.value = match
383
- currentSystemPrompt.value = match.value.replace(/\n/g, ' ')
384
- } else {
385
- selectedPrompt.value = customPrompt
386
- currentSystemPrompt.value = thread.systemPrompt
387
- }
388
- } else {
389
- // Preserve existing selected prompt
390
- // selectedPrompt.value = null
391
- // currentSystemPrompt.value = ''
392
- }
393
- }
394
-
395
- if (!newId) {
396
- chatPrompt.reset()
397
- }
398
- nextTick(addCopyButtons)
399
- }, { immediate: true })
400
-
401
- // Watch selectedPrompt and update currentSystemPrompt
402
- watch(selectedPrompt, (newPrompt) => {
403
- // If using a custom prompt, keep whatever is already in currentSystemPrompt
404
- if (newPrompt && newPrompt.id === '_custom_') return
405
- const prompt = newPrompt && config.prompts.find(p => p.id === newPrompt.id)
406
- currentSystemPrompt.value = prompt ? prompt.value.replace(/\n/g,' ') : ''
407
- }, { immediate: true })
408
-
409
- watch(() => [selectedModel.value, selectedPrompt.value], () => {
410
- localStorage.setItem(ai.prefsKey, JSON.stringify({
411
- model: selectedModel.value,
412
- systemPrompt: selectedPrompt.value
413
- }))
414
- })
415
-
416
- async function exportThreads() {
417
- if (isExporting.value) return
418
-
419
- isExporting.value = true
420
- try {
421
- // Load all threads from IndexedDB
422
- await threads.loadThreads()
423
- const allThreads = threads.threads.value
424
-
425
- // Create export data with metadata
426
- const exportData = {
427
- exportedAt: new Date().toISOString(),
428
- version: '1.0',
429
- source: 'llmspy',
430
- threadCount: allThreads.length,
431
- threads: allThreads
432
- }
433
-
434
- // Create and download JSON file
435
- const jsonString = JSON.stringify(exportData, null, 2)
436
- const blob = new Blob([jsonString], { type: 'application/json' })
437
- const url = URL.createObjectURL(blob)
438
-
439
- const link = document.createElement('a')
440
- link.href = url
441
- link.download = `llmsthreads-export-${new Date().toISOString().split('T')[0]}.json`
442
- document.body.appendChild(link)
443
- link.click()
444
- document.body.removeChild(link)
445
- URL.revokeObjectURL(url)
446
-
447
- } catch (error) {
448
- console.error('Failed to export threads:', error)
449
- alert('Failed to export threads: ' + error.message)
450
- } finally {
451
- isExporting.value = false
452
- }
453
- }
454
-
455
- async function exportRequests() {
456
- if (isExporting.value) return
457
-
458
- isExporting.value = true
459
- try {
460
- // Load all threads from IndexedDB
461
- const allRequests = await threads.getAllRequests()
462
-
463
- // Create export data with metadata
464
- const exportData = {
465
- exportedAt: new Date().toISOString(),
466
- version: '1.0',
467
- source: 'llmspy',
468
- requestsCount: allRequests.length,
469
- requests: allRequests
470
- }
471
-
472
- // Create and download JSON file
473
- const jsonString = JSON.stringify(exportData, null, 2)
474
- const blob = new Blob([jsonString], { type: 'application/json' })
475
- const url = URL.createObjectURL(blob)
476
-
477
- const link = document.createElement('a')
478
- link.href = url
479
- link.download = `llmsrequests-export-${new Date().toISOString().split('T')[0]}.json`
480
- document.body.appendChild(link)
481
- link.click()
482
- document.body.removeChild(link)
483
- URL.revokeObjectURL(url)
484
-
485
- } catch (error) {
486
- console.error('Failed to export requests:', error)
487
- alert('Failed to export requests: ' + error.message)
488
- } finally {
489
- isExporting.value = false
490
- }
491
- }
492
-
493
- function triggerImport() {
494
- if (isImporting.value) return
495
- fileInput.value?.click()
496
- }
497
-
498
- async function handleFileImport(event) {
499
- const file = event.target.files?.[0]
500
- if (!file) return
501
-
502
- isImporting.value = true
503
- var importType = 'threads'
504
- try {
505
- const text = await file.text()
506
- const importData = JSON.parse(text)
507
- importType = importData.threads
508
- ? 'threads'
509
- : importData.requests
510
- ? 'requests'
511
- : 'unknown'
512
-
513
- // Import threads one by one
514
- let importedCount = 0
515
- let existingCount = 0
516
-
517
- const db = await threads.initDB()
518
-
519
- if (importData.threads) {
520
- if (!Array.isArray(importData.threads)) {
521
- throw new Error('Invalid import file: missing or invalid threads array')
522
- }
523
-
524
- const threadIds = new Set(await threads.getAllThreadIds())
525
-
526
- for (const threadData of importData.threads) {
527
- if (!threadData.id) {
528
- console.warn('Skipping thread without ID:', threadData)
529
- continue
530
- }
531
-
532
- try {
533
- // Check if thread already exists
534
- const existingThread = threadIds.has(threadData.id)
535
- if (existingThread) {
536
- existingCount++
537
- } else {
538
- // Add new thread directly to IndexedDB
539
- const tx = db.transaction(['threads'], 'readwrite')
540
- await tx.objectStore('threads').add(threadData)
541
- await tx.complete
542
- importedCount++
543
- }
544
- } catch (error) {
545
- console.error('Failed to import thread:', threadData.id, error)
546
- }
547
- }
548
-
549
- // Reload threads to reflect changes
550
- await threads.loadThreads()
551
-
552
- alert(`Import completed!\nNew threads: ${importedCount}\nExisting threads: ${existingCount}`)
553
- }
554
- if (importData.requests) {
555
- if (!Array.isArray(importData.requests)) {
556
- throw new Error('Invalid import file: missing or invalid requests array')
557
- }
558
-
559
- const requestIds = new Set(await threads.getAllRequestIds())
560
-
561
- for (const requestData of importData.requests) {
562
- if (!requestData.id) {
563
- console.warn('Skipping request without ID:', requestData)
564
- continue
565
- }
566
-
567
- try {
568
- // Check if request already exists
569
- const existingRequest = requestIds.has(requestData.id)
570
- if (existingRequest) {
571
- existingCount++
572
- } else {
573
- // Add new request directly to IndexedDB
574
- const db = await threads.initDB()
575
- const tx = db.transaction(['requests'], 'readwrite')
576
- await tx.objectStore('requests').add(requestData)
577
- await tx.complete
578
- importedCount++
579
- }
580
- } catch (error) {
581
- console.error('Failed to import request:', requestData.id, error)
582
- }
583
- }
584
-
585
- alert(`Import completed!\nNew requests: ${importedCount}\nExisting requests: ${existingCount}`)
586
- }
587
-
588
- } catch (error) {
589
- console.error('Failed to import ' + importType + ':', error)
590
- alert('Failed to import ' + importType + ': ' + error.message)
591
- } finally {
592
- isImporting.value = false
593
- // Clear the file input
594
- if (fileInput.value) {
595
- fileInput.value.value = ''
596
- }
597
- }
598
- }
599
-
600
- function configUpdated() {
601
- console.log('configUpdated', selectedModel.value, models.length, models.includes(selectedModel.value))
602
- if (selectedModel.value && !models.includes(selectedModel.value)) {
603
- selectedModel.value = config.defaults.text.model || ''
604
- }
605
- }
606
-
607
- // Format timestamp
608
- const formatTime = (timestamp) => {
609
- return new Date(timestamp).toLocaleTimeString([], {
610
- hour: '2-digit',
611
- minute: '2-digit'
612
- })
613
- }
614
-
615
- // Reasoning collapse state and helpers
616
- const expandedReasoning = ref(new Set())
617
- const isReasoningExpanded = (id) => expandedReasoning.value.has(id)
618
- const toggleReasoning = (id) => {
619
- const s = new Set(expandedReasoning.value)
620
- if (s.has(id)) {
621
- s.delete(id)
622
- } else {
623
- s.add(id)
624
- }
625
- expandedReasoning.value = s
626
- }
627
- const formatReasoning = (r) => typeof r === 'string' ? r : JSON.stringify(r, null, 2)
628
-
629
- // Copy message content to clipboard
630
- const copyMessageContent = async (message) => {
631
- try {
632
- copying.value = message
633
- await navigator.clipboard.writeText(message.content)
634
- // Could add a toast notification here if desired
635
- } catch (err) {
636
- console.error('Failed to copy message content:', err)
637
- // Fallback for older browsers
638
- const textArea = document.createElement('textarea')
639
- textArea.value = message.content
640
- document.body.appendChild(textArea)
641
- textArea.select()
642
- document.execCommand('copy')
643
- document.body.removeChild(textArea)
644
- }
645
- setTimeout(() => { copying.value = null }, 2000)
646
- }
647
-
648
- // Redo a user message (clear all messages after it and re-run)
649
- const redoMessage = async (message) => {
650
- if (!currentThread.value || message.role !== 'user') return
651
-
652
- try {
653
- const threadId = currentThread.value.id
654
-
655
- // Clear all messages after this one
656
- await threads.redoMessageFromThread(threadId, message.id)
657
-
658
- // Extract the actual message content (remove media indicators if present)
659
- let messageContent = message.content
660
- // Remove media indicators like [🖼️ filename] or [🔉 filename] or [📎 filename]
661
- messageContent = messageContent.replace(/\n\n\[[🖼️🔉📎] [^\]]+\]$/, '')
662
-
663
- // Set the message text in the chat prompt
664
- chatPrompt.messageText.value = messageContent
665
-
666
- // Clear any attached files since we're re-running
667
- chatPrompt.attachedFiles.value = []
668
-
669
- // Trigger send by simulating the send action
670
- // We'll use a small delay to ensure the UI updates
671
- await nextTick()
672
-
673
- // Find the send button and click it
674
- const sendButton = document.querySelector('button[title*="Send"]')
675
- if (sendButton && !sendButton.disabled) {
676
- sendButton.click()
677
- }
678
- } catch (error) {
679
- console.error('Failed to redo message:', error)
680
- errorStatus.value = {
681
- errorCode: 'Error',
682
- message: 'Failed to redo message: ' + error.message,
683
- stackTrace: null
684
- }
685
- }
686
- }
687
-
688
- // Edit a user message
689
- const editMessage = (message) => {
690
- if (!currentThread.value || message.role !== 'user') return
691
-
692
- editingMessage.value = message
693
- editingMessageId.value = message.id
694
- // Extract the actual message content (remove media indicators if present)
695
- let messageContent = message.content
696
- messageContent = messageContent.replace(/\n\n\[[🖼️🔉📎] [^\]]+\]$/, '')
697
- editingMessageContent.value = messageContent
698
- }
699
-
700
- // Save edited message
701
- const saveEditedMessage = async () => {
702
- if (!currentThread.value || !editingMessage.value || !editingMessageContent.value.trim()) return
703
-
704
- try {
705
- const threadId = currentThread.value.id
706
- const messageId = editingMessage.value.id
707
- const updatedContent = editingMessageContent.value
708
-
709
- // Update the message content
710
- editingMessage.value.content = updatedContent
711
- await threads.updateMessageInThread(threadId, messageId, { content: updatedContent })
712
-
713
- // Clear editing state
714
- editingMessageId.value = null
715
- editingMessageContent.value = ''
716
- editingMessage.value = null
717
-
718
- // Now redo the message (clear all responses after it and re-run)
719
- await nextTick()
720
- await threads.redoMessageFromThread(threadId, messageId)
721
-
722
- // Set the message text in the chat prompt
723
- chatPrompt.messageText.value = updatedContent
724
-
725
- // Clear any attached files since we're re-running
726
- chatPrompt.attachedFiles.value = []
727
-
728
- // Trigger send by simulating the send action
729
- await nextTick()
730
-
731
- // Find the send button and click it
732
- const sendButton = document.querySelector('button[title*="Send"]')
733
- if (sendButton && !sendButton.disabled) {
734
- sendButton.click()
735
- }
736
- } catch (error) {
737
- console.error('Failed to save edited message:', error)
738
- errorStatus.value = {
739
- errorCode: 'Error',
740
- message: 'Failed to save edited message: ' + error.message,
741
- stackTrace: null
742
- }
743
- }
744
- }
745
-
746
- // Cancel editing
747
- const cancelEdit = () => {
748
- editingMessageId.value = null
749
- editingMessageContent.value = ''
750
- editingMessage.value = null
751
- }
752
-
753
- // Cancel pending request
754
- const cancelRequest = () => {
755
- chatPrompt.cancel()
756
- }
757
-
758
- function tokensTitle(usage) {
759
- let title = []
760
- if (usage.tokens && usage.price) {
761
- const msg = parseFloat(usage.price) > 0
762
- ? `${usage.tokens} tokens @ ${usage.price} = ${tokenCost(usage.price, usage.tokens)}`
763
- : `${usage.tokens} tokens`
764
- const duration = usage.duration ? ` in ${usage.duration}ms` : ''
765
- title.push(msg + duration)
766
- }
767
- return title.join('\n')
768
- }
769
- const numFmt = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', minimumFractionDigits: 6 })
770
- function tokenCost(price, tokens) {
771
- if (!price || !tokens) return ''
772
- return numFmt.format(parseFloat(price) * tokens)
773
- }
774
-
775
- onMounted(() => {
776
- setTimeout(addCopyButtons, 1)
777
- })
778
-
779
- return {
780
- config,
781
- models,
782
- threads,
783
- prompts,
784
- isGenerating,
785
- customPromptValue,
786
- currentThread,
787
- selectedModel,
788
- selectedPrompt,
789
- currentSystemPrompt,
790
- showSystemPrompt,
791
- messagesContainer,
792
- errorStatus,
793
- copying,
794
- editingMessageId,
795
- editingMessageContent,
796
- editingMessage,
797
- formatTime,
798
- renderMarkdown,
799
- isReasoningExpanded,
800
- toggleReasoning,
801
- formatReasoning,
802
- copyMessageContent,
803
- redoMessage,
804
- editMessage,
805
- saveEditedMessage,
806
- cancelEdit,
807
- cancelRequest,
808
- configUpdated,
809
- exportThreads,
810
- exportRequests,
811
- isExporting,
812
- triggerImport,
813
- handleFileImport,
814
- isImporting,
815
- fileInput,
816
- tokensTitle,
817
- humanifyMs,
818
- humanifyNumber,
819
- formatCost,
820
- statsTitle,
821
- }
822
- }
823
- }