llms-py 2.0.34__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llms/__init__.py +3 -1
- llms/__pycache__/__init__.cpython-312.pyc +0 -0
- llms/__pycache__/__init__.cpython-313.pyc +0 -0
- llms/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/__pycache__/__main__.cpython-312.pyc +0 -0
- llms/__pycache__/__main__.cpython-314.pyc +0 -0
- llms/__pycache__/llms.cpython-312.pyc +0 -0
- llms/__pycache__/main.cpython-312.pyc +0 -0
- llms/__pycache__/main.cpython-313.pyc +0 -0
- llms/__pycache__/main.cpython-314.pyc +0 -0
- llms/__pycache__/plugins.cpython-314.pyc +0 -0
- llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +154 -238
- llms/extensions/app/README.md +20 -0
- llms/extensions/app/__init__.py +530 -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 +644 -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 → extensions/app/ui}/Recents.mjs +91 -65
- llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +124 -58
- llms/extensions/app/ui/threadStore.mjs +411 -0
- llms/extensions/core_tools/CALCULATOR.md +32 -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/README.md +61 -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 +482 -0
- llms/extensions/katex/README.md +39 -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/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
- llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
- llms/extensions/providers/anthropic.py +229 -0
- llms/extensions/providers/chutes.py +155 -0
- llms/extensions/providers/google.py +378 -0
- llms/extensions/providers/nvidia.py +105 -0
- llms/extensions/providers/openai.py +156 -0
- llms/extensions/providers/openrouter.py +72 -0
- llms/extensions/system_prompts/README.md +22 -0
- 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 +280 -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/extensions/tools/ui/index.mjs +204 -0
- llms/index.html +35 -77
- llms/llms.json +357 -1186
- llms/main.py +2847 -999
- llms/providers-extra.json +356 -0
- llms/providers.json +1 -0
- llms/ui/App.mjs +151 -60
- llms/ui/ai.mjs +132 -60
- llms/ui/app.css +2173 -161
- llms/ui/ctx.mjs +365 -0
- llms/ui/index.mjs +129 -0
- llms/ui/lib/charts.mjs +9 -13
- llms/ui/lib/servicestack-vue.mjs +3 -3
- llms/ui/lib/vue.min.mjs +10 -9
- llms/ui/lib/vue.mjs +1796 -1635
- llms/ui/markdown.mjs +18 -7
- llms/ui/modules/chat/ChatBody.mjs +691 -0
- llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +9 -9
- llms/ui/modules/chat/index.mjs +828 -0
- llms/ui/modules/layout.mjs +243 -0
- llms/ui/modules/model-selector.mjs +851 -0
- llms/ui/tailwind.input.css +496 -80
- llms/ui/utils.mjs +161 -93
- {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/METADATA +1 -1
- llms_py-3.0.0.dist-info/RECORD +202 -0
- llms/ui/Avatar.mjs +0 -85
- llms/ui/Brand.mjs +0 -52
- llms/ui/ChatPrompt.mjs +0 -590
- llms/ui/Main.mjs +0 -823
- llms/ui/ModelSelector.mjs +0 -78
- llms/ui/OAuthSignIn.mjs +0 -92
- llms/ui/ProviderIcon.mjs +0 -30
- llms/ui/ProviderStatus.mjs +0 -105
- llms/ui/SignIn.mjs +0 -64
- llms/ui/SystemPromptEditor.mjs +0 -31
- llms/ui/SystemPromptSelector.mjs +0 -56
- llms/ui/Welcome.mjs +0 -8
- llms/ui/threadStore.mjs +0 -563
- llms/ui.json +0 -1069
- llms_py-2.0.34.dist-info/RECORD +0 -48
- {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/WHEEL +0 -0
- {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {llms_py-2.0.34.dist-info → llms_py-3.0.0.dist-info}/top_level.txt +0 -0
llms/ui/Brand.mjs
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
template:`
|
|
3
|
-
<div class="flex-shrink-0 pl-2 pr-4 py-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 min-h-16 select-none">
|
|
4
|
-
<div class="flex items-center justify-between">
|
|
5
|
-
<div class="flex items-center space-x-2">
|
|
6
|
-
<button type="button"
|
|
7
|
-
@click="$emit('toggle-sidebar')"
|
|
8
|
-
class="group relative text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
|
9
|
-
title="Collapse sidebar"
|
|
10
|
-
>
|
|
11
|
-
<div class="relative size-5">
|
|
12
|
-
<!-- Default sidebar icon -->
|
|
13
|
-
<svg class="absolute inset-0 group-hover:hidden" 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">
|
|
14
|
-
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
15
|
-
<line x1="9" y1="3" x2="9" y2="21"></line>
|
|
16
|
-
</svg>
|
|
17
|
-
<!-- Hover state: |← icon -->
|
|
18
|
-
<svg class="absolute inset-0 hidden group-hover:block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="m10.071 4.929l1.414 1.414L6.828 11H16v2H6.828l4.657 4.657l-1.414 1.414L3 12zM18.001 19V5h2v14z"/></svg>
|
|
19
|
-
</div>
|
|
20
|
-
</button>
|
|
21
|
-
|
|
22
|
-
<button type="button"
|
|
23
|
-
@click="$emit('home')"
|
|
24
|
-
class="text-lg font-semibold text-gray-900 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
|
25
|
-
title="Go back to initial state"
|
|
26
|
-
>
|
|
27
|
-
History
|
|
28
|
-
</button>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<div class="flex items-center space-x-2">
|
|
32
|
-
<button type="button"
|
|
33
|
-
@click="$emit('analytics')"
|
|
34
|
-
class="text-gray-900 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
|
35
|
-
title="Analytics"
|
|
36
|
-
>
|
|
37
|
-
<svg class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M5 22a1 1 0 0 1-1-1v-8a1 1 0 0 1 2 0v8a1 1 0 0 1-1 1m5 0a1 1 0 0 1-1-1V3a1 1 0 0 1 2 0v18a1 1 0 0 1-1 1m5 0a1 1 0 0 1-1-1V9a1 1 0 0 1 2 0v12a1 1 0 0 1-1 1m5 0a1 1 0 0 1-1-1v-4a1 1 0 0 1 2 0v4a1 1 0 0 1-1 1"/></svg>
|
|
38
|
-
</button>
|
|
39
|
-
|
|
40
|
-
<button type="button"
|
|
41
|
-
@click="$emit('new')"
|
|
42
|
-
class="text-gray-900 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
|
43
|
-
title="New Chat"
|
|
44
|
-
>
|
|
45
|
-
<svg class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></g></svg>
|
|
46
|
-
</button>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
`,
|
|
51
|
-
emits:['home','new','analytics','toggle-sidebar'],
|
|
52
|
-
}
|
llms/ui/ChatPrompt.mjs
DELETED
|
@@ -1,590 +0,0 @@
|
|
|
1
|
-
import { ref, nextTick, inject, unref } from 'vue'
|
|
2
|
-
import { useRouter } from 'vue-router'
|
|
3
|
-
import { lastRightPart } from '@servicestack/client'
|
|
4
|
-
import { deepClone, fileToDataUri, fileToBase64, addCopyButtons, toModelInfo, tokenCost } from './utils.mjs'
|
|
5
|
-
|
|
6
|
-
const imageExts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
|
|
7
|
-
const audioExts = 'mp3,wav,ogg,flac,m4a,opus,webm'.split(',')
|
|
8
|
-
|
|
9
|
-
export function useChatPrompt() {
|
|
10
|
-
const messageText = ref('')
|
|
11
|
-
const attachedFiles = ref([])
|
|
12
|
-
const isGenerating = ref(false)
|
|
13
|
-
const errorStatus = ref(null)
|
|
14
|
-
const abortController = ref(null)
|
|
15
|
-
const hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
|
|
16
|
-
const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
|
|
17
|
-
const hasFile = () => attachedFiles.value.length > 0
|
|
18
|
-
// const hasText = () => !hasImage() && !hasAudio() && !hasFile()
|
|
19
|
-
|
|
20
|
-
function reset() {
|
|
21
|
-
// Ensure initial state is ready to accept input
|
|
22
|
-
isGenerating.value = false
|
|
23
|
-
attachedFiles.value = []
|
|
24
|
-
messageText.value = ''
|
|
25
|
-
abortController.value = null
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function cancel() {
|
|
29
|
-
// Cancel the pending request
|
|
30
|
-
if (abortController.value) {
|
|
31
|
-
abortController.value.abort()
|
|
32
|
-
}
|
|
33
|
-
// Reset UI state
|
|
34
|
-
isGenerating.value = false
|
|
35
|
-
abortController.value = null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
messageText,
|
|
40
|
-
attachedFiles,
|
|
41
|
-
errorStatus,
|
|
42
|
-
isGenerating,
|
|
43
|
-
abortController,
|
|
44
|
-
get generating() {
|
|
45
|
-
return isGenerating.value
|
|
46
|
-
},
|
|
47
|
-
hasImage,
|
|
48
|
-
hasAudio,
|
|
49
|
-
hasFile,
|
|
50
|
-
// hasText,
|
|
51
|
-
reset,
|
|
52
|
-
cancel,
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export default {
|
|
57
|
-
template:`
|
|
58
|
-
<div class="mx-auto max-w-3xl">
|
|
59
|
-
<SettingsDialog :isOpen="showSettings" @close="showSettings = false" />
|
|
60
|
-
<div class="flex space-x-2">
|
|
61
|
-
<!-- Attach (+) button and Settings button -->
|
|
62
|
-
<div class="mt-1.5 flex flex-col space-y-1 items-center">
|
|
63
|
-
<div>
|
|
64
|
-
<button type="button"
|
|
65
|
-
@click="triggerFilePicker"
|
|
66
|
-
:disabled="isGenerating || !model"
|
|
67
|
-
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"
|
|
68
|
-
title="Attach image or audio">
|
|
69
|
-
<svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
|
|
70
|
-
<path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"></path>
|
|
71
|
-
</svg>
|
|
72
|
-
</button>
|
|
73
|
-
<!-- Hidden file input -->
|
|
74
|
-
<input ref="fileInput" type="file" multiple @change="onFilesSelected"
|
|
75
|
-
class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
76
|
-
/>
|
|
77
|
-
</div>
|
|
78
|
-
<div>
|
|
79
|
-
<button type="button" title="Settings" @click="showSettings = true"
|
|
80
|
-
:disabled="isGenerating || !model"
|
|
81
|
-
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">
|
|
82
|
-
<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>
|
|
83
|
-
</button>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
<div class="flex-1">
|
|
88
|
-
<div class="relative">
|
|
89
|
-
<textarea
|
|
90
|
-
ref="refMessage"
|
|
91
|
-
v-model="messageText"
|
|
92
|
-
@keydown.enter.exact.prevent="sendMessage"
|
|
93
|
-
@keydown.enter.shift.exact="addNewLine"
|
|
94
|
-
@paste="onPaste"
|
|
95
|
-
@dragover="onDragOver"
|
|
96
|
-
@dragleave="onDragLeave"
|
|
97
|
-
@drop="onDrop"
|
|
98
|
-
placeholder="Type message... (Enter to send, Shift+Enter for new line, drag & drop or paste files)"
|
|
99
|
-
rows="3"
|
|
100
|
-
:class="[
|
|
101
|
-
'block w-full rounded-md border px-3 py-2 pr-12 text-sm text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-900 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-1',
|
|
102
|
-
isDragging
|
|
103
|
-
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-500'
|
|
104
|
-
: 'border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500'
|
|
105
|
-
]"
|
|
106
|
-
:disabled="isGenerating || !model"
|
|
107
|
-
></textarea>
|
|
108
|
-
<button v-if="!isGenerating" title="Send (Enter)" type="button"
|
|
109
|
-
@click="sendMessage"
|
|
110
|
-
:disabled="!messageText.trim() || isGenerating || !model"
|
|
111
|
-
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">
|
|
112
|
-
<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>
|
|
113
|
-
</button>
|
|
114
|
-
<button v-else title="Cancel request" type="button"
|
|
115
|
-
@click="cancelRequest"
|
|
116
|
-
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">
|
|
117
|
-
<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">
|
|
118
|
-
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
119
|
-
</svg>
|
|
120
|
-
</button>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
<!-- Attached files preview -->
|
|
124
|
-
<div v-if="attachedFiles.length" class="mt-2 flex flex-wrap gap-2">
|
|
125
|
-
<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">
|
|
126
|
-
<span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
|
|
127
|
-
<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">
|
|
128
|
-
<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>
|
|
129
|
-
</button>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
<div v-if="!model" class="mt-2 text-sm text-red-600 dark:text-red-400">
|
|
134
|
-
Please select a model
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
139
|
-
`,
|
|
140
|
-
props: {
|
|
141
|
-
model: {
|
|
142
|
-
type: String,
|
|
143
|
-
default: ''
|
|
144
|
-
},
|
|
145
|
-
systemPrompt: {
|
|
146
|
-
type: String,
|
|
147
|
-
default: ''
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
setup(props) {
|
|
151
|
-
const ai = inject('ai')
|
|
152
|
-
const chatSettings = inject('chatSettings')
|
|
153
|
-
const router = useRouter()
|
|
154
|
-
const config = inject('config')
|
|
155
|
-
const chatPrompt = inject('chatPrompt')
|
|
156
|
-
const {
|
|
157
|
-
messageText,
|
|
158
|
-
attachedFiles,
|
|
159
|
-
isGenerating,
|
|
160
|
-
errorStatus,
|
|
161
|
-
hasImage,
|
|
162
|
-
hasAudio,
|
|
163
|
-
hasFile
|
|
164
|
-
} = chatPrompt
|
|
165
|
-
const threads = inject('threads')
|
|
166
|
-
const {
|
|
167
|
-
currentThread,
|
|
168
|
-
} = threads
|
|
169
|
-
|
|
170
|
-
const fileInput = ref(null)
|
|
171
|
-
const refMessage = ref(null)
|
|
172
|
-
const showSettings = ref(false)
|
|
173
|
-
const { applySettings } = chatSettings
|
|
174
|
-
|
|
175
|
-
// File attachments (+) handlers
|
|
176
|
-
const triggerFilePicker = () => {
|
|
177
|
-
if (fileInput.value) fileInput.value.click()
|
|
178
|
-
}
|
|
179
|
-
const onFilesSelected = (e) => {
|
|
180
|
-
const files = Array.from(e.target?.files || [])
|
|
181
|
-
if (files.length) attachedFiles.value.push(...files)
|
|
182
|
-
// allow re-selecting the same file
|
|
183
|
-
if (fileInput.value) fileInput.value.value = ''
|
|
184
|
-
|
|
185
|
-
if (!messageText.value.trim()) {
|
|
186
|
-
if (hasImage()) {
|
|
187
|
-
messageText.value = getTextContent(config.defaults.image)
|
|
188
|
-
} else if (hasAudio()) {
|
|
189
|
-
messageText.value = getTextContent(config.defaults.audio)
|
|
190
|
-
} else {
|
|
191
|
-
messageText.value = getTextContent(config.defaults.file)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const removeAttachment = (i) => {
|
|
196
|
-
attachedFiles.value.splice(i, 1)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Helper function to add files and set default message
|
|
200
|
-
const addFilesAndSetMessage = (files) => {
|
|
201
|
-
if (files.length === 0) return
|
|
202
|
-
|
|
203
|
-
attachedFiles.value.push(...files)
|
|
204
|
-
|
|
205
|
-
// Set default message text if empty
|
|
206
|
-
if (!messageText.value.trim()) {
|
|
207
|
-
if (hasImage()) {
|
|
208
|
-
messageText.value = getTextContent(config.defaults.image)
|
|
209
|
-
} else if (hasAudio()) {
|
|
210
|
-
messageText.value = getTextContent(config.defaults.audio)
|
|
211
|
-
} else {
|
|
212
|
-
messageText.value = getTextContent(config.defaults.file)
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Handle paste events for clipboard images, audio, and files
|
|
218
|
-
const onPaste = async (e) => {
|
|
219
|
-
// Use the paste event's clipboardData directly (works best for paste events)
|
|
220
|
-
const items = e.clipboardData?.items
|
|
221
|
-
if (!items) return
|
|
222
|
-
|
|
223
|
-
const files = []
|
|
224
|
-
|
|
225
|
-
// Check all clipboard items
|
|
226
|
-
for (let i = 0; i < items.length; i++) {
|
|
227
|
-
const item = items[i]
|
|
228
|
-
|
|
229
|
-
// Handle files (images, audio, etc.)
|
|
230
|
-
if (item.kind === 'file') {
|
|
231
|
-
const file = item.getAsFile()
|
|
232
|
-
if (file) {
|
|
233
|
-
// Generate a better filename based on type
|
|
234
|
-
let filename = file.name
|
|
235
|
-
if (!filename || filename === 'image.png' || filename === 'blob') {
|
|
236
|
-
const ext = file.type.split('/')[1] || 'png'
|
|
237
|
-
const timestamp = new Date().getTime()
|
|
238
|
-
if (file.type.startsWith('image/')) {
|
|
239
|
-
filename = `pasted-image-${timestamp}.${ext}`
|
|
240
|
-
} else if (file.type.startsWith('audio/')) {
|
|
241
|
-
filename = `pasted-audio-${timestamp}.${ext}`
|
|
242
|
-
} else {
|
|
243
|
-
filename = `pasted-file-${timestamp}.${ext}`
|
|
244
|
-
}
|
|
245
|
-
// Create a new File object with the better name
|
|
246
|
-
files.push(new File([file], filename, { type: file.type }))
|
|
247
|
-
} else {
|
|
248
|
-
files.push(file)
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (files.length > 0) {
|
|
255
|
-
e.preventDefault()
|
|
256
|
-
addFilesAndSetMessage(files)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Handle drag and drop events
|
|
261
|
-
const isDragging = ref(false)
|
|
262
|
-
|
|
263
|
-
const onDragOver = (e) => {
|
|
264
|
-
e.preventDefault()
|
|
265
|
-
e.stopPropagation()
|
|
266
|
-
isDragging.value = true
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const onDragLeave = (e) => {
|
|
270
|
-
e.preventDefault()
|
|
271
|
-
e.stopPropagation()
|
|
272
|
-
isDragging.value = false
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const onDrop = (e) => {
|
|
276
|
-
e.preventDefault()
|
|
277
|
-
e.stopPropagation()
|
|
278
|
-
isDragging.value = false
|
|
279
|
-
|
|
280
|
-
const files = Array.from(e.dataTransfer?.files || [])
|
|
281
|
-
if (files.length > 0) {
|
|
282
|
-
addFilesAndSetMessage(files)
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function createChatRequest() {
|
|
287
|
-
if (hasImage()) {
|
|
288
|
-
return deepClone(config.defaults.image)
|
|
289
|
-
}
|
|
290
|
-
if (hasAudio()) {
|
|
291
|
-
return deepClone(config.defaults.audio)
|
|
292
|
-
}
|
|
293
|
-
if (attachedFiles.value.length) {
|
|
294
|
-
return deepClone(config.defaults.file)
|
|
295
|
-
}
|
|
296
|
-
const text = deepClone(config.defaults.text)
|
|
297
|
-
return text
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function getTextContent(chat) {
|
|
301
|
-
const textMessage = chat.messages.find(m =>
|
|
302
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
|
|
303
|
-
return textMessage?.content.find(c => c.type === 'text')?.text || ''
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Send message
|
|
307
|
-
const sendMessage = async () => {
|
|
308
|
-
if (!messageText.value.trim() || isGenerating.value || !props.model) return
|
|
309
|
-
|
|
310
|
-
// Clear any existing error message
|
|
311
|
-
errorStatus.value = null
|
|
312
|
-
|
|
313
|
-
let message = messageText.value.trim()
|
|
314
|
-
if (attachedFiles.value.length) {
|
|
315
|
-
const names = attachedFiles.value.map(f => f.name).join(', ')
|
|
316
|
-
const mediaType = imageExts.some(ext => names.includes(ext))
|
|
317
|
-
? '🖼️'
|
|
318
|
-
: audioExts.some(ext => names.includes(ext))
|
|
319
|
-
? '🔉'
|
|
320
|
-
: '📎'
|
|
321
|
-
message += `\n\n[${mediaType} ${names}]`
|
|
322
|
-
}
|
|
323
|
-
messageText.value = ''
|
|
324
|
-
|
|
325
|
-
// Create AbortController for this request
|
|
326
|
-
const controller = new AbortController()
|
|
327
|
-
chatPrompt.abortController.value = controller
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
let threadId
|
|
331
|
-
|
|
332
|
-
// Create thread if none exists
|
|
333
|
-
if (!currentThread.value) {
|
|
334
|
-
const newThread = await threads.createThread('New Chat', props.model, props.systemPrompt)
|
|
335
|
-
threadId = newThread.id
|
|
336
|
-
// Navigate to the new thread URL
|
|
337
|
-
router.push(`${ai.base}/c/${newThread.id}`)
|
|
338
|
-
} else {
|
|
339
|
-
threadId = currentThread.value.id
|
|
340
|
-
// Update the existing thread's model and systemPrompt to match current selection
|
|
341
|
-
await threads.updateThread(threadId, {
|
|
342
|
-
model: props.model.id,
|
|
343
|
-
info: toModelInfo(props.model),
|
|
344
|
-
systemPrompt: props.systemPrompt
|
|
345
|
-
})
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Get the thread to check for duplicates
|
|
349
|
-
let thread = await threads.getThread(threadId)
|
|
350
|
-
const lastMessage = thread.messages[thread.messages.length - 1]
|
|
351
|
-
const isDuplicate = lastMessage && lastMessage.role === 'user' && lastMessage.content === message
|
|
352
|
-
|
|
353
|
-
// Add user message only if it's not a duplicate
|
|
354
|
-
if (!isDuplicate) {
|
|
355
|
-
await threads.addMessageToThread(threadId, {
|
|
356
|
-
role: 'user',
|
|
357
|
-
content: message
|
|
358
|
-
})
|
|
359
|
-
// Reload thread after adding message
|
|
360
|
-
thread = await threads.getThread(threadId)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
isGenerating.value = true
|
|
364
|
-
const messages = [...thread.messages]
|
|
365
|
-
|
|
366
|
-
// Add system prompt if present
|
|
367
|
-
if (props.systemPrompt?.trim()) {
|
|
368
|
-
messages.unshift({
|
|
369
|
-
role: 'system',
|
|
370
|
-
content: [
|
|
371
|
-
{ type: 'text', text: props.systemPrompt }
|
|
372
|
-
]
|
|
373
|
-
})
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const chatRequest = createChatRequest()
|
|
377
|
-
chatRequest.model = props.model.id
|
|
378
|
-
|
|
379
|
-
// Apply user settings
|
|
380
|
-
applySettings(chatRequest)
|
|
381
|
-
|
|
382
|
-
console.debug('chatRequest', chatRequest, hasImage(), hasAudio(), attachedFiles.value.length, attachedFiles.value)
|
|
383
|
-
|
|
384
|
-
function setContentText(chatRequest, text) {
|
|
385
|
-
// Replace text message
|
|
386
|
-
const textImage = chatRequest.messages.find(m =>
|
|
387
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
|
|
388
|
-
for (const c of textImage.content) {
|
|
389
|
-
if (c.type === 'text') {
|
|
390
|
-
c.text = text
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (hasImage()) {
|
|
396
|
-
const imageMessage = chatRequest.messages.find(m =>
|
|
397
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'image_url'))
|
|
398
|
-
console.debug('hasImage', chatRequest, imageMessage)
|
|
399
|
-
if (imageMessage) {
|
|
400
|
-
const imgs = []
|
|
401
|
-
let imagePart = deepClone(imageMessage.content.find(c => c.type === 'image_url'))
|
|
402
|
-
for (const f of attachedFiles.value) {
|
|
403
|
-
if (imageExts.includes(lastRightPart(f.name, '.'))) {
|
|
404
|
-
imagePart.image_url.url = await fileToDataUri(f)
|
|
405
|
-
}
|
|
406
|
-
imgs.push(imagePart)
|
|
407
|
-
}
|
|
408
|
-
imageMessage.content = imageMessage.content.filter(c => c.type !== 'image_url')
|
|
409
|
-
imageMessage.content = [...imgs, ...imageMessage.content]
|
|
410
|
-
setContentText(chatRequest, message)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
} else if (hasAudio()) {
|
|
414
|
-
console.debug('hasAudio', chatRequest)
|
|
415
|
-
const audioMessage = chatRequest.messages.find(m =>
|
|
416
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'input_audio'))
|
|
417
|
-
if (audioMessage) {
|
|
418
|
-
const audios = []
|
|
419
|
-
let audioPart = deepClone(audioMessage.content.find(c => c.type === 'input_audio'))
|
|
420
|
-
for (const f of attachedFiles.value) {
|
|
421
|
-
if (audioExts.includes(lastRightPart(f.name, '.'))) {
|
|
422
|
-
audioPart.input_audio.data = await fileToBase64(f)
|
|
423
|
-
}
|
|
424
|
-
audios.push(audioPart)
|
|
425
|
-
}
|
|
426
|
-
audioMessage.content = audioMessage.content.filter(c => c.type !== 'input_audio')
|
|
427
|
-
audioMessage.content = [...audios, ...audioMessage.content]
|
|
428
|
-
setContentText(chatRequest, message)
|
|
429
|
-
}
|
|
430
|
-
} else if (attachedFiles.value.length) {
|
|
431
|
-
console.debug('hasFile', chatRequest)
|
|
432
|
-
const fileMessage = chatRequest.messages.find(m =>
|
|
433
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'file'))
|
|
434
|
-
if (fileMessage) {
|
|
435
|
-
const files = []
|
|
436
|
-
let filePart = deepClone(fileMessage.content.find(c => c.type === 'file'))
|
|
437
|
-
for (const f of attachedFiles.value) {
|
|
438
|
-
filePart.file.file_data = await fileToDataUri(f)
|
|
439
|
-
filePart.file.filename = f.name
|
|
440
|
-
files.push(filePart)
|
|
441
|
-
}
|
|
442
|
-
fileMessage.content = fileMessage.content.filter(c => c.type !== 'file')
|
|
443
|
-
fileMessage.content = [...files, ...fileMessage.content]
|
|
444
|
-
setContentText(chatRequest, message)
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
} else {
|
|
448
|
-
console.debug('hasText', chatRequest)
|
|
449
|
-
// Chat template message needs to be empty
|
|
450
|
-
chatRequest.messages = []
|
|
451
|
-
messages.forEach(m => chatRequest.messages.push({
|
|
452
|
-
role: m.role,
|
|
453
|
-
content: typeof m.content === 'string'
|
|
454
|
-
? [{ type: 'text', text: m.content }]
|
|
455
|
-
: m.content
|
|
456
|
-
}))
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
chatRequest.metadata ??= {}
|
|
460
|
-
chatRequest.metadata.threadId = threadId
|
|
461
|
-
|
|
462
|
-
// Send to API
|
|
463
|
-
console.debug('chatRequest', chatRequest)
|
|
464
|
-
const startTime = Date.now()
|
|
465
|
-
const response = await ai.post('/v1/chat/completions', {
|
|
466
|
-
body: JSON.stringify(chatRequest),
|
|
467
|
-
signal: controller.signal
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
let result = null
|
|
471
|
-
if (!response.ok) {
|
|
472
|
-
errorStatus.value = {
|
|
473
|
-
errorCode: `HTTP ${response.status} ${response.statusText}`,
|
|
474
|
-
message: null,
|
|
475
|
-
stackTrace: null
|
|
476
|
-
}
|
|
477
|
-
let errorBody = null
|
|
478
|
-
try {
|
|
479
|
-
errorBody = await response.text()
|
|
480
|
-
if (errorBody) {
|
|
481
|
-
// Try to parse as JSON for better formatting
|
|
482
|
-
try {
|
|
483
|
-
const errorJson = JSON.parse(errorBody)
|
|
484
|
-
const status = errorJson?.responseStatus
|
|
485
|
-
if (status) {
|
|
486
|
-
errorStatus.value.errorCode += ` ${status.errorCode}`
|
|
487
|
-
errorStatus.value.message = status.message
|
|
488
|
-
errorStatus.value.stackTrace = status.stackTrace
|
|
489
|
-
} else {
|
|
490
|
-
errorStatus.value.stackTrace = JSON.stringify(errorJson, null, 2)
|
|
491
|
-
}
|
|
492
|
-
} catch (e) {
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
} catch (e) {
|
|
496
|
-
// If we can't read the response body, just use the status
|
|
497
|
-
}
|
|
498
|
-
} else {
|
|
499
|
-
try {
|
|
500
|
-
result = await response.json()
|
|
501
|
-
console.debug('chatResponse', JSON.stringify(result, null, 2))
|
|
502
|
-
} catch (e) {
|
|
503
|
-
errorStatus.value = {
|
|
504
|
-
errorCode: 'Error',
|
|
505
|
-
message: e.message,
|
|
506
|
-
stackTrace: null
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (result?.error) {
|
|
512
|
-
errorStatus.value ??= {
|
|
513
|
-
errorCode: 'Error',
|
|
514
|
-
}
|
|
515
|
-
errorStatus.value.message = result.error
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (!errorStatus.value) {
|
|
519
|
-
// Add assistant response (save entire message including reasoning)
|
|
520
|
-
const assistantMessage = result.choices?.[0]?.message
|
|
521
|
-
|
|
522
|
-
const usage = result.usage
|
|
523
|
-
if (usage) {
|
|
524
|
-
if (result.metadata?.pricing) {
|
|
525
|
-
const [ input, output ] = result.metadata.pricing.split('/')
|
|
526
|
-
usage.duration = result.metadata.duration ?? (Date.now() - startTime)
|
|
527
|
-
usage.input = input
|
|
528
|
-
usage.output = output
|
|
529
|
-
usage.tokens = usage.completion_tokens
|
|
530
|
-
usage.price = usage.output
|
|
531
|
-
usage.cost = tokenCost(usage.prompt_tokens * parseFloat(input) + usage.completion_tokens * parseFloat(output))
|
|
532
|
-
}
|
|
533
|
-
await threads.logRequest(threadId, props.model, chatRequest, result)
|
|
534
|
-
}
|
|
535
|
-
await threads.addMessageToThread(threadId, assistantMessage, usage)
|
|
536
|
-
|
|
537
|
-
nextTick(addCopyButtons)
|
|
538
|
-
|
|
539
|
-
attachedFiles.value = []
|
|
540
|
-
// Error will be cleared when user sends next message (no auto-timeout)
|
|
541
|
-
}
|
|
542
|
-
} catch (error) {
|
|
543
|
-
// Check if the error is due to abort
|
|
544
|
-
if (error.name === 'AbortError') {
|
|
545
|
-
console.log('Request was cancelled by user')
|
|
546
|
-
// Don't show error for cancelled requests
|
|
547
|
-
} else {
|
|
548
|
-
// Re-throw other errors to be handled by outer catch
|
|
549
|
-
throw error
|
|
550
|
-
}
|
|
551
|
-
} finally {
|
|
552
|
-
isGenerating.value = false
|
|
553
|
-
chatPrompt.abortController.value = null
|
|
554
|
-
// Restore focus to the textarea
|
|
555
|
-
nextTick(() => {
|
|
556
|
-
refMessage.value?.focus()
|
|
557
|
-
})
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const cancelRequest = () => {
|
|
562
|
-
chatPrompt.cancel()
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const addNewLine = () => {
|
|
566
|
-
// Enter key already adds new line
|
|
567
|
-
//messageText.value += '\n'
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
return {
|
|
571
|
-
isGenerating,
|
|
572
|
-
attachedFiles,
|
|
573
|
-
messageText,
|
|
574
|
-
fileInput,
|
|
575
|
-
refMessage,
|
|
576
|
-
showSettings,
|
|
577
|
-
isDragging,
|
|
578
|
-
triggerFilePicker,
|
|
579
|
-
onFilesSelected,
|
|
580
|
-
onPaste,
|
|
581
|
-
onDragOver,
|
|
582
|
-
onDragLeave,
|
|
583
|
-
onDrop,
|
|
584
|
-
removeAttachment,
|
|
585
|
-
sendMessage,
|
|
586
|
-
cancelRequest,
|
|
587
|
-
addNewLine,
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|