llms-py 3.0.10__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/extensions/app/__init__.py +0 -1
- llms/extensions/app/db.py +7 -3
- llms/extensions/app/ui/threadStore.mjs +10 -3
- 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/__init__.py +0 -38
- llms/extensions/providers/anthropic.py +28 -1
- llms/extensions/providers/cerebras.py +0 -1
- llms/extensions/providers/google.py +112 -34
- 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/ui/index.mjs +6 -10
- llms/extensions/tools/__init__.py +5 -82
- llms/extensions/tools/ui/index.mjs +194 -63
- llms/main.py +502 -146
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +530 -0
- llms/ui/ctx.mjs +53 -6
- llms/ui/modules/chat/ChatBody.mjs +200 -20
- llms/ui/modules/chat/index.mjs +108 -104
- llms/ui/tailwind.input.css +10 -0
- llms/ui/utils.mjs +25 -1
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/METADATA +2 -2
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/RECORD +41 -24
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
llms/ui/ctx.mjs
CHANGED
|
@@ -392,12 +392,11 @@ export class AppContext {
|
|
|
392
392
|
if (Array.isArray(content)) {
|
|
393
393
|
content = content.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
394
394
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// }
|
|
395
|
+
if (content && content.startsWith('---')) {
|
|
396
|
+
const headerEnd = content.indexOf('---', 3)
|
|
397
|
+
const header = content.substring(3, headerEnd).trim()
|
|
398
|
+
content = '<div class="frontmatter">' + header + '</div>\n' + content.substring(headerEnd + 3)
|
|
399
|
+
}
|
|
401
400
|
return this.marked.parse(content || '')
|
|
402
401
|
}
|
|
403
402
|
|
|
@@ -409,4 +408,52 @@ export class AppContext {
|
|
|
409
408
|
}
|
|
410
409
|
return this.renderMarkdown(content)
|
|
411
410
|
}
|
|
411
|
+
|
|
412
|
+
createChatContext({ request, thread, context }) {
|
|
413
|
+
if (!request.messages) request.messages = []
|
|
414
|
+
if (!request.metadata) request.metadata = {}
|
|
415
|
+
if (!context) context = {}
|
|
416
|
+
Object.assign(context, {
|
|
417
|
+
systemPrompt: '',
|
|
418
|
+
requiredSystemPrompts: [],
|
|
419
|
+
}, context)
|
|
420
|
+
return {
|
|
421
|
+
request,
|
|
422
|
+
thread,
|
|
423
|
+
context,
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
completeChatContext({ request, thread, context }) {
|
|
428
|
+
|
|
429
|
+
let existingSystemPrompt = request.messages.find(m => m.role === 'system')?.content
|
|
430
|
+
|
|
431
|
+
let existingMessages = request.messages.filter(m => m.role == 'assistant' || m.role == 'tool')
|
|
432
|
+
if (existingMessages.length) {
|
|
433
|
+
const messageTypes = {}
|
|
434
|
+
request.messages.forEach(m => {
|
|
435
|
+
messageTypes[m.role] = (messageTypes[m.role] || 0) + 1
|
|
436
|
+
})
|
|
437
|
+
const summary = JSON.stringify(messageTypes).replace(/"/g, '')
|
|
438
|
+
console.debug(`completeChatContext(${summary})`, request)
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let newSystemPrompts = context.requiredSystemPrompts ?? []
|
|
443
|
+
if (context.systemPrompt) {
|
|
444
|
+
newSystemPrompts.push(context.systemPrompt)
|
|
445
|
+
}
|
|
446
|
+
if (existingSystemPrompt) {
|
|
447
|
+
newSystemPrompts.push(existingSystemPrompt)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let newSystemPrompt = newSystemPrompts.join('\n\n')
|
|
451
|
+
if (newSystemPrompt) {
|
|
452
|
+
// add or replace system prompt
|
|
453
|
+
request.messages = request.messages.filter(m => m.role !== 'system')
|
|
454
|
+
request.messages.unshift({ role: 'system', content: newSystemPrompt })
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
console.debug(`completeChatContext()`, request)
|
|
458
|
+
}
|
|
412
459
|
}
|
|
@@ -29,6 +29,11 @@ function embedHtml(html) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
const ro = new ResizeObserver(sendHeight);
|
|
32
|
+
window.addEventListener('message', (e) => {
|
|
33
|
+
if (e.data && e.data.type === 'stop-resize') {
|
|
34
|
+
ro.disconnect();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
32
37
|
window.addEventListener('load', () => {
|
|
33
38
|
// Inject styles to prevent infinite loops
|
|
34
39
|
const style = document.createElement('style');
|
|
@@ -55,8 +60,8 @@ function embedHtml(html) {
|
|
|
55
60
|
|
|
56
61
|
export const TypeText = {
|
|
57
62
|
template: `
|
|
58
|
-
<div v-if="text.type === 'text'">
|
|
59
|
-
<div v-html="html"></div>
|
|
63
|
+
<div data-type="text" v-if="text.type === 'text'">
|
|
64
|
+
<div v-html="html?.trim()" class="whitespace-pre-wrap"></div>
|
|
60
65
|
</div>
|
|
61
66
|
`,
|
|
62
67
|
props: {
|
|
@@ -72,7 +77,7 @@ export const TypeText = {
|
|
|
72
77
|
return ctx.fmt.markdown(props.text.text)
|
|
73
78
|
} catch (e) {
|
|
74
79
|
console.error('TypeText: markdown', e)
|
|
75
|
-
return `<div
|
|
80
|
+
return `<div>${props.text.text}</div>`
|
|
76
81
|
}
|
|
77
82
|
})
|
|
78
83
|
return { html }
|
|
@@ -161,7 +166,7 @@ export const LightboxImage = {
|
|
|
161
166
|
|
|
162
167
|
export const TypeImage = {
|
|
163
168
|
template: `
|
|
164
|
-
<div v-if="image.type === 'image_url'">
|
|
169
|
+
<div data-type="image" v-if="image.type === 'image_url'">
|
|
165
170
|
<LightboxImage :src="$ctx.resolveUrl(image.image_url.url)" />
|
|
166
171
|
</div>
|
|
167
172
|
`,
|
|
@@ -175,7 +180,7 @@ export const TypeImage = {
|
|
|
175
180
|
|
|
176
181
|
export const TypeAudio = {
|
|
177
182
|
template: `
|
|
178
|
-
<div v-if="audio.type === 'audio_url'">
|
|
183
|
+
<div data-type="audio" v-if="audio.type === 'audio_url'">
|
|
179
184
|
<slot></slot>
|
|
180
185
|
<audio controls :src="$ctx.resolveUrl(audio.audio_url.url)" class="h-8 w-64"></audio>
|
|
181
186
|
</div>
|
|
@@ -190,7 +195,7 @@ export const TypeAudio = {
|
|
|
190
195
|
|
|
191
196
|
export const TypeFile = {
|
|
192
197
|
template: `
|
|
193
|
-
<a v-if="file.type === 'file'" :href="$ctx.resolveUrl(file.file.file_data)" target="_blank"
|
|
198
|
+
<a data-type="file" v-if="file.type === 'file'" :href="$ctx.resolveUrl(file.file.file_data)" target="_blank"
|
|
194
199
|
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
200
|
<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
201
|
<span class="max-w-xs truncate">{{ file.file.filename || 'Attachment' }}</span>
|
|
@@ -211,7 +216,7 @@ export const ViewType = {
|
|
|
211
216
|
<TypeImage v-else-if="result.type === 'image_url'" :image="result" />
|
|
212
217
|
<TypeAudio v-else-if="result.type === 'audio_url'" :audio="result" />
|
|
213
218
|
<TypeFile v-else-if="result.type === 'file'" :file="result" />
|
|
214
|
-
<div v-else>
|
|
219
|
+
<div data-type="other" v-else>
|
|
215
220
|
<HtmlFormat :value="result" :classes="$utils.htmlFormatClasses" />
|
|
216
221
|
</div>
|
|
217
222
|
</div>
|
|
@@ -346,17 +351,167 @@ export const MessageReasoning = {
|
|
|
346
351
|
}
|
|
347
352
|
}
|
|
348
353
|
|
|
354
|
+
export const TextViewer = {
|
|
355
|
+
template: `
|
|
356
|
+
<div v-if="text.length > 200" class="relative group">
|
|
357
|
+
<div class="absolute top-0 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center space-x-2 bg-gray-50/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-md px-2 py-1 z-10 border border-gray-200 dark:border-gray-700 shadow-sm">
|
|
358
|
+
<!-- Style Selector -->
|
|
359
|
+
<div class="relative flex items-center">
|
|
360
|
+
<button type="button" @click="toggleDropdown" class="text-[10px] uppercase font-bold tracking-wider text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none flex items-center select-none">
|
|
361
|
+
<span>{{ prefs || 'pre' }}</span>
|
|
362
|
+
<svg class="mb-0.5 size-3 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M6 9l6 6 6-6"/></svg>
|
|
363
|
+
</button>
|
|
364
|
+
<!-- Popover -->
|
|
365
|
+
<div v-if="dropdownOpen" class="absolute right-0 top-full w-28 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-20 overflow-hidden">
|
|
366
|
+
<button
|
|
367
|
+
v-for="style in textStyles"
|
|
368
|
+
:key="style"
|
|
369
|
+
@click="setStyle(style)"
|
|
370
|
+
class="block w-full text-left px-3 py-1.5 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors uppercase tracking-wider font-medium"
|
|
371
|
+
:class="{ 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20': prefs === style }"
|
|
372
|
+
>
|
|
373
|
+
{{ style }}
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<div class="w-px h-3 bg-gray-300 dark:bg-gray-600"></div>
|
|
379
|
+
|
|
380
|
+
<!-- Text Length -->
|
|
381
|
+
<span class="text-xs text-gray-500 dark:text-gray-400 tabular-nums" :title="text.length + ' characters'">
|
|
382
|
+
{{ $fmt.humanifyNumber(text.length) }}
|
|
383
|
+
</span>
|
|
384
|
+
|
|
385
|
+
<!-- Copy Button -->
|
|
386
|
+
<button type="button" @click="copyToClipboard" class="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 focus:outline-none p-0.5 rounded transition-colors" title="Copy to clipboard">
|
|
387
|
+
<svg v-if="copied" class="size-4 text-green-600 dark:text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="m9.55 18l-5.7-5.7l1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4z"/></svg>
|
|
388
|
+
<svg v-else xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2m0 16H8V7h11z"/></svg>
|
|
389
|
+
</button>
|
|
390
|
+
|
|
391
|
+
<!-- Maximize Toggle -->
|
|
392
|
+
<button type="button" @click="toggleMaximized" class="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 focus:outline-none p-0.5 rounded transition-colors" :title="isMaximized ? 'Minimize' : 'Maximize'">
|
|
393
|
+
<svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
394
|
+
<path v-if="isMaximized" fill="currentColor" d="M9 9H3V7h4V3h2zm0 6H3v2h4v4h2zm12 0h-6v6h2v-4h4zm-6-6h6V7h-4V3h-2z"/>
|
|
395
|
+
<path v-else fill="currentColor" d="M3 3h6v2H5v4H3zm0 18h6v-2H5v-4H3zm12 0h6v-6h-2v4h-4zm6-18h-6v2h4v4h2z"/>
|
|
396
|
+
</svg>
|
|
397
|
+
</button>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<!-- Content -->
|
|
401
|
+
<div :class="containerClass">
|
|
402
|
+
<div v-if="prefs === 'markdown'" class="prose prose-sm max-w-none dark:prose-invert">
|
|
403
|
+
<div v-html="$fmt.markdown(text)"></div>
|
|
404
|
+
</div>
|
|
405
|
+
<div v-else-if="prefs === 'preview' && jsonValue">
|
|
406
|
+
<HtmlFormat :value="jsonValue" />
|
|
407
|
+
</div>
|
|
408
|
+
<div v-else :class="['p-0.5', contentClass]">{{ text }}</div>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
<div v-else class="whitespace-pre-wrap">{{ text }}</div>
|
|
412
|
+
`,
|
|
413
|
+
props: {
|
|
414
|
+
prefsName: String,
|
|
415
|
+
text: String,
|
|
416
|
+
},
|
|
417
|
+
setup(props) {
|
|
418
|
+
const ctx = inject('ctx')
|
|
419
|
+
const prefs = ref('pre')
|
|
420
|
+
const maximized = ref({})
|
|
421
|
+
const dropdownOpen = ref(false)
|
|
422
|
+
const hash = computed(() => ctx.utils.hashString(props.text))
|
|
423
|
+
const jsonValue = computed(() => ctx.utils.toJsonObject(props.text))
|
|
424
|
+
const textStyles = computed(() => {
|
|
425
|
+
const ret = ['pre', 'normal', 'markdown']
|
|
426
|
+
if (jsonValue.value) {
|
|
427
|
+
ret.push('preview')
|
|
428
|
+
}
|
|
429
|
+
return ret
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
const toggleDropdown = () => {
|
|
433
|
+
dropdownOpen.value = !dropdownOpen.value
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const copied = ref(false)
|
|
437
|
+
const copyToClipboard = () => {
|
|
438
|
+
navigator.clipboard.writeText(props.text)
|
|
439
|
+
copied.value = true
|
|
440
|
+
setTimeout(() => {
|
|
441
|
+
copied.value = false
|
|
442
|
+
}, 2000)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const setStyle = (style) => {
|
|
446
|
+
prefs.value = style
|
|
447
|
+
dropdownOpen.value = false
|
|
448
|
+
const key = props.prefsName || 'default'
|
|
449
|
+
const currentPrefs = ctx.getPrefs().textStyle || {}
|
|
450
|
+
ctx.setPrefs({
|
|
451
|
+
textStyle: {
|
|
452
|
+
...currentPrefs,
|
|
453
|
+
[key]: style
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
onMounted(() => {
|
|
459
|
+
const current = ctx.getPrefs()
|
|
460
|
+
const key = props.prefsName || 'default'
|
|
461
|
+
if (current.textStyle && current.textStyle[key]) {
|
|
462
|
+
prefs.value = current.textStyle[key]
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
const isMaximized = computed(() => maximized.value[hash.value])
|
|
467
|
+
|
|
468
|
+
const toggleMaximized = () => {
|
|
469
|
+
maximized.value[hash.value] = !maximized.value[hash.value]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const containerClass = computed(() => {
|
|
473
|
+
return isMaximized.value ? 'w-full h-full' : 'max-h-60 overflow-y-auto'
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
const contentClass = computed(() => {
|
|
477
|
+
if (prefs.value === 'pre') return 'whitespace-pre-wrap font-mono text-xs'
|
|
478
|
+
if (prefs.value === 'normal') return 'font-sans text-sm'
|
|
479
|
+
return ''
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
hash,
|
|
484
|
+
textStyles,
|
|
485
|
+
prefs,
|
|
486
|
+
jsonValue,
|
|
487
|
+
dropdownOpen,
|
|
488
|
+
toggleDropdown,
|
|
489
|
+
setStyle,
|
|
490
|
+
isMaximized,
|
|
491
|
+
toggleMaximized,
|
|
492
|
+
|
|
493
|
+
containerClass,
|
|
494
|
+
contentClass,
|
|
495
|
+
copied,
|
|
496
|
+
copyToClipboard
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
349
501
|
export const ToolArguments = {
|
|
350
502
|
template: `
|
|
351
503
|
<div ref="refArgs" v-if="dict" class="not-prose">
|
|
352
504
|
<div class="prose html-format">
|
|
353
505
|
<table class="table-object border-none">
|
|
354
506
|
<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%">
|
|
507
|
+
<td data-arg="name" class="align-top py-2 px-4 text-left text-sm font-medium tracking-wider whitespace-nowrap lowercase">{{ k }}</td>
|
|
508
|
+
<td data-arg="html" v-if="$utils.isHtml(v)" style="margin:0;padding:0;width:100%">
|
|
357
509
|
<div v-html="embedHtml(v)" class="w-full h-full"></div>
|
|
358
510
|
</td>
|
|
359
|
-
<td v-else class="align-top py-2 px-4 text-sm
|
|
511
|
+
<td data-arg="string" v-else-if="typeof v === 'string'" class="align-top py-2 px-4 text-sm">
|
|
512
|
+
<TextViewer prefsName="toolArgs" :text="v" />
|
|
513
|
+
</td>
|
|
514
|
+
<td data-arg="value" v-else class="align-top py-2 px-4 text-sm whitespace-pre-wrap">
|
|
360
515
|
<HtmlFormat :value="v" :classes="$utils.htmlFormatClasses" />
|
|
361
516
|
</td>
|
|
362
517
|
</tr>
|
|
@@ -373,6 +528,7 @@ export const ToolArguments = {
|
|
|
373
528
|
},
|
|
374
529
|
setup(props) {
|
|
375
530
|
const refArgs = ref()
|
|
531
|
+
const maximized = ref({})
|
|
376
532
|
const dict = computed(() => {
|
|
377
533
|
if (isEmpty(props.value)) return null
|
|
378
534
|
const ret = tryParseJson(props.value)
|
|
@@ -385,11 +541,23 @@ export const ToolArguments = {
|
|
|
385
541
|
})
|
|
386
542
|
|
|
387
543
|
const handleMessage = (event) => {
|
|
544
|
+
console.log('handleMessage', event)
|
|
388
545
|
if (event.data?.type === 'iframe-resize' && typeof event.data.height === 'number') {
|
|
389
546
|
const iframes = refArgs.value?.querySelectorAll('iframe')
|
|
390
547
|
iframes?.forEach(iframe => {
|
|
391
548
|
if (iframe.contentWindow === event.source) {
|
|
392
|
-
|
|
549
|
+
const messages = document.getElementById('messages')
|
|
550
|
+
const maxHeight = messages ? messages.clientHeight : window.innerHeight
|
|
551
|
+
const calculatedHeight = event.data.height + 30
|
|
552
|
+
const targetHeight = Math.min(calculatedHeight, maxHeight)
|
|
553
|
+
|
|
554
|
+
if (iframe.style.height !== targetHeight + 'px') {
|
|
555
|
+
iframe.style.height = targetHeight + 'px'
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (calculatedHeight > maxHeight) {
|
|
559
|
+
event.source.postMessage({ type: 'stop-resize' }, '*')
|
|
560
|
+
}
|
|
393
561
|
}
|
|
394
562
|
})
|
|
395
563
|
}
|
|
@@ -409,6 +577,7 @@ export const ToolArguments = {
|
|
|
409
577
|
|
|
410
578
|
return {
|
|
411
579
|
refArgs,
|
|
580
|
+
maximized,
|
|
412
581
|
dict,
|
|
413
582
|
list,
|
|
414
583
|
isEmpty,
|
|
@@ -439,9 +608,11 @@ export const ToolOutput = {
|
|
|
439
608
|
</span>
|
|
440
609
|
</div>
|
|
441
610
|
</div>
|
|
442
|
-
<div class="
|
|
443
|
-
<
|
|
444
|
-
|
|
611
|
+
<div class="px-3 py-2">
|
|
612
|
+
<div v-if="$ctx.prefs.toolFormat !== 'preview' || !hasJsonStructure(output.content)">
|
|
613
|
+
<TextViewer prefsName="toolOutput" :text="output.content" />
|
|
614
|
+
</div>
|
|
615
|
+
<div v-else class="not-prose text-xs">
|
|
445
616
|
<HtmlFormat v-if="tryParseJson(output.content)" :value="tryParseJson(output.content)" :classes="$utils.htmlFormatClasses" />
|
|
446
617
|
<div v-else class="text-gray-500 italic p-2">Invalid JSON content</div>
|
|
447
618
|
</div>
|
|
@@ -466,7 +637,7 @@ export const ChatBody = {
|
|
|
466
637
|
template: `
|
|
467
638
|
<div class="flex flex-col h-full">
|
|
468
639
|
<!-- Messages Area -->
|
|
469
|
-
<div class="flex-1 overflow-y-auto" ref="messagesContainer">
|
|
640
|
+
<div id="messages" class="flex-1 overflow-y-auto" ref="messagesContainer">
|
|
470
641
|
<div class="mx-auto max-w-6xl px-4 py-6">
|
|
471
642
|
|
|
472
643
|
<div v-if="!$ai.hasAccess">
|
|
@@ -484,7 +655,7 @@ export const ChatBody = {
|
|
|
484
655
|
<ThreadHeader v-if="currentThread" :thread="currentThread" class="mb-2" />
|
|
485
656
|
<div class="space-y-2" v-if="currentThread?.messages?.length">
|
|
486
657
|
<div
|
|
487
|
-
v-for="message in
|
|
658
|
+
v-for="message in currentThreadMessages"
|
|
488
659
|
:key="message.timestamp"
|
|
489
660
|
v-show="!(message.role === 'tool' && isToolLinked(message))"
|
|
490
661
|
class="flex items-start space-x-3 group"
|
|
@@ -524,7 +695,7 @@ export const ChatBody = {
|
|
|
524
695
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700'"
|
|
525
696
|
>
|
|
526
697
|
<!-- Copy button in top right corner -->
|
|
527
|
-
<button
|
|
698
|
+
<button v-if="message.content"
|
|
528
699
|
type="button"
|
|
529
700
|
@click="copyMessageContent(message)"
|
|
530
701
|
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"
|
|
@@ -666,7 +837,7 @@ export const ChatBody = {
|
|
|
666
837
|
</div>
|
|
667
838
|
|
|
668
839
|
<!-- Thread error message bubble -->
|
|
669
|
-
<div v-if="currentThread?.error" class="mt-8 flex items-center
|
|
840
|
+
<div v-if="currentThread?.error" class="mt-8 flex items-center">
|
|
670
841
|
<!-- Avatar outside the bubble -->
|
|
671
842
|
<div class="flex-shrink-0">
|
|
672
843
|
<div class="size-8 rounded-full bg-red-600 dark:bg-red-500 text-white flex items-center justify-center text-lg font-bold">
|
|
@@ -674,13 +845,17 @@ export const ChatBody = {
|
|
|
674
845
|
</div>
|
|
675
846
|
</div>
|
|
676
847
|
<!-- 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">
|
|
848
|
+
<div class="ml-3 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
849
|
<div class="flex items-start space-x-2">
|
|
679
850
|
<div class="flex-1 min-w-0">
|
|
680
851
|
<div v-if="currentThread.error" class="text-base mb-1">{{ currentThread.error }}</div>
|
|
681
852
|
</div>
|
|
682
853
|
</div>
|
|
683
854
|
</div>
|
|
855
|
+
<button type="button" @click="$chat.sendUserMessage('retry')" title="Retry request"
|
|
856
|
+
class="ml-1 px-3 py-1 rounded text-sm text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-900/30 border border-transparent hover:border-gray-300 dark:hover:border-gray-600 transition-all">
|
|
857
|
+
retry
|
|
858
|
+
</button>
|
|
684
859
|
</div>
|
|
685
860
|
|
|
686
861
|
<!-- Error message bubble -->
|
|
@@ -714,7 +889,7 @@ export const ChatBody = {
|
|
|
714
889
|
</div>
|
|
715
890
|
</div>
|
|
716
891
|
</div>
|
|
717
|
-
<ThreadFooter v-if="$threads.threadDetails.value[currentThread.id]" :thread="$threads.threadDetails.value[currentThread.id]" />
|
|
892
|
+
<ThreadFooter v-if="!$threads.watchingThread && $threads.threadDetails.value[currentThread.id]" :thread="$threads.threadDetails.value[currentThread.id]" />
|
|
718
893
|
</div>
|
|
719
894
|
</div>
|
|
720
895
|
</div>
|
|
@@ -949,12 +1124,17 @@ export const ChatBody = {
|
|
|
949
1124
|
ctx.setPrefs(prefs.value)
|
|
950
1125
|
}
|
|
951
1126
|
|
|
1127
|
+
const ignoreUserMessages = ['proceed', 'retry']
|
|
1128
|
+
const currentThreadMessages = computed(() =>
|
|
1129
|
+
currentThread.value?.messages?.filter(x => x.role !== 'system' && !(x.role === 'user' && Array.isArray(x.content) && ignoreUserMessages.includes(x.content[0]?.text))))
|
|
1130
|
+
|
|
952
1131
|
return {
|
|
953
1132
|
prefs,
|
|
954
1133
|
setPrefs,
|
|
955
1134
|
config,
|
|
956
1135
|
models,
|
|
957
1136
|
currentThread,
|
|
1137
|
+
currentThreadMessages,
|
|
958
1138
|
selectedModel,
|
|
959
1139
|
selectedModelObj,
|
|
960
1140
|
messagesContainer,
|