llms-py 3.0.0__py3-none-any.whl → 3.0.0b2__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/index.html +37 -26
- llms/llms.json +21 -70
- llms/main.py +731 -1426
- llms/providers.json +1 -1
- llms/{extensions/analytics/ui/index.mjs → ui/Analytics.mjs} +238 -154
- llms/ui/App.mjs +63 -133
- llms/ui/Avatar.mjs +86 -0
- llms/ui/Brand.mjs +52 -0
- llms/ui/ChatPrompt.mjs +597 -0
- llms/ui/Main.mjs +862 -0
- llms/ui/OAuthSignIn.mjs +61 -0
- llms/ui/ProviderIcon.mjs +36 -0
- llms/ui/ProviderStatus.mjs +104 -0
- llms/{extensions/app/ui → ui}/Recents.mjs +57 -82
- llms/ui/{modules/chat/SettingsDialog.mjs → SettingsDialog.mjs} +9 -9
- llms/{extensions/app/ui/index.mjs → ui/Sidebar.mjs} +57 -122
- llms/ui/SignIn.mjs +65 -0
- llms/ui/Welcome.mjs +8 -0
- llms/ui/ai.mjs +13 -117
- llms/ui/app.css +49 -1776
- llms/ui/index.mjs +171 -87
- llms/ui/lib/charts.mjs +13 -9
- llms/ui/lib/servicestack-vue.mjs +3 -3
- llms/ui/lib/vue.min.mjs +9 -10
- llms/ui/lib/vue.mjs +1602 -1763
- llms/ui/markdown.mjs +2 -10
- llms/ui/model-selector.mjs +686 -0
- llms/ui/tailwind.input.css +1 -55
- llms/ui/threadStore.mjs +583 -0
- llms/ui/utils.mjs +118 -113
- llms/ui.json +1069 -0
- {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/METADATA +1 -1
- llms_py-3.0.0b2.dist-info/RECORD +58 -0
- llms/extensions/app/README.md +0 -20
- llms/extensions/app/__init__.py +0 -530
- 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 +0 -644
- llms/extensions/app/db_manager.py +0 -195
- llms/extensions/app/requests.json +0 -9073
- llms/extensions/app/threads.json +0 -15290
- llms/extensions/app/ui/threadStore.mjs +0 -411
- llms/extensions/core_tools/CALCULATOR.md +0 -32
- llms/extensions/core_tools/__init__.py +0 -598
- llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +0 -201
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +0 -185
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +0 -101
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +0 -160
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +0 -66
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +0 -27
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +0 -72
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +0 -119
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +0 -98
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +0 -225
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +0 -344
- llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +0 -9884
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +0 -942
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +0 -118
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +0 -962
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +0 -62
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +0 -402
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +0 -40
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +0 -135
- llms/extensions/core_tools/ui/index.mjs +0 -650
- llms/extensions/gallery/README.md +0 -61
- llms/extensions/gallery/__init__.py +0 -61
- 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 +0 -298
- llms/extensions/gallery/ui/index.mjs +0 -482
- llms/extensions/katex/README.md +0 -39
- llms/extensions/katex/__init__.py +0 -6
- llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/katex/ui/README.md +0 -125
- llms/extensions/katex/ui/contrib/auto-render.js +0 -338
- llms/extensions/katex/ui/contrib/auto-render.min.js +0 -1
- llms/extensions/katex/ui/contrib/auto-render.mjs +0 -244
- llms/extensions/katex/ui/contrib/copy-tex.js +0 -127
- llms/extensions/katex/ui/contrib/copy-tex.min.js +0 -1
- llms/extensions/katex/ui/contrib/copy-tex.mjs +0 -105
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +0 -109
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +0 -1
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +0 -24
- llms/extensions/katex/ui/contrib/mhchem.js +0 -3213
- llms/extensions/katex/ui/contrib/mhchem.min.js +0 -1
- llms/extensions/katex/ui/contrib/mhchem.mjs +0 -3109
- llms/extensions/katex/ui/contrib/render-a11y-string.js +0 -887
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +0 -1
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +0 -800
- 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 +0 -92
- llms/extensions/katex/ui/katex-swap.css +0 -1230
- llms/extensions/katex/ui/katex-swap.min.css +0 -1
- llms/extensions/katex/ui/katex.css +0 -1230
- llms/extensions/katex/ui/katex.js +0 -19080
- llms/extensions/katex/ui/katex.min.css +0 -1
- llms/extensions/katex/ui/katex.min.js +0 -1
- llms/extensions/katex/ui/katex.min.mjs +0 -1
- llms/extensions/katex/ui/katex.mjs +0 -18547
- llms/extensions/providers/__init__.py +0 -18
- 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 +0 -229
- llms/extensions/providers/chutes.py +0 -155
- llms/extensions/providers/google.py +0 -378
- llms/extensions/providers/nvidia.py +0 -105
- llms/extensions/providers/openai.py +0 -156
- llms/extensions/providers/openrouter.py +0 -72
- llms/extensions/system_prompts/README.md +0 -22
- llms/extensions/system_prompts/__init__.py +0 -45
- llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/system_prompts/ui/index.mjs +0 -280
- llms/extensions/system_prompts/ui/prompts.json +0 -1067
- llms/extensions/tools/__init__.py +0 -5
- llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/extensions/tools/ui/index.mjs +0 -204
- llms/providers-extra.json +0 -356
- llms/ui/ctx.mjs +0 -365
- llms/ui/modules/chat/ChatBody.mjs +0 -691
- llms/ui/modules/chat/index.mjs +0 -828
- llms/ui/modules/layout.mjs +0 -243
- llms/ui/modules/model-selector.mjs +0 -851
- llms_py-3.0.0.dist-info/RECORD +0 -202
- {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/WHEEL +0 -0
- {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.0.dist-info → llms_py-3.0.0b2.dist-info}/top_level.txt +0 -0
llms/ui/ChatPrompt.mjs
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
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, uploadFile } from './utils.mjs'
|
|
5
|
+
import { toRaw } from 'vue'
|
|
6
|
+
|
|
7
|
+
const imageExts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
|
|
8
|
+
const audioExts = 'mp3,wav,ogg,flac,m4a,opus,webm'.split(',')
|
|
9
|
+
|
|
10
|
+
export function useChatPrompt() {
|
|
11
|
+
const messageText = ref('')
|
|
12
|
+
const attachedFiles = ref([])
|
|
13
|
+
const isGenerating = ref(false)
|
|
14
|
+
const errorStatus = ref(null)
|
|
15
|
+
const abortController = ref(null)
|
|
16
|
+
const hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
|
|
17
|
+
const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
|
|
18
|
+
const hasFile = () => attachedFiles.value.length > 0
|
|
19
|
+
// const hasText = () => !hasImage() && !hasAudio() && !hasFile()
|
|
20
|
+
|
|
21
|
+
const editingMessageId = ref(null)
|
|
22
|
+
|
|
23
|
+
function reset() {
|
|
24
|
+
// Ensure initial state is ready to accept input
|
|
25
|
+
isGenerating.value = false
|
|
26
|
+
attachedFiles.value = []
|
|
27
|
+
messageText.value = ''
|
|
28
|
+
abortController.value = null
|
|
29
|
+
editingMessageId.value = null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cancel() {
|
|
33
|
+
// Cancel the pending request
|
|
34
|
+
if (abortController.value) {
|
|
35
|
+
abortController.value.abort()
|
|
36
|
+
}
|
|
37
|
+
// Reset UI state
|
|
38
|
+
isGenerating.value = false
|
|
39
|
+
abortController.value = null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
messageText,
|
|
44
|
+
attachedFiles,
|
|
45
|
+
errorStatus,
|
|
46
|
+
isGenerating,
|
|
47
|
+
abortController,
|
|
48
|
+
editingMessageId,
|
|
49
|
+
get generating() {
|
|
50
|
+
return isGenerating.value
|
|
51
|
+
},
|
|
52
|
+
hasImage,
|
|
53
|
+
hasAudio,
|
|
54
|
+
hasFile,
|
|
55
|
+
// hasText,
|
|
56
|
+
reset,
|
|
57
|
+
cancel,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default {
|
|
62
|
+
template: `
|
|
63
|
+
<div class="mx-auto max-w-3xl">
|
|
64
|
+
<SettingsDialog :isOpen="showSettings" @close="showSettings = false" />
|
|
65
|
+
<div class="flex space-x-2">
|
|
66
|
+
<!-- Attach (+) button and Settings button -->
|
|
67
|
+
<div class="mt-1.5 flex flex-col space-y-1 items-center">
|
|
68
|
+
<div>
|
|
69
|
+
<button type="button"
|
|
70
|
+
@click="triggerFilePicker"
|
|
71
|
+
:disabled="isGenerating || !model"
|
|
72
|
+
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"
|
|
73
|
+
title="Attach image or audio">
|
|
74
|
+
<svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
|
|
75
|
+
<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>
|
|
76
|
+
</svg>
|
|
77
|
+
</button>
|
|
78
|
+
<!-- Hidden file input -->
|
|
79
|
+
<input ref="fileInput" type="file" multiple @change="onFilesSelected"
|
|
80
|
+
class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
<div>
|
|
84
|
+
<button type="button" title="Settings" @click="showSettings = true"
|
|
85
|
+
:disabled="isGenerating || !model"
|
|
86
|
+
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">
|
|
87
|
+
<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>
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="flex-1">
|
|
93
|
+
<div class="relative">
|
|
94
|
+
<textarea
|
|
95
|
+
ref="refMessage"
|
|
96
|
+
v-model="messageText"
|
|
97
|
+
@keydown.enter.exact.prevent="sendMessage"
|
|
98
|
+
@keydown.enter.shift.exact="addNewLine"
|
|
99
|
+
@paste="onPaste"
|
|
100
|
+
@dragover="onDragOver"
|
|
101
|
+
@dragleave="onDragLeave"
|
|
102
|
+
@drop="onDrop"
|
|
103
|
+
placeholder="Type message... (Enter to send, Shift+Enter for new line, drag & drop or paste files)"
|
|
104
|
+
rows="3"
|
|
105
|
+
:class="[
|
|
106
|
+
'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',
|
|
107
|
+
isDragging
|
|
108
|
+
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-500'
|
|
109
|
+
: 'border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500'
|
|
110
|
+
]"
|
|
111
|
+
:disabled="isGenerating || !model"
|
|
112
|
+
></textarea>
|
|
113
|
+
<button v-if="!isGenerating" title="Send (Enter)" type="button"
|
|
114
|
+
@click="sendMessage"
|
|
115
|
+
:disabled="!messageText.trim() || isGenerating || !model"
|
|
116
|
+
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">
|
|
117
|
+
<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>
|
|
118
|
+
</button>
|
|
119
|
+
<button v-else title="Cancel request" type="button"
|
|
120
|
+
@click="cancelRequest"
|
|
121
|
+
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">
|
|
122
|
+
<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">
|
|
123
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
124
|
+
</svg>
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Attached files preview -->
|
|
129
|
+
<div v-if="attachedFiles.length" class="mt-2 flex flex-wrap gap-2">
|
|
130
|
+
<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">
|
|
131
|
+
<span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
|
|
132
|
+
<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">
|
|
133
|
+
<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>
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div v-if="!model" class="mt-2 text-sm text-red-600 dark:text-red-400">
|
|
139
|
+
Please select a model
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
`,
|
|
145
|
+
props: {
|
|
146
|
+
model: {
|
|
147
|
+
type: Object,
|
|
148
|
+
default: null
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
setup(props) {
|
|
152
|
+
const ctx = inject('ctx')
|
|
153
|
+
const config = ctx.state.config
|
|
154
|
+
const ai = ctx.ai
|
|
155
|
+
const chatSettings = inject('chatSettings')
|
|
156
|
+
const router = useRouter()
|
|
157
|
+
const chatPrompt = inject('chatPrompt')
|
|
158
|
+
const {
|
|
159
|
+
messageText,
|
|
160
|
+
attachedFiles,
|
|
161
|
+
isGenerating,
|
|
162
|
+
errorStatus,
|
|
163
|
+
hasImage,
|
|
164
|
+
hasAudio,
|
|
165
|
+
hasFile,
|
|
166
|
+
editingMessageId
|
|
167
|
+
} = chatPrompt
|
|
168
|
+
const threads = inject('threads')
|
|
169
|
+
const {
|
|
170
|
+
currentThread,
|
|
171
|
+
} = threads
|
|
172
|
+
|
|
173
|
+
const fileInput = ref(null)
|
|
174
|
+
const refMessage = ref(null)
|
|
175
|
+
const showSettings = ref(false)
|
|
176
|
+
const { applySettings } = chatSettings
|
|
177
|
+
|
|
178
|
+
// File attachments (+) handlers
|
|
179
|
+
const triggerFilePicker = () => {
|
|
180
|
+
if (fileInput.value) fileInput.value.click()
|
|
181
|
+
}
|
|
182
|
+
const onFilesSelected = async (e) => {
|
|
183
|
+
const files = Array.from(e.target?.files || [])
|
|
184
|
+
if (files.length) {
|
|
185
|
+
// Upload files immediately
|
|
186
|
+
const uploadedFiles = await Promise.all(files.map(async f => {
|
|
187
|
+
try {
|
|
188
|
+
const response = await uploadFile(f)
|
|
189
|
+
const metadata = {
|
|
190
|
+
url: response.url,
|
|
191
|
+
name: f.name,
|
|
192
|
+
size: response.size,
|
|
193
|
+
type: f.type,
|
|
194
|
+
width: response.width,
|
|
195
|
+
height: response.height,
|
|
196
|
+
threadId: currentThread.value?.id,
|
|
197
|
+
created: Date.now()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
...metadata,
|
|
202
|
+
file: f // Keep original file for preview/fallback if needed
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('File upload failed:', error)
|
|
206
|
+
errorStatus.value = {
|
|
207
|
+
errorCode: 'Upload Failed',
|
|
208
|
+
message: `Failed to upload ${f.name}: ${error.message}`
|
|
209
|
+
}
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
}))
|
|
213
|
+
|
|
214
|
+
attachedFiles.value.push(...uploadedFiles.filter(f => f))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// allow re-selecting the same file
|
|
218
|
+
if (fileInput.value) fileInput.value.value = ''
|
|
219
|
+
|
|
220
|
+
if (!messageText.value.trim()) {
|
|
221
|
+
if (hasImage()) {
|
|
222
|
+
messageText.value = getTextContent(config.defaults.image)
|
|
223
|
+
} else if (hasAudio()) {
|
|
224
|
+
messageText.value = getTextContent(config.defaults.audio)
|
|
225
|
+
} else {
|
|
226
|
+
messageText.value = getTextContent(config.defaults.file)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const removeAttachment = (i) => {
|
|
231
|
+
attachedFiles.value.splice(i, 1)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Helper function to add files and set default message
|
|
235
|
+
const addFilesAndSetMessage = (files) => {
|
|
236
|
+
if (files.length === 0) return
|
|
237
|
+
|
|
238
|
+
attachedFiles.value.push(...files)
|
|
239
|
+
|
|
240
|
+
// Set default message text if empty
|
|
241
|
+
if (!messageText.value.trim()) {
|
|
242
|
+
if (hasImage()) {
|
|
243
|
+
messageText.value = getTextContent(config.defaults.image)
|
|
244
|
+
} else if (hasAudio()) {
|
|
245
|
+
messageText.value = getTextContent(config.defaults.audio)
|
|
246
|
+
} else {
|
|
247
|
+
messageText.value = getTextContent(config.defaults.file)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Handle paste events for clipboard images, audio, and files
|
|
253
|
+
const onPaste = async (e) => {
|
|
254
|
+
// Use the paste event's clipboardData directly (works best for paste events)
|
|
255
|
+
const items = e.clipboardData?.items
|
|
256
|
+
if (!items) return
|
|
257
|
+
|
|
258
|
+
const files = []
|
|
259
|
+
|
|
260
|
+
// Check all clipboard items
|
|
261
|
+
for (let i = 0; i < items.length; i++) {
|
|
262
|
+
const item = items[i]
|
|
263
|
+
|
|
264
|
+
// Handle files (images, audio, etc.)
|
|
265
|
+
if (item.kind === 'file') {
|
|
266
|
+
const file = item.getAsFile()
|
|
267
|
+
if (file) {
|
|
268
|
+
// Generate a better filename based on type
|
|
269
|
+
let filename = file.name
|
|
270
|
+
if (!filename || filename === 'image.png' || filename === 'blob') {
|
|
271
|
+
const ext = file.type.split('/')[1] || 'png'
|
|
272
|
+
const timestamp = new Date().getTime()
|
|
273
|
+
if (file.type.startsWith('image/')) {
|
|
274
|
+
filename = `pasted-image-${timestamp}.${ext}`
|
|
275
|
+
} else if (file.type.startsWith('audio/')) {
|
|
276
|
+
filename = `pasted-audio-${timestamp}.${ext}`
|
|
277
|
+
} else {
|
|
278
|
+
filename = `pasted-file-${timestamp}.${ext}`
|
|
279
|
+
}
|
|
280
|
+
// Create a new File object with the better name
|
|
281
|
+
files.push(new File([file], filename, { type: file.type }))
|
|
282
|
+
} else {
|
|
283
|
+
files.push(file)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (files.length > 0) {
|
|
290
|
+
e.preventDefault()
|
|
291
|
+
// Reuse the same logic as onFilesSelected for consistency
|
|
292
|
+
const event = { target: { files: files } }
|
|
293
|
+
await onFilesSelected(event)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Handle drag and drop events
|
|
298
|
+
const isDragging = ref(false)
|
|
299
|
+
|
|
300
|
+
const onDragOver = (e) => {
|
|
301
|
+
e.preventDefault()
|
|
302
|
+
e.stopPropagation()
|
|
303
|
+
isDragging.value = true
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const onDragLeave = (e) => {
|
|
307
|
+
e.preventDefault()
|
|
308
|
+
e.stopPropagation()
|
|
309
|
+
isDragging.value = false
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const onDrop = async (e) => {
|
|
313
|
+
e.preventDefault()
|
|
314
|
+
e.stopPropagation()
|
|
315
|
+
isDragging.value = false
|
|
316
|
+
|
|
317
|
+
const files = Array.from(e.dataTransfer?.files || [])
|
|
318
|
+
if (files.length > 0) {
|
|
319
|
+
// Reuse the same logic as onFilesSelected for consistency
|
|
320
|
+
const event = { target: { files: files } }
|
|
321
|
+
await onFilesSelected(event)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getTextContent(chat) {
|
|
326
|
+
const textMessage = chat.messages.find(m =>
|
|
327
|
+
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
|
|
328
|
+
return textMessage?.content.find(c => c.type === 'text')?.text || ''
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Send message
|
|
332
|
+
const sendMessage = async () => {
|
|
333
|
+
if (!messageText.value.trim() || isGenerating.value || !props.model) return
|
|
334
|
+
|
|
335
|
+
// Clear any existing error message
|
|
336
|
+
errorStatus.value = null
|
|
337
|
+
|
|
338
|
+
// 1. Construct Structured Content (Text + Attachments)
|
|
339
|
+
let text = messageText.value.trim()
|
|
340
|
+
let content = []
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
messageText.value = ''
|
|
344
|
+
|
|
345
|
+
// Add Text Block
|
|
346
|
+
content.push({ type: 'text', text: text })
|
|
347
|
+
|
|
348
|
+
// Add Attachment Blocks
|
|
349
|
+
for (const f of attachedFiles.value) {
|
|
350
|
+
const ext = lastRightPart(f.name, '.')
|
|
351
|
+
if (imageExts.includes(ext)) {
|
|
352
|
+
content.push({ type: 'image_url', image_url: { url: f.url } })
|
|
353
|
+
} else if (audioExts.includes(ext)) {
|
|
354
|
+
content.push({ type: 'input_audio', input_audio: { data: f.url, format: ext } })
|
|
355
|
+
} else {
|
|
356
|
+
content.push({ type: 'file', file: { file_data: f.url, filename: f.name } })
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Create AbortController for this request
|
|
361
|
+
const controller = new AbortController()
|
|
362
|
+
chatPrompt.abortController.value = controller
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
let threadId
|
|
366
|
+
|
|
367
|
+
// Create thread if none exists
|
|
368
|
+
if (!currentThread.value) {
|
|
369
|
+
const newThread = await threads.createThread({
|
|
370
|
+
title: 'New Chat',
|
|
371
|
+
model: props.model.id,
|
|
372
|
+
info: toModelInfo(props.model),
|
|
373
|
+
})
|
|
374
|
+
threadId = newThread.id
|
|
375
|
+
// Navigate to the new thread URL
|
|
376
|
+
router.push(`${ai.base}/c/${newThread.id}`)
|
|
377
|
+
} else {
|
|
378
|
+
threadId = currentThread.value.id
|
|
379
|
+
// Update the existing thread's model to match current selection
|
|
380
|
+
await threads.updateThread(threadId, {
|
|
381
|
+
model: props.model.name,
|
|
382
|
+
info: toModelInfo(props.model),
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Get the thread to check for duplicates
|
|
387
|
+
let thread = await threads.getThread(threadId)
|
|
388
|
+
|
|
389
|
+
// Handle Editing / Redo Logic
|
|
390
|
+
if (editingMessageId.value) {
|
|
391
|
+
// Check if message still exists
|
|
392
|
+
const messageExists = thread.messages.find(m => m.id === editingMessageId.value)
|
|
393
|
+
if (messageExists) {
|
|
394
|
+
// Update the message content
|
|
395
|
+
await threads.updateMessageInThread(threadId, editingMessageId.value, { content: content })
|
|
396
|
+
// Redo from this message (clears subsequent)
|
|
397
|
+
await threads.redoMessageFromThread(threadId, editingMessageId.value)
|
|
398
|
+
|
|
399
|
+
// Clear editing state
|
|
400
|
+
editingMessageId.value = null
|
|
401
|
+
} else {
|
|
402
|
+
// Fallback if message was deleted
|
|
403
|
+
editingMessageId.value = null
|
|
404
|
+
}
|
|
405
|
+
// Refresh thread state
|
|
406
|
+
thread = await threads.getThread(threadId)
|
|
407
|
+
} else {
|
|
408
|
+
// Regular Send Logic
|
|
409
|
+
const lastMessage = thread.messages[thread.messages.length - 1]
|
|
410
|
+
|
|
411
|
+
// Check duplicate based on text content extracted from potential array
|
|
412
|
+
const getLastText = (msgContent) => {
|
|
413
|
+
if (typeof msgContent === 'string') return msgContent
|
|
414
|
+
if (Array.isArray(msgContent)) return msgContent.find(c => c.type === 'text')?.text || ''
|
|
415
|
+
return ''
|
|
416
|
+
}
|
|
417
|
+
const newText = text // content[0].text
|
|
418
|
+
const lastText = lastMessage && lastMessage.role === 'user' ? getLastText(lastMessage.content) : null
|
|
419
|
+
|
|
420
|
+
const isDuplicate = lastText === newText
|
|
421
|
+
|
|
422
|
+
// Add user message only if it's not a duplicate
|
|
423
|
+
// Note: We are saving the FULL STRUCTURED CONTENT array here
|
|
424
|
+
if (!isDuplicate) {
|
|
425
|
+
await threads.addMessageToThread(threadId, {
|
|
426
|
+
role: 'user',
|
|
427
|
+
content: content
|
|
428
|
+
})
|
|
429
|
+
// Reload thread after adding message
|
|
430
|
+
thread = await threads.getThread(threadId)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
isGenerating.value = true
|
|
435
|
+
|
|
436
|
+
// Construct API Request from History
|
|
437
|
+
const request = {
|
|
438
|
+
model: props.model.name,
|
|
439
|
+
messages: [],
|
|
440
|
+
metadata: {}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Add History
|
|
444
|
+
thread.messages.forEach(m => {
|
|
445
|
+
request.messages.push({
|
|
446
|
+
role: m.role,
|
|
447
|
+
content: m.content
|
|
448
|
+
})
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
// Apply user settings
|
|
452
|
+
applySettings(request)
|
|
453
|
+
request.metadata.threadId = threadId
|
|
454
|
+
|
|
455
|
+
const ctxRequest = {
|
|
456
|
+
request,
|
|
457
|
+
thread,
|
|
458
|
+
}
|
|
459
|
+
ctx.chatRequestFilters.forEach(f => f(ctxRequest))
|
|
460
|
+
|
|
461
|
+
console.debug('chatRequest', request)
|
|
462
|
+
|
|
463
|
+
// Send to API
|
|
464
|
+
const startTime = Date.now()
|
|
465
|
+
const res = await ai.post('/v1/chat/completions', {
|
|
466
|
+
body: JSON.stringify(request),
|
|
467
|
+
signal: controller.signal
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
let response = null
|
|
471
|
+
if (!res.ok) {
|
|
472
|
+
errorStatus.value = {
|
|
473
|
+
errorCode: `HTTP ${res.status} ${res.statusText}`,
|
|
474
|
+
message: null,
|
|
475
|
+
stackTrace: null
|
|
476
|
+
}
|
|
477
|
+
let errorBody = null
|
|
478
|
+
try {
|
|
479
|
+
errorBody = await res.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
|
+
response = await res.json()
|
|
501
|
+
const ctxResponse = {
|
|
502
|
+
response,
|
|
503
|
+
thread,
|
|
504
|
+
}
|
|
505
|
+
ctx.chatResponseFilters.forEach(f => f(ctxResponse))
|
|
506
|
+
console.debug('chatResponse', JSON.stringify(response, null, 2))
|
|
507
|
+
} catch (e) {
|
|
508
|
+
errorStatus.value = {
|
|
509
|
+
errorCode: 'Error',
|
|
510
|
+
message: e.message,
|
|
511
|
+
stackTrace: null
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (response?.error) {
|
|
517
|
+
errorStatus.value ??= {
|
|
518
|
+
errorCode: 'Error',
|
|
519
|
+
}
|
|
520
|
+
errorStatus.value.message = response.error
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (!errorStatus.value) {
|
|
524
|
+
// Add assistant response (save entire message including reasoning)
|
|
525
|
+
const assistantMessage = response.choices?.[0]?.message
|
|
526
|
+
|
|
527
|
+
const usage = response.usage
|
|
528
|
+
if (usage) {
|
|
529
|
+
if (response.metadata?.pricing) {
|
|
530
|
+
const [input, output] = response.metadata.pricing.split('/')
|
|
531
|
+
usage.duration = response.metadata.duration ?? (Date.now() - startTime)
|
|
532
|
+
usage.input = input
|
|
533
|
+
usage.output = output
|
|
534
|
+
usage.tokens = usage.completion_tokens
|
|
535
|
+
usage.price = usage.output
|
|
536
|
+
usage.cost = tokenCost(usage.prompt_tokens / 1_000_000 * parseFloat(input) + usage.completion_tokens / 1_000_000 * parseFloat(output))
|
|
537
|
+
}
|
|
538
|
+
await threads.logRequest(threadId, props.model, request, response)
|
|
539
|
+
}
|
|
540
|
+
await threads.addMessageToThread(threadId, assistantMessage, usage)
|
|
541
|
+
|
|
542
|
+
nextTick(addCopyButtons)
|
|
543
|
+
|
|
544
|
+
attachedFiles.value = []
|
|
545
|
+
// Error will be cleared when user sends next message (no auto-timeout)
|
|
546
|
+
} else {
|
|
547
|
+
ctx.chatErrorFilters.forEach(f => f(errorStatus.value))
|
|
548
|
+
}
|
|
549
|
+
} catch (error) {
|
|
550
|
+
// Check if the error is due to abort
|
|
551
|
+
if (error.name === 'AbortError') {
|
|
552
|
+
console.log('Request was cancelled by user')
|
|
553
|
+
// Don't show error for cancelled requests
|
|
554
|
+
} else {
|
|
555
|
+
// Re-throw other errors to be handled by outer catch
|
|
556
|
+
throw error
|
|
557
|
+
}
|
|
558
|
+
} finally {
|
|
559
|
+
isGenerating.value = false
|
|
560
|
+
chatPrompt.abortController.value = null
|
|
561
|
+
// Restore focus to the textarea
|
|
562
|
+
nextTick(() => {
|
|
563
|
+
refMessage.value?.focus()
|
|
564
|
+
})
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const cancelRequest = () => {
|
|
569
|
+
chatPrompt.cancel()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const addNewLine = () => {
|
|
573
|
+
// Enter key already adds new line
|
|
574
|
+
//messageText.value += '\n'
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
isGenerating,
|
|
579
|
+
attachedFiles,
|
|
580
|
+
messageText,
|
|
581
|
+
fileInput,
|
|
582
|
+
refMessage,
|
|
583
|
+
showSettings,
|
|
584
|
+
isDragging,
|
|
585
|
+
triggerFilePicker,
|
|
586
|
+
onFilesSelected,
|
|
587
|
+
onPaste,
|
|
588
|
+
onDragOver,
|
|
589
|
+
onDragLeave,
|
|
590
|
+
onDrop,
|
|
591
|
+
removeAttachment,
|
|
592
|
+
sendMessage,
|
|
593
|
+
cancelRequest,
|
|
594
|
+
addNewLine,
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|