llms-py 3.0.6__py3-none-any.whl → 3.0.8__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/extensions/analytics/ui/index.mjs +1 -1
- llms/extensions/app/__init__.py +3 -1
- llms/extensions/app/ui/Recents.mjs +1 -1
- llms/extensions/app/ui/threadStore.mjs +4 -3
- llms/extensions/core_tools/__init__.py +14 -12
- llms/extensions/providers/__init__.py +2 -0
- llms/extensions/providers/anthropic.py +1 -1
- llms/extensions/providers/chutes.py +7 -9
- llms/extensions/providers/google.py +3 -3
- llms/extensions/providers/nvidia.py +9 -11
- llms/extensions/providers/openai.py +1 -3
- llms/extensions/providers/zai.py +182 -0
- llms/extensions/tools/__init__.py +140 -1
- llms/extensions/tools/ui/index.mjs +552 -50
- llms/llms.json +14 -2
- llms/main.py +461 -99
- llms/providers-extra.json +38 -0
- llms/providers.json +1 -1
- llms/ui/App.mjs +1 -1
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +287 -18
- llms/ui/ctx.mjs +16 -3
- llms/ui/index.mjs +1 -1
- llms/ui/modules/chat/ChatBody.mjs +384 -107
- llms/ui/modules/chat/index.mjs +18 -4
- llms/ui/tailwind.input.css +54 -0
- llms/ui/utils.mjs +33 -4
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/METADATA +1 -1
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/RECORD +33 -32
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/WHEEL +0 -0
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.6.dist-info → llms_py-3.0.8.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,252 @@
|
|
|
1
1
|
import { ref, computed, nextTick, watch, onMounted, onUnmounted, inject } from 'vue'
|
|
2
2
|
import { useRouter, useRoute } from 'vue-router'
|
|
3
3
|
|
|
4
|
-
|
|
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
|
+
export const TypeText = {
|
|
16
|
+
template: `
|
|
17
|
+
<div v-if="text.type === 'text'">
|
|
18
|
+
<div v-html="html"></div>
|
|
19
|
+
</div>
|
|
20
|
+
`,
|
|
21
|
+
props: {
|
|
22
|
+
text: {
|
|
23
|
+
type: Object,
|
|
24
|
+
required: true
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
setup(props) {
|
|
28
|
+
const ctx = inject('ctx')
|
|
29
|
+
const html = computed(() => {
|
|
30
|
+
try {
|
|
31
|
+
return ctx.fmt.markdown(props.text.text)
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error('TypeText: markdown', e)
|
|
34
|
+
return `<div class="whitespace-pre-wrap">${props.text.text}</div>`
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
return { html }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const LightboxImage = {
|
|
42
|
+
template: `
|
|
43
|
+
<div>
|
|
44
|
+
<!-- Thumbnail -->
|
|
45
|
+
<div
|
|
46
|
+
class="cursor-zoom-in hover:opacity-90 transition-opacity"
|
|
47
|
+
@click="isOpen = true"
|
|
48
|
+
>
|
|
49
|
+
<img
|
|
50
|
+
:src="src"
|
|
51
|
+
:alt="alt"
|
|
52
|
+
:width="width"
|
|
53
|
+
:height="height"
|
|
54
|
+
:class="imageClass"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Lightbox Modal -->
|
|
59
|
+
<div v-if="isOpen"
|
|
60
|
+
class="fixed inset-0 z-100 flex items-center justify-center bg-black/90 p-4"
|
|
61
|
+
@click="isOpen = false"
|
|
62
|
+
>
|
|
63
|
+
<button type="button"
|
|
64
|
+
class="absolute top-4 right-4 p-2 text-white hover:bg-white/10 rounded-lg transition-colors"
|
|
65
|
+
@click="isOpen = false"
|
|
66
|
+
aria-label="Close lightbox"
|
|
67
|
+
>
|
|
68
|
+
<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>
|
|
69
|
+
</button>
|
|
70
|
+
<div class="relative max-w-7xl max-h-[90vh] w-full h-full flex items-center justify-center">
|
|
71
|
+
<img
|
|
72
|
+
:src="src"
|
|
73
|
+
:alt="alt"
|
|
74
|
+
:width="width"
|
|
75
|
+
:height="height"
|
|
76
|
+
class="max-w-full max-h-full w-auto h-auto object-contain rounded"
|
|
77
|
+
@click.stop
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
`,
|
|
83
|
+
props: {
|
|
84
|
+
src: {
|
|
85
|
+
type: String,
|
|
86
|
+
required: true
|
|
87
|
+
},
|
|
88
|
+
alt: {
|
|
89
|
+
type: String,
|
|
90
|
+
default: ''
|
|
91
|
+
},
|
|
92
|
+
width: {
|
|
93
|
+
type: [Number, String],
|
|
94
|
+
default: undefined
|
|
95
|
+
},
|
|
96
|
+
height: {
|
|
97
|
+
type: [Number, String],
|
|
98
|
+
default: undefined
|
|
99
|
+
},
|
|
100
|
+
imageClass: {
|
|
101
|
+
type: String,
|
|
102
|
+
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]'
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
setup(props) {
|
|
106
|
+
const ctx = inject('ctx')
|
|
107
|
+
const isOpen = ref(false)
|
|
108
|
+
|
|
109
|
+
let sub
|
|
110
|
+
onMounted(() => {
|
|
111
|
+
sub = ctx.events.subscribe(`keydown:Escape`, () => isOpen.value = false)
|
|
112
|
+
})
|
|
113
|
+
onUnmounted(() => sub?.unsubscribe())
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
isOpen
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const TypeImage = {
|
|
122
|
+
template: `
|
|
123
|
+
<div v-if="image.type === 'image_url'">
|
|
124
|
+
<LightboxImage :src="$ctx.resolveUrl(image.image_url.url)" />
|
|
125
|
+
</div>
|
|
126
|
+
`,
|
|
127
|
+
props: {
|
|
128
|
+
image: {
|
|
129
|
+
type: Object,
|
|
130
|
+
required: true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const TypeAudio = {
|
|
136
|
+
template: `
|
|
137
|
+
<div v-if="audio.type === 'audio_url'">
|
|
138
|
+
<slot></slot>
|
|
139
|
+
<audio controls :src="$ctx.resolveUrl(audio.audio_url.url)" class="h-8 w-64"></audio>
|
|
140
|
+
</div>
|
|
141
|
+
`,
|
|
142
|
+
props: {
|
|
143
|
+
audio: {
|
|
144
|
+
type: Object,
|
|
145
|
+
required: true
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const TypeFile = {
|
|
151
|
+
template: `
|
|
152
|
+
<a v-if="file.type === 'file'" :href="$ctx.resolveUrl(file.file.file_data)" target="_blank"
|
|
153
|
+
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">
|
|
154
|
+
<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>
|
|
155
|
+
<span class="max-w-xs truncate">{{ file.file.filename || 'Attachment' }}</span>
|
|
156
|
+
</a>
|
|
157
|
+
`,
|
|
158
|
+
props: {
|
|
159
|
+
file: {
|
|
160
|
+
type: Object,
|
|
161
|
+
required: true
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const ViewType = {
|
|
167
|
+
template: `
|
|
168
|
+
<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">
|
|
169
|
+
<TypeText v-if="result.type === 'text'" :text="result" />
|
|
170
|
+
<TypeImage v-else-if="result.type === 'image_url'" :image="result" />
|
|
171
|
+
<TypeAudio v-else-if="result.type === 'audio_url'" :audio="result" />
|
|
172
|
+
<TypeFile v-else-if="result.type === 'file'" :file="result" />
|
|
173
|
+
<div v-else>
|
|
174
|
+
<HtmlFormat :value="result" :classes="$utils.htmlFormatClasses" />
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
`,
|
|
178
|
+
props: {
|
|
179
|
+
result: {
|
|
180
|
+
type: Object,
|
|
181
|
+
required: true
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export const ViewTypes = {
|
|
186
|
+
template: `
|
|
187
|
+
<div v-if="results?.length" class="flex flex-col gap-2">
|
|
188
|
+
<div v-if="texts.length > 0" :class="cls">
|
|
189
|
+
<div v-if="hasResources" v-for="(text, i) in texts" :key="'raw-' + i" class="text-xs whitespace-pre-wrap">{{text.text}}</div>
|
|
190
|
+
<TypeText v-else v-for="(text, i) in texts" :key="'text-' + i" :text="text" />
|
|
191
|
+
</div>
|
|
192
|
+
<div v-if="images.length > 0" :class="cls">
|
|
193
|
+
<TypeImage v-for="(image, i) in images" :key="'image-' + i" :image="image" />
|
|
194
|
+
</div>
|
|
195
|
+
<div v-if="audios.length > 0" :class="cls">
|
|
196
|
+
<TypeAudio v-for="(audio, i) in audios" :key="'audio-' + i" :audio="audio" />
|
|
197
|
+
</div>
|
|
198
|
+
<div v-if="files.length > 0" :class="cls">
|
|
199
|
+
<TypeFile v-for="(file, i) in files" :key="'file-' + i" :file="file" />
|
|
200
|
+
</div>
|
|
201
|
+
<div v-if="others.length > 0" :class="cls">
|
|
202
|
+
<HtmlFormat v-for="(other, i) in others" :key="'other-' + i" :value="other" :classes="$utils.htmlFormatClasses" />
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
`,
|
|
206
|
+
props: {
|
|
207
|
+
results: {
|
|
208
|
+
type: Array,
|
|
209
|
+
required: true
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
setup(props) {
|
|
213
|
+
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"
|
|
214
|
+
const texts = computed(() => props.results.filter(r => r.type === 'text'))
|
|
215
|
+
const images = computed(() => props.results.filter(r => r.type === 'image_url'))
|
|
216
|
+
const audios = computed(() => props.results.filter(r => r.type === 'audio_url'))
|
|
217
|
+
const files = computed(() => props.results.filter(r => r.type === 'file'))
|
|
218
|
+
const others = computed(() => props.results.filter(r => r.type !== 'text' && r.type !== 'image_url' && r.type !== 'audio_url' && r.type !== 'file'))
|
|
219
|
+
// If has resources, render as plain-text to avoid rendering resources multiple times
|
|
220
|
+
const hasResources = computed(() => images.value.length > 0 || audios.value.length > 0 || files.value.length > 0 || others.value.length > 0)
|
|
221
|
+
return { cls, texts, images, audios, files, others, hasResources }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export const ViewToolTypes = {
|
|
225
|
+
template: `<ViewTypes v-if="results?.length" :results="results" />`,
|
|
226
|
+
props: {
|
|
227
|
+
output: Object,
|
|
228
|
+
},
|
|
229
|
+
setup(props) {
|
|
230
|
+
const results = computed(() => {
|
|
231
|
+
const ret = []
|
|
232
|
+
if (!props.output) return ret
|
|
233
|
+
if (props.output.images) {
|
|
234
|
+
ret.push(...props.output.images)
|
|
235
|
+
}
|
|
236
|
+
if (props.output.audios) {
|
|
237
|
+
ret.push(...props.output.audios)
|
|
238
|
+
}
|
|
239
|
+
if (props.output.files) {
|
|
240
|
+
ret.push(...props.output.files)
|
|
241
|
+
}
|
|
242
|
+
return ret
|
|
243
|
+
})
|
|
244
|
+
return { results }
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
export const MessageUsage = {
|
|
5
250
|
template: `
|
|
6
251
|
<div class="mt-2 text-xs opacity-70">
|
|
7
252
|
<span v-if="message.model" @click="$chat.setSelectedModel({ name: message.model })" title="Select model"><span class="cursor-pointer hover:underline">{{ message.model }}</span> • </span>
|
|
@@ -10,7 +255,7 @@ const MessageUsage = {
|
|
|
10
255
|
•
|
|
11
256
|
{{ $fmt.humanifyNumber(usage.tokens) }} tokens
|
|
12
257
|
<span v-if="usage.cost">· {{ $fmt.tokenCostLong(usage.cost) }}</span>
|
|
13
|
-
<span v-if="usage.duration"> in {{ $fmt.humanifyMs(usage.duration) }}</span>
|
|
258
|
+
<span v-if="usage.duration"> in {{ $fmt.humanifyMs(usage.duration * 1000) }}</span>
|
|
14
259
|
</span>
|
|
15
260
|
</div>
|
|
16
261
|
`,
|
|
@@ -20,14 +265,14 @@ const MessageUsage = {
|
|
|
20
265
|
}
|
|
21
266
|
}
|
|
22
267
|
|
|
23
|
-
const MessageReasoning = {
|
|
268
|
+
export const MessageReasoning = {
|
|
24
269
|
template: `
|
|
25
270
|
<div class="mt-2 mb-2">
|
|
26
|
-
<button type="button" @click="toggleReasoning(message.
|
|
27
|
-
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" :class="isReasoningExpanded(message.
|
|
28
|
-
<span>{{ isReasoningExpanded(message.
|
|
271
|
+
<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">
|
|
272
|
+
<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>
|
|
273
|
+
<span>{{ isReasoningExpanded(message.timestamp) ? 'Hide reasoning' : 'Show reasoning' }}</span>
|
|
29
274
|
</button>
|
|
30
|
-
<div v-if="isReasoningExpanded(message.
|
|
275
|
+
<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">
|
|
31
276
|
<div v-if="typeof reasoning === 'string'" v-html="$fmt.markdown(reasoning)" class="prose prose-xs max-w-none dark:prose-invert"></div>
|
|
32
277
|
<pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto">{{ formatReasoning(reasoning) }}</pre>
|
|
33
278
|
</div>
|
|
@@ -60,11 +305,125 @@ const MessageReasoning = {
|
|
|
60
305
|
}
|
|
61
306
|
}
|
|
62
307
|
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
308
|
+
export const ToolArguments = {
|
|
309
|
+
template: `
|
|
310
|
+
<div ref="refArgs" v-if="dict" class="not-prose">
|
|
311
|
+
<div class="prose html-format">
|
|
312
|
+
<table class="table-object border-none">
|
|
313
|
+
<tr v-for="(v, k) in dict" :key="k">
|
|
314
|
+
<td class="align-top py-2 px-4 text-left text-sm font-medium tracking-wider whitespace-nowrap lowercase">{{ k }}</td>
|
|
315
|
+
<td v-if="$utils.isHtml(v)" style="margin:0;padding:0;width:100%">
|
|
316
|
+
<div v-html="embedHtml(v)" class="w-full h-full"></div>
|
|
317
|
+
</td>
|
|
318
|
+
<td v-else class="align-top py-2 px-4 text-sm whitespace-pre-wrap">
|
|
319
|
+
<HtmlFormat :value="v" :classes="$utils.htmlFormatClasses" />
|
|
320
|
+
</td>
|
|
321
|
+
</tr>
|
|
322
|
+
</table>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
<div v-else-if="list" class="not-prose px-3 py-2">
|
|
326
|
+
<HtmlFormat :value="list" :classes="$utils.htmlFormatClasses" />
|
|
327
|
+
</div>
|
|
328
|
+
<pre v-else-if="!isEmpty(value)" class="tool-arguments">{{ value }}</pre>
|
|
329
|
+
`,
|
|
330
|
+
props: {
|
|
331
|
+
value: String,
|
|
67
332
|
},
|
|
333
|
+
setup(props) {
|
|
334
|
+
const refArgs = ref()
|
|
335
|
+
function isEmpty(v) {
|
|
336
|
+
return !v || v === '{}' || v === '[]' || v === 'null' || v === 'undefined' || v === '""' || v === "''" || v === "``"
|
|
337
|
+
}
|
|
338
|
+
function embedHtml(html) {
|
|
339
|
+
const resizeScript = `<script>
|
|
340
|
+
let lastH = 0;
|
|
341
|
+
const sendHeight = () => {
|
|
342
|
+
const body = document.body;
|
|
343
|
+
if (!body) return;
|
|
344
|
+
// Force re-calc
|
|
345
|
+
const h = document.documentElement.getBoundingClientRect().height;
|
|
346
|
+
if (Math.abs(h - lastH) > 2) {
|
|
347
|
+
lastH = h;
|
|
348
|
+
window.parent.postMessage({ type: 'iframe-resize', height: h }, '*');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const ro = new ResizeObserver(sendHeight);
|
|
352
|
+
window.addEventListener('load', () => {
|
|
353
|
+
// Inject styles to prevent infinite loops
|
|
354
|
+
const style = document.createElement('style');
|
|
355
|
+
style.textContent = 'html, body { height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 0 !important; overflow: hidden !important; }';
|
|
356
|
+
document.head.appendChild(style);
|
|
357
|
+
|
|
358
|
+
const body = document.body;
|
|
359
|
+
if (body) {
|
|
360
|
+
ro.observe(body);
|
|
361
|
+
ro.observe(document.documentElement);
|
|
362
|
+
sendHeight();
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
<\/script>`
|
|
366
|
+
|
|
367
|
+
const escaped = (html + resizeScript)
|
|
368
|
+
.replace(/&/g, '&')
|
|
369
|
+
.replace(/</g, '<')
|
|
370
|
+
.replace(/>/g, '>')
|
|
371
|
+
.replace(/"/g, '"')
|
|
372
|
+
.replace(/'/g, ''')
|
|
373
|
+
return `<iframe srcdoc="${escaped}" sandbox="allow-scripts" style="width:100%;height:auto;border:none;"></iframe>`
|
|
374
|
+
}
|
|
375
|
+
const dict = computed(() => {
|
|
376
|
+
if (isEmpty(props.value)) return null
|
|
377
|
+
const ret = tryParseJson(props.value)
|
|
378
|
+
return typeof ret === 'object' && !Array.isArray(ret) ? ret : null
|
|
379
|
+
})
|
|
380
|
+
const list = computed(() => {
|
|
381
|
+
if (isEmpty(props.value)) return null
|
|
382
|
+
const ret = tryParseJson(props.value)
|
|
383
|
+
return Array.isArray(ret) && ret.length > 0 ? ret : null
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
const handleMessage = (event) => {
|
|
387
|
+
if (event.data?.type === 'iframe-resize' && typeof event.data.height === 'number') {
|
|
388
|
+
const iframes = refArgs.value?.querySelectorAll('iframe')
|
|
389
|
+
iframes?.forEach(iframe => {
|
|
390
|
+
if (iframe.contentWindow === event.source) {
|
|
391
|
+
iframe.style.height = (event.data.height + 30) + 'px'
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
onMounted(() => {
|
|
398
|
+
window.addEventListener('message', handleMessage)
|
|
399
|
+
const hasIframes = refArgs.value?.querySelector('iframe')
|
|
400
|
+
if (hasIframes) {
|
|
401
|
+
refArgs.value.classList.add('has-iframes')
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
onUnmounted(() => {
|
|
406
|
+
window.removeEventListener('message', handleMessage)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
refArgs,
|
|
411
|
+
dict,
|
|
412
|
+
list,
|
|
413
|
+
isEmpty,
|
|
414
|
+
embedHtml,
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export const ToolOutput = {
|
|
420
|
+
template: ``,
|
|
421
|
+
setup(props) {
|
|
422
|
+
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export const ChatBody = {
|
|
68
427
|
template: `
|
|
69
428
|
<div class="flex flex-col h-full">
|
|
70
429
|
<!-- Messages Area -->
|
|
@@ -87,7 +446,7 @@ export default {
|
|
|
87
446
|
<div class="space-y-2" v-if="currentThread?.messages?.length">
|
|
88
447
|
<div
|
|
89
448
|
v-for="message in currentThread.messages.filter(x => x.role !== 'system')"
|
|
90
|
-
:key="message.
|
|
449
|
+
:key="message.timestamp"
|
|
91
450
|
v-show="!(message.role === 'tool' && isToolLinked(message))"
|
|
92
451
|
class="flex items-start space-x-3 group"
|
|
93
452
|
:class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
|
|
@@ -109,7 +468,7 @@ export default {
|
|
|
109
468
|
</div>
|
|
110
469
|
|
|
111
470
|
<!-- Delete button (shown on hover) -->
|
|
112
|
-
<button type="button" @click.stop="$threads.deleteMessageFromThread(currentThread.id, message.
|
|
471
|
+
<button type="button" @click.stop="$threads.deleteMessageFromThread(currentThread.id, message.timestamp)"
|
|
113
472
|
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"
|
|
114
473
|
title="Delete message">
|
|
115
474
|
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -162,11 +521,7 @@ export default {
|
|
|
162
521
|
<span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium">Tool Call</span>
|
|
163
522
|
</div>
|
|
164
523
|
|
|
165
|
-
|
|
166
|
-
<div v-if="tool.function.arguments && tool.function.arguments != '{}'" class="not-prose px-3 py-2">
|
|
167
|
-
<HtmlFormat v-if="hasJsonStructure(tool.function.arguments)" :value="tryParseJson(tool.function.arguments)" :classes="customHtmlClasses" />
|
|
168
|
-
<pre v-else class="tool-arguments">{{ tool.function.arguments }}</pre>
|
|
169
|
-
</div>
|
|
524
|
+
<ToolArguments :value="tool.function.arguments" />
|
|
170
525
|
|
|
171
526
|
<!-- Tool Output (Nested) -->
|
|
172
527
|
<div v-if="getToolOutput(tool.id)" class="border-t border-gray-200 dark:border-gray-700">
|
|
@@ -192,10 +547,11 @@ export default {
|
|
|
192
547
|
<div class="not-prose px-3 py-2">
|
|
193
548
|
<pre v-if="prefs.toolFormat !== 'preview' || !hasJsonStructure(getToolOutput(tool.id).content)" class="tool-output">{{ getToolOutput(tool.id).content }}</pre>
|
|
194
549
|
<div v-else class="text-xs">
|
|
195
|
-
<HtmlFormat v-if="tryParseJson(getToolOutput(tool.id).content)" :value="tryParseJson(getToolOutput(tool.id).content)" :classes="
|
|
550
|
+
<HtmlFormat v-if="tryParseJson(getToolOutput(tool.id).content)" :value="tryParseJson(getToolOutput(tool.id).content)" :classes="$utils.htmlFormatClasses" />
|
|
196
551
|
<div v-else class="text-gray-500 italic p-2">Invalid JSON content</div>
|
|
197
552
|
</div>
|
|
198
553
|
</div>
|
|
554
|
+
<ViewToolTypes :output="getToolOutput(tool.id)" />
|
|
199
555
|
</div>
|
|
200
556
|
</div>
|
|
201
557
|
</div>
|
|
@@ -222,45 +578,24 @@ export default {
|
|
|
222
578
|
<!-- Assistant Images -->
|
|
223
579
|
<div v-if="message.images && message.images.length > 0" class="mt-2 flex flex-wrap gap-2">
|
|
224
580
|
<template v-for="(img, i) in message.images" :key="i">
|
|
225
|
-
<
|
|
226
|
-
<img :src="resolveUrl(img.image_url.url)" class="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]" />
|
|
227
|
-
</div>
|
|
581
|
+
<TypeImage v-if="img.type === 'image_url'" :image="img" />
|
|
228
582
|
</template>
|
|
229
583
|
</div>
|
|
230
584
|
|
|
231
585
|
<!-- Assistant Audios -->
|
|
232
586
|
<div v-if="message.audios && message.audios.length > 0" class="mt-2 flex flex-wrap gap-2">
|
|
233
587
|
<template v-for="(audio, i) in message.audios" :key="i">
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
588
|
+
<TypeAudio v-if="audio.type === 'audio_url'" :audio="audio"
|
|
589
|
+
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">
|
|
590
|
+
<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>
|
|
591
|
+
</TypeAudio>
|
|
237
592
|
</template>
|
|
238
593
|
</div>
|
|
239
594
|
|
|
240
595
|
<!-- User Message with separate attachments -->
|
|
241
596
|
<div v-else-if="message.role !== 'assistant' && message.role !== 'tool'">
|
|
242
|
-
<div v-html="$fmt.
|
|
243
|
-
|
|
244
|
-
<!-- Attachments Grid -->
|
|
245
|
-
<div v-if="hasAttachments(message)" class="mt-2 flex flex-wrap gap-2">
|
|
246
|
-
<template v-for="(part, i) in getAttachments(message)" :key="i">
|
|
247
|
-
<!-- Image -->
|
|
248
|
-
<div v-if="part.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(part.image_url.url)">
|
|
249
|
-
<img :src="part.image_url.url" class="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]" />
|
|
250
|
-
</div>
|
|
251
|
-
<!-- Audio -->
|
|
252
|
-
<div v-else-if="part.type === 'input_audio'" 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">
|
|
253
|
-
<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>
|
|
254
|
-
<audio controls :src="part.input_audio.data" class="h-8 w-48"></audio>
|
|
255
|
-
</div>
|
|
256
|
-
<!-- File -->
|
|
257
|
-
<a v-else-if="part.type === 'file'" :href="part.file.file_data" target="_blank"
|
|
258
|
-
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">
|
|
259
|
-
<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>
|
|
260
|
-
<span class="max-w-xs truncate">{{ part.file.filename || 'Attachment' }}</span>
|
|
261
|
-
</a>
|
|
262
|
-
</template>
|
|
263
|
-
</div>
|
|
597
|
+
<div v-html="$fmt.content(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
|
|
598
|
+
<ViewTypes :results="getAttachments(message)" />
|
|
264
599
|
</div>
|
|
265
600
|
|
|
266
601
|
<MessageUsage :message="message" :usage="getMessageUsage(message)" />
|
|
@@ -289,7 +624,7 @@ export default {
|
|
|
289
624
|
|
|
290
625
|
<div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 dark:text-gray-400 text-sm">
|
|
291
626
|
<span :title="$fmt.statsTitle(currentThread.stats)">
|
|
292
|
-
{{ 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) }}
|
|
627
|
+
{{ 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) }}
|
|
293
628
|
</span>
|
|
294
629
|
</div>
|
|
295
630
|
|
|
@@ -377,21 +712,6 @@ export default {
|
|
|
377
712
|
<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')">
|
|
378
713
|
<ChatPrompt :model="$chat.getSelectedModel()" />
|
|
379
714
|
</div>
|
|
380
|
-
|
|
381
|
-
<!-- Lightbox -->
|
|
382
|
-
<div v-if="lightboxUrl" class="fixed inset-0 z-[100] bg-black/90 flex items-center justify-center p-4 cursor-pointer"
|
|
383
|
-
@click="closeLightbox">
|
|
384
|
-
<button type="button" @click="closeLightbox"
|
|
385
|
-
class="absolute top-4 right-4 text-white/70 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors z-[101]"
|
|
386
|
-
title="Close">
|
|
387
|
-
<svg class="size-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
388
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
389
|
-
</svg>
|
|
390
|
-
</button>
|
|
391
|
-
<div class="relative max-w-full max-h-full">
|
|
392
|
-
<img :src="lightboxUrl" class="max-w-full max-h-[90vh] object-contain rounded-sm shadow-2xl" @click.stop />
|
|
393
|
-
</div>
|
|
394
|
-
</div>
|
|
395
715
|
</div>
|
|
396
716
|
`,
|
|
397
717
|
setup() {
|
|
@@ -414,14 +734,6 @@ export default {
|
|
|
414
734
|
})
|
|
415
735
|
const messagesContainer = ref(null)
|
|
416
736
|
const copying = ref(null)
|
|
417
|
-
const lightboxUrl = ref(null)
|
|
418
|
-
|
|
419
|
-
const openLightbox = (url) => {
|
|
420
|
-
lightboxUrl.value = url
|
|
421
|
-
}
|
|
422
|
-
const closeLightbox = () => {
|
|
423
|
-
lightboxUrl.value = null
|
|
424
|
-
}
|
|
425
737
|
|
|
426
738
|
const resolveUrl = (url) => {
|
|
427
739
|
if (url && url.startsWith('~')) {
|
|
@@ -595,10 +907,7 @@ export default {
|
|
|
595
907
|
}
|
|
596
908
|
|
|
597
909
|
let sub
|
|
598
|
-
onMounted(() =>
|
|
599
|
-
sub = ctx.events.subscribe(`keydown:Escape`, closeLightbox)
|
|
600
|
-
setTimeout(ctx.chat.addCopyButtons, 1)
|
|
601
|
-
})
|
|
910
|
+
onMounted(() => setTimeout(ctx.chat.addCopyButtons, 1))
|
|
602
911
|
onUnmounted(() => sub?.unsubscribe())
|
|
603
912
|
|
|
604
913
|
const getToolOutput = (toolCallId) => {
|
|
@@ -624,34 +933,6 @@ export default {
|
|
|
624
933
|
return currentThread.value?.messages?.some(m => m.role === 'assistant' && m.tool_calls?.some(tc => tc.id === message.tool_call_id))
|
|
625
934
|
}
|
|
626
935
|
|
|
627
|
-
const tryParseJson = (str) => {
|
|
628
|
-
try {
|
|
629
|
-
return JSON.parse(str)
|
|
630
|
-
} catch (e) {
|
|
631
|
-
return null
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
const hasJsonStructure = (str) => {
|
|
635
|
-
return tryParseJson(str) != null
|
|
636
|
-
}
|
|
637
|
-
/**
|
|
638
|
-
* @param {object|array} type
|
|
639
|
-
* @param {'div'|'table'|'thead'|'th'|'tr'|'td'} tag
|
|
640
|
-
* @param {number} depth
|
|
641
|
-
* @param {string} cls
|
|
642
|
-
* @param {number} index
|
|
643
|
-
*/
|
|
644
|
-
const customHtmlClasses = (type, tag, depth, cls, index) => {
|
|
645
|
-
cls = cls.replace('shadow ring-1 ring-black/5 md:rounded-lg', '')
|
|
646
|
-
if (tag == 'th') {
|
|
647
|
-
cls += ' lowercase'
|
|
648
|
-
}
|
|
649
|
-
if (tag == 'td') {
|
|
650
|
-
cls += ' whitespace-pre-wrap'
|
|
651
|
-
}
|
|
652
|
-
return cls
|
|
653
|
-
}
|
|
654
|
-
|
|
655
936
|
function setPrefs(o) {
|
|
656
937
|
Object.assign(prefs.value, o)
|
|
657
938
|
ctx.setPrefs(prefs.value)
|
|
@@ -673,16 +954,12 @@ export default {
|
|
|
673
954
|
configUpdated,
|
|
674
955
|
getAttachments,
|
|
675
956
|
hasAttachments,
|
|
676
|
-
lightboxUrl,
|
|
677
|
-
openLightbox,
|
|
678
|
-
closeLightbox,
|
|
679
957
|
resolveUrl,
|
|
680
958
|
getMessageUsage,
|
|
681
959
|
getToolOutput,
|
|
682
960
|
isToolLinked,
|
|
683
961
|
tryParseJson,
|
|
684
962
|
hasJsonStructure,
|
|
685
|
-
customHtmlClasses,
|
|
686
963
|
}
|
|
687
964
|
}
|
|
688
965
|
}
|
llms/ui/modules/chat/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import { ref, watch, computed, nextTick, inject } from 'vue'
|
|
3
|
-
import { $$, createElement, lastRightPart, ApiResult, createErrorStatus
|
|
3
|
+
import { $$, createElement, lastRightPart, ApiResult, createErrorStatus } from "@servicestack/client"
|
|
4
4
|
import SettingsDialog, { useSettings } from './SettingsDialog.mjs'
|
|
5
|
-
import ChatBody from './ChatBody.mjs'
|
|
5
|
+
import { ChatBody, LightboxImage, TypeText, TypeImage, TypeAudio, TypeFile, ViewType, ViewTypes, ViewToolTypes, ToolArguments, ToolOutput, MessageUsage, MessageReasoning } from './ChatBody.mjs'
|
|
6
6
|
import { AppContext } from '../../ctx.mjs'
|
|
7
7
|
|
|
8
8
|
const imageExts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
|
|
@@ -466,11 +466,11 @@ const ChatPrompt = {
|
|
|
466
466
|
</div>
|
|
467
467
|
|
|
468
468
|
<!-- Image Aspect Ratio Selector -->
|
|
469
|
-
<div v-if="$chat.canGenerateImage(model)"
|
|
469
|
+
<div v-if="$chat.canGenerateImage(model)">
|
|
470
470
|
<select name="aspect_ratio" v-model="$state.selectedAspectRatio"
|
|
471
471
|
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">
|
|
472
472
|
<option v-for="(ratio, size) in imageAspectRatios" :key="size" :value="size">
|
|
473
|
-
{{
|
|
473
|
+
{{ ratio }}
|
|
474
474
|
</option>
|
|
475
475
|
</select>
|
|
476
476
|
</div>
|
|
@@ -923,7 +923,21 @@ export default {
|
|
|
923
923
|
ctx.components({
|
|
924
924
|
SettingsDialog,
|
|
925
925
|
ChatPrompt,
|
|
926
|
+
|
|
926
927
|
ChatBody,
|
|
928
|
+
MessageUsage,
|
|
929
|
+
MessageReasoning,
|
|
930
|
+
LightboxImage,
|
|
931
|
+
TypeText,
|
|
932
|
+
TypeImage,
|
|
933
|
+
TypeAudio,
|
|
934
|
+
TypeFile,
|
|
935
|
+
ViewType,
|
|
936
|
+
ViewTypes,
|
|
937
|
+
ViewToolTypes,
|
|
938
|
+
ToolArguments,
|
|
939
|
+
ToolOutput,
|
|
940
|
+
|
|
927
941
|
HomeTools,
|
|
928
942
|
Home,
|
|
929
943
|
ThreadHeader,
|