llms-py 2.0.20__py3-none-any.whl → 3.0.10__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/db.py +359 -0
- llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +254 -327
- llms/extensions/app/README.md +20 -0
- llms/extensions/app/__init__.py +589 -0
- llms/extensions/app/db.py +536 -0
- llms/{ui → extensions/app/ui}/Recents.mjs +99 -73
- llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +139 -68
- llms/extensions/app/ui/threadStore.mjs +433 -0
- llms/extensions/core_tools/CALCULATOR.md +32 -0
- llms/extensions/core_tools/__init__.py +637 -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/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -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/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 +63 -0
- llms/extensions/gallery/db.py +243 -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/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 +22 -0
- llms/extensions/providers/anthropic.py +233 -0
- llms/extensions/providers/cerebras.py +37 -0
- llms/extensions/providers/chutes.py +153 -0
- llms/extensions/providers/google.py +481 -0
- llms/extensions/providers/nvidia.py +103 -0
- llms/extensions/providers/openai.py +154 -0
- llms/extensions/providers/openrouter.py +74 -0
- llms/extensions/providers/zai.py +182 -0
- llms/extensions/system_prompts/README.md +22 -0
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/ui/index.mjs +280 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +144 -0
- llms/extensions/tools/ui/index.mjs +706 -0
- llms/index.html +36 -62
- llms/llms.json +180 -879
- llms/main.py +3640 -899
- llms/providers-extra.json +394 -0
- llms/providers.json +1 -0
- llms/ui/App.mjs +176 -8
- llms/ui/ai.mjs +156 -20
- llms/ui/app.css +3161 -244
- llms/ui/ctx.mjs +412 -0
- llms/ui/index.mjs +131 -0
- llms/ui/lib/chart.js +14 -0
- llms/ui/lib/charts.mjs +16 -0
- llms/ui/lib/color.js +14 -0
- llms/ui/lib/highlight.min.mjs +1243 -0
- llms/ui/lib/idb.min.mjs +8 -0
- llms/ui/lib/marked.min.mjs +8 -0
- llms/ui/lib/servicestack-client.mjs +1 -0
- llms/ui/lib/servicestack-vue.mjs +37 -0
- llms/ui/lib/vue-router.min.mjs +6 -0
- llms/ui/lib/vue.min.mjs +13 -0
- llms/ui/lib/vue.mjs +18530 -0
- llms/ui/markdown.mjs +25 -14
- llms/ui/modules/chat/ChatBody.mjs +976 -0
- llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
- llms/ui/modules/chat/index.mjs +991 -0
- llms/ui/modules/icons.mjs +46 -0
- llms/ui/modules/layout.mjs +271 -0
- llms/ui/modules/model-selector.mjs +811 -0
- llms/ui/tailwind.input.css +550 -78
- llms/ui/typography.css +54 -36
- llms/ui/utils.mjs +197 -92
- llms_py-3.0.10.dist-info/METADATA +49 -0
- llms_py-3.0.10.dist-info/RECORD +177 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
- llms/ui/Avatar.mjs +0 -28
- llms/ui/Brand.mjs +0 -34
- llms/ui/ChatPrompt.mjs +0 -443
- llms/ui/Main.mjs +0 -740
- llms/ui/ModelSelector.mjs +0 -60
- llms/ui/ProviderIcon.mjs +0 -29
- llms/ui/ProviderStatus.mjs +0 -105
- llms/ui/SignIn.mjs +0 -64
- llms/ui/SystemPromptEditor.mjs +0 -31
- llms/ui/SystemPromptSelector.mjs +0 -36
- llms/ui/Welcome.mjs +0 -8
- llms/ui/threadStore.mjs +0 -524
- llms/ui.json +0 -1069
- llms_py-2.0.20.dist-info/METADATA +0 -931
- llms_py-2.0.20.dist-info/RECORD +0 -36
- {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/top_level.txt +0 -0
llms/ui/ChatPrompt.mjs
DELETED
|
@@ -1,443 +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 hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
|
|
15
|
-
const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
|
|
16
|
-
const hasFile = () => attachedFiles.value.length > 0
|
|
17
|
-
// const hasText = () => !hasImage() && !hasAudio() && !hasFile()
|
|
18
|
-
|
|
19
|
-
function reset() {
|
|
20
|
-
// Ensure initial state is ready to accept input
|
|
21
|
-
isGenerating.value = false
|
|
22
|
-
attachedFiles.value = []
|
|
23
|
-
messageText.value = ''
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
messageText,
|
|
28
|
-
attachedFiles,
|
|
29
|
-
errorStatus,
|
|
30
|
-
isGenerating,
|
|
31
|
-
get generating() {
|
|
32
|
-
return isGenerating.value
|
|
33
|
-
},
|
|
34
|
-
hasImage,
|
|
35
|
-
hasAudio,
|
|
36
|
-
hasFile,
|
|
37
|
-
// hasText,
|
|
38
|
-
reset,
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export default {
|
|
43
|
-
template:`
|
|
44
|
-
<div class="mx-auto max-w-3xl">
|
|
45
|
-
<SettingsDialog :isOpen="showSettings" @close="showSettings = false" />
|
|
46
|
-
<div class="flex space-x-2">
|
|
47
|
-
<!-- Attach (+) button and Settings button -->
|
|
48
|
-
<div class="mt-1.5 flex flex-col space-y-1 items-center">
|
|
49
|
-
<div>
|
|
50
|
-
<button type="button"
|
|
51
|
-
@click="triggerFilePicker"
|
|
52
|
-
:disabled="isGenerating || !model"
|
|
53
|
-
class="size-8 flex items-center justify-center rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed"
|
|
54
|
-
title="Attach image or audio">
|
|
55
|
-
<svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
|
|
56
|
-
<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>
|
|
57
|
-
</svg>
|
|
58
|
-
</button>
|
|
59
|
-
<!-- Hidden file input -->
|
|
60
|
-
<input ref="fileInput" type="file" multiple @change="onFilesSelected"
|
|
61
|
-
class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
62
|
-
/>
|
|
63
|
-
</div>
|
|
64
|
-
<div>
|
|
65
|
-
<button type="button" title="Settings" @click="showSettings = true"
|
|
66
|
-
:disabled="isGenerating || !model"
|
|
67
|
-
class="size-8 flex items-center justify-center rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed">
|
|
68
|
-
<svg class="size-4 text-gray-600 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>
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
<div class="flex-1">
|
|
74
|
-
<div class="relative">
|
|
75
|
-
<textarea
|
|
76
|
-
ref="messageInput"
|
|
77
|
-
v-model="messageText"
|
|
78
|
-
@keydown.enter.exact.prevent="sendMessage"
|
|
79
|
-
@keydown.enter.shift.exact="addNewLine"
|
|
80
|
-
placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
|
|
81
|
-
rows="3"
|
|
82
|
-
class="block w-full rounded-md border border-gray-300 px-3 py-2 pr-12 text-sm placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
83
|
-
:disabled="isGenerating || !model"
|
|
84
|
-
></textarea>
|
|
85
|
-
<button title="Send (Enter)" type="button"
|
|
86
|
-
@click="sendMessage"
|
|
87
|
-
:disabled="!messageText.trim() || isGenerating || !model"
|
|
88
|
-
class="absolute bottom-2 right-2 size-8 flex items-center justify-center rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed disabled:border-gray-200 transition-colors">
|
|
89
|
-
<svg v-if="isGenerating" class="size-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
90
|
-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
91
|
-
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
92
|
-
</svg>
|
|
93
|
-
<svg v-else 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>
|
|
94
|
-
</button>
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<!-- Attached files preview -->
|
|
98
|
-
<div v-if="attachedFiles.length" class="mt-2 flex flex-wrap gap-2">
|
|
99
|
-
<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 text-xs text-gray-700 bg-gray-50">
|
|
100
|
-
<span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
|
|
101
|
-
<button type="button" class="text-gray-500 hover:text-gray-700" @click="removeAttachment(i)" title="Remove Attachment">
|
|
102
|
-
<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>
|
|
103
|
-
</button>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<div v-if="!model" class="mt-2 text-sm text-red-600">
|
|
108
|
-
Please select a model
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
`,
|
|
114
|
-
props: {
|
|
115
|
-
model: {
|
|
116
|
-
type: String,
|
|
117
|
-
default: ''
|
|
118
|
-
},
|
|
119
|
-
systemPrompt: {
|
|
120
|
-
type: String,
|
|
121
|
-
default: ''
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
setup(props) {
|
|
125
|
-
const ai = inject('ai')
|
|
126
|
-
const chatSettings = inject('chatSettings')
|
|
127
|
-
const router = useRouter()
|
|
128
|
-
const config = inject('config')
|
|
129
|
-
const chatPrompt = inject('chatPrompt')
|
|
130
|
-
const {
|
|
131
|
-
messageText,
|
|
132
|
-
attachedFiles,
|
|
133
|
-
isGenerating,
|
|
134
|
-
errorStatus,
|
|
135
|
-
hasImage,
|
|
136
|
-
hasAudio,
|
|
137
|
-
hasFile
|
|
138
|
-
} = chatPrompt
|
|
139
|
-
const threads = inject('threads')
|
|
140
|
-
const {
|
|
141
|
-
currentThread,
|
|
142
|
-
} = threads
|
|
143
|
-
|
|
144
|
-
const fileInput = ref(null)
|
|
145
|
-
const showSettings = ref(false)
|
|
146
|
-
const { applySettings } = chatSettings
|
|
147
|
-
|
|
148
|
-
// File attachments (+) handlers
|
|
149
|
-
const triggerFilePicker = () => {
|
|
150
|
-
if (fileInput.value) fileInput.value.click()
|
|
151
|
-
}
|
|
152
|
-
const onFilesSelected = (e) => {
|
|
153
|
-
const files = Array.from(e.target?.files || [])
|
|
154
|
-
if (files.length) attachedFiles.value.push(...files)
|
|
155
|
-
// allow re-selecting the same file
|
|
156
|
-
if (fileInput.value) fileInput.value.value = ''
|
|
157
|
-
|
|
158
|
-
if (!messageText.value.trim()) {
|
|
159
|
-
if (hasImage()) {
|
|
160
|
-
messageText.value = getTextContent(config.defaults.image)
|
|
161
|
-
} else if (hasAudio()) {
|
|
162
|
-
messageText.value = getTextContent(config.defaults.audio)
|
|
163
|
-
} else {
|
|
164
|
-
messageText.value = getTextContent(config.defaults.file)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const removeAttachment = (i) => {
|
|
169
|
-
attachedFiles.value.splice(i, 1)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function createChatRequest() {
|
|
173
|
-
if (hasImage()) {
|
|
174
|
-
return deepClone(config.defaults.image)
|
|
175
|
-
}
|
|
176
|
-
if (hasAudio()) {
|
|
177
|
-
return deepClone(config.defaults.audio)
|
|
178
|
-
}
|
|
179
|
-
if (attachedFiles.value.length) {
|
|
180
|
-
return deepClone(config.defaults.file)
|
|
181
|
-
}
|
|
182
|
-
const text = deepClone(config.defaults.text)
|
|
183
|
-
return text
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function getTextContent(chat) {
|
|
187
|
-
const textMessage = chat.messages.find(m =>
|
|
188
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
|
|
189
|
-
return textMessage?.content.find(c => c.type === 'text')?.text || ''
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Send message
|
|
193
|
-
const sendMessage = async () => {
|
|
194
|
-
if (!messageText.value.trim() || isGenerating.value || !props.model) return
|
|
195
|
-
|
|
196
|
-
// Clear any existing error message
|
|
197
|
-
errorStatus.value = null
|
|
198
|
-
|
|
199
|
-
let message = messageText.value.trim()
|
|
200
|
-
if (attachedFiles.value.length) {
|
|
201
|
-
const names = attachedFiles.value.map(f => f.name).join(', ')
|
|
202
|
-
const mediaType = imageExts.some(ext => names.includes(ext))
|
|
203
|
-
? '🖼️'
|
|
204
|
-
: audioExts.some(ext => names.includes(ext))
|
|
205
|
-
? '🔉'
|
|
206
|
-
: '📎'
|
|
207
|
-
message += `\n\n[${mediaType} ${names}]`
|
|
208
|
-
}
|
|
209
|
-
messageText.value = ''
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
let threadId
|
|
213
|
-
|
|
214
|
-
// Create thread if none exists
|
|
215
|
-
if (!currentThread.value) {
|
|
216
|
-
const newThread = await threads.createThread('New Chat', props.model, props.systemPrompt)
|
|
217
|
-
threadId = newThread.id
|
|
218
|
-
// Navigate to the new thread URL
|
|
219
|
-
router.push(`${ai.base}/c/${newThread.id}`)
|
|
220
|
-
} else {
|
|
221
|
-
threadId = currentThread.value.id
|
|
222
|
-
// Update the existing thread's model and systemPrompt to match current selection
|
|
223
|
-
await threads.updateThread(threadId, {
|
|
224
|
-
model: props.model.id,
|
|
225
|
-
info: toModelInfo(props.model),
|
|
226
|
-
systemPrompt: props.systemPrompt
|
|
227
|
-
})
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Get the thread to check for duplicates
|
|
231
|
-
let thread = await threads.getThread(threadId)
|
|
232
|
-
const lastMessage = thread.messages[thread.messages.length - 1]
|
|
233
|
-
const isDuplicate = lastMessage && lastMessage.role === 'user' && lastMessage.content === message
|
|
234
|
-
|
|
235
|
-
// Add user message only if it's not a duplicate
|
|
236
|
-
if (!isDuplicate) {
|
|
237
|
-
await threads.addMessageToThread(threadId, {
|
|
238
|
-
role: 'user',
|
|
239
|
-
content: message
|
|
240
|
-
})
|
|
241
|
-
// Reload thread after adding message
|
|
242
|
-
thread = await threads.getThread(threadId)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
isGenerating.value = true
|
|
246
|
-
const messages = [...thread.messages]
|
|
247
|
-
|
|
248
|
-
// Add system prompt if present
|
|
249
|
-
if (props.systemPrompt?.trim()) {
|
|
250
|
-
messages.unshift({
|
|
251
|
-
role: 'system',
|
|
252
|
-
content: [
|
|
253
|
-
{ type: 'text', text: props.systemPrompt }
|
|
254
|
-
]
|
|
255
|
-
})
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const chatRequest = createChatRequest()
|
|
259
|
-
chatRequest.model = props.model.id
|
|
260
|
-
|
|
261
|
-
// Apply user settings
|
|
262
|
-
applySettings(chatRequest)
|
|
263
|
-
|
|
264
|
-
console.debug('chatRequest', chatRequest, hasImage(), hasAudio(), attachedFiles.value.length, attachedFiles.value)
|
|
265
|
-
|
|
266
|
-
function setContentText(chatRequest, text) {
|
|
267
|
-
// Replace text message
|
|
268
|
-
const textImage = chatRequest.messages.find(m =>
|
|
269
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
|
|
270
|
-
for (const c of textImage.content) {
|
|
271
|
-
if (c.type === 'text') {
|
|
272
|
-
c.text = text
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (hasImage()) {
|
|
278
|
-
const imageMessage = chatRequest.messages.find(m =>
|
|
279
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'image_url'))
|
|
280
|
-
console.debug('hasImage', chatRequest, imageMessage)
|
|
281
|
-
if (imageMessage) {
|
|
282
|
-
const imgs = []
|
|
283
|
-
let imagePart = deepClone(imageMessage.content.find(c => c.type === 'image_url'))
|
|
284
|
-
for (const f of attachedFiles.value) {
|
|
285
|
-
if (imageExts.includes(lastRightPart(f.name, '.'))) {
|
|
286
|
-
imagePart.image_url.url = await fileToDataUri(f)
|
|
287
|
-
}
|
|
288
|
-
imgs.push(imagePart)
|
|
289
|
-
}
|
|
290
|
-
imageMessage.content = imageMessage.content.filter(c => c.type !== 'image_url')
|
|
291
|
-
imageMessage.content = [...imgs, ...imageMessage.content]
|
|
292
|
-
setContentText(chatRequest, message)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
} else if (hasAudio()) {
|
|
296
|
-
console.debug('hasAudio', chatRequest)
|
|
297
|
-
const audioMessage = chatRequest.messages.find(m =>
|
|
298
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'input_audio'))
|
|
299
|
-
if (audioMessage) {
|
|
300
|
-
const audios = []
|
|
301
|
-
let audioPart = deepClone(audioMessage.content.find(c => c.type === 'input_audio'))
|
|
302
|
-
for (const f of attachedFiles.value) {
|
|
303
|
-
if (audioExts.includes(lastRightPart(f.name, '.'))) {
|
|
304
|
-
audioPart.input_audio.data = await fileToBase64(f)
|
|
305
|
-
}
|
|
306
|
-
audios.push(audioPart)
|
|
307
|
-
}
|
|
308
|
-
audioMessage.content = audioMessage.content.filter(c => c.type !== 'input_audio')
|
|
309
|
-
audioMessage.content = [...audios, ...audioMessage.content]
|
|
310
|
-
setContentText(chatRequest, message)
|
|
311
|
-
}
|
|
312
|
-
} else if (attachedFiles.value.length) {
|
|
313
|
-
console.debug('hasFile', chatRequest)
|
|
314
|
-
const fileMessage = chatRequest.messages.find(m =>
|
|
315
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'file'))
|
|
316
|
-
if (fileMessage) {
|
|
317
|
-
const files = []
|
|
318
|
-
let filePart = deepClone(fileMessage.content.find(c => c.type === 'file'))
|
|
319
|
-
for (const f of attachedFiles.value) {
|
|
320
|
-
filePart.file.file_data = await fileToDataUri(f)
|
|
321
|
-
filePart.file.filename = f.name
|
|
322
|
-
files.push(filePart)
|
|
323
|
-
}
|
|
324
|
-
fileMessage.content = fileMessage.content.filter(c => c.type !== 'file')
|
|
325
|
-
fileMessage.content = [...files, ...fileMessage.content]
|
|
326
|
-
setContentText(chatRequest, message)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
} else {
|
|
330
|
-
console.debug('hasText', chatRequest)
|
|
331
|
-
// Chat template message needs to be empty
|
|
332
|
-
chatRequest.messages = []
|
|
333
|
-
messages.forEach(m => chatRequest.messages.push({
|
|
334
|
-
role: m.role,
|
|
335
|
-
content: typeof m.content === 'string'
|
|
336
|
-
? [{ type: 'text', text: m.content }]
|
|
337
|
-
: m.content
|
|
338
|
-
}))
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Send to API
|
|
342
|
-
console.debug('chatRequest', chatRequest)
|
|
343
|
-
const startTime = Date.now()
|
|
344
|
-
const response = await ai.post('/v1/chat/completions', {
|
|
345
|
-
body: JSON.stringify(chatRequest)
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
let result = null
|
|
349
|
-
if (!response.ok) {
|
|
350
|
-
errorStatus.value = {
|
|
351
|
-
errorCode: `HTTP ${response.status} ${response.statusText}`,
|
|
352
|
-
message: null,
|
|
353
|
-
stackTrace: null
|
|
354
|
-
}
|
|
355
|
-
let errorBody = null
|
|
356
|
-
try {
|
|
357
|
-
errorBody = await response.text()
|
|
358
|
-
if (errorBody) {
|
|
359
|
-
// Try to parse as JSON for better formatting
|
|
360
|
-
try {
|
|
361
|
-
const errorJson = JSON.parse(errorBody)
|
|
362
|
-
const status = errorJson?.responseStatus
|
|
363
|
-
if (status) {
|
|
364
|
-
errorStatus.value.errorCode += ` ${status.errorCode}`
|
|
365
|
-
errorStatus.value.message = status.message
|
|
366
|
-
errorStatus.value.stackTrace = status.stackTrace
|
|
367
|
-
} else {
|
|
368
|
-
errorStatus.value.stackTrace = JSON.stringify(errorJson, null, 2)
|
|
369
|
-
}
|
|
370
|
-
} catch (e) {
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
} catch (e) {
|
|
374
|
-
// If we can't read the response body, just use the status
|
|
375
|
-
}
|
|
376
|
-
} else {
|
|
377
|
-
try {
|
|
378
|
-
result = await response.json()
|
|
379
|
-
console.debug('chatResponse', JSON.stringify(result, null, 2))
|
|
380
|
-
} catch (e) {
|
|
381
|
-
errorStatus.value = {
|
|
382
|
-
errorCode: 'Error',
|
|
383
|
-
message: e.message,
|
|
384
|
-
stackTrace: null
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (result?.error) {
|
|
390
|
-
errorStatus.value ??= {
|
|
391
|
-
errorCode: 'Error',
|
|
392
|
-
}
|
|
393
|
-
errorStatus.value.message = result.error
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (!errorStatus.value) {
|
|
397
|
-
// Add assistant response (save entire message including reasoning)
|
|
398
|
-
const assistantMessage = result.choices?.[0]?.message
|
|
399
|
-
|
|
400
|
-
const usage = result.usage
|
|
401
|
-
if (usage) {
|
|
402
|
-
if (result.metadata?.pricing) {
|
|
403
|
-
const [ input, output ] = result.metadata.pricing.split('/')
|
|
404
|
-
usage.duration = result.metadata.duration ?? (Date.now() - startTime)
|
|
405
|
-
usage.input = input
|
|
406
|
-
usage.output = output
|
|
407
|
-
usage.tokens = usage.completion_tokens
|
|
408
|
-
usage.price = usage.output
|
|
409
|
-
usage.cost = tokenCost(usage.prompt_tokens * parseFloat(input) + usage.completion_tokens * parseFloat(output))
|
|
410
|
-
}
|
|
411
|
-
await threads.logRequest(threadId, props.model, chatRequest, result)
|
|
412
|
-
}
|
|
413
|
-
await threads.addMessageToThread(threadId, assistantMessage, usage)
|
|
414
|
-
|
|
415
|
-
nextTick(addCopyButtons)
|
|
416
|
-
|
|
417
|
-
attachedFiles.value = []
|
|
418
|
-
// Error will be cleared when user sends next message (no auto-timeout)
|
|
419
|
-
}
|
|
420
|
-
} finally {
|
|
421
|
-
isGenerating.value = false
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const addNewLine = () => {
|
|
426
|
-
// Enter key already adds new line
|
|
427
|
-
//messageText.value += '\n'
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
isGenerating,
|
|
432
|
-
attachedFiles,
|
|
433
|
-
messageText,
|
|
434
|
-
fileInput,
|
|
435
|
-
showSettings,
|
|
436
|
-
triggerFilePicker,
|
|
437
|
-
onFilesSelected,
|
|
438
|
-
removeAttachment,
|
|
439
|
-
sendMessage,
|
|
440
|
-
addNewLine,
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|