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
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
import { ref, computed, nextTick, watch, onMounted, onUnmounted, inject } from 'vue'
|
|
2
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
3
|
+
|
|
4
|
+
function tryParseJson(str) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(str)
|
|
7
|
+
} catch (e) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function hasJsonStructure(str) {
|
|
12
|
+
return tryParseJson(str) != null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isEmpty(v) {
|
|
16
|
+
return !v || v === '{}' || v === '[]' || v === 'null' || v === 'undefined' || v === '""' || v === "''" || v === "``"
|
|
17
|
+
}
|
|
18
|
+
function embedHtml(html) {
|
|
19
|
+
const resizeScript = `<script>
|
|
20
|
+
let lastH = 0;
|
|
21
|
+
const sendHeight = () => {
|
|
22
|
+
const body = document.body;
|
|
23
|
+
if (!body) return;
|
|
24
|
+
// Force re-calc
|
|
25
|
+
const h = document.documentElement.getBoundingClientRect().height;
|
|
26
|
+
if (Math.abs(h - lastH) > 2) {
|
|
27
|
+
lastH = h;
|
|
28
|
+
window.parent.postMessage({ type: 'iframe-resize', height: h }, '*');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const ro = new ResizeObserver(sendHeight);
|
|
32
|
+
window.addEventListener('load', () => {
|
|
33
|
+
// Inject styles to prevent infinite loops
|
|
34
|
+
const style = document.createElement('style');
|
|
35
|
+
style.textContent = 'html, body { height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 0 !important; overflow: hidden !important; }';
|
|
36
|
+
document.head.appendChild(style);
|
|
37
|
+
|
|
38
|
+
const body = document.body;
|
|
39
|
+
if (body) {
|
|
40
|
+
ro.observe(body);
|
|
41
|
+
ro.observe(document.documentElement);
|
|
42
|
+
sendHeight();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
<\/script>`
|
|
46
|
+
|
|
47
|
+
const escaped = (html + resizeScript)
|
|
48
|
+
.replace(/&/g, '&')
|
|
49
|
+
.replace(/</g, '<')
|
|
50
|
+
.replace(/>/g, '>')
|
|
51
|
+
.replace(/"/g, '"')
|
|
52
|
+
.replace(/'/g, ''')
|
|
53
|
+
return `<iframe srcdoc="${escaped}" sandbox="allow-scripts" style="width:100%;height:auto;border:none;"></iframe>`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const TypeText = {
|
|
57
|
+
template: `
|
|
58
|
+
<div v-if="text.type === 'text'">
|
|
59
|
+
<div v-html="html"></div>
|
|
60
|
+
</div>
|
|
61
|
+
`,
|
|
62
|
+
props: {
|
|
63
|
+
text: {
|
|
64
|
+
type: Object,
|
|
65
|
+
required: true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
setup(props) {
|
|
69
|
+
const ctx = inject('ctx')
|
|
70
|
+
const html = computed(() => {
|
|
71
|
+
try {
|
|
72
|
+
return ctx.fmt.markdown(props.text.text)
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error('TypeText: markdown', e)
|
|
75
|
+
return `<div class="whitespace-pre-wrap">${props.text.text}</div>`
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
return { html }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const LightboxImage = {
|
|
83
|
+
template: `
|
|
84
|
+
<div>
|
|
85
|
+
<!-- Thumbnail -->
|
|
86
|
+
<div
|
|
87
|
+
class="cursor-zoom-in hover:opacity-90 transition-opacity"
|
|
88
|
+
@click="isOpen = true"
|
|
89
|
+
>
|
|
90
|
+
<img
|
|
91
|
+
:src="src"
|
|
92
|
+
:alt="alt"
|
|
93
|
+
:width="width"
|
|
94
|
+
:height="height"
|
|
95
|
+
:class="imageClass"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Lightbox Modal -->
|
|
100
|
+
<div v-if="isOpen"
|
|
101
|
+
class="fixed inset-0 z-100 flex items-center justify-center bg-black/90 p-4"
|
|
102
|
+
@click="isOpen = false"
|
|
103
|
+
>
|
|
104
|
+
<button type="button"
|
|
105
|
+
class="absolute top-4 right-4 p-2 text-white hover:bg-white/10 rounded-lg transition-colors"
|
|
106
|
+
@click="isOpen = false"
|
|
107
|
+
aria-label="Close lightbox"
|
|
108
|
+
>
|
|
109
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-6 h-6"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
110
|
+
</button>
|
|
111
|
+
<div class="relative max-w-7xl max-h-[90vh] w-full h-full flex items-center justify-center">
|
|
112
|
+
<img
|
|
113
|
+
:src="src"
|
|
114
|
+
:alt="alt"
|
|
115
|
+
:width="width"
|
|
116
|
+
:height="height"
|
|
117
|
+
class="max-w-full max-h-full w-auto h-auto object-contain rounded"
|
|
118
|
+
@click.stop
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
`,
|
|
124
|
+
props: {
|
|
125
|
+
src: {
|
|
126
|
+
type: String,
|
|
127
|
+
required: true
|
|
128
|
+
},
|
|
129
|
+
alt: {
|
|
130
|
+
type: String,
|
|
131
|
+
default: ''
|
|
132
|
+
},
|
|
133
|
+
width: {
|
|
134
|
+
type: [Number, String],
|
|
135
|
+
default: undefined
|
|
136
|
+
},
|
|
137
|
+
height: {
|
|
138
|
+
type: [Number, String],
|
|
139
|
+
default: undefined
|
|
140
|
+
},
|
|
141
|
+
imageClass: {
|
|
142
|
+
type: String,
|
|
143
|
+
default: 'max-w-[400px] max-h-96 rounded-lg border border-gray-200 dark:border-gray-700 object-contain bg-gray-50 dark:bg-gray-900 shadow-sm transition-transform hover:scale-[1.02]'
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
setup(props) {
|
|
147
|
+
const ctx = inject('ctx')
|
|
148
|
+
const isOpen = ref(false)
|
|
149
|
+
|
|
150
|
+
let sub
|
|
151
|
+
onMounted(() => {
|
|
152
|
+
sub = ctx.events.subscribe(`keydown:Escape`, () => isOpen.value = false)
|
|
153
|
+
})
|
|
154
|
+
onUnmounted(() => sub?.unsubscribe())
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
isOpen
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const TypeImage = {
|
|
163
|
+
template: `
|
|
164
|
+
<div v-if="image.type === 'image_url'">
|
|
165
|
+
<LightboxImage :src="$ctx.resolveUrl(image.image_url.url)" />
|
|
166
|
+
</div>
|
|
167
|
+
`,
|
|
168
|
+
props: {
|
|
169
|
+
image: {
|
|
170
|
+
type: Object,
|
|
171
|
+
required: true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const TypeAudio = {
|
|
177
|
+
template: `
|
|
178
|
+
<div v-if="audio.type === 'audio_url'">
|
|
179
|
+
<slot></slot>
|
|
180
|
+
<audio controls :src="$ctx.resolveUrl(audio.audio_url.url)" class="h-8 w-64"></audio>
|
|
181
|
+
</div>
|
|
182
|
+
`,
|
|
183
|
+
props: {
|
|
184
|
+
audio: {
|
|
185
|
+
type: Object,
|
|
186
|
+
required: true
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const TypeFile = {
|
|
192
|
+
template: `
|
|
193
|
+
<a v-if="file.type === 'file'" :href="$ctx.resolveUrl(file.file.file_data)" target="_blank"
|
|
194
|
+
class="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-sm text-blue-600 dark:text-blue-400 hover:underline">
|
|
195
|
+
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
|
196
|
+
<span class="max-w-xs truncate">{{ file.file.filename || 'Attachment' }}</span>
|
|
197
|
+
</a>
|
|
198
|
+
`,
|
|
199
|
+
props: {
|
|
200
|
+
file: {
|
|
201
|
+
type: Object,
|
|
202
|
+
required: true
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const ViewType = {
|
|
208
|
+
template: `
|
|
209
|
+
<div class="flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
|
210
|
+
<TypeText v-if="result.type === 'text'" :text="result" />
|
|
211
|
+
<TypeImage v-else-if="result.type === 'image_url'" :image="result" />
|
|
212
|
+
<TypeAudio v-else-if="result.type === 'audio_url'" :audio="result" />
|
|
213
|
+
<TypeFile v-else-if="result.type === 'file'" :file="result" />
|
|
214
|
+
<div v-else>
|
|
215
|
+
<HtmlFormat :value="result" :classes="$utils.htmlFormatClasses" />
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
`,
|
|
219
|
+
props: {
|
|
220
|
+
result: {
|
|
221
|
+
type: Object,
|
|
222
|
+
required: true
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
export const ViewTypes = {
|
|
227
|
+
template: `
|
|
228
|
+
<div v-if="results?.length" class="flex flex-col gap-2">
|
|
229
|
+
<div v-if="texts.length > 0" :class="cls">
|
|
230
|
+
<div v-if="hasResources" v-for="(text, i) in texts" :key="'raw-' + i" class="text-xs whitespace-pre-wrap">{{text.text}}</div>
|
|
231
|
+
<TypeText v-else v-for="(text, i) in texts" :key="'text-' + i" :text="text" />
|
|
232
|
+
</div>
|
|
233
|
+
<div v-if="images.length > 0" :class="cls">
|
|
234
|
+
<TypeImage v-for="(image, i) in images" :key="'image-' + i" :image="image" />
|
|
235
|
+
</div>
|
|
236
|
+
<div v-if="audios.length > 0" :class="cls">
|
|
237
|
+
<TypeAudio v-for="(audio, i) in audios" :key="'audio-' + i" :audio="audio" />
|
|
238
|
+
</div>
|
|
239
|
+
<div v-if="files.length > 0" :class="cls">
|
|
240
|
+
<TypeFile v-for="(file, i) in files" :key="'file-' + i" :file="file" />
|
|
241
|
+
</div>
|
|
242
|
+
<div v-if="others.length > 0" :class="cls">
|
|
243
|
+
<HtmlFormat v-for="(other, i) in others" :key="'other-' + i" :value="other" :classes="$utils.htmlFormatClasses" />
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
`,
|
|
247
|
+
props: {
|
|
248
|
+
results: {
|
|
249
|
+
type: Array,
|
|
250
|
+
required: true
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
setup(props) {
|
|
254
|
+
const cls = "flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"
|
|
255
|
+
const texts = computed(() => props.results.filter(r => r.type === 'text'))
|
|
256
|
+
const images = computed(() => props.results.filter(r => r.type === 'image_url'))
|
|
257
|
+
const audios = computed(() => props.results.filter(r => r.type === 'audio_url'))
|
|
258
|
+
const files = computed(() => props.results.filter(r => r.type === 'file'))
|
|
259
|
+
const others = computed(() => props.results.filter(r => r.type !== 'text' && r.type !== 'image_url' && r.type !== 'audio_url' && r.type !== 'file'))
|
|
260
|
+
// If has resources, render as plain-text to avoid rendering resources multiple times
|
|
261
|
+
const hasResources = computed(() => images.value.length > 0 || audios.value.length > 0 || files.value.length > 0 || others.value.length > 0)
|
|
262
|
+
return { cls, texts, images, audios, files, others, hasResources }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
export const ViewToolTypes = {
|
|
266
|
+
template: `<ViewTypes v-if="results?.length" :results="results" />`,
|
|
267
|
+
props: {
|
|
268
|
+
output: Object,
|
|
269
|
+
},
|
|
270
|
+
setup(props) {
|
|
271
|
+
const results = computed(() => {
|
|
272
|
+
const ret = []
|
|
273
|
+
if (!props.output) return ret
|
|
274
|
+
if (props.output.images) {
|
|
275
|
+
ret.push(...props.output.images)
|
|
276
|
+
}
|
|
277
|
+
if (props.output.audios) {
|
|
278
|
+
ret.push(...props.output.audios)
|
|
279
|
+
}
|
|
280
|
+
if (props.output.files) {
|
|
281
|
+
ret.push(...props.output.files)
|
|
282
|
+
}
|
|
283
|
+
return ret
|
|
284
|
+
})
|
|
285
|
+
return { results }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
export const MessageUsage = {
|
|
291
|
+
template: `
|
|
292
|
+
<div class="mt-2 text-xs opacity-70">
|
|
293
|
+
<span v-if="message.model" @click="$chat.setSelectedModel({ name: message.model })" title="Select model"><span class="cursor-pointer hover:underline">{{ message.model }}</span> • </span>
|
|
294
|
+
<span>{{ $fmt.time(message.timestamp) }}</span>
|
|
295
|
+
<span v-if="usage" :title="$fmt.tokensTitle(usage)">
|
|
296
|
+
•
|
|
297
|
+
{{ $fmt.humanifyNumber(usage.tokens) }} tokens
|
|
298
|
+
<span v-if="usage.cost">· {{ $fmt.tokenCostLong(usage.cost) }}</span>
|
|
299
|
+
<span v-if="usage.duration"> in {{ $fmt.humanifyMs(usage.duration * 1000) }}</span>
|
|
300
|
+
</span>
|
|
301
|
+
</div>
|
|
302
|
+
`,
|
|
303
|
+
props: {
|
|
304
|
+
usage: Object,
|
|
305
|
+
message: Object,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export const MessageReasoning = {
|
|
310
|
+
template: `
|
|
311
|
+
<div class="mt-2 mb-2">
|
|
312
|
+
<button type="button" @click="toggleReasoning(message.timestamp)" class="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 flex items-center space-x-1">
|
|
313
|
+
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" :class="isReasoningExpanded(message.timestamp) ? 'transform rotate-90' : ''"><path fill="currentColor" d="M7 5l6 5l-6 5z"/></svg>
|
|
314
|
+
<span>{{ isReasoningExpanded(message.timestamp) ? 'Hide reasoning' : 'Show reasoning' }}</span>
|
|
315
|
+
</button>
|
|
316
|
+
<div v-if="isReasoningExpanded(message.timestamp)" class="reasoning mt-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 p-2">
|
|
317
|
+
<div v-if="typeof reasoning === 'string'" v-html="$fmt.markdown(reasoning)" class="prose prose-xs max-w-none dark:prose-invert"></div>
|
|
318
|
+
<pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto">{{ formatReasoning(reasoning) }}</pre>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
`,
|
|
322
|
+
props: {
|
|
323
|
+
reasoning: String,
|
|
324
|
+
message: Object,
|
|
325
|
+
},
|
|
326
|
+
setup(props) {
|
|
327
|
+
const expandedReasoning = ref(new Set())
|
|
328
|
+
const isReasoningExpanded = (id) => expandedReasoning.value.has(id)
|
|
329
|
+
const toggleReasoning = (id) => {
|
|
330
|
+
const s = new Set(expandedReasoning.value)
|
|
331
|
+
if (s.has(id)) {
|
|
332
|
+
s.delete(id)
|
|
333
|
+
} else {
|
|
334
|
+
s.add(id)
|
|
335
|
+
}
|
|
336
|
+
expandedReasoning.value = s
|
|
337
|
+
}
|
|
338
|
+
const formatReasoning = (r) => typeof r === 'string' ? r : JSON.stringify(r, null, 2)
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
expandedReasoning,
|
|
342
|
+
isReasoningExpanded,
|
|
343
|
+
toggleReasoning,
|
|
344
|
+
formatReasoning,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export const ToolArguments = {
|
|
350
|
+
template: `
|
|
351
|
+
<div ref="refArgs" v-if="dict" class="not-prose">
|
|
352
|
+
<div class="prose html-format">
|
|
353
|
+
<table class="table-object border-none">
|
|
354
|
+
<tr v-for="(v, k) in dict" :key="k">
|
|
355
|
+
<td class="align-top py-2 px-4 text-left text-sm font-medium tracking-wider whitespace-nowrap lowercase">{{ k }}</td>
|
|
356
|
+
<td v-if="$utils.isHtml(v)" style="margin:0;padding:0;width:100%">
|
|
357
|
+
<div v-html="embedHtml(v)" class="w-full h-full"></div>
|
|
358
|
+
</td>
|
|
359
|
+
<td v-else class="align-top py-2 px-4 text-sm whitespace-pre-wrap">
|
|
360
|
+
<HtmlFormat :value="v" :classes="$utils.htmlFormatClasses" />
|
|
361
|
+
</td>
|
|
362
|
+
</tr>
|
|
363
|
+
</table>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
<div v-else-if="list" class="not-prose px-3 py-2">
|
|
367
|
+
<HtmlFormat :value="list" :classes="$utils.htmlFormatClasses" />
|
|
368
|
+
</div>
|
|
369
|
+
<pre v-else-if="!isEmpty(value)" class="tool-arguments">{{ value }}</pre>
|
|
370
|
+
`,
|
|
371
|
+
props: {
|
|
372
|
+
value: String,
|
|
373
|
+
},
|
|
374
|
+
setup(props) {
|
|
375
|
+
const refArgs = ref()
|
|
376
|
+
const dict = computed(() => {
|
|
377
|
+
if (isEmpty(props.value)) return null
|
|
378
|
+
const ret = tryParseJson(props.value)
|
|
379
|
+
return typeof ret === 'object' && !Array.isArray(ret) ? ret : null
|
|
380
|
+
})
|
|
381
|
+
const list = computed(() => {
|
|
382
|
+
if (isEmpty(props.value)) return null
|
|
383
|
+
const ret = tryParseJson(props.value)
|
|
384
|
+
return Array.isArray(ret) && ret.length > 0 ? ret : null
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
const handleMessage = (event) => {
|
|
388
|
+
if (event.data?.type === 'iframe-resize' && typeof event.data.height === 'number') {
|
|
389
|
+
const iframes = refArgs.value?.querySelectorAll('iframe')
|
|
390
|
+
iframes?.forEach(iframe => {
|
|
391
|
+
if (iframe.contentWindow === event.source) {
|
|
392
|
+
iframe.style.height = (event.data.height + 30) + 'px'
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
onMounted(() => {
|
|
399
|
+
window.addEventListener('message', handleMessage)
|
|
400
|
+
const hasIframes = refArgs.value?.querySelector('iframe')
|
|
401
|
+
if (hasIframes) {
|
|
402
|
+
refArgs.value.classList.add('has-iframes')
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
onUnmounted(() => {
|
|
407
|
+
window.removeEventListener('message', handleMessage)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
refArgs,
|
|
412
|
+
dict,
|
|
413
|
+
list,
|
|
414
|
+
isEmpty,
|
|
415
|
+
embedHtml,
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export const ToolOutput = {
|
|
421
|
+
template: `
|
|
422
|
+
<div v-if="output" class="border-t border-gray-200 dark:border-gray-700">
|
|
423
|
+
<div class="px-3 py-1.5 flex justify-between items-center border-b border-gray-200 dark:border-gray-800 bg-gray-50/30 dark:bg-gray-800">
|
|
424
|
+
<div class="flex items-center gap-2 ">
|
|
425
|
+
<svg class="size-3.5 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
|
426
|
+
<span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium">Output</span>
|
|
427
|
+
</div>
|
|
428
|
+
<div v-if="hasJsonStructure(output.content)" class="flex items-center gap-2 text-[10px] uppercase tracking-wider font-medium select-none">
|
|
429
|
+
<span @click="$ctx.setPrefs({ toolFormat: 'text' })"
|
|
430
|
+
class="cursor-pointer transition-colors"
|
|
431
|
+
:class="$ctx.prefs.toolFormat !== 'preview' ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'">
|
|
432
|
+
text
|
|
433
|
+
</span>
|
|
434
|
+
<span class="text-gray-300 dark:text-gray-700">|</span>
|
|
435
|
+
<span @click="$ctx.setPrefs({ toolFormat: 'preview' })"
|
|
436
|
+
class="cursor-pointer transition-colors"
|
|
437
|
+
:class="$ctx.prefs.toolFormat == 'preview' ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'">
|
|
438
|
+
preview
|
|
439
|
+
</span>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
<div class="not-prose px-3 py-2">
|
|
443
|
+
<pre v-if="$ctx.prefs.toolFormat !== 'preview' || !hasJsonStructure(output.content)" class="tool-output">{{ output.content }}</pre>
|
|
444
|
+
<div v-else class="text-xs">
|
|
445
|
+
<HtmlFormat v-if="tryParseJson(output.content)" :value="tryParseJson(output.content)" :classes="$utils.htmlFormatClasses" />
|
|
446
|
+
<div v-else class="text-gray-500 italic p-2">Invalid JSON content</div>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
<ViewToolTypes :output="output" class="p-2" />
|
|
450
|
+
</div>
|
|
451
|
+
`,
|
|
452
|
+
props: {
|
|
453
|
+
tool: Object,
|
|
454
|
+
output: Object,
|
|
455
|
+
},
|
|
456
|
+
setup(props) {
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
tryParseJson,
|
|
460
|
+
hasJsonStructure,
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export const ChatBody = {
|
|
466
|
+
template: `
|
|
467
|
+
<div class="flex flex-col h-full">
|
|
468
|
+
<!-- Messages Area -->
|
|
469
|
+
<div class="flex-1 overflow-y-auto" ref="messagesContainer">
|
|
470
|
+
<div class="mx-auto max-w-6xl px-4 py-6">
|
|
471
|
+
|
|
472
|
+
<div v-if="!$ai.hasAccess">
|
|
473
|
+
<OAuthSignIn v-if="$ai.authType === 'oauth'" @done="$ai.signIn($event)" />
|
|
474
|
+
<SignIn v-else @done="$ai.signIn($event)" />
|
|
475
|
+
</div>
|
|
476
|
+
<!-- Welcome message when no thread is selected -->
|
|
477
|
+
<div v-else-if="!currentThread" class="text-center py-12">
|
|
478
|
+
<Welcome />
|
|
479
|
+
<HomeTools />
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
<!-- Messages -->
|
|
483
|
+
<div v-else-if="currentThread">
|
|
484
|
+
<ThreadHeader v-if="currentThread" :thread="currentThread" class="mb-2" />
|
|
485
|
+
<div class="space-y-2" v-if="currentThread?.messages?.length">
|
|
486
|
+
<div
|
|
487
|
+
v-for="message in currentThread.messages.filter(x => x.role !== 'system')"
|
|
488
|
+
:key="message.timestamp"
|
|
489
|
+
v-show="!(message.role === 'tool' && isToolLinked(message))"
|
|
490
|
+
class="flex items-start space-x-3 group"
|
|
491
|
+
:class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
|
|
492
|
+
>
|
|
493
|
+
<!-- Avatar outside the bubble -->
|
|
494
|
+
<div class="flex-shrink-0 flex flex-col justify-center">
|
|
495
|
+
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
|
|
496
|
+
:class="message.role === 'user'
|
|
497
|
+
? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
|
|
498
|
+
: message.role === 'tool'
|
|
499
|
+
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-800'
|
|
500
|
+
: 'bg-gray-600 dark:bg-gray-500 text-white'"
|
|
501
|
+
>
|
|
502
|
+
<span v-if="message.role === 'user'">U</span>
|
|
503
|
+
<svg v-else-if="message.role === 'tool'" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
504
|
+
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
|
|
505
|
+
</svg>
|
|
506
|
+
<span v-else>AI</span>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
<!-- Delete button (shown on hover) -->
|
|
510
|
+
<button type="button" @click.stop="$threads.deleteMessageFromThread(currentThread.id, message.timestamp)"
|
|
511
|
+
class="mx-auto opacity-0 group-hover:opacity-100 mt-2 rounded text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-all"
|
|
512
|
+
title="Delete message">
|
|
513
|
+
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
514
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
515
|
+
</svg>
|
|
516
|
+
</button>
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
<!-- Message bubble -->
|
|
520
|
+
<div
|
|
521
|
+
class="message rounded-lg px-4 py-3 relative group"
|
|
522
|
+
:class="message.role === 'user'
|
|
523
|
+
? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
|
|
524
|
+
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700'"
|
|
525
|
+
>
|
|
526
|
+
<!-- Copy button in top right corner -->
|
|
527
|
+
<button
|
|
528
|
+
type="button"
|
|
529
|
+
@click="copyMessageContent(message)"
|
|
530
|
+
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 focus:outline-none focus:ring-0"
|
|
531
|
+
:class="message.role === 'user' ? 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'"
|
|
532
|
+
title="Copy message content"
|
|
533
|
+
>
|
|
534
|
+
<svg v-if="copying === message" class="size-4 text-green-500 dark:text-green-400" 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>
|
|
535
|
+
<svg v-else class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
536
|
+
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
|
|
537
|
+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
|
|
538
|
+
</svg>
|
|
539
|
+
</button>
|
|
540
|
+
|
|
541
|
+
<div
|
|
542
|
+
v-if="message.role === 'assistant'"
|
|
543
|
+
v-html="$fmt.markdown(message.content)"
|
|
544
|
+
class="prose prose-sm max-w-none dark:prose-invert"
|
|
545
|
+
></div>
|
|
546
|
+
|
|
547
|
+
<!-- Collapsible reasoning section -->
|
|
548
|
+
<MessageReasoning v-if="message.role === 'assistant' && (message.reasoning || message.thinking || message.reasoning_content)"
|
|
549
|
+
:reasoning="message.reasoning || message.thinking || message.reasoning_content" :message="message" />
|
|
550
|
+
|
|
551
|
+
<!-- Tool Calls & Outputs -->
|
|
552
|
+
<div v-if="message.tool_calls && message.tool_calls.length > 0" class="mb-3 space-y-4">
|
|
553
|
+
<div v-for="(tool, i) in message.tool_calls" :key="i" class="rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden">
|
|
554
|
+
<!-- Tool Call Header -->
|
|
555
|
+
<div class="px-3 py-2 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between bg-gray-50/30 dark:bg-gray-800 space-x-4">
|
|
556
|
+
<div class="flex items-center gap-2">
|
|
557
|
+
<svg class="size-3.5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
|
|
558
|
+
<span class="font-mono text-xs font-bold text-gray-700 dark:text-gray-300">{{ tool.function.name }}</span>
|
|
559
|
+
</div>
|
|
560
|
+
<span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium">Tool Call</span>
|
|
561
|
+
</div>
|
|
562
|
+
|
|
563
|
+
<ToolArguments :value="tool.function.arguments" />
|
|
564
|
+
|
|
565
|
+
<ToolOutput :tool="tool" :output="getToolOutput(tool.id)" />
|
|
566
|
+
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
<!-- Tool Output (Orphaned) -->
|
|
571
|
+
<div v-if="message.role === 'tool' && !isToolLinked(message)" class="text-sm">
|
|
572
|
+
<div class="flex items-center gap-2 mb-1 opacity-70">
|
|
573
|
+
<div class="flex items-center text-xs font-mono font-medium text-gray-500 uppercase tracking-wider">
|
|
574
|
+
<svg class="size-3 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
|
575
|
+
Tool Output
|
|
576
|
+
</div>
|
|
577
|
+
<div v-if="message.name" class="text-xs font-mono bg-gray-200 dark:bg-gray-700 px-1.5 rounded text-gray-700 dark:text-gray-300">
|
|
578
|
+
{{ message.name }}
|
|
579
|
+
</div>
|
|
580
|
+
<div v-if="message.tool_call_id" class="text-[10px] font-mono text-gray-400">
|
|
581
|
+
{{ message.tool_call_id.slice(0,8) }}
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
<div class="not-prose bg-white dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-800 p-2 overflow-x-auto">
|
|
585
|
+
<pre class="tool-output">{{ message.content }}</pre>
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
|
|
589
|
+
<!-- Assistant Images -->
|
|
590
|
+
<div v-if="message.images && message.images.length > 0" class="mt-2 flex flex-wrap gap-2">
|
|
591
|
+
<template v-for="(img, i) in message.images" :key="i">
|
|
592
|
+
<TypeImage v-if="img.type === 'image_url'" :image="img" />
|
|
593
|
+
</template>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
<!-- Assistant Audios -->
|
|
597
|
+
<div v-if="message.audios && message.audios.length > 0" class="mt-2 flex flex-wrap gap-2">
|
|
598
|
+
<template v-for="(audio, i) in message.audios" :key="i">
|
|
599
|
+
<TypeAudio v-if="audio.type === 'audio_url'" :audio="audio"
|
|
600
|
+
class="flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
|
601
|
+
<svg class="w-5 h-5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
|
|
602
|
+
</TypeAudio>
|
|
603
|
+
</template>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
<!-- User Message with separate attachments -->
|
|
607
|
+
<div v-else-if="message.role !== 'assistant' && message.role !== 'tool'">
|
|
608
|
+
<div v-html="$fmt.content(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
|
|
609
|
+
<ViewTypes :results="getAttachments(message)" />
|
|
610
|
+
</div>
|
|
611
|
+
|
|
612
|
+
<MessageUsage :message="message" :usage="getMessageUsage(message)" />
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
<!-- Edit and Redo buttons (shown on hover for user messages, outside bubble) -->
|
|
616
|
+
<div v-if="message.role === 'user'" class="flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity mt-1">
|
|
617
|
+
<button type="button" @click.stop="editMessage(message)"
|
|
618
|
+
class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 dark:text-gray-500 hover:text-green-600 dark:hover:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/30 transition-all"
|
|
619
|
+
title="Edit message">
|
|
620
|
+
<svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
621
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
622
|
+
</svg>
|
|
623
|
+
Edit
|
|
624
|
+
</button>
|
|
625
|
+
<button type="button" @click.stop="redoMessage(message)"
|
|
626
|
+
class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-all"
|
|
627
|
+
title="Redo message (clears all responses after this message and re-runs it)">
|
|
628
|
+
<svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
629
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
630
|
+
</svg>
|
|
631
|
+
Redo
|
|
632
|
+
</button>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 dark:text-gray-400 text-sm">
|
|
637
|
+
<span :title="$fmt.statsTitle(currentThread.stats)">
|
|
638
|
+
{{ currentThread.stats.cost ? $fmt.costLong(currentThread.stats.cost) + ' for ' : '' }} {{ $fmt.humanifyNumber(currentThread.stats.inputTokens) }} → {{ $fmt.humanifyNumber(currentThread.stats.outputTokens) }} tokens over {{ currentThread.stats.requests }} request{{currentThread.stats.requests===1?'':'s'}} in {{ $fmt.humanifyMs(currentThread.stats.duration * 1000) }}
|
|
639
|
+
</span>
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<!-- Loading indicator -->
|
|
643
|
+
<div v-if="$threads.watchingThread" class="flex items-start space-x-3 group">
|
|
644
|
+
<!-- Avatar outside the bubble -->
|
|
645
|
+
<div class="flex-shrink-0">
|
|
646
|
+
<div class="w-8 h-8 rounded-full bg-gray-600 dark:bg-gray-500 text-white flex items-center justify-center text-sm font-medium">
|
|
647
|
+
AI
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
|
|
651
|
+
<!-- Loading bubble -->
|
|
652
|
+
<div class="rounded-lg px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
|
|
653
|
+
<div class="flex space-x-1">
|
|
654
|
+
<div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"></div>
|
|
655
|
+
<div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
|
|
656
|
+
<div class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
|
|
660
|
+
<!-- Cancel button -->
|
|
661
|
+
<button type="button" @click="$threads.cancelThread()"
|
|
662
|
+
class="px-3 py-1 rounded text-sm text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 border border-transparent hover:border-red-300 dark:hover:border-red-600 transition-all"
|
|
663
|
+
title="Cancel request">
|
|
664
|
+
cancel
|
|
665
|
+
</button>
|
|
666
|
+
</div>
|
|
667
|
+
|
|
668
|
+
<!-- Thread error message bubble -->
|
|
669
|
+
<div v-if="currentThread?.error" class="mt-8 flex items-center space-x-3">
|
|
670
|
+
<!-- Avatar outside the bubble -->
|
|
671
|
+
<div class="flex-shrink-0">
|
|
672
|
+
<div class="size-8 rounded-full bg-red-600 dark:bg-red-500 text-white flex items-center justify-center text-lg font-bold">
|
|
673
|
+
!
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
<!-- Error bubble -->
|
|
677
|
+
<div class="max-w-[85%] rounded-lg px-3 py-1 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 shadow-sm">
|
|
678
|
+
<div class="flex items-start space-x-2">
|
|
679
|
+
<div class="flex-1 min-w-0">
|
|
680
|
+
<div v-if="currentThread.error" class="text-base mb-1">{{ currentThread.error }}</div>
|
|
681
|
+
</div>
|
|
682
|
+
</div>
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
|
|
686
|
+
<!-- Error message bubble -->
|
|
687
|
+
<div v-if="$state.error" class="mt-8 flex items-start space-x-3">
|
|
688
|
+
<!-- Avatar outside the bubble -->
|
|
689
|
+
<div class="flex-shrink-0">
|
|
690
|
+
<div class="size-8 rounded-full bg-red-600 dark:bg-red-500 text-white flex items-center justify-center text-lg font-bold">
|
|
691
|
+
!
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
|
|
695
|
+
<!-- Error bubble -->
|
|
696
|
+
<div class="max-w-[85%] rounded-lg px-4 py-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 shadow-sm">
|
|
697
|
+
<div class="flex items-start space-x-2">
|
|
698
|
+
<div class="flex-1 min-w-0">
|
|
699
|
+
<div class="flex justify-between items-start">
|
|
700
|
+
<div class="text-base font-medium mb-1">{{ $state.error?.errorCode || 'Error' }}</div>
|
|
701
|
+
<button type="button" @click="$ctx.clearError()" title="Clear Error"
|
|
702
|
+
class="text-red-400 dark:text-red-300 hover:text-red-600 dark:hover:text-red-100 flex-shrink-0">
|
|
703
|
+
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
704
|
+
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
705
|
+
</svg>
|
|
706
|
+
</button>
|
|
707
|
+
</div>
|
|
708
|
+
<div v-if="$state.error?.message" class="text-base mb-1">{{ $state.error.message }}</div>
|
|
709
|
+
<div v-if="$state.error?.stackTrace" class="mt-2 text-sm whitespace-pre-wrap break-words max-h-80 overflow-y-auto font-mono p-2 border border-red-200/70 dark:border-red-800/70">
|
|
710
|
+
{{ $state.error.stackTrace }}
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
<ThreadFooter v-if="$threads.threadDetails.value[currentThread.id]" :thread="$threads.threadDetails.value[currentThread.id]" />
|
|
718
|
+
</div>
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<!-- Input Area -->
|
|
723
|
+
<div v-if="$ai.hasAccess" :class="$ctx.cls('chat-input', 'flex-shrink-0 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6 py-4')">
|
|
724
|
+
<ChatPrompt :model="$chat.getSelectedModel()" />
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
`,
|
|
728
|
+
setup() {
|
|
729
|
+
const ctx = inject('ctx')
|
|
730
|
+
const models = ctx.state.models
|
|
731
|
+
const config = ctx.state.config
|
|
732
|
+
const threads = ctx.threads
|
|
733
|
+
const chatPrompt = ctx.chat
|
|
734
|
+
const { currentThread } = threads
|
|
735
|
+
|
|
736
|
+
const router = useRouter()
|
|
737
|
+
const route = useRoute()
|
|
738
|
+
|
|
739
|
+
const prefs = ref(ctx.getPrefs())
|
|
740
|
+
|
|
741
|
+
const selectedModel = ref(prefs.value.model || config.defaults.text.model || '')
|
|
742
|
+
const selectedModelObj = computed(() => {
|
|
743
|
+
if (!selectedModel.value || !models) return null
|
|
744
|
+
return models.find(m => m.name === selectedModel.value) || models.find(m => m.id === selectedModel.value)
|
|
745
|
+
})
|
|
746
|
+
const messagesContainer = ref(null)
|
|
747
|
+
const copying = ref(null)
|
|
748
|
+
|
|
749
|
+
const resolveUrl = (url) => {
|
|
750
|
+
if (url && url.startsWith('~')) {
|
|
751
|
+
return '/' + url
|
|
752
|
+
}
|
|
753
|
+
return ctx.ai.resolveUrl(url)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Auto-scroll to bottom when new messages arrive
|
|
757
|
+
const scrollToBottom = async () => {
|
|
758
|
+
await nextTick()
|
|
759
|
+
if (messagesContainer.value) {
|
|
760
|
+
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Watch for new messages and scroll
|
|
765
|
+
watch(() => currentThread.value?.messages?.length, scrollToBottom)
|
|
766
|
+
|
|
767
|
+
// Watch for route changes and load the appropriate thread
|
|
768
|
+
watch(() => route.params.id, async (newId) => {
|
|
769
|
+
// console.debug('watch route.params.id', newId)
|
|
770
|
+
ctx.clearError()
|
|
771
|
+
threads.setCurrentThreadFromRoute(newId, router)
|
|
772
|
+
|
|
773
|
+
if (!newId) {
|
|
774
|
+
chatPrompt.reset()
|
|
775
|
+
}
|
|
776
|
+
nextTick(ctx.chat.addCopyButtons)
|
|
777
|
+
}, { immediate: true })
|
|
778
|
+
|
|
779
|
+
watch(() => [selectedModel.value], () => {
|
|
780
|
+
ctx.setPrefs({
|
|
781
|
+
model: selectedModel.value,
|
|
782
|
+
})
|
|
783
|
+
})
|
|
784
|
+
function configUpdated() {
|
|
785
|
+
console.log('configUpdated', selectedModel.value, models.length, models.includes(selectedModel.value))
|
|
786
|
+
if (selectedModel.value && !models.includes(selectedModel.value)) {
|
|
787
|
+
selectedModel.value = config.defaults.text.model || ''
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const copyMessageContent = async (message) => {
|
|
792
|
+
let content = ''
|
|
793
|
+
if (Array.isArray(message.content)) {
|
|
794
|
+
content = message.content.map(part => {
|
|
795
|
+
if (part.type === 'text') return part.text
|
|
796
|
+
if (part.type === 'image_url') {
|
|
797
|
+
const name = part.image_url.url.split('/').pop() || 'image'
|
|
798
|
+
return `\n\n`
|
|
799
|
+
}
|
|
800
|
+
if (part.type === 'input_audio') {
|
|
801
|
+
const name = part.input_audio.data.split('/').pop() || 'audio'
|
|
802
|
+
return `\n[${name}](${part.input_audio.data})\n`
|
|
803
|
+
}
|
|
804
|
+
if (part.type === 'file') {
|
|
805
|
+
const name = part.file.filename || part.file.file_data.split('/').pop() || 'file'
|
|
806
|
+
return `\n[${name}](${part.file.file_data})`
|
|
807
|
+
}
|
|
808
|
+
return ''
|
|
809
|
+
}).join('\n')
|
|
810
|
+
} else {
|
|
811
|
+
content = message.content
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
try {
|
|
815
|
+
copying.value = message
|
|
816
|
+
await navigator.clipboard.writeText(content)
|
|
817
|
+
// Could add a toast notification here if desired
|
|
818
|
+
} catch (err) {
|
|
819
|
+
console.error('Failed to copy message content:', err)
|
|
820
|
+
// Fallback for older browsers
|
|
821
|
+
const textArea = document.createElement('textarea')
|
|
822
|
+
textArea.value = content
|
|
823
|
+
document.body.appendChild(textArea)
|
|
824
|
+
textArea.select()
|
|
825
|
+
document.execCommand('copy')
|
|
826
|
+
document.body.removeChild(textArea)
|
|
827
|
+
}
|
|
828
|
+
setTimeout(() => { copying.value = null }, 2000)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const getAttachments = (message) => {
|
|
832
|
+
if (!Array.isArray(message.content)) return []
|
|
833
|
+
return message.content.filter(c => c.type === 'image_url' || c.type === 'input_audio' || c.type === 'file')
|
|
834
|
+
}
|
|
835
|
+
const hasAttachments = (message) => getAttachments(message).length > 0
|
|
836
|
+
|
|
837
|
+
// Helper to extract content and files from message
|
|
838
|
+
const extractMessageState = async (message) => {
|
|
839
|
+
let text = ''
|
|
840
|
+
let files = []
|
|
841
|
+
const getCacheInfos = []
|
|
842
|
+
|
|
843
|
+
if (Array.isArray(message.content)) {
|
|
844
|
+
for (const part of message.content) {
|
|
845
|
+
if (part.type === 'text') {
|
|
846
|
+
text += part.text
|
|
847
|
+
} else if (part.type === 'image_url') {
|
|
848
|
+
const url = part.image_url.url
|
|
849
|
+
const name = url.split('/').pop() || 'image'
|
|
850
|
+
files.push({ name, url, type: 'image/png' }) // Assume image
|
|
851
|
+
getCacheInfos.push(url)
|
|
852
|
+
} else if (part.type === 'input_audio') {
|
|
853
|
+
const url = part.input_audio.data
|
|
854
|
+
const name = url.split('/').pop() || 'audio'
|
|
855
|
+
files.push({ name, url, type: 'audio/wav' }) // Assume audio
|
|
856
|
+
getCacheInfos.push(url)
|
|
857
|
+
} else if (part.type === 'file') {
|
|
858
|
+
const url = part.file.file_data
|
|
859
|
+
const name = part.file.filename || url.split('/').pop() || 'file'
|
|
860
|
+
files.push({ name, url })
|
|
861
|
+
getCacheInfos.push(url)
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
text = message.content
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const infos = await ctx.ai.fetchCacheInfos(getCacheInfos)
|
|
869
|
+
// replace name with info.name
|
|
870
|
+
for (let i = 0; i < files.length; i++) {
|
|
871
|
+
const url = files[i]?.url
|
|
872
|
+
const info = infos[url]
|
|
873
|
+
if (info) {
|
|
874
|
+
files[i].name = info.name
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return { text, files }
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Redo a user message (clear all messages after this one and re-run)
|
|
882
|
+
const redoMessage = async (message) => {
|
|
883
|
+
if (!currentThread.value || message.role !== 'user') return
|
|
884
|
+
|
|
885
|
+
const threadId = currentThread.value.id
|
|
886
|
+
|
|
887
|
+
// Clear all messages after this one
|
|
888
|
+
await threads.redoMessageFromThread(threadId, message.timestamp)
|
|
889
|
+
|
|
890
|
+
const state = await extractMessageState(message)
|
|
891
|
+
|
|
892
|
+
// Set the message text in the chat prompt
|
|
893
|
+
chatPrompt.messageText.value = state.text
|
|
894
|
+
|
|
895
|
+
// Restore attached files
|
|
896
|
+
chatPrompt.attachedFiles.value = state.files
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Edit a user message
|
|
900
|
+
const editMessage = async (message) => {
|
|
901
|
+
if (!currentThread.value || message.role !== 'user') return
|
|
902
|
+
|
|
903
|
+
// set the message in the input box
|
|
904
|
+
const state = await extractMessageState(message)
|
|
905
|
+
chatPrompt.messageText.value = state.text
|
|
906
|
+
chatPrompt.attachedFiles.value = state.files
|
|
907
|
+
chatPrompt.editingMessage.value = message.timestamp
|
|
908
|
+
|
|
909
|
+
// Focus the textarea
|
|
910
|
+
nextTick(() => {
|
|
911
|
+
const textarea = document.querySelector('textarea')
|
|
912
|
+
if (textarea) {
|
|
913
|
+
textarea.focus()
|
|
914
|
+
// Set cursor to end
|
|
915
|
+
textarea.selectionStart = textarea.selectionEnd = textarea.value.length
|
|
916
|
+
}
|
|
917
|
+
})
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
let sub
|
|
921
|
+
onMounted(() => setTimeout(ctx.chat.addCopyButtons, 1))
|
|
922
|
+
onUnmounted(() => sub?.unsubscribe())
|
|
923
|
+
|
|
924
|
+
const getToolOutput = (toolCallId) => {
|
|
925
|
+
return currentThread.value?.messages?.find(m => m.role === 'tool' && m.tool_call_id === toolCallId)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const getMessageUsage = (message) => {
|
|
929
|
+
if (message.usage) return message.usage
|
|
930
|
+
if (message.tool_calls?.length) {
|
|
931
|
+
const toolUsages = message.tool_calls.map(tc => getToolOutput(tc.id)?.usage)
|
|
932
|
+
const agg = {
|
|
933
|
+
tokens: toolUsages.reduce((a, b) => a + (b?.tokens || 0), 0),
|
|
934
|
+
cost: toolUsages.reduce((a, b) => a + (b?.cost || 0), 0),
|
|
935
|
+
duration: toolUsages.reduce((a, b) => a + (b?.duration || 0), 0)
|
|
936
|
+
}
|
|
937
|
+
return agg
|
|
938
|
+
}
|
|
939
|
+
return null
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const isToolLinked = (message) => {
|
|
943
|
+
if (message.role !== 'tool') return false
|
|
944
|
+
return currentThread.value?.messages?.some(m => m.role === 'assistant' && m.tool_calls?.some(tc => tc.id === message.tool_call_id))
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function setPrefs(o) {
|
|
948
|
+
Object.assign(prefs.value, o)
|
|
949
|
+
ctx.setPrefs(prefs.value)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
prefs,
|
|
954
|
+
setPrefs,
|
|
955
|
+
config,
|
|
956
|
+
models,
|
|
957
|
+
currentThread,
|
|
958
|
+
selectedModel,
|
|
959
|
+
selectedModelObj,
|
|
960
|
+
messagesContainer,
|
|
961
|
+
copying,
|
|
962
|
+
copyMessageContent,
|
|
963
|
+
redoMessage,
|
|
964
|
+
editMessage,
|
|
965
|
+
configUpdated,
|
|
966
|
+
getAttachments,
|
|
967
|
+
hasAttachments,
|
|
968
|
+
resolveUrl,
|
|
969
|
+
getMessageUsage,
|
|
970
|
+
getToolOutput,
|
|
971
|
+
isToolLinked,
|
|
972
|
+
tryParseJson,
|
|
973
|
+
hasJsonStructure,
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|