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.
- llms/__pycache__/main.cpython-314.pyc +0 -0
- llms/{ui/modules/analytics.mjs → extensions/analytics/ui/index.mjs} +55 -164
- llms/extensions/app/__init__.py +519 -0
- llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
- llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
- llms/extensions/app/db.py +641 -0
- llms/extensions/app/db_manager.py +195 -0
- llms/extensions/app/requests.json +9073 -0
- llms/extensions/app/threads.json +15290 -0
- llms/{ui/modules/threads → extensions/app/ui}/Recents.mjs +82 -55
- llms/{ui/modules/threads → extensions/app/ui}/index.mjs +83 -20
- llms/extensions/app/ui/threadStore.mjs +407 -0
- llms/extensions/core_tools/__init__.py +598 -0
- llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +9884 -0
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
- llms/extensions/core_tools/ui/index.mjs +650 -0
- llms/extensions/gallery/__init__.py +61 -0
- llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
- llms/extensions/gallery/db.py +298 -0
- llms/extensions/gallery/ui/index.mjs +481 -0
- llms/extensions/katex/__init__.py +6 -0
- llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/katex/ui/README.md +125 -0
- llms/extensions/katex/ui/contrib/auto-render.js +338 -0
- llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
- llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
- llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
- llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
- llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
- llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
- llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
- llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- llms/extensions/katex/ui/index.mjs +92 -0
- llms/extensions/katex/ui/katex-swap.css +1230 -0
- llms/extensions/katex/ui/katex-swap.min.css +1 -0
- llms/extensions/katex/ui/katex.css +1230 -0
- llms/extensions/katex/ui/katex.js +19080 -0
- llms/extensions/katex/ui/katex.min.css +1 -0
- llms/extensions/katex/ui/katex.min.js +1 -0
- llms/extensions/katex/ui/katex.min.mjs +1 -0
- llms/extensions/katex/ui/katex.mjs +18547 -0
- llms/extensions/providers/__init__.py +18 -0
- llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
- llms/{providers → extensions/providers}/__pycache__/nvidia.cpython-314.pyc +0 -0
- llms/{providers → extensions/providers}/__pycache__/openai.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
- llms/{providers → extensions/providers}/anthropic.py +45 -5
- llms/{providers → extensions/providers}/chutes.py +21 -18
- llms/{providers → extensions/providers}/google.py +99 -27
- llms/{providers → extensions/providers}/nvidia.py +6 -8
- llms/{providers → extensions/providers}/openai.py +3 -6
- llms/{providers → extensions/providers}/openrouter.py +12 -10
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/system_prompts/ui/index.mjs +285 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +5 -0
- llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/{ui/modules/tools.mjs → extensions/tools/ui/index.mjs} +12 -10
- llms/index.html +26 -38
- llms/llms.json +20 -1
- llms/main.py +845 -245
- llms/providers-extra.json +0 -32
- llms/ui/App.mjs +18 -20
- llms/ui/ai.mjs +38 -15
- llms/ui/app.css +1440 -59
- llms/ui/ctx.mjs +154 -18
- llms/ui/index.mjs +17 -14
- llms/ui/lib/vue.min.mjs +10 -9
- llms/ui/lib/vue.mjs +1796 -1635
- llms/ui/markdown.mjs +4 -2
- llms/ui/modules/chat/ChatBody.mjs +101 -334
- llms/ui/modules/chat/HomeTools.mjs +12 -0
- llms/ui/modules/chat/SettingsDialog.mjs +1 -1
- llms/ui/modules/chat/index.mjs +351 -314
- llms/ui/modules/layout.mjs +2 -26
- llms/ui/modules/model-selector.mjs +3 -3
- llms/ui/tailwind.input.css +35 -1
- llms/ui/utils.mjs +33 -3
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/METADATA +1 -1
- llms_py-3.0.0b8.dist-info/RECORD +198 -0
- llms/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
- llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
- llms/providers/__pycache__/google.cpython-314.pyc +0 -0
- llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
- llms/ui/modules/threads/threadStore.mjs +0 -586
- llms_py-3.0.0b6.dist-info/RECORD +0 -66
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/WHEEL +0 -0
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.0b6.dist-info → llms_py-3.0.0b8.dist-info}/top_level.txt +0 -0
llms/ui/modules/chat/index.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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="
|
|
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="
|
|
457
|
+
:disabled="$threads.watchingThread || !model"
|
|
244
458
|
></textarea>
|
|
245
|
-
<button v-if="
|
|
459
|
+
<button v-if="!$threads.watchingThread" title="Send (Enter)" type="button"
|
|
246
460
|
@click="sendMessage"
|
|
247
|
-
:disabled="!messageText.trim() ||
|
|
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="
|
|
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="
|
|
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
|
-
|
|
309
|
-
} =
|
|
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
|
-
|
|
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
|
|
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
|
|
468
|
-
if (
|
|
654
|
+
if (!messageText.value?.trim() && !hasImage() && !hasAudio() && !hasFile()) return
|
|
655
|
+
if (ctx.threads.isWatchingThread.value || !props.model) return
|
|
469
656
|
|
|
470
|
-
|
|
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
|
-
|
|
481
|
-
content.push({ type: 'text', text: text })
|
|
665
|
+
let thread
|
|
482
666
|
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
725
|
-
|
|
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({
|