llms-py 2.0.9__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 +4 -0
- llms/__main__.py +9 -0
- llms/db.py +359 -0
- llms/extensions/analytics/ui/index.mjs +1444 -0
- llms/extensions/app/README.md +20 -0
- llms/extensions/app/__init__.py +589 -0
- llms/extensions/app/db.py +536 -0
- {llms_py-2.0.9.data/data → llms/extensions/app}/ui/Recents.mjs +100 -73
- llms_py-2.0.9.data/data/ui/Sidebar.mjs → llms/extensions/app/ui/index.mjs +150 -79
- 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 +58 -0
- llms/llms.json +400 -0
- llms/main.py +4407 -0
- llms/providers-extra.json +394 -0
- llms/providers.json +1 -0
- llms/ui/App.mjs +188 -0
- llms/ui/ai.mjs +217 -0
- llms/ui/app.css +7081 -0
- 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/servicestack-vue.mjs +37 -0
- llms/ui/lib/vue.min.mjs +13 -0
- llms/ui/lib/vue.mjs +18530 -0
- {llms_py-2.0.9.data/data → llms}/ui/markdown.mjs +33 -15
- llms/ui/modules/chat/ChatBody.mjs +976 -0
- llms/ui/modules/chat/SettingsDialog.mjs +374 -0
- 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 +742 -0
- {llms_py-2.0.9.data/data → llms}/ui/typography.css +133 -7
- llms/ui/utils.mjs +261 -0
- llms_py-3.0.10.dist-info/METADATA +49 -0
- llms_py-3.0.10.dist-info/RECORD +177 -0
- llms_py-3.0.10.dist-info/entry_points.txt +2 -0
- {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
- llms.py +0 -1402
- llms_py-2.0.9.data/data/index.html +0 -64
- llms_py-2.0.9.data/data/llms.json +0 -447
- llms_py-2.0.9.data/data/requirements.txt +0 -1
- llms_py-2.0.9.data/data/ui/App.mjs +0 -20
- llms_py-2.0.9.data/data/ui/ChatPrompt.mjs +0 -389
- llms_py-2.0.9.data/data/ui/Main.mjs +0 -680
- llms_py-2.0.9.data/data/ui/app.css +0 -3951
- llms_py-2.0.9.data/data/ui/lib/servicestack-vue.min.mjs +0 -37
- llms_py-2.0.9.data/data/ui/lib/vue.min.mjs +0 -12
- llms_py-2.0.9.data/data/ui/tailwind.input.css +0 -261
- llms_py-2.0.9.data/data/ui/threadStore.mjs +0 -273
- llms_py-2.0.9.data/data/ui/utils.mjs +0 -114
- llms_py-2.0.9.data/data/ui.json +0 -1069
- llms_py-2.0.9.dist-info/METADATA +0 -941
- llms_py-2.0.9.dist-info/RECORD +0 -30
- llms_py-2.0.9.dist-info/entry_points.txt +0 -2
- {llms_py-2.0.9.data/data → llms}/ui/fav.svg +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/highlight.min.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/idb.min.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/marked.min.mjs +0 -0
- /llms_py-2.0.9.data/data/ui/lib/servicestack-client.min.mjs → /llms/ui/lib/servicestack-client.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/vue-router.min.mjs +0 -0
- {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
- {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/top_level.txt +0 -0
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import { ref, nextTick, inject } from 'vue'
|
|
2
|
-
import { useRouter } from 'vue-router'
|
|
3
|
-
import { lastRightPart } from '@servicestack/client'
|
|
4
|
-
import { deepClone, fileToDataUri, fileToBase64, addCopyButtons } 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 errorMessage = 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
|
-
}
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
messageText,
|
|
29
|
-
attachedFiles,
|
|
30
|
-
errorStatus,
|
|
31
|
-
errorMessage,
|
|
32
|
-
isGenerating,
|
|
33
|
-
get generating() {
|
|
34
|
-
return isGenerating.value
|
|
35
|
-
},
|
|
36
|
-
hasImage,
|
|
37
|
-
hasAudio,
|
|
38
|
-
hasFile,
|
|
39
|
-
// hasText,
|
|
40
|
-
reset,
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export default {
|
|
45
|
-
template:`
|
|
46
|
-
<div class="mx-auto max-w-3xl">
|
|
47
|
-
<div class="flex space-x-3">
|
|
48
|
-
<!-- Attach (+) button -->
|
|
49
|
-
<div>
|
|
50
|
-
<button type="button"
|
|
51
|
-
@click="triggerFilePicker"
|
|
52
|
-
:disabled="isGenerating || !model"
|
|
53
|
-
class="mt-2 h-10 w-10 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="w-5 h-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">
|
|
56
|
-
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
57
|
-
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
58
|
-
</svg>
|
|
59
|
-
</button>
|
|
60
|
-
<!-- Hidden file input -->
|
|
61
|
-
<input ref="fileInput" type="file" multiple @change="onFilesSelected"
|
|
62
|
-
class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
63
|
-
/>
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
<div class="flex-1">
|
|
67
|
-
<textarea
|
|
68
|
-
ref="messageInput"
|
|
69
|
-
v-model="messageText"
|
|
70
|
-
@keydown.enter.exact.prevent="sendMessage"
|
|
71
|
-
@keydown.enter.shift.exact="addNewLine"
|
|
72
|
-
placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
|
|
73
|
-
rows="2"
|
|
74
|
-
class="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
75
|
-
:disabled="isGenerating || !model"
|
|
76
|
-
></textarea>
|
|
77
|
-
|
|
78
|
-
<!-- Attached files preview -->
|
|
79
|
-
<div v-if="attachedFiles.length" class="mt-2 flex flex-wrap gap-2">
|
|
80
|
-
<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">
|
|
81
|
-
<span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
|
|
82
|
-
<button type="button" class="text-gray-500 hover:text-gray-700" @click="removeAttachment(i)" title="Remove Attachment">
|
|
83
|
-
<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>
|
|
84
|
-
</button>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
|
|
88
|
-
<div v-if="!model" class="mt-2 text-sm text-red-600">
|
|
89
|
-
Please select a model
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
<div>
|
|
94
|
-
<button title="Send (Enter)" type="button"
|
|
95
|
-
@click="sendMessage"
|
|
96
|
-
:disabled="!messageText.trim() || isGenerating || !model"
|
|
97
|
-
class="mt-2 p-2 flex items-center justify-center rounded-full bg-gray-700 text-white transition-colors hover:opacity-70 focus-visible:outline-none focus-visible:outline-black disabled:bg-[#D7D7D7] disabled:text-[#f4f4f4] disabled:hover:opacity-100 dark:bg-white dark:text-black dark:focus-visible:outline-white disabled:dark:bg-token-text-quaternary dark:disabled:text-token-main-surface-secondary">
|
|
98
|
-
<svg v-if="isGenerating" class="size-6 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
99
|
-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
100
|
-
<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>
|
|
101
|
-
</svg>
|
|
102
|
-
<svg v-else class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="m3.165 19.503l7.362-16.51c.59-1.324 2.355-1.324 2.946 0l7.362 16.51c.667 1.495-.814 3.047-2.202 2.306l-5.904-3.152c-.459-.245-1-.245-1.458 0l-5.904 3.152c-1.388.74-2.87-.81-2.202-2.306"></path></svg>
|
|
103
|
-
</button>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
`,
|
|
108
|
-
props: {
|
|
109
|
-
model: {
|
|
110
|
-
type: String,
|
|
111
|
-
default: ''
|
|
112
|
-
},
|
|
113
|
-
systemPrompt: {
|
|
114
|
-
type: String,
|
|
115
|
-
default: ''
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
setup(props) {
|
|
119
|
-
const router = useRouter()
|
|
120
|
-
const config = inject('config')
|
|
121
|
-
const chatPrompt = inject('chatPrompt')
|
|
122
|
-
const {
|
|
123
|
-
messageText,
|
|
124
|
-
attachedFiles,
|
|
125
|
-
isGenerating,
|
|
126
|
-
errorStatus,
|
|
127
|
-
errorMessage,
|
|
128
|
-
hasImage,
|
|
129
|
-
hasAudio,
|
|
130
|
-
hasFile
|
|
131
|
-
} = chatPrompt
|
|
132
|
-
const threads = inject('threads')
|
|
133
|
-
const {
|
|
134
|
-
currentThread,
|
|
135
|
-
} = threads
|
|
136
|
-
|
|
137
|
-
const fileInput = ref(null)
|
|
138
|
-
|
|
139
|
-
// File attachments (+) handlers
|
|
140
|
-
const triggerFilePicker = () => {
|
|
141
|
-
if (fileInput.value) fileInput.value.click()
|
|
142
|
-
}
|
|
143
|
-
const onFilesSelected = (e) => {
|
|
144
|
-
const files = Array.from(e.target?.files || [])
|
|
145
|
-
if (files.length) attachedFiles.value.push(...files)
|
|
146
|
-
// allow re-selecting the same file
|
|
147
|
-
if (fileInput.value) fileInput.value.value = ''
|
|
148
|
-
|
|
149
|
-
if (!messageText.value.trim()) {
|
|
150
|
-
if (hasImage()) {
|
|
151
|
-
messageText.value = getTextContent(config.defaults.image)
|
|
152
|
-
} else if (hasAudio()) {
|
|
153
|
-
messageText.value = getTextContent(config.defaults.audio)
|
|
154
|
-
} else {
|
|
155
|
-
messageText.value = getTextContent(config.defaults.file)
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const removeAttachment = (i) => {
|
|
160
|
-
attachedFiles.value.splice(i, 1)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function createChatRequest() {
|
|
164
|
-
if (hasImage()) {
|
|
165
|
-
return deepClone(config.defaults.image)
|
|
166
|
-
}
|
|
167
|
-
if (hasAudio()) {
|
|
168
|
-
return deepClone(config.defaults.audio)
|
|
169
|
-
}
|
|
170
|
-
if (attachedFiles.value.length) {
|
|
171
|
-
return deepClone(config.defaults.file)
|
|
172
|
-
}
|
|
173
|
-
const text = deepClone(config.defaults.text)
|
|
174
|
-
return text
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function getTextContent(chat) {
|
|
178
|
-
const textMessage = chat.messages.find(m =>
|
|
179
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
|
|
180
|
-
return textMessage?.content.find(c => c.type === 'text')?.text || ''
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Send message
|
|
184
|
-
const sendMessage = async () => {
|
|
185
|
-
if (!messageText.value.trim() || isGenerating.value || !props.model) return
|
|
186
|
-
|
|
187
|
-
// Clear any existing error message
|
|
188
|
-
errorStatus.value = errorMessage.value = null
|
|
189
|
-
|
|
190
|
-
let message = messageText.value.trim()
|
|
191
|
-
if (attachedFiles.value.length) {
|
|
192
|
-
const names = attachedFiles.value.map(f => f.name).join(', ')
|
|
193
|
-
const mediaType = imageExts.some(ext => names.includes(ext))
|
|
194
|
-
? '🖼️'
|
|
195
|
-
: audioExts.some(ext => names.includes(ext))
|
|
196
|
-
? '🔉'
|
|
197
|
-
: '📎'
|
|
198
|
-
message += `\n\n[${mediaType} ${names}]`
|
|
199
|
-
}
|
|
200
|
-
messageText.value = ''
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
let threadId
|
|
204
|
-
|
|
205
|
-
// Create thread if none exists
|
|
206
|
-
if (!currentThread.value) {
|
|
207
|
-
const newThread = await threads.createThread('New Chat', props.model, props.systemPrompt)
|
|
208
|
-
threadId = newThread.id
|
|
209
|
-
// Navigate to the new thread URL
|
|
210
|
-
router.push(`/c/${newThread.id}`)
|
|
211
|
-
} else {
|
|
212
|
-
threadId = currentThread.value.id
|
|
213
|
-
// Update the existing thread's model and systemPrompt to match current selection
|
|
214
|
-
await threads.updateThread(threadId, {
|
|
215
|
-
model: props.model,
|
|
216
|
-
systemPrompt: props.systemPrompt
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Add user message
|
|
221
|
-
await threads.addMessageToThread(threadId, {
|
|
222
|
-
role: 'user',
|
|
223
|
-
content: message
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
isGenerating.value = true
|
|
227
|
-
|
|
228
|
-
// Get the updated thread to prepare chat request
|
|
229
|
-
const thread = await threads.getThread(threadId)
|
|
230
|
-
const messages = [...thread.messages]
|
|
231
|
-
|
|
232
|
-
// Add system prompt if present
|
|
233
|
-
if (props.systemPrompt?.trim()) {
|
|
234
|
-
messages.unshift({
|
|
235
|
-
role: 'system',
|
|
236
|
-
content: props.systemPrompt.trim()
|
|
237
|
-
})
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const chatRequest = createChatRequest()
|
|
241
|
-
chatRequest.model = props.model
|
|
242
|
-
|
|
243
|
-
console.log('chatRequest', chatRequest, hasImage(), hasAudio(), attachedFiles.value.length, attachedFiles.value)
|
|
244
|
-
|
|
245
|
-
function setContentText(chatRequest, text) {
|
|
246
|
-
// Replace text message
|
|
247
|
-
const textImage = chatRequest.messages.find(m =>
|
|
248
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
|
|
249
|
-
for (const c of textImage.content) {
|
|
250
|
-
if (c.type === 'text') {
|
|
251
|
-
c.text = text
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (hasImage()) {
|
|
257
|
-
const imageMessage = chatRequest.messages.find(m =>
|
|
258
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'image_url'))
|
|
259
|
-
console.log('hasImage', chatRequest, imageMessage)
|
|
260
|
-
if (imageMessage) {
|
|
261
|
-
const imgs = []
|
|
262
|
-
let imagePart = deepClone(imageMessage.content.find(c => c.type === 'image_url'))
|
|
263
|
-
for (const f of attachedFiles.value) {
|
|
264
|
-
if (imageExts.includes(lastRightPart(f.name, '.'))) {
|
|
265
|
-
imagePart.image_url.url = await fileToDataUri(f)
|
|
266
|
-
}
|
|
267
|
-
imgs.push(imagePart)
|
|
268
|
-
}
|
|
269
|
-
imageMessage.content = imageMessage.content.filter(c => c.type !== 'image_url')
|
|
270
|
-
imageMessage.content = [...imgs, ...imageMessage.content]
|
|
271
|
-
setContentText(chatRequest, message)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
} else if (hasAudio()) {
|
|
275
|
-
console.log('hasAudio', chatRequest)
|
|
276
|
-
const audioMessage = chatRequest.messages.find(m =>
|
|
277
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'input_audio'))
|
|
278
|
-
if (audioMessage) {
|
|
279
|
-
const audios = []
|
|
280
|
-
let audioPart = deepClone(audioMessage.content.find(c => c.type === 'input_audio'))
|
|
281
|
-
for (const f of attachedFiles.value) {
|
|
282
|
-
if (audioExts.includes(lastRightPart(f.name, '.'))) {
|
|
283
|
-
audioPart.input_audio.data = await fileToBase64(f)
|
|
284
|
-
}
|
|
285
|
-
audios.push(audioPart)
|
|
286
|
-
}
|
|
287
|
-
audioMessage.content = audioMessage.content.filter(c => c.type !== 'input_audio')
|
|
288
|
-
audioMessage.content = [...audios, ...audioMessage.content]
|
|
289
|
-
setContentText(chatRequest, message)
|
|
290
|
-
}
|
|
291
|
-
} else if (attachedFiles.value.length) {
|
|
292
|
-
console.log('hasFile', chatRequest)
|
|
293
|
-
const fileMessage = chatRequest.messages.find(m =>
|
|
294
|
-
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'file'))
|
|
295
|
-
if (fileMessage) {
|
|
296
|
-
const files = []
|
|
297
|
-
let filePart = deepClone(fileMessage.content.find(c => c.type === 'file'))
|
|
298
|
-
for (const f of attachedFiles.value) {
|
|
299
|
-
filePart.file.file_data = await fileToDataUri(f)
|
|
300
|
-
filePart.file.filename = f.name
|
|
301
|
-
files.push(filePart)
|
|
302
|
-
}
|
|
303
|
-
fileMessage.content = fileMessage.content.filter(c => c.type !== 'file')
|
|
304
|
-
fileMessage.content = [...files, ...fileMessage.content]
|
|
305
|
-
setContentText(chatRequest, message)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
} else {
|
|
309
|
-
console.log('hasText', chatRequest)
|
|
310
|
-
// Chat template message needs to be empty
|
|
311
|
-
chatRequest.messages = []
|
|
312
|
-
messages.forEach(m => chatRequest.messages.push({
|
|
313
|
-
role: m.role,
|
|
314
|
-
content: m.content
|
|
315
|
-
}))
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Send to API
|
|
319
|
-
const response = await fetch('/v1/chat/completions', {
|
|
320
|
-
method: 'POST',
|
|
321
|
-
headers: {
|
|
322
|
-
'Content-Type': 'application/json'
|
|
323
|
-
},
|
|
324
|
-
body: JSON.stringify(chatRequest)
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
if (!response.ok) {
|
|
328
|
-
errorStatus.value = `HTTP ${response.status} ${response.statusText}`
|
|
329
|
-
let errorBody = ''
|
|
330
|
-
try {
|
|
331
|
-
errorBody = await response.text()
|
|
332
|
-
if (errorBody) {
|
|
333
|
-
// Try to parse as JSON for better formatting
|
|
334
|
-
try {
|
|
335
|
-
const errorJson = JSON.parse(errorBody)
|
|
336
|
-
errorBody = JSON.stringify(errorJson, null, 2)
|
|
337
|
-
} catch (e) {
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
} catch (e) {
|
|
341
|
-
// If we can't read the response body, just use the status
|
|
342
|
-
}
|
|
343
|
-
throw new Error(errorBody || '')
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const result = await response.json()
|
|
347
|
-
|
|
348
|
-
if (result.error) {
|
|
349
|
-
throw new Error(result.error)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Add assistant response (save entire message including reasoning)
|
|
353
|
-
const assistantMessage = result.choices?.[0]?.message
|
|
354
|
-
await threads.addMessageToThread(threadId, assistantMessage)
|
|
355
|
-
|
|
356
|
-
nextTick(addCopyButtons)
|
|
357
|
-
|
|
358
|
-
attachedFiles.value = []
|
|
359
|
-
|
|
360
|
-
} catch (error) {
|
|
361
|
-
console.error('Error sending message:', error)
|
|
362
|
-
errorMessage.value = error.message
|
|
363
|
-
|
|
364
|
-
// Error will be cleared when user sends next message (no auto-timeout)
|
|
365
|
-
} finally {
|
|
366
|
-
isGenerating.value = false
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const addNewLine = () => {
|
|
371
|
-
// Enter key already adds new line
|
|
372
|
-
//messageText.value += '\n'
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return {
|
|
376
|
-
isGenerating,
|
|
377
|
-
attachedFiles,
|
|
378
|
-
errorStatus,
|
|
379
|
-
errorMessage,
|
|
380
|
-
messageText,
|
|
381
|
-
fileInput,
|
|
382
|
-
triggerFilePicker,
|
|
383
|
-
onFilesSelected,
|
|
384
|
-
removeAttachment,
|
|
385
|
-
sendMessage,
|
|
386
|
-
addNewLine,
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|