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
@@ -1,9 +1,10 @@
1
1
 
2
2
  import { ref, computed, watch, nextTick, inject } from 'vue'
3
3
  import { useRouter } from 'vue-router'
4
- import { $$, createElement, lastRightPart } from "@servicestack/client"
4
+ import { $$, createElement, lastRightPart, ApiResult, createErrorStatus, pick } from "@servicestack/client"
5
5
  import SettingsDialog, { useSettings } from './SettingsDialog.mjs'
6
6
  import ChatBody from './ChatBody.mjs'
7
+ import HomeTools from './HomeTools.mjs'
7
8
  import { AppContext } from '../../ctx.mjs'
8
9
 
9
10
  const imageExts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
@@ -55,6 +56,11 @@ const imageAspectRatios = {
55
56
  '1344×768': '16:9',
56
57
  '1536×672': '21:9',
57
58
  }
59
+ // Reverse lookup
60
+ const imageRatioSizes = Object.entries(imageAspectRatios).reduce((acc, [key, value]) => {
61
+ acc[value] = key
62
+ return acc
63
+ }, {})
58
64
 
59
65
  const svg = {
60
66
  clipboard: `<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path d="M8 5H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1M8 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M8 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m0 0h2a2 2 0 0 1 2 2v3m2 4H10m0 0l3-3m-3 3l3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>`,
@@ -112,33 +118,17 @@ export function addCopyButtons() {
112
118
  export function useChatPrompt(ctx) {
113
119
  const messageText = ref('')
114
120
  const attachedFiles = ref([])
115
- const isGenerating = ref(false)
116
- const errorStatus = ref(null)
117
- const abortController = ref(null)
118
121
  const hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
119
122
  const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
120
123
  const hasFile = () => attachedFiles.value.length > 0
121
- // const hasText = () => !hasImage() && !hasAudio() && !hasFile()
122
124
 
123
- const editingMessageId = ref(null)
125
+ const editingMessage = ref(null)
124
126
 
125
127
  function reset() {
126
128
  // Ensure initial state is ready to accept input
127
- isGenerating.value = false
128
129
  attachedFiles.value = []
129
130
  messageText.value = ''
130
- abortController.value = null
131
- editingMessageId.value = null
132
- }
133
-
134
- function cancel() {
135
- // Cancel the pending request
136
- if (abortController.value) {
137
- abortController.value.abort()
138
- }
139
- // Reset UI state
140
- isGenerating.value = false
141
- abortController.value = null
131
+ editingMessage.value = null
142
132
  }
143
133
 
144
134
  const settings = useSettings()
@@ -149,7 +139,15 @@ export function useChatPrompt(ctx) {
149
139
 
150
140
  function getSelectedModel() {
151
141
  const candidates = [ctx.state.selectedModel, ctx.state.config.defaults.text.model]
152
- return candidates.map(name => name && getModel(name)).find(x => !!x)
142
+ const ret = candidates.map(name => name && getModel(name)).find(x => !!x)
143
+ if (!ret) {
144
+ // Try to find a model in the latest threads
145
+ for (const thread in ctx.threads.threads) {
146
+ const model = thread.model && getModel(thread.model)
147
+ if (model) return model
148
+ }
149
+ }
150
+ return ret
153
151
  }
154
152
 
155
153
  function setSelectedModel(model) {
@@ -165,28 +163,244 @@ export function useChatPrompt(ctx) {
165
163
  return getModel(model)?.provider
166
164
  }
167
165
 
166
+ const canGenerateImage = model => {
167
+ return model?.modalities?.output?.includes('image')
168
+ }
169
+ const canGenerateAudio = model => {
170
+ return model?.modalities?.output?.includes('audio')
171
+ }
172
+
173
+ function applySettings(request) {
174
+ settings.applySettings(request)
175
+ }
176
+
177
+ function createContent({ text, files }) {
178
+ let content = []
179
+
180
+ // Add Text Block
181
+ if (text) {
182
+ content.push({ type: 'text', text })
183
+ }
184
+
185
+ // Add Attachment Blocks
186
+ if (Array.isArray(files)) {
187
+ for (const f of files) {
188
+ const ext = lastRightPart(f.name, '.')
189
+ if (imageExts.includes(ext)) {
190
+ content.push({ type: 'image_url', image_url: { url: f.url } })
191
+ } else if (audioExts.includes(ext)) {
192
+ content.push({ type: 'input_audio', input_audio: { data: f.url, format: ext } })
193
+ } else {
194
+ content.push({ type: 'file', file: { file_data: f.url, filename: f.name } })
195
+ }
196
+ }
197
+ }
198
+ return content
199
+ }
200
+
201
+ function createRequest({ model, text, files, systemPrompt, aspectRatio }) {
202
+ // Construct API Request from History
203
+ const request = {
204
+ model: model.name,
205
+ messages: [],
206
+ metadata: {}
207
+ }
208
+
209
+ // Apply user settings
210
+ applySettings(request)
211
+
212
+ if (systemPrompt) {
213
+ request.messages = request.messages.filter(m => m.role !== 'system')
214
+ request.messages.unshift({
215
+ role: 'system',
216
+ content: systemPrompt
217
+ })
218
+ }
219
+
220
+ if (canGenerateImage(model)) {
221
+ request.image_config = {
222
+ aspect_ratio: aspectRatio || imageAspectRatios[ctx.state.selectedAspectRatio] || '1:1'
223
+ }
224
+ request.modalities = ["image", "text"]
225
+ }
226
+ else if (canGenerateAudio(model)) {
227
+ request.modalities = ["audio", "text"]
228
+ }
229
+
230
+ if (text) {
231
+ const content = createContent({ text, files })
232
+ request.messages.push({
233
+ role: 'user',
234
+ content
235
+ })
236
+ }
237
+
238
+ return request
239
+ }
240
+
241
+ async function completion({ request, model, thread, controller, store }) {
242
+ try {
243
+ let error
244
+ if (!model) {
245
+ if (request.model) {
246
+ model = getModel(request.model)
247
+ } else {
248
+ model = getModel(request.model) ?? getSelectedModel()
249
+ }
250
+ }
251
+
252
+ if (!model) {
253
+ return new ApiResult({
254
+ error: createErrorStatus(`Model ${request.model || ''} not found`, 'NotFound')
255
+ })
256
+ }
257
+
258
+ if (!request.messages) request.messages = []
259
+ if (!request.metadata) request.metadata = {}
260
+
261
+ if (store && !thread) {
262
+ const title = getTextContent(request) || 'New Chat'
263
+ thread = await ctx.threads.startNewThread({ title, model })
264
+ }
265
+
266
+ const threadId = thread?.id || ctx.threads.generateThreadId()
267
+
268
+ const ctxRequest = {
269
+ request,
270
+ thread,
271
+ }
272
+ ctx.chatRequestFilters.forEach(f => f(ctxRequest))
273
+
274
+ console.debug('completion.request', request)
275
+
276
+ // Send to API
277
+ const startTime = Date.now()
278
+ const res = await ctx.post('/v1/chat/completions', {
279
+ body: JSON.stringify(request),
280
+ signal: controller?.signal
281
+ })
282
+
283
+ let response = null
284
+ if (!res.ok) {
285
+ error = createErrorStatus('', `HTTP ${res.status} ${res.statusText}`)
286
+ let errorBody = null
287
+ try {
288
+ errorBody = await res.text()
289
+ if (errorBody) {
290
+ // Try to parse as JSON for better formatting
291
+ try {
292
+ const errorJson = JSON.parse(errorBody)
293
+ const status = errorJson?.responseStatus
294
+ if (status) {
295
+ error.errorCode += ` ${status.errorCode}`
296
+ error.message = status.message
297
+ error.stackTrace = status.stackTrace
298
+ } else {
299
+ error.stackTrace = JSON.stringify(errorJson, null, 2)
300
+ }
301
+ } catch (e) {
302
+ }
303
+ }
304
+ } catch (e) {
305
+ // If we can't read the response body, just use the status
306
+ }
307
+ } else {
308
+ try {
309
+ response = await res.json()
310
+ const ctxResponse = {
311
+ response,
312
+ thread,
313
+ }
314
+ ctx.chatResponseFilters.forEach(f => f(ctxResponse))
315
+ console.debug('completion.response', JSON.stringify(response, null, 2))
316
+ } catch (e) {
317
+ error = createErrorStatus(e.message)
318
+ }
319
+ }
320
+
321
+ if (response?.error) {
322
+ error ??= createErrorStatus()
323
+ error.message = response.error
324
+ }
325
+
326
+ if (error) {
327
+ ctx.chatErrorFilters.forEach(f => f(error))
328
+ return new ApiResult({ error })
329
+ }
330
+
331
+ if (!error) {
332
+ // Add tool history messages if any
333
+ if (response.tool_history && Array.isArray(response.tool_history)) {
334
+ for (const msg of response.tool_history) {
335
+ if (msg.role === 'assistant') {
336
+ msg.model = model.name // tag with model
337
+ }
338
+ }
339
+ }
340
+
341
+ // Add assistant response (save entire message including reasoning)
342
+ const assistantMessage = response.choices?.[0]?.message
343
+
344
+ const usage = response.usage
345
+ if (usage) {
346
+ if (response.metadata?.pricing) {
347
+ const [input, output] = response.metadata.pricing.split('/')
348
+ usage.duration = response.metadata.duration ?? (Date.now() - startTime)
349
+ usage.input = input
350
+ usage.output = output
351
+ usage.tokens = usage.completion_tokens
352
+ usage.price = usage.output
353
+ usage.cost = ctx.fmt.tokenCost(usage.prompt_tokens / 1_000_000 * parseFloat(input) + usage.completion_tokens / 1_000_000 * parseFloat(output))
354
+ }
355
+ }
356
+
357
+ nextTick(addCopyButtons)
358
+
359
+ return new ApiResult({ response })
360
+ }
361
+ } catch (e) {
362
+ console.log('completion.error', e)
363
+ return new ApiResult({ error: createErrorStatus(e.message, 'ChatFailed') })
364
+ }
365
+ }
366
+ function getTextContent(chat) {
367
+ const textMessage = chat.messages.find(m =>
368
+ m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
369
+ return textMessage?.content.find(c => c.type === 'text')?.text || ''
370
+ }
371
+ function getAnswer(response) {
372
+ const textMessage = response.choices?.[0]?.message
373
+ return textMessage?.content || ''
374
+ }
375
+ function selectAspectRatio(ratio) {
376
+ const selectedAspectRatio = imageRatioSizes[ratio] || '1024×1024'
377
+ console.log(`selectAspectRatio(${ratio})`, selectedAspectRatio)
378
+ ctx.setState({ selectedAspectRatio })
379
+ }
380
+
168
381
  return {
382
+ completion,
383
+ createContent,
384
+ createRequest,
385
+ applySettings,
169
386
  messageText,
170
387
  attachedFiles,
171
- errorStatus,
172
- isGenerating,
173
- abortController,
174
- editingMessageId,
175
- get generating() {
176
- return isGenerating.value
177
- },
388
+ editingMessage,
178
389
  hasImage,
179
390
  hasAudio,
180
391
  hasFile,
181
- // hasText,
182
392
  reset,
183
- cancel,
184
393
  settings,
185
394
  addCopyButtons,
186
395
  getModel,
187
396
  getSelectedModel,
188
397
  setSelectedModel,
189
398
  getProviderForModel,
399
+ canGenerateImage,
400
+ canGenerateAudio,
401
+ getTextContent,
402
+ getAnswer,
403
+ selectAspectRatio,
190
404
  }
191
405
  }
192
406
 
@@ -200,7 +414,7 @@ const ChatPrompt = {
200
414
  <div>
201
415
  <button type="button"
202
416
  @click="triggerFilePicker"
203
- :disabled="isGenerating || !model"
417
+ :disabled="$threads.isWatchingThread.value || !model"
204
418
  class="size-8 flex items-center justify-center rounded-md border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:text-gray-400 disabled:cursor-not-allowed"
205
419
  title="Attach image or audio">
206
420
  <svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
@@ -214,7 +428,7 @@ const ChatPrompt = {
214
428
  </div>
215
429
  <div>
216
430
  <button type="button" title="Settings" @click="showSettings = true"
217
- :disabled="isGenerating || !model"
431
+ :disabled="$threads.watchingThread || !model"
218
432
  class="size-8 flex items-center justify-center rounded-md border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:text-gray-400 disabled:cursor-not-allowed">
219
433
  <svg class="size-4 text-gray-600 dark:text-gray-400 disabled:text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><path d="M40,88H73a32,32,0,0,0,62,0h81a8,8,0,0,0,0-16H135a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16Zm64-24A16,16,0,1,1,88,80,16,16,0,0,1,104,64ZM216,168H199a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16h97a32,32,0,0,0,62,0h17a8,8,0,0,0,0-16Zm-48,24a16,16,0,1,1,16-16A16,16,0,0,1,168,192Z"></path></svg>
220
434
  </button>
@@ -240,16 +454,16 @@ const ChatPrompt = {
240
454
  ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-500'
241
455
  : 'border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500'
242
456
  ]"
243
- :disabled="isGenerating || !model"
457
+ :disabled="$threads.watchingThread || !model"
244
458
  ></textarea>
245
- <button v-if="!isGenerating" title="Send (Enter)" type="button"
459
+ <button v-if="!$threads.watchingThread" title="Send (Enter)" type="button"
246
460
  @click="sendMessage"
247
- :disabled="!messageText.trim() || isGenerating || !model"
461
+ :disabled="!messageText.trim() || $threads.watchingThread || !model"
248
462
  class="absolute bottom-2 right-2 size-8 flex items-center justify-center rounded-md border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:text-gray-400 disabled:cursor-not-allowed disabled:border-gray-200 dark:disabled:border-gray-700 transition-colors">
249
463
  <svg class="size-5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="20" stroke-dashoffset="20" d="M12 21l0 -17.5"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.2s" values="20;0"/></path><path stroke-dasharray="12" stroke-dashoffset="12" d="M12 3l7 7M12 3l-7 7"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.2s" dur="0.2s" values="12;0"/></path></g></svg>
250
464
  </button>
251
465
  <button v-else title="Cancel request" type="button"
252
- @click="cancelRequest"
466
+ @click="$threads.cancelThread()"
253
467
  class="absolute bottom-2 right-2 size-8 flex items-center justify-center rounded-md border border-red-300 dark:border-red-600 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors">
254
468
  <svg class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
255
469
  <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
@@ -260,7 +474,7 @@ const ChatPrompt = {
260
474
  <!-- Attachments & Image Options -->
261
475
  <div class="mt-2 flex justify-between items-start gap-2">
262
476
  <div class="flex flex-wrap gap-2">
263
- <div v-for="(f,i) in attachedFiles" :key="i" class="flex items-center gap-2 px-2 py-1 rounded-md border border-gray-300 dark:border-gray-600 text-xs text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800">
477
+ <div v-for="(f,i) in $chat.attachedFiles.value" :key="i" class="flex items-center gap-2 px-2 py-1 rounded-md border border-gray-300 dark:border-gray-600 text-xs text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800">
264
478
  <span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
265
479
  <button type="button" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" @click="removeAttachment(i)" title="Remove Attachment">
266
480
  <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
@@ -269,7 +483,7 @@ const ChatPrompt = {
269
483
  </div>
270
484
 
271
485
  <!-- Image Aspect Ratio Selector -->
272
- <div v-if="canGenerateImages" class="min-w-[120px]">
486
+ <div v-if="$chat.canGenerateImage(model)" class="min-w-[120px]">
273
487
  <select name="aspect_ratio" v-model="$state.selectedAspectRatio"
274
488
  class="block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-xs text-gray-700 dark:text-gray-300 pl-2 pr-6 py-1 focus:ring-blue-500 focus:border-blue-500">
275
489
  <option v-for="(ratio, size) in imageAspectRatios" :key="size" :value="size">
@@ -282,6 +496,7 @@ const ChatPrompt = {
282
496
  <div v-if="!model" class="mt-2 text-sm text-red-600 dark:text-red-400">
283
497
  Please select a model
284
498
  </div>
499
+ </div>
285
500
  </div>
286
501
  </div>
287
502
  `,
@@ -294,32 +509,17 @@ const ChatPrompt = {
294
509
  setup(props) {
295
510
  const ctx = inject('ctx')
296
511
  const config = ctx.state.config
297
- const ai = ctx.ai
298
- const router = useRouter()
299
- const chatPrompt = ctx.chat
300
512
  const {
301
513
  messageText,
302
- attachedFiles,
303
- isGenerating,
304
- errorStatus,
305
514
  hasImage,
306
515
  hasAudio,
307
516
  hasFile,
308
- editingMessageId,
309
- } = chatPrompt
310
- const threads = ctx.threads
311
- const {
312
- currentThread,
313
- } = ctx.threads
517
+ getTextContent,
518
+ } = ctx.chat
314
519
 
315
520
  const fileInput = ref(null)
316
521
  const refMessage = ref(null)
317
522
  const showSettings = ref(false)
318
- const { applySettings } = ctx.chat.settings
319
-
320
- const canGenerateImages = computed(() => {
321
- return props.model?.modalities?.output?.includes('image')
322
- })
323
523
 
324
524
  // File attachments (+) handlers
325
525
  const triggerFilePicker = () => {
@@ -339,7 +539,7 @@ const ChatPrompt = {
339
539
  type: f.type,
340
540
  width: response.width,
341
541
  height: response.height,
342
- threadId: currentThread.value?.id,
542
+ threadId: ctx.threads.currentThread.value?.id,
343
543
  created: Date.now()
344
544
  }
345
545
 
@@ -348,22 +548,21 @@ const ChatPrompt = {
348
548
  file: f // Keep original file for preview/fallback if needed
349
549
  }
350
550
  } catch (error) {
351
- console.error('File upload failed:', error)
352
- errorStatus.value = {
551
+ ctx.setError({
353
552
  errorCode: 'Upload Failed',
354
553
  message: `Failed to upload ${f.name}: ${error.message}`
355
- }
554
+ })
356
555
  return null
357
556
  }
358
557
  }))
359
558
 
360
- attachedFiles.value.push(...uploadedFiles.filter(f => f))
559
+ ctx.chat.attachedFiles.value.push(...uploadedFiles.filter(f => f))
361
560
  }
362
561
 
363
562
  // allow re-selecting the same file
364
563
  if (fileInput.value) fileInput.value.value = ''
365
564
 
366
- if (!messageText.value.trim()) {
565
+ if (!messageText.value?.trim()) {
367
566
  if (hasImage()) {
368
567
  messageText.value = getTextContent(config.defaults.image)
369
568
  } else if (hasAudio()) {
@@ -374,7 +573,7 @@ const ChatPrompt = {
374
573
  }
375
574
  }
376
575
  const removeAttachment = (i) => {
377
- attachedFiles.value.splice(i, 1)
576
+ ctx.chat.attachedFiles.value.splice(i, 1)
378
577
  }
379
578
 
380
579
  // Handle paste events for clipboard images, audio, and files
@@ -450,279 +649,112 @@ const ChatPrompt = {
450
649
  }
451
650
  }
452
651
 
453
- function getTextContent(chat) {
454
- const textMessage = chat.messages.find(m =>
455
- m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
456
- return textMessage?.content.find(c => c.type === 'text')?.text || ''
457
- }
458
-
459
- function toModelInfo(model) {
460
- if (!model) return undefined
461
- const { id, name, provider, cost, modalities } = model
462
- return ctx.utils.deepClone({ id, name, provider, cost, modalities })
463
- }
464
-
465
652
  // Send message
466
653
  const sendMessage = async () => {
467
- if (!messageText.value.trim() && !hasImage() && !hasAudio() && !hasFile()) return
468
- if (isGenerating.value || !props.model) return
654
+ if (!messageText.value?.trim() && !hasImage() && !hasAudio() && !hasFile()) return
655
+ if (ctx.threads.isWatchingThread.value || !props.model) return
469
656
 
470
- // Clear any existing error message
471
- errorStatus.value = null
657
+ ctx.clearError()
472
658
 
473
659
  // 1. Construct Structured Content (Text + Attachments)
474
660
  let text = messageText.value.trim()
475
- let content = []
476
-
477
661
 
478
662
  messageText.value = ''
663
+ let content = ctx.chat.createContent({ text, files: ctx.chat.attachedFiles.value })
479
664
 
480
- // Add Text Block
481
- content.push({ type: 'text', text: text })
665
+ let thread
482
666
 
483
- // Add Attachment Blocks
484
- for (const f of attachedFiles.value) {
485
- const ext = lastRightPart(f.name, '.')
486
- if (imageExts.includes(ext)) {
487
- content.push({ type: 'image_url', image_url: { url: f.url } })
488
- } else if (audioExts.includes(ext)) {
489
- content.push({ type: 'input_audio', input_audio: { data: f.url, format: ext } })
490
- } else {
491
- content.push({ type: 'file', file: { file_data: f.url, filename: f.name } })
492
- }
667
+ // Create thread if none exists
668
+ if (!ctx.threads.currentThread.value) {
669
+ thread = await ctx.threads.startNewThread({ model: props.model })
670
+ } else {
671
+ thread = ctx.threads.currentThread.value
493
672
  }
494
673
 
495
- // Create AbortController for this request
496
- const controller = new AbortController()
497
- chatPrompt.abortController.value = controller
498
- const model = props.model.name
499
-
500
- try {
501
- let threadId
674
+ let threadId = thread.id
675
+ let messages = thread.messages || []
676
+ if (!threadId) {
677
+ console.error('No thread ID found', thread, ctx.threads.currentThread.value)
678
+ return
679
+ }
502
680
 
503
- // Create thread if none exists
504
- if (!currentThread.value) {
505
- const newThread = await threads.createThread({
506
- title: 'New Chat',
507
- model,
508
- info: toModelInfo(props.model),
509
- })
510
- threadId = newThread.id
511
- // Navigate to the new thread URL
512
- router.push(`${ai.base}/c/${newThread.id}`)
513
- } else {
514
- threadId = currentThread.value.id
515
- // Update the existing thread's model to match current selection
516
- await threads.updateThread(threadId, {
517
- model,
518
- info: toModelInfo(props.model),
519
- })
681
+ // Handle Editing / Redo Logic
682
+ const editingMessage = ctx.chat.editingMessage.value
683
+ if (editingMessage) {
684
+ let messageIndex = messages.findIndex(m => m.timestamp === editingMessage)
685
+ if (messageIndex == -1) {
686
+ messageIndex = messages.findLastIndex(m => m.role === 'user')
520
687
  }
688
+ console.log('Editing message', editingMessage, messageIndex, messages)
521
689
 
522
- // Get the thread to check for duplicates
523
- let thread = await threads.getThread(threadId)
524
-
525
- // Handle Editing / Redo Logic
526
- if (editingMessageId.value) {
527
- // Check if message still exists
528
- const messageExists = thread.messages.find(m => m.id === editingMessageId.value)
529
- if (messageExists) {
530
- // Update the message content
531
- await threads.updateMessageInThread(threadId, editingMessageId.value, { content: content })
532
- // Redo from this message (clears subsequent)
533
- await threads.redoMessageFromThread(threadId, editingMessageId.value)
534
-
535
- // Clear editing state
536
- editingMessageId.value = null
537
- } else {
538
- // Fallback if message was deleted
539
- editingMessageId.value = null
540
- }
541
- // Refresh thread state
542
- thread = await threads.getThread(threadId)
690
+ if (messageIndex >= 0) {
691
+ messages[messageIndex].content = content
692
+ // Truncate messages to only include up to the edited message
693
+ messages.length = messageIndex + 1
543
694
  } else {
544
- // Regular Send Logic
545
- const lastMessage = thread.messages[thread.messages.length - 1]
546
-
547
- // Check duplicate based on text content extracted from potential array
548
- const getLastText = (msgContent) => {
549
- if (typeof msgContent === 'string') return msgContent
550
- if (Array.isArray(msgContent)) return msgContent.find(c => c.type === 'text')?.text || ''
551
- return ''
552
- }
553
- const newText = text // content[0].text
554
- const lastText = lastMessage && lastMessage.role === 'user' ? getLastText(lastMessage.content) : null
555
-
556
- const isDuplicate = lastText === newText
557
-
558
- // Add user message only if it's not a duplicate
559
- // Note: We are saving the FULL STRUCTURED CONTENT array here
560
- if (!isDuplicate) {
561
- await threads.addMessageToThread(threadId, {
562
- role: 'user',
563
- content: content,
564
- model: props.model.name,
565
- })
566
- // Reload thread after adding message
567
- thread = await threads.getThread(threadId)
568
- }
569
- }
570
-
571
- isGenerating.value = true
572
-
573
- // Construct API Request from History
574
- const request = {
575
- model,
576
- messages: [],
577
- metadata: {}
578
- }
579
-
580
- // Add History
581
- thread.messages.forEach(m => {
582
- request.messages.push({
583
- role: m.role,
584
- content: m.content
695
+ messages.push({
696
+ timestamp: new Date().valueOf(),
697
+ role: 'user',
698
+ content,
585
699
  })
586
- })
587
-
588
- // Apply user settings
589
- applySettings(request)
590
-
591
- if (canGenerateImages.value) {
592
- request.image_config = {
593
- aspect_ratio: imageAspectRatios[ctx.state.selectedAspectRatio] || '1:1'
594
- }
595
- request.modalities = ["image", "text"]
596
700
  }
597
-
598
- request.metadata.threadId = threadId
599
-
600
- const ctxRequest = {
601
- request,
602
- thread,
701
+ } else {
702
+ // Regular Send Logic
703
+ const lastMessage = messages[messages.length - 1]
704
+
705
+ // Check duplicate based on text content extracted from potential array
706
+ const getLastText = (msgContent) => {
707
+ if (typeof msgContent === 'string') return msgContent
708
+ if (Array.isArray(msgContent)) return msgContent.find(c => c.type === 'text')?.text || ''
709
+ return ''
603
710
  }
604
- ctx.chatRequestFilters.forEach(f => f(ctxRequest))
605
-
606
- console.debug('chatRequest', request)
607
-
608
- // Send to API
609
- const startTime = Date.now()
610
- const res = await ai.post('/v1/chat/completions', {
611
- body: JSON.stringify(request),
612
- signal: controller.signal
613
- })
614
-
615
- let response = null
616
- if (!res.ok) {
617
- errorStatus.value = {
618
- errorCode: `HTTP ${res.status} ${res.statusText}`,
619
- message: null,
620
- stackTrace: null
621
- }
622
- let errorBody = null
623
- try {
624
- errorBody = await res.text()
625
- if (errorBody) {
626
- // Try to parse as JSON for better formatting
627
- try {
628
- const errorJson = JSON.parse(errorBody)
629
- const status = errorJson?.responseStatus
630
- if (status) {
631
- errorStatus.value.errorCode += ` ${status.errorCode}`
632
- errorStatus.value.message = status.message
633
- errorStatus.value.stackTrace = status.stackTrace
634
- } else {
635
- errorStatus.value.stackTrace = JSON.stringify(errorJson, null, 2)
636
- }
637
- } catch (e) {
638
- }
639
- }
640
- } catch (e) {
641
- // If we can't read the response body, just use the status
642
- }
643
- } else {
644
- try {
645
- response = await res.json()
646
- const ctxResponse = {
647
- response,
648
- thread,
649
- }
650
- ctx.chatResponseFilters.forEach(f => f(ctxResponse))
651
- console.debug('chatResponse', JSON.stringify(response, null, 2))
652
- } catch (e) {
653
- errorStatus.value = {
654
- errorCode: 'Error',
655
- message: e.message,
656
- stackTrace: null
657
- }
658
- }
659
- }
660
-
661
- if (response?.error) {
662
- errorStatus.value ??= {
663
- errorCode: 'Error',
664
- }
665
- errorStatus.value.message = response.error
711
+ const newText = text // content[0].text
712
+ const lastText = lastMessage && lastMessage.role === 'user' ? getLastText(lastMessage.content) : null
713
+ const isDuplicate = lastText === newText
714
+
715
+ // Add user message only if it's not a duplicate
716
+ // Note: We are saving the FULL STRUCTURED CONTENT array here
717
+ if (!isDuplicate) {
718
+ messages.push({
719
+ timestamp: new Date().valueOf(),
720
+ role: 'user',
721
+ content,
722
+ })
666
723
  }
724
+ }
667
725
 
668
- if (!errorStatus.value) {
669
- // Add tool history messages if any
670
- if (response.tool_history && Array.isArray(response.tool_history)) {
671
- for (const msg of response.tool_history) {
672
- if (msg.role === 'assistant') {
673
- msg.model = props.model.name // tag with model
674
- }
675
- await threads.addMessageToThread(threadId, msg)
676
- }
677
- }
678
-
679
- // Add assistant response (save entire message including reasoning)
680
- const assistantMessage = response.choices?.[0]?.message
681
-
682
- const usage = response.usage
683
- if (usage) {
684
- if (response.metadata?.pricing) {
685
- const [input, output] = response.metadata.pricing.split('/')
686
- usage.duration = response.metadata.duration ?? (Date.now() - startTime)
687
- usage.input = input
688
- usage.output = output
689
- usage.tokens = usage.completion_tokens
690
- usage.price = usage.output
691
- usage.cost = ctx.fmt.tokenCost(usage.prompt_tokens / 1_000_000 * parseFloat(input) + usage.completion_tokens / 1_000_000 * parseFloat(output))
692
- }
693
- await threads.logRequest(threadId, props.model, request, response)
694
- }
695
- assistantMessage.model = props.model.name
696
- await threads.addMessageToThread(threadId, assistantMessage, usage)
697
-
698
- nextTick(addCopyButtons)
726
+ const request = ctx.chat.createRequest({ model: props.model })
727
+
728
+ // Add Thread History
729
+ messages.forEach(m => {
730
+ request.messages.push(m)
731
+ })
732
+
733
+ // Update Thread Title if not set or is default
734
+ if (!thread.title || thread.title === 'New Chat' || request.title === 'New Chat') {
735
+ request.title = text.length > 100
736
+ ? text.slice(0, 100) + '...'
737
+ : text
738
+ console.debug(`changing thread title from '${thread.title}' to '${request.title}'`)
739
+ } else {
740
+ console.debug(`thread title is '${thread.title}'`, request.title)
741
+ }
699
742
 
700
- attachedFiles.value = []
701
- // Error will be cleared when user sends next message (no auto-timeout)
702
- } else {
703
- ctx.chatErrorFilters.forEach(f => f(errorStatus.value))
704
- }
705
- } catch (error) {
706
- // Check if the error is due to abort
707
- if (error.name === 'AbortError') {
708
- console.log('Request was cancelled by user')
709
- // Don't show error for cancelled requests
710
- } else {
711
- // Re-throw other errors to be handled by outer catch
712
- throw error
713
- }
714
- } finally {
715
- isGenerating.value = false
716
- chatPrompt.abortController.value = null
717
- // Restore focus to the textarea
718
- nextTick(() => {
719
- refMessage.value?.focus()
720
- })
743
+ const api = await ctx.threads.queueChat(threadId, request)
744
+ if (api.response) {
745
+ // success
746
+ ctx.chat.editingMessage.value = null
747
+ ctx.chat.attachedFiles.value = []
748
+ thread = api.response
749
+ ctx.threads.replaceThread(thread)
750
+ } else {
751
+ ctx.setError(api.error)
721
752
  }
722
- }
723
753
 
724
- const cancelRequest = () => {
725
- chatPrompt.cancel()
754
+ // Restore focus to the textarea
755
+ nextTick(() => {
756
+ refMessage.value?.focus()
757
+ })
726
758
  }
727
759
 
728
760
  const addNewLine = () => {
@@ -734,9 +766,15 @@ const ChatPrompt = {
734
766
  ctx.setPrefs({ aspectRatio: newValue })
735
767
  })
736
768
 
769
+ watch(() => ctx.layout.path, newValue => {
770
+ if (newValue === '/' || newValue.startsWith('/c/')) {
771
+ nextTick(() => {
772
+ refMessage.value?.focus()
773
+ })
774
+ }
775
+ })
776
+
737
777
  return {
738
- isGenerating,
739
- attachedFiles,
740
778
  messageText,
741
779
  fileInput,
742
780
  refMessage,
@@ -750,10 +788,8 @@ const ChatPrompt = {
750
788
  onDrop,
751
789
  removeAttachment,
752
790
  sendMessage,
753
- cancelRequest,
754
791
  addNewLine,
755
792
  imageAspectRatios,
756
- canGenerateImages,
757
793
  }
758
794
  }
759
795
  }
@@ -766,6 +802,7 @@ export default {
766
802
  SettingsDialog,
767
803
  ChatPrompt,
768
804
  ChatBody,
805
+ HomeTools,
769
806
  Home,
770
807
  })
771
808
  ctx.setGlobals({