llms-py 2.0.20__py3-none-any.whl → 3.0.18__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 +588 -0
- llms/extensions/app/db.py +540 -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 +440 -0
- llms/extensions/computer/README.md +96 -0
- llms/extensions/computer/__init__.py +59 -0
- llms/extensions/computer/base.py +80 -0
- llms/extensions/computer/bash.py +185 -0
- llms/extensions/computer/computer.py +523 -0
- llms/extensions/computer/edit.py +299 -0
- llms/extensions/computer/filesystem.py +542 -0
- llms/extensions/computer/platform.py +461 -0
- llms/extensions/computer/run.py +37 -0
- llms/extensions/core_tools/CALCULATOR.md +32 -0
- llms/extensions/core_tools/__init__.py +599 -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 +260 -0
- llms/extensions/providers/cerebras.py +36 -0
- llms/extensions/providers/chutes.py +153 -0
- llms/extensions/providers/google.py +559 -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/skills/LICENSE +202 -0
- llms/extensions/skills/__init__.py +130 -0
- llms/extensions/skills/errors.py +25 -0
- llms/extensions/skills/models.py +39 -0
- llms/extensions/skills/parser.py +178 -0
- llms/extensions/skills/ui/index.mjs +376 -0
- llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
- llms/extensions/skills/validator.py +177 -0
- llms/extensions/system_prompts/README.md +22 -0
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/ui/index.mjs +276 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +67 -0
- llms/extensions/tools/ui/index.mjs +837 -0
- llms/index.html +36 -62
- llms/llms.json +180 -879
- llms/main.py +4009 -912
- 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 +3768 -321
- llms/ui/ctx.mjs +459 -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 +1156 -0
- llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
- llms/ui/modules/chat/index.mjs +995 -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 +560 -78
- llms/ui/typography.css +54 -36
- llms/ui/utils.mjs +221 -92
- llms_py-3.0.18.dist-info/METADATA +49 -0
- llms_py-3.0.18.dist-info/RECORD +194 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.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.18.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
|
|
2
|
+
import { ref, watch, computed, nextTick, inject } from 'vue'
|
|
3
|
+
import { $$, createElement, lastRightPart, ApiResult, createErrorStatus } from "@servicestack/client"
|
|
4
|
+
import SettingsDialog, { useSettings } from './SettingsDialog.mjs'
|
|
5
|
+
import { ChatBody, LightboxImage, TypeText, TypeImage, TypeAudio, TypeFile, ViewType, ViewTypes, ViewToolTypes, TextViewer, ToolArguments, ToolOutput, MessageUsage, MessageReasoning } from './ChatBody.mjs'
|
|
6
|
+
import { AppContext } from '../../ctx.mjs'
|
|
7
|
+
|
|
8
|
+
const imageExts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
|
|
9
|
+
const audioExts = 'mp3,wav,ogg,flac,m4a,opus,webm'.split(',')
|
|
10
|
+
|
|
11
|
+
/* Example image generation request: https://openrouter.ai/docs/guides/overview/multimodal/image-generation
|
|
12
|
+
{
|
|
13
|
+
"model": "google/gemini-2.5-flash-image-preview",
|
|
14
|
+
"messages": [
|
|
15
|
+
{
|
|
16
|
+
"role": "user",
|
|
17
|
+
"content": "Create a picture of a nano banana dish in a fancy restaurant with a Gemini theme"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"modalities": ["image", "text"],
|
|
21
|
+
"image_config": {
|
|
22
|
+
"aspect_ratio": "16:9"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
Example response:
|
|
26
|
+
{
|
|
27
|
+
"choices": [
|
|
28
|
+
{
|
|
29
|
+
"message": {
|
|
30
|
+
"role": "assistant",
|
|
31
|
+
"content": "I've generated a beautiful sunset image for you.",
|
|
32
|
+
"images": [
|
|
33
|
+
{
|
|
34
|
+
"type": "image_url",
|
|
35
|
+
"image_url": {
|
|
36
|
+
"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
*/
|
|
45
|
+
const imageAspectRatios = {
|
|
46
|
+
'1024×1024': '1:1',
|
|
47
|
+
'832×1248': '2:3',
|
|
48
|
+
'1248×832': '3:2',
|
|
49
|
+
'864×1184': '3:4',
|
|
50
|
+
'1184×864': '4:3',
|
|
51
|
+
'896×1152': '4:5',
|
|
52
|
+
'1152×896': '5:4',
|
|
53
|
+
'768×1344': '9:16',
|
|
54
|
+
'1344×768': '16:9',
|
|
55
|
+
'1536×672': '21:9',
|
|
56
|
+
}
|
|
57
|
+
// Reverse lookup
|
|
58
|
+
const imageRatioSizes = Object.entries(imageAspectRatios).reduce((acc, [key, value]) => {
|
|
59
|
+
acc[value] = key
|
|
60
|
+
return acc
|
|
61
|
+
}, {})
|
|
62
|
+
|
|
63
|
+
const svg = {
|
|
64
|
+
clipboard: `<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path d="M8 5H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1M8 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M8 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m0 0h2a2 2 0 0 1 2 2v3m2 4H10m0 0l3-3m-3 3l3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>`,
|
|
65
|
+
check: `<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>`,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function copyBlock(btn) {
|
|
69
|
+
// console.log('copyBlock',btn)
|
|
70
|
+
const label = btn.previousElementSibling
|
|
71
|
+
const code = btn.parentElement.nextElementSibling
|
|
72
|
+
label.classList.remove('hidden')
|
|
73
|
+
label.innerHTML = 'copied'
|
|
74
|
+
btn.classList.add('border-gray-600', 'bg-gray-700')
|
|
75
|
+
btn.classList.remove('border-gray-700')
|
|
76
|
+
btn.innerHTML = svg.check
|
|
77
|
+
navigator.clipboard.writeText(code.innerText)
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
label.classList.add('hidden')
|
|
80
|
+
label.innerHTML = ''
|
|
81
|
+
btn.innerHTML = svg.clipboard
|
|
82
|
+
btn.classList.remove('border-gray-600', 'bg-gray-700')
|
|
83
|
+
btn.classList.add('border-gray-700')
|
|
84
|
+
}, 2000)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function addCopyButtonToCodeBlocks(sel) {
|
|
88
|
+
globalThis.copyBlock ??= copyBlock
|
|
89
|
+
//console.log('addCopyButtonToCodeBlocks', sel, [...$$(sel)].length)
|
|
90
|
+
|
|
91
|
+
$$(sel).forEach(code => {
|
|
92
|
+
let pre = code.parentElement;
|
|
93
|
+
if (pre.classList.contains('group')) return
|
|
94
|
+
pre.classList.add('relative', 'group')
|
|
95
|
+
|
|
96
|
+
const div = createElement('div', { attrs: { className: 'opacity-0 group-hover:opacity-100 transition-opacity duration-100 flex absolute right-2 -mt-1 select-none' } })
|
|
97
|
+
const label = createElement('div', { attrs: { className: 'hidden font-sans p-1 px-2 mr-1 rounded-md border border-gray-600 bg-gray-700 text-gray-400' } })
|
|
98
|
+
const btn = createElement('button', {
|
|
99
|
+
attrs: {
|
|
100
|
+
type: 'button',
|
|
101
|
+
className: 'p-1 rounded-md border block text-gray-500 hover:text-gray-400 border-gray-700 hover:border-gray-600',
|
|
102
|
+
onclick: 'copyBlock(this)'
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
btn.innerHTML = svg.clipboard
|
|
106
|
+
div.appendChild(label)
|
|
107
|
+
div.appendChild(btn)
|
|
108
|
+
pre.insertBefore(div, code)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function addCopyButtons() {
|
|
113
|
+
addCopyButtonToCodeBlocks('.prose pre>code')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function useChatPrompt(ctx) {
|
|
117
|
+
const messageText = ref('')
|
|
118
|
+
const promptHistory = ref([])
|
|
119
|
+
const attachedFiles = ref([])
|
|
120
|
+
const hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
|
|
121
|
+
const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
|
|
122
|
+
const hasFile = () => attachedFiles.value.length > 0
|
|
123
|
+
|
|
124
|
+
const editingMessage = ref(null)
|
|
125
|
+
|
|
126
|
+
function reset() {
|
|
127
|
+
// Ensure initial state is ready to accept input
|
|
128
|
+
attachedFiles.value = []
|
|
129
|
+
messageText.value = ''
|
|
130
|
+
editingMessage.value = null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const settings = useSettings()
|
|
134
|
+
|
|
135
|
+
function getModel(name) {
|
|
136
|
+
return ctx.state.models.find(x => x.name === name) ?? ctx.state.models.find(x => x.id === name)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getSelectedModel() {
|
|
140
|
+
const candidates = [ctx.state.selectedModel, ctx.state.config.defaults.text.model]
|
|
141
|
+
const ret = candidates.map(name => name && getModel(name)).find(x => !!x)
|
|
142
|
+
if (!ret) {
|
|
143
|
+
// Try to find a model in the latest threads
|
|
144
|
+
for (const thread in ctx.threads.threads) {
|
|
145
|
+
const model = thread.model && getModel(thread.model)
|
|
146
|
+
if (model) return model
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return ret
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function setSelectedModel(model) {
|
|
153
|
+
ctx.setState({
|
|
154
|
+
selectedModel: model.name
|
|
155
|
+
})
|
|
156
|
+
ctx.setPrefs({
|
|
157
|
+
model: model.name
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getProviderForModel(model) {
|
|
162
|
+
return getModel(model)?.provider
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const canGenerateImage = model => {
|
|
166
|
+
return model?.modalities?.output?.includes('image')
|
|
167
|
+
}
|
|
168
|
+
const canGenerateAudio = model => {
|
|
169
|
+
return model?.modalities?.output?.includes('audio')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function applySettings(request) {
|
|
173
|
+
settings.applySettings(request)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function createContent({ text, files }) {
|
|
177
|
+
let content = []
|
|
178
|
+
|
|
179
|
+
// Add Text Block
|
|
180
|
+
if (text) {
|
|
181
|
+
content.push({ type: 'text', text })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Add Attachment Blocks
|
|
185
|
+
if (Array.isArray(files)) {
|
|
186
|
+
for (const f of files) {
|
|
187
|
+
const ext = lastRightPart(f.name, '.')
|
|
188
|
+
if (imageExts.includes(ext)) {
|
|
189
|
+
content.push({ type: 'image_url', image_url: { url: f.url } })
|
|
190
|
+
} else if (audioExts.includes(ext)) {
|
|
191
|
+
content.push({ type: 'input_audio', input_audio: { data: f.url, format: ext } })
|
|
192
|
+
} else {
|
|
193
|
+
content.push({ type: 'file', file: { file_data: f.url, filename: f.name } })
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return content
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function createRequest({ model, text, files, systemPrompt, aspectRatio }) {
|
|
201
|
+
// Construct API Request from History
|
|
202
|
+
const request = {
|
|
203
|
+
model: model.name,
|
|
204
|
+
messages: [],
|
|
205
|
+
metadata: {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Apply user settings
|
|
209
|
+
applySettings(request)
|
|
210
|
+
|
|
211
|
+
if (systemPrompt) {
|
|
212
|
+
request.messages = request.messages.filter(m => m.role !== 'system')
|
|
213
|
+
request.messages.unshift({
|
|
214
|
+
role: 'system',
|
|
215
|
+
content: systemPrompt
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (canGenerateImage(model)) {
|
|
220
|
+
request.image_config = {
|
|
221
|
+
aspect_ratio: aspectRatio || imageAspectRatios[ctx.state.selectedAspectRatio] || '1:1'
|
|
222
|
+
}
|
|
223
|
+
request.modalities = ["image", "text"]
|
|
224
|
+
}
|
|
225
|
+
else if (canGenerateAudio(model)) {
|
|
226
|
+
request.modalities = ["audio", "text"]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (text) {
|
|
230
|
+
const content = createContent({ text, files })
|
|
231
|
+
request.messages.push({
|
|
232
|
+
role: 'user',
|
|
233
|
+
content
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return request
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function completion({ request, thread, model, controller, redirect }) {
|
|
241
|
+
try {
|
|
242
|
+
let error
|
|
243
|
+
if (!model) {
|
|
244
|
+
if (request.model) {
|
|
245
|
+
model = getModel(request.model)
|
|
246
|
+
} else {
|
|
247
|
+
model = getModel(request.model) ?? getSelectedModel()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!model) {
|
|
252
|
+
return ctx.createErrorResult({ message: `Model ${request.model || ''} not found`, errorCode: 'NotFound' })
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!thread) {
|
|
256
|
+
const title = getTextContent(request) || 'New Chat'
|
|
257
|
+
thread = await ctx.threads.startNewThread({ title, model, redirect })
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const ctxRequest = ctx.createChatContext({ request, thread })
|
|
261
|
+
ctx.chatRequestFilters.forEach(f => f(ctxRequest))
|
|
262
|
+
ctx.completeChatContext(ctxRequest)
|
|
263
|
+
|
|
264
|
+
// Send to API
|
|
265
|
+
const startTime = Date.now()
|
|
266
|
+
const res = await ctx.post('/v1/chat/completions', {
|
|
267
|
+
body: JSON.stringify(request),
|
|
268
|
+
signal: controller?.signal
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
let response = null
|
|
272
|
+
if (!res.ok) {
|
|
273
|
+
error = ctx.createErrorStatus({ message: `HTTP ${res.status} ${res.statusText}` })
|
|
274
|
+
let errorBody = null
|
|
275
|
+
try {
|
|
276
|
+
errorBody = await res.text()
|
|
277
|
+
if (errorBody) {
|
|
278
|
+
// Try to parse as JSON for better formatting
|
|
279
|
+
try {
|
|
280
|
+
const errorJson = JSON.parse(errorBody)
|
|
281
|
+
const status = errorJson?.responseStatus
|
|
282
|
+
if (status) {
|
|
283
|
+
error.errorCode += ` ${status.errorCode}`
|
|
284
|
+
error.message = status.message
|
|
285
|
+
error.stackTrace = status.stackTrace
|
|
286
|
+
} else {
|
|
287
|
+
error.stackTrace = JSON.stringify(errorJson, null, 2)
|
|
288
|
+
}
|
|
289
|
+
} catch (e) {
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch (e) {
|
|
293
|
+
// If we can't read the response body, just use the status
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
try {
|
|
297
|
+
response = await res.json()
|
|
298
|
+
const ctxResponse = {
|
|
299
|
+
response,
|
|
300
|
+
thread,
|
|
301
|
+
}
|
|
302
|
+
ctx.chatResponseFilters.forEach(f => f(ctxResponse))
|
|
303
|
+
console.debug('completion.response', JSON.stringify(response, null, 2))
|
|
304
|
+
} catch (e) {
|
|
305
|
+
error = createErrorStatus(e.message)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (response?.error) {
|
|
310
|
+
error ??= createErrorStatus()
|
|
311
|
+
error.message = response.error
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (error) {
|
|
315
|
+
ctx.chatErrorFilters.forEach(f => f(error))
|
|
316
|
+
return new ApiResult({ error })
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!error) {
|
|
320
|
+
// Add tool history messages if any
|
|
321
|
+
if (response.tool_history && Array.isArray(response.tool_history)) {
|
|
322
|
+
for (const msg of response.tool_history) {
|
|
323
|
+
if (msg.role === 'assistant') {
|
|
324
|
+
msg.model = model.name // tag with model
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
nextTick(addCopyButtons)
|
|
330
|
+
|
|
331
|
+
return new ApiResult({ response })
|
|
332
|
+
}
|
|
333
|
+
} catch (e) {
|
|
334
|
+
console.log('completion.error', e)
|
|
335
|
+
return new ApiResult({ error: createErrorStatus(e.message, 'ChatFailed') })
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function getTextContent(chat) {
|
|
339
|
+
const textMessage = chat.messages.find(m =>
|
|
340
|
+
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
|
|
341
|
+
return textMessage?.content.find(c => c.type === 'text')?.text || ''
|
|
342
|
+
}
|
|
343
|
+
function getAnswer(response) {
|
|
344
|
+
const textMessage = response.choices?.[0]?.message
|
|
345
|
+
return textMessage?.content || ''
|
|
346
|
+
}
|
|
347
|
+
function selectAspectRatio(ratio) {
|
|
348
|
+
const selectedAspectRatio = imageRatioSizes[ratio] || '1024×1024'
|
|
349
|
+
console.log(`selectAspectRatio(${ratio})`, selectedAspectRatio)
|
|
350
|
+
ctx.setState({ selectedAspectRatio })
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function sendUserMessage(text, { model, redirect = true } = {}) {
|
|
354
|
+
ctx.clearError()
|
|
355
|
+
|
|
356
|
+
if (!model) {
|
|
357
|
+
model = getSelectedModel()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let content = createContent({ text, files: attachedFiles.value })
|
|
361
|
+
|
|
362
|
+
let thread
|
|
363
|
+
|
|
364
|
+
// Create thread if none exists
|
|
365
|
+
if (!ctx.threads.currentThread.value) {
|
|
366
|
+
thread = await ctx.threads.startNewThread({ model, redirect })
|
|
367
|
+
} else {
|
|
368
|
+
thread = ctx.threads.currentThread.value
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let threadId = thread.id
|
|
372
|
+
let messages = thread.messages || []
|
|
373
|
+
if (!threadId) {
|
|
374
|
+
console.error('No thread ID found', thread, ctx.threads.currentThread.value)
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Handle Editing / Redo Logic
|
|
379
|
+
const editingMsg = editingMessage.value
|
|
380
|
+
if (editingMsg) {
|
|
381
|
+
let messageIndex = messages.findIndex(m => m.timestamp === editingMsg)
|
|
382
|
+
if (messageIndex == -1) {
|
|
383
|
+
messageIndex = messages.findLastIndex(m => m.role === 'user')
|
|
384
|
+
}
|
|
385
|
+
console.log('Editing message', editingMsg, messageIndex, messages)
|
|
386
|
+
|
|
387
|
+
if (messageIndex >= 0) {
|
|
388
|
+
messages[messageIndex].content = content
|
|
389
|
+
// Truncate messages to only include up to the edited message
|
|
390
|
+
messages.length = messageIndex + 1
|
|
391
|
+
} else {
|
|
392
|
+
messages.push({
|
|
393
|
+
timestamp: new Date().valueOf(),
|
|
394
|
+
role: 'user',
|
|
395
|
+
content,
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
// Regular Send Logic
|
|
400
|
+
const lastMessage = messages[messages.length - 1]
|
|
401
|
+
|
|
402
|
+
// Check duplicate based on text content extracted from potential array
|
|
403
|
+
const getLastText = (msgContent) => {
|
|
404
|
+
if (typeof msgContent === 'string') return msgContent
|
|
405
|
+
if (Array.isArray(msgContent)) return msgContent.find(c => c.type === 'text')?.text || ''
|
|
406
|
+
return ''
|
|
407
|
+
}
|
|
408
|
+
const newText = text // content[0].text
|
|
409
|
+
const lastText = lastMessage && lastMessage.role === 'user' ? getLastText(lastMessage.content) : null
|
|
410
|
+
const isDuplicate = lastText === newText
|
|
411
|
+
|
|
412
|
+
// Add user message only if it's not a duplicate
|
|
413
|
+
// Note: We are saving the FULL STRUCTURED CONTENT array here
|
|
414
|
+
if (!isDuplicate) {
|
|
415
|
+
messages.push({
|
|
416
|
+
timestamp: new Date().valueOf(),
|
|
417
|
+
role: 'user',
|
|
418
|
+
content,
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const request = createRequest({ model })
|
|
424
|
+
|
|
425
|
+
// Add Thread History
|
|
426
|
+
messages.forEach(m => {
|
|
427
|
+
request.messages.push(m)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
// Update Thread Title if not set or is default
|
|
431
|
+
if (!thread.title || thread.title === 'New Chat' || request.title === 'New Chat') {
|
|
432
|
+
request.title = text.length > 100
|
|
433
|
+
? text.slice(0, 100) + '...'
|
|
434
|
+
: text
|
|
435
|
+
console.debug(`changing thread title from '${thread.title}' to '${request.title}'`)
|
|
436
|
+
} else {
|
|
437
|
+
console.debug(`thread title is '${thread.title}'`, request.title)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const api = await ctx.threads.queueChat({ request, thread })
|
|
441
|
+
if (api.response) {
|
|
442
|
+
// success
|
|
443
|
+
editingMessage.value = null
|
|
444
|
+
attachedFiles.value = []
|
|
445
|
+
thread = api.response
|
|
446
|
+
ctx.threads.replaceThread(thread)
|
|
447
|
+
} else {
|
|
448
|
+
ctx.setError(api.error)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
completion,
|
|
454
|
+
createContent,
|
|
455
|
+
createRequest,
|
|
456
|
+
applySettings,
|
|
457
|
+
promptHistory,
|
|
458
|
+
messageText,
|
|
459
|
+
attachedFiles,
|
|
460
|
+
editingMessage,
|
|
461
|
+
hasImage,
|
|
462
|
+
hasAudio,
|
|
463
|
+
hasFile,
|
|
464
|
+
reset,
|
|
465
|
+
settings,
|
|
466
|
+
addCopyButtons,
|
|
467
|
+
getModel,
|
|
468
|
+
getSelectedModel,
|
|
469
|
+
setSelectedModel,
|
|
470
|
+
getProviderForModel,
|
|
471
|
+
canGenerateImage,
|
|
472
|
+
canGenerateAudio,
|
|
473
|
+
getTextContent,
|
|
474
|
+
getAnswer,
|
|
475
|
+
selectAspectRatio,
|
|
476
|
+
sendUserMessage,
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const ChatPrompt = {
|
|
481
|
+
template: `
|
|
482
|
+
<div class="mx-auto max-w-3xl">
|
|
483
|
+
<SettingsDialog :isOpen="showSettings" @close="showSettings = false" />
|
|
484
|
+
<div class="flex space-x-2">
|
|
485
|
+
<!-- Attach (+) button and Settings button -->
|
|
486
|
+
<div class="mt-1.5 flex flex-col space-y-1 items-center">
|
|
487
|
+
<div>
|
|
488
|
+
<button type="button"
|
|
489
|
+
@click="triggerFilePicker"
|
|
490
|
+
:disabled="$threads.isWatchingThread.value || !model"
|
|
491
|
+
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"
|
|
492
|
+
title="Attach image or audio">
|
|
493
|
+
<svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
|
|
494
|
+
<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>
|
|
495
|
+
</svg>
|
|
496
|
+
</button>
|
|
497
|
+
<!-- Hidden file input -->
|
|
498
|
+
<input ref="fileInput" type="file" multiple @change="onFilesSelected"
|
|
499
|
+
class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
500
|
+
/>
|
|
501
|
+
</div>
|
|
502
|
+
<div>
|
|
503
|
+
<button type="button" title="Settings" @click="showSettings = true"
|
|
504
|
+
:disabled="$threads.watchingThread || !model"
|
|
505
|
+
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">
|
|
506
|
+
<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>
|
|
507
|
+
</button>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
|
|
511
|
+
<div class="flex-1">
|
|
512
|
+
<div class="relative">
|
|
513
|
+
<textarea
|
|
514
|
+
ref="refMessage"
|
|
515
|
+
v-model="messageText"
|
|
516
|
+
@keydown="onKeyDown"
|
|
517
|
+
@keydown.enter.exact.prevent="sendMessage"
|
|
518
|
+
@keydown.enter.shift.exact="addNewLine"
|
|
519
|
+
@paste="onPaste"
|
|
520
|
+
@dragover="onDragOver"
|
|
521
|
+
@dragleave="onDragLeave"
|
|
522
|
+
@drop="onDrop"
|
|
523
|
+
placeholder="Type message... (Enter to send, Shift+Enter for new line, drag & drop or paste files)"
|
|
524
|
+
rows="3"
|
|
525
|
+
:class="[
|
|
526
|
+
'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',
|
|
527
|
+
isDragging
|
|
528
|
+
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-500'
|
|
529
|
+
: 'border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500'
|
|
530
|
+
]"
|
|
531
|
+
:disabled="$threads.watchingThread || !model"
|
|
532
|
+
></textarea>
|
|
533
|
+
<button v-if="!$threads.watchingThread" title="Send (Enter)" type="button"
|
|
534
|
+
@click="sendMessage"
|
|
535
|
+
:disabled="!messageText.trim() || $threads.watchingThread || !model"
|
|
536
|
+
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">
|
|
537
|
+
<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>
|
|
538
|
+
</button>
|
|
539
|
+
<button v-else title="Cancel request" type="button"
|
|
540
|
+
@click="$threads.cancelThread()"
|
|
541
|
+
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">
|
|
542
|
+
<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">
|
|
543
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
544
|
+
</svg>
|
|
545
|
+
</button>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<!-- Attachments & Image Options -->
|
|
549
|
+
<div class="mt-2 flex justify-between items-start gap-2">
|
|
550
|
+
<div class="flex flex-wrap gap-2">
|
|
551
|
+
<div v-for="(f,i) in $chat.attachedFiles.value" :key="i" class="flex items-center gap-2 px-2 py-1 rounded-md border border-gray-300 dark:border-gray-600 text-xs text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800">
|
|
552
|
+
<span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
|
|
553
|
+
<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">
|
|
554
|
+
<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>
|
|
555
|
+
</button>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<!-- Image Aspect Ratio Selector -->
|
|
560
|
+
<div v-if="$chat.canGenerateImage(model)">
|
|
561
|
+
<select name="aspect_ratio" v-model="$state.selectedAspectRatio"
|
|
562
|
+
class="block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-xs text-gray-700 dark:text-gray-300 pl-2 pr-6 py-1 focus:ring-blue-500 focus:border-blue-500">
|
|
563
|
+
<option v-for="(ratio, size) in imageAspectRatios" :key="size" :value="size">
|
|
564
|
+
{{ ratio }}
|
|
565
|
+
</option>
|
|
566
|
+
</select>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
<div v-if="!model" class="mt-2 text-sm text-red-600 dark:text-red-400">
|
|
571
|
+
Please select a model
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
`,
|
|
577
|
+
props: {
|
|
578
|
+
model: {
|
|
579
|
+
type: Object,
|
|
580
|
+
default: null
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
setup(props) {
|
|
584
|
+
const ctx = inject('ctx')
|
|
585
|
+
const config = ctx.state.config
|
|
586
|
+
const {
|
|
587
|
+
messageText,
|
|
588
|
+
promptHistory,
|
|
589
|
+
hasImage,
|
|
590
|
+
hasAudio,
|
|
591
|
+
hasFile,
|
|
592
|
+
getTextContent,
|
|
593
|
+
sendUserMessage,
|
|
594
|
+
} = ctx.chat
|
|
595
|
+
|
|
596
|
+
const fileInput = ref(null)
|
|
597
|
+
const refMessage = ref(null)
|
|
598
|
+
const showSettings = ref(false)
|
|
599
|
+
const historyIndex = ref(-1)
|
|
600
|
+
const isNavigatingHistory = ref(false)
|
|
601
|
+
|
|
602
|
+
// File attachments (+) handlers
|
|
603
|
+
const triggerFilePicker = () => {
|
|
604
|
+
if (fileInput.value) fileInput.value.click()
|
|
605
|
+
}
|
|
606
|
+
const onFilesSelected = async (e) => {
|
|
607
|
+
const files = Array.from(e.target?.files || [])
|
|
608
|
+
if (files.length) {
|
|
609
|
+
// Upload files immediately
|
|
610
|
+
const uploadedFiles = await Promise.all(files.map(async f => {
|
|
611
|
+
try {
|
|
612
|
+
const response = await ctx.ai.uploadFile(f)
|
|
613
|
+
const metadata = {
|
|
614
|
+
url: response.url,
|
|
615
|
+
name: f.name,
|
|
616
|
+
size: response.size,
|
|
617
|
+
type: f.type,
|
|
618
|
+
width: response.width,
|
|
619
|
+
height: response.height,
|
|
620
|
+
threadId: ctx.threads.currentThread.value?.id,
|
|
621
|
+
created: Date.now()
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
...metadata,
|
|
626
|
+
file: f // Keep original file for preview/fallback if needed
|
|
627
|
+
}
|
|
628
|
+
} catch (error) {
|
|
629
|
+
ctx.setError({
|
|
630
|
+
errorCode: 'Upload Failed',
|
|
631
|
+
message: `Failed to upload ${f.name}: ${error.message}`
|
|
632
|
+
})
|
|
633
|
+
return null
|
|
634
|
+
}
|
|
635
|
+
}))
|
|
636
|
+
|
|
637
|
+
ctx.chat.attachedFiles.value.push(...uploadedFiles.filter(f => f))
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// allow re-selecting the same file
|
|
641
|
+
if (fileInput.value) fileInput.value.value = ''
|
|
642
|
+
|
|
643
|
+
if (!messageText.value?.trim()) {
|
|
644
|
+
if (hasImage()) {
|
|
645
|
+
messageText.value = getTextContent(config.defaults.image)
|
|
646
|
+
} else if (hasAudio()) {
|
|
647
|
+
messageText.value = getTextContent(config.defaults.audio)
|
|
648
|
+
} else {
|
|
649
|
+
messageText.value = getTextContent(config.defaults.file)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const removeAttachment = (i) => {
|
|
654
|
+
ctx.chat.attachedFiles.value.splice(i, 1)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Handle paste events for clipboard images, audio, and files
|
|
658
|
+
const onPaste = async (e) => {
|
|
659
|
+
// Use the paste event's clipboardData directly (works best for paste events)
|
|
660
|
+
const items = e.clipboardData?.items
|
|
661
|
+
if (!items) return
|
|
662
|
+
|
|
663
|
+
const files = []
|
|
664
|
+
|
|
665
|
+
// Check all clipboard items
|
|
666
|
+
for (let i = 0; i < items.length; i++) {
|
|
667
|
+
const item = items[i]
|
|
668
|
+
|
|
669
|
+
// Handle files (images, audio, etc.)
|
|
670
|
+
if (item.kind === 'file') {
|
|
671
|
+
const file = item.getAsFile()
|
|
672
|
+
if (file) {
|
|
673
|
+
// Generate a better filename based on type
|
|
674
|
+
let filename = file.name
|
|
675
|
+
if (!filename || filename === 'image.png' || filename === 'blob') {
|
|
676
|
+
const ext = file.type.split('/')[1] || 'png'
|
|
677
|
+
const timestamp = new Date().getTime()
|
|
678
|
+
if (file.type.startsWith('image/')) {
|
|
679
|
+
filename = `pasted-image-${timestamp}.${ext}`
|
|
680
|
+
} else if (file.type.startsWith('audio/')) {
|
|
681
|
+
filename = `pasted-audio-${timestamp}.${ext}`
|
|
682
|
+
} else {
|
|
683
|
+
filename = `pasted-file-${timestamp}.${ext}`
|
|
684
|
+
}
|
|
685
|
+
// Create a new File object with the better name
|
|
686
|
+
files.push(new File([file], filename, { type: file.type }))
|
|
687
|
+
} else {
|
|
688
|
+
files.push(file)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (files.length > 0) {
|
|
695
|
+
e.preventDefault()
|
|
696
|
+
// Reuse the same logic as onFilesSelected for consistency
|
|
697
|
+
const event = { target: { files: files } }
|
|
698
|
+
await onFilesSelected(event)
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Handle drag and drop events
|
|
703
|
+
const isDragging = ref(false)
|
|
704
|
+
|
|
705
|
+
const onDragOver = (e) => {
|
|
706
|
+
e.preventDefault()
|
|
707
|
+
e.stopPropagation()
|
|
708
|
+
isDragging.value = true
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const onDragLeave = (e) => {
|
|
712
|
+
e.preventDefault()
|
|
713
|
+
e.stopPropagation()
|
|
714
|
+
isDragging.value = false
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const onDrop = async (e) => {
|
|
718
|
+
e.preventDefault()
|
|
719
|
+
e.stopPropagation()
|
|
720
|
+
isDragging.value = false
|
|
721
|
+
|
|
722
|
+
const files = Array.from(e.dataTransfer?.files || [])
|
|
723
|
+
if (files.length > 0) {
|
|
724
|
+
// Reuse the same logic as onFilesSelected for consistency
|
|
725
|
+
const event = { target: { files: files } }
|
|
726
|
+
await onFilesSelected(event)
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Send message
|
|
731
|
+
const sendMessage = async () => {
|
|
732
|
+
if (!messageText.value?.trim() && !hasImage() && !hasAudio() && !hasFile()) return
|
|
733
|
+
if (ctx.threads.isWatchingThread.value || !props.model) return
|
|
734
|
+
|
|
735
|
+
// 1. Construct Structured Content (Text + Attachments)
|
|
736
|
+
let text = messageText.value.trim()
|
|
737
|
+
|
|
738
|
+
if (text) {
|
|
739
|
+
const idx = promptHistory.value.indexOf(text)
|
|
740
|
+
if (idx !== -1) {
|
|
741
|
+
promptHistory.value.splice(idx, 1)
|
|
742
|
+
}
|
|
743
|
+
promptHistory.value.push(text)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
messageText.value = ''
|
|
747
|
+
|
|
748
|
+
await sendUserMessage(text, { model: props.model })
|
|
749
|
+
|
|
750
|
+
// Restore focus to the textarea
|
|
751
|
+
nextTick(() => {
|
|
752
|
+
refMessage.value?.focus()
|
|
753
|
+
})
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const addNewLine = () => {
|
|
757
|
+
// Enter key already adds new line
|
|
758
|
+
//messageText.value += '\n'
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const onKeyDown = (e) => {
|
|
762
|
+
if (e.key === 'ArrowUp') {
|
|
763
|
+
if (refMessage.value.selectionStart === 0 && refMessage.value.selectionEnd === 0) {
|
|
764
|
+
if (promptHistory.value.length > 0) {
|
|
765
|
+
e.preventDefault()
|
|
766
|
+
if (historyIndex.value === -1) {
|
|
767
|
+
historyIndex.value = promptHistory.value.length - 1
|
|
768
|
+
} else {
|
|
769
|
+
historyIndex.value = Math.max(0, historyIndex.value - 1)
|
|
770
|
+
}
|
|
771
|
+
isNavigatingHistory.value = true
|
|
772
|
+
messageText.value = promptHistory.value[historyIndex.value]
|
|
773
|
+
nextTick(() => {
|
|
774
|
+
refMessage.value.setSelectionRange(0, 0)
|
|
775
|
+
})
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
} else if (e.key === 'ArrowDown') {
|
|
779
|
+
if (historyIndex.value !== -1) {
|
|
780
|
+
e.preventDefault()
|
|
781
|
+
if (historyIndex.value < promptHistory.value.length - 1) {
|
|
782
|
+
historyIndex.value++
|
|
783
|
+
isNavigatingHistory.value = true
|
|
784
|
+
messageText.value = promptHistory.value[historyIndex.value]
|
|
785
|
+
} else {
|
|
786
|
+
historyIndex.value = -1
|
|
787
|
+
isNavigatingHistory.value = true
|
|
788
|
+
messageText.value = ''
|
|
789
|
+
}
|
|
790
|
+
nextTick(() => {
|
|
791
|
+
refMessage.value.setSelectionRange(0, 0)
|
|
792
|
+
})
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
watch(messageText, (newValue) => {
|
|
798
|
+
if (!isNavigatingHistory.value) {
|
|
799
|
+
historyIndex.value = -1
|
|
800
|
+
}
|
|
801
|
+
isNavigatingHistory.value = false
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
watch(() => ctx.state.selectedAspectRatio, newValue => {
|
|
805
|
+
ctx.setPrefs({ aspectRatio: newValue })
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
watch(() => ctx.layout.path, newValue => {
|
|
809
|
+
if (newValue === '/' || newValue.startsWith('/c/')) {
|
|
810
|
+
nextTick(() => {
|
|
811
|
+
refMessage.value?.focus()
|
|
812
|
+
})
|
|
813
|
+
}
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
return {
|
|
817
|
+
messageText,
|
|
818
|
+
fileInput,
|
|
819
|
+
refMessage,
|
|
820
|
+
showSettings,
|
|
821
|
+
isDragging,
|
|
822
|
+
triggerFilePicker,
|
|
823
|
+
onFilesSelected,
|
|
824
|
+
onPaste,
|
|
825
|
+
onDragOver,
|
|
826
|
+
onDragLeave,
|
|
827
|
+
onDrop,
|
|
828
|
+
removeAttachment,
|
|
829
|
+
sendMessage,
|
|
830
|
+
addNewLine,
|
|
831
|
+
onKeyDown,
|
|
832
|
+
imageAspectRatios,
|
|
833
|
+
sendUserMessage,
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const HomeTools = {
|
|
839
|
+
template: `
|
|
840
|
+
<div class="mt-4 flex space-x-3 justify-center items-center">
|
|
841
|
+
<DarkModeToggle />
|
|
842
|
+
</div>
|
|
843
|
+
`,
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const ThreadHeader = {
|
|
847
|
+
template: `
|
|
848
|
+
<div v-if="showComponents.length" class="flex items-center justify-center gap-2">
|
|
849
|
+
<div v-for="component in showComponents">
|
|
850
|
+
<component :is="component" :thread="thread" />
|
|
851
|
+
</div>
|
|
852
|
+
</div>
|
|
853
|
+
`,
|
|
854
|
+
props: { thread: Object },
|
|
855
|
+
setup(props) {
|
|
856
|
+
const ctx = inject('ctx')
|
|
857
|
+
const showComponents = computed(() => {
|
|
858
|
+
const args = { thread: props.thread }
|
|
859
|
+
return Object.values(ctx.threadHeaderComponents).filter(def => def.show(args)).map(def => def.component)
|
|
860
|
+
})
|
|
861
|
+
return {
|
|
862
|
+
showComponents,
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const ThreadFooter = {
|
|
868
|
+
template: `
|
|
869
|
+
<div v-if="showComponents.length">
|
|
870
|
+
<div v-for="component in showComponents">
|
|
871
|
+
<component :is="component" :thread="thread" />
|
|
872
|
+
</div>
|
|
873
|
+
</div>
|
|
874
|
+
`,
|
|
875
|
+
props: { thread: Object },
|
|
876
|
+
setup(props) {
|
|
877
|
+
const ctx = inject('ctx')
|
|
878
|
+
const showComponents = computed(() => {
|
|
879
|
+
const args = { thread: props.thread }
|
|
880
|
+
return Object.values(ctx.threadFooterComponents).filter(def => def.show(args)).map(def => def.component)
|
|
881
|
+
})
|
|
882
|
+
return {
|
|
883
|
+
showComponents,
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const ThreadModel = {
|
|
889
|
+
template: `
|
|
890
|
+
<span @click="$chat.setSelectedModel({ name: thread.model})"
|
|
891
|
+
class="flex items-center cursor-pointer px-1.5 py-0.5 text-xs rounded text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100 transition-colors border hover:border-gray-300 dark:hover:border-gray-700">
|
|
892
|
+
<ProviderIcon class="size-4 mr-1" :provider="$chat.getProviderForModel(thread.model)" />
|
|
893
|
+
{{thread.model}}
|
|
894
|
+
</span>
|
|
895
|
+
`,
|
|
896
|
+
props: { thread: Object },
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const ThreadTools = {
|
|
900
|
+
template: `
|
|
901
|
+
<div class="text-sm flex items-center gap-1 flex items-center px-1.5 py-0.5 text-xs rounded text-gray-600 dark:text-gray-300 border cursor-help" :title="title">
|
|
902
|
+
<svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 10h3V7L6.5 3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1-3 3l-6-6a6 6 0 0 1-8-8z"/></svg>
|
|
903
|
+
<span v-if="toolFns.length==1">{{toolFns[0].function.name}}</span>
|
|
904
|
+
<span v-else-if="toolFns.length>1">{{toolFns.length}} Tools</span>
|
|
905
|
+
</div>
|
|
906
|
+
`,
|
|
907
|
+
props: { thread: Object },
|
|
908
|
+
setup(props) {
|
|
909
|
+
const toolFns = computed(() => props.thread.tools.filter(x => x.type === 'function'))
|
|
910
|
+
const title = computed(() => toolFns.value.length == 1
|
|
911
|
+
? toolFns.value[0].function.name
|
|
912
|
+
: toolFns.value.length > 1
|
|
913
|
+
? toolFns.value.map(x => x.function.name).join('\n')
|
|
914
|
+
: '')
|
|
915
|
+
return {
|
|
916
|
+
toolFns,
|
|
917
|
+
title,
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
export default {
|
|
923
|
+
/**@param {AppContext} ctx */
|
|
924
|
+
install(ctx) {
|
|
925
|
+
const Home = ChatBody
|
|
926
|
+
ctx.components({
|
|
927
|
+
SettingsDialog,
|
|
928
|
+
ChatPrompt,
|
|
929
|
+
|
|
930
|
+
ChatBody,
|
|
931
|
+
MessageUsage,
|
|
932
|
+
MessageReasoning,
|
|
933
|
+
LightboxImage,
|
|
934
|
+
TypeText,
|
|
935
|
+
TypeImage,
|
|
936
|
+
TypeAudio,
|
|
937
|
+
TypeFile,
|
|
938
|
+
ViewType,
|
|
939
|
+
ViewTypes,
|
|
940
|
+
ViewToolTypes,
|
|
941
|
+
TextViewer,
|
|
942
|
+
ToolArguments,
|
|
943
|
+
ToolOutput,
|
|
944
|
+
|
|
945
|
+
HomeTools,
|
|
946
|
+
Home,
|
|
947
|
+
ThreadHeader,
|
|
948
|
+
ThreadFooter,
|
|
949
|
+
})
|
|
950
|
+
ctx.setGlobals({
|
|
951
|
+
chat: useChatPrompt(ctx)
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
ctx.setLeftIcons({
|
|
955
|
+
chat: {
|
|
956
|
+
component: {
|
|
957
|
+
template: `<svg @click="$ctx.togglePath($ctx.layout.path?.startsWith('/c/') ? $ctx.layout.path : '/')" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M8 2.19c3.13 0 5.68 2.25 5.68 5s-2.55 5-5.68 5a5.7 5.7 0 0 1-1.89-.29l-.75-.26l-.56.56a14 14 0 0 1-2 1.55a.13.13 0 0 1-.07 0v-.06a6.58 6.58 0 0 0 .15-4.29a5.25 5.25 0 0 1-.55-2.16c0-2.77 2.55-5 5.68-5M8 .94c-3.83 0-6.93 2.81-6.93 6.27a6.4 6.4 0 0 0 .64 2.64a5.53 5.53 0 0 1-.18 3.48a1.32 1.32 0 0 0 2 1.5a15 15 0 0 0 2.16-1.71a6.8 6.8 0 0 0 2.31.36c3.83 0 6.93-2.81 6.93-6.27S11.83.94 8 .94"/><ellipse cx="5.2" cy="7.7" fill="currentColor" rx=".8" ry=".75"/><ellipse cx="8" cy="7.7" fill="currentColor" rx=".8" ry=".75"/><ellipse cx="10.8" cy="7.7" fill="currentColor" rx=".8" ry=".75"/></svg>`,
|
|
958
|
+
},
|
|
959
|
+
isActive({ path }) {
|
|
960
|
+
return path === '/' || path.startsWith('/c/')
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
const title = 'Chat'
|
|
966
|
+
ctx.setState({
|
|
967
|
+
title
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
const meta = { title }
|
|
971
|
+
ctx.routes.push(...[
|
|
972
|
+
{ path: '/', component: Home, meta },
|
|
973
|
+
{ path: '/c/:id', component: ChatBody, meta },
|
|
974
|
+
])
|
|
975
|
+
|
|
976
|
+
ctx.setThreadHeaders({
|
|
977
|
+
model: {
|
|
978
|
+
component: ThreadModel,
|
|
979
|
+
show({ thread }) { return thread.model }
|
|
980
|
+
},
|
|
981
|
+
tools: {
|
|
982
|
+
component: ThreadTools,
|
|
983
|
+
show({ thread }) { return (thread.tools || []).filter(x => x.type === 'function').length }
|
|
984
|
+
}
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
const prefs = ctx.getPrefs()
|
|
988
|
+
if (prefs.model) {
|
|
989
|
+
ctx.state.selectedModel = prefs.model
|
|
990
|
+
}
|
|
991
|
+
ctx.setState({
|
|
992
|
+
selectedAspectRatio: prefs.aspectRatio || '1:1',
|
|
993
|
+
})
|
|
994
|
+
}
|
|
995
|
+
}
|