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