llms-py 3.0.14__py3-none-any.whl → 3.0.16__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/ui/threadStore.mjs +10 -3
- llms/extensions/computer/README.md +96 -0
- llms/extensions/computer/__init__.py +59 -0
- llms/extensions/{computer_use → computer}/bash.py +2 -2
- llms/extensions/{computer_use → computer}/edit.py +10 -14
- llms/extensions/computer/filesystem.py +542 -0
- llms/extensions/core_tools/__init__.py +0 -38
- llms/extensions/providers/google.py +57 -30
- 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 +362 -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 +93 -5
- llms/main.py +215 -35
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +527 -22
- llms/ui/ctx.mjs +53 -6
- llms/ui/modules/chat/ChatBody.mjs +186 -24
- llms/ui/modules/chat/index.mjs +107 -103
- llms/ui/tailwind.input.css +10 -0
- llms/ui/utils.mjs +25 -1
- {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/METADATA +1 -1
- {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/RECORD +37 -27
- {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/WHEEL +1 -1
- llms/extensions/computer_use/__init__.py +0 -27
- /llms/extensions/{computer_use → computer}/base.py +0 -0
- /llms/extensions/{computer_use → computer}/computer.py +0 -0
- /llms/extensions/{computer_use → computer}/platform.py +0 -0
- /llms/extensions/{computer_use → computer}/run.py +0 -0
- {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.14.dist-info → llms_py-3.0.16.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.14.dist-info → llms_py-3.0.16.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');
|
|
@@ -346,6 +351,153 @@ 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">
|
|
@@ -356,21 +508,8 @@ export const ToolArguments = {
|
|
|
356
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 data-arg="string" v-else-if="typeof v === 'string'" class="align-top py-2 px-4 text-sm
|
|
360
|
-
<
|
|
361
|
-
<button type="button" @click="maximized[k] = !maximized[k]" class="absolute top-0 right-3 opacity-0 group-hover:opacity-50 transition-opacity duration-200 rounded focus:outline-none focus:ring-0">
|
|
362
|
-
<div class="flex items-center space-x-1">
|
|
363
|
-
<span class="text-xs text-gray-600 dark:text-gray-400" :title="v.length + ' characters'">{{ $fmt.humanifyNumber(v.length) }}</span>
|
|
364
|
-
<svg class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
365
|
-
<path v-if="maximized[k]" fill="currentColor" d="M9 9H3V7h4V3h2zm0 6H3v2h4v4h2zm12 0h-6v6h2v-4h4zm-6-6h6V7h-4V3h-2z"/>
|
|
366
|
-
<path v-else fill="currentColor" d="M3 3h6v2H5v4H3zm0 18h6v-2H5v-4H3zm12 0h6v-6h-2v4h-4zm6-18h-6v2h4v4h2z"/>
|
|
367
|
-
</svg>
|
|
368
|
-
</div>
|
|
369
|
-
</button>
|
|
370
|
-
<div v-if="!maximized[k]" class="max-h-60 overflow-y-auto">{{ v }}</div>
|
|
371
|
-
<div v-else class="w-full h-full">{{ v }}</div>
|
|
372
|
-
</div>
|
|
373
|
-
<div v-else>{{ v }}</div>
|
|
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" />
|
|
374
513
|
</td>
|
|
375
514
|
<td data-arg="value" v-else class="align-top py-2 px-4 text-sm whitespace-pre-wrap">
|
|
376
515
|
<HtmlFormat :value="v" :classes="$utils.htmlFormatClasses" />
|
|
@@ -402,11 +541,23 @@ export const ToolArguments = {
|
|
|
402
541
|
})
|
|
403
542
|
|
|
404
543
|
const handleMessage = (event) => {
|
|
544
|
+
console.log('handleMessage', event)
|
|
405
545
|
if (event.data?.type === 'iframe-resize' && typeof event.data.height === 'number') {
|
|
406
546
|
const iframes = refArgs.value?.querySelectorAll('iframe')
|
|
407
547
|
iframes?.forEach(iframe => {
|
|
408
548
|
if (iframe.contentWindow === event.source) {
|
|
409
|
-
|
|
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
|
+
}
|
|
410
561
|
}
|
|
411
562
|
})
|
|
412
563
|
}
|
|
@@ -457,9 +608,11 @@ export const ToolOutput = {
|
|
|
457
608
|
</span>
|
|
458
609
|
</div>
|
|
459
610
|
</div>
|
|
460
|
-
<div class="
|
|
461
|
-
<
|
|
462
|
-
|
|
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">
|
|
463
616
|
<HtmlFormat v-if="tryParseJson(output.content)" :value="tryParseJson(output.content)" :classes="$utils.htmlFormatClasses" />
|
|
464
617
|
<div v-else class="text-gray-500 italic p-2">Invalid JSON content</div>
|
|
465
618
|
</div>
|
|
@@ -484,7 +637,7 @@ export const ChatBody = {
|
|
|
484
637
|
template: `
|
|
485
638
|
<div class="flex flex-col h-full">
|
|
486
639
|
<!-- Messages Area -->
|
|
487
|
-
<div class="flex-1 overflow-y-auto" ref="messagesContainer">
|
|
640
|
+
<div id="messages" class="flex-1 overflow-y-auto" ref="messagesContainer">
|
|
488
641
|
<div class="mx-auto max-w-6xl px-4 py-6">
|
|
489
642
|
|
|
490
643
|
<div v-if="!$ai.hasAccess">
|
|
@@ -502,7 +655,7 @@ export const ChatBody = {
|
|
|
502
655
|
<ThreadHeader v-if="currentThread" :thread="currentThread" class="mb-2" />
|
|
503
656
|
<div class="space-y-2" v-if="currentThread?.messages?.length">
|
|
504
657
|
<div
|
|
505
|
-
v-for="message in
|
|
658
|
+
v-for="message in currentThreadMessages"
|
|
506
659
|
:key="message.timestamp"
|
|
507
660
|
v-show="!(message.role === 'tool' && isToolLinked(message))"
|
|
508
661
|
class="flex items-start space-x-3 group"
|
|
@@ -684,7 +837,7 @@ export const ChatBody = {
|
|
|
684
837
|
</div>
|
|
685
838
|
|
|
686
839
|
<!-- Thread error message bubble -->
|
|
687
|
-
<div v-if="currentThread?.error" class="mt-8 flex items-center
|
|
840
|
+
<div v-if="currentThread?.error" class="mt-8 flex items-center">
|
|
688
841
|
<!-- Avatar outside the bubble -->
|
|
689
842
|
<div class="flex-shrink-0">
|
|
690
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">
|
|
@@ -692,13 +845,17 @@ export const ChatBody = {
|
|
|
692
845
|
</div>
|
|
693
846
|
</div>
|
|
694
847
|
<!-- Error bubble -->
|
|
695
|
-
<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">
|
|
696
849
|
<div class="flex items-start space-x-2">
|
|
697
850
|
<div class="flex-1 min-w-0">
|
|
698
851
|
<div v-if="currentThread.error" class="text-base mb-1">{{ currentThread.error }}</div>
|
|
699
852
|
</div>
|
|
700
853
|
</div>
|
|
701
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>
|
|
702
859
|
</div>
|
|
703
860
|
|
|
704
861
|
<!-- Error message bubble -->
|
|
@@ -732,7 +889,7 @@ export const ChatBody = {
|
|
|
732
889
|
</div>
|
|
733
890
|
</div>
|
|
734
891
|
</div>
|
|
735
|
-
<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]" />
|
|
736
893
|
</div>
|
|
737
894
|
</div>
|
|
738
895
|
</div>
|
|
@@ -967,12 +1124,17 @@ export const ChatBody = {
|
|
|
967
1124
|
ctx.setPrefs(prefs.value)
|
|
968
1125
|
}
|
|
969
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
|
+
|
|
970
1131
|
return {
|
|
971
1132
|
prefs,
|
|
972
1133
|
setPrefs,
|
|
973
1134
|
config,
|
|
974
1135
|
models,
|
|
975
1136
|
currentThread,
|
|
1137
|
+
currentThreadMessages,
|
|
976
1138
|
selectedModel,
|
|
977
1139
|
selectedModelObj,
|
|
978
1140
|
messagesContainer,
|
llms/ui/modules/chat/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { ref, watch, computed, nextTick, inject } from 'vue'
|
|
3
3
|
import { $$, createElement, lastRightPart, ApiResult, createErrorStatus } from "@servicestack/client"
|
|
4
4
|
import SettingsDialog, { useSettings } from './SettingsDialog.mjs'
|
|
5
|
-
import { ChatBody, LightboxImage, TypeText, TypeImage, TypeAudio, TypeFile, ViewType, ViewTypes, ViewToolTypes, ToolArguments, ToolOutput, MessageUsage, MessageReasoning } from './ChatBody.mjs'
|
|
5
|
+
import { ChatBody, LightboxImage, TypeText, TypeImage, TypeAudio, TypeFile, ViewType, ViewTypes, ViewToolTypes, TextViewer, 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(',')
|
|
@@ -252,23 +252,14 @@ export function useChatPrompt(ctx) {
|
|
|
252
252
|
return ctx.createErrorResult({ message: `Model ${request.model || ''} not found`, errorCode: 'NotFound' })
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
if (!request.messages) request.messages = []
|
|
256
|
-
if (!request.metadata) request.metadata = {}
|
|
257
|
-
|
|
258
255
|
if (!thread) {
|
|
259
256
|
const title = getTextContent(request) || 'New Chat'
|
|
260
257
|
thread = await ctx.threads.startNewThread({ title, model, redirect })
|
|
261
258
|
}
|
|
262
259
|
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
const ctxRequest = {
|
|
266
|
-
request,
|
|
267
|
-
thread,
|
|
268
|
-
}
|
|
260
|
+
const ctxRequest = ctx.createChatContext({ request, thread })
|
|
269
261
|
ctx.chatRequestFilters.forEach(f => f(ctxRequest))
|
|
270
|
-
|
|
271
|
-
console.debug('completion.request', request)
|
|
262
|
+
ctx.completeChatContext(ctxRequest)
|
|
272
263
|
|
|
273
264
|
// Send to API
|
|
274
265
|
const startTime = Date.now()
|
|
@@ -359,6 +350,105 @@ export function useChatPrompt(ctx) {
|
|
|
359
350
|
ctx.setState({ selectedAspectRatio })
|
|
360
351
|
}
|
|
361
352
|
|
|
353
|
+
async function sendUserMessage(text, { model, redirect = true } = {}) {
|
|
354
|
+
ctx.clearError()
|
|
355
|
+
|
|
356
|
+
if (!model) {
|
|
357
|
+
model = getSelectedModel()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let content = createContent({ text, files: attachedFiles.value })
|
|
361
|
+
|
|
362
|
+
let thread
|
|
363
|
+
|
|
364
|
+
// Create thread if none exists
|
|
365
|
+
if (!ctx.threads.currentThread.value) {
|
|
366
|
+
thread = await ctx.threads.startNewThread({ model, redirect })
|
|
367
|
+
} else {
|
|
368
|
+
thread = ctx.threads.currentThread.value
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let threadId = thread.id
|
|
372
|
+
let messages = thread.messages || []
|
|
373
|
+
if (!threadId) {
|
|
374
|
+
console.error('No thread ID found', thread, ctx.threads.currentThread.value)
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Handle Editing / Redo Logic
|
|
379
|
+
const editingMsg = editingMessage.value
|
|
380
|
+
if (editingMsg) {
|
|
381
|
+
let messageIndex = messages.findIndex(m => m.timestamp === editingMsg)
|
|
382
|
+
if (messageIndex == -1) {
|
|
383
|
+
messageIndex = messages.findLastIndex(m => m.role === 'user')
|
|
384
|
+
}
|
|
385
|
+
console.log('Editing message', editingMsg, messageIndex, messages)
|
|
386
|
+
|
|
387
|
+
if (messageIndex >= 0) {
|
|
388
|
+
messages[messageIndex].content = content
|
|
389
|
+
// Truncate messages to only include up to the edited message
|
|
390
|
+
messages.length = messageIndex + 1
|
|
391
|
+
} else {
|
|
392
|
+
messages.push({
|
|
393
|
+
timestamp: new Date().valueOf(),
|
|
394
|
+
role: 'user',
|
|
395
|
+
content,
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
// Regular Send Logic
|
|
400
|
+
const lastMessage = messages[messages.length - 1]
|
|
401
|
+
|
|
402
|
+
// Check duplicate based on text content extracted from potential array
|
|
403
|
+
const getLastText = (msgContent) => {
|
|
404
|
+
if (typeof msgContent === 'string') return msgContent
|
|
405
|
+
if (Array.isArray(msgContent)) return msgContent.find(c => c.type === 'text')?.text || ''
|
|
406
|
+
return ''
|
|
407
|
+
}
|
|
408
|
+
const newText = text // content[0].text
|
|
409
|
+
const lastText = lastMessage && lastMessage.role === 'user' ? getLastText(lastMessage.content) : null
|
|
410
|
+
const isDuplicate = lastText === newText
|
|
411
|
+
|
|
412
|
+
// Add user message only if it's not a duplicate
|
|
413
|
+
// Note: We are saving the FULL STRUCTURED CONTENT array here
|
|
414
|
+
if (!isDuplicate) {
|
|
415
|
+
messages.push({
|
|
416
|
+
timestamp: new Date().valueOf(),
|
|
417
|
+
role: 'user',
|
|
418
|
+
content,
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const request = createRequest({ model })
|
|
424
|
+
|
|
425
|
+
// Add Thread History
|
|
426
|
+
messages.forEach(m => {
|
|
427
|
+
request.messages.push(m)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
// Update Thread Title if not set or is default
|
|
431
|
+
if (!thread.title || thread.title === 'New Chat' || request.title === 'New Chat') {
|
|
432
|
+
request.title = text.length > 100
|
|
433
|
+
? text.slice(0, 100) + '...'
|
|
434
|
+
: text
|
|
435
|
+
console.debug(`changing thread title from '${thread.title}' to '${request.title}'`)
|
|
436
|
+
} else {
|
|
437
|
+
console.debug(`thread title is '${thread.title}'`, request.title)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const api = await ctx.threads.queueChat({ request, thread })
|
|
441
|
+
if (api.response) {
|
|
442
|
+
// success
|
|
443
|
+
editingMessage.value = null
|
|
444
|
+
attachedFiles.value = []
|
|
445
|
+
thread = api.response
|
|
446
|
+
ctx.threads.replaceThread(thread)
|
|
447
|
+
} else {
|
|
448
|
+
ctx.setError(api.error)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
362
452
|
return {
|
|
363
453
|
completion,
|
|
364
454
|
createContent,
|
|
@@ -383,6 +473,7 @@ export function useChatPrompt(ctx) {
|
|
|
383
473
|
getTextContent,
|
|
384
474
|
getAnswer,
|
|
385
475
|
selectAspectRatio,
|
|
476
|
+
sendUserMessage,
|
|
386
477
|
}
|
|
387
478
|
}
|
|
388
479
|
|
|
@@ -499,6 +590,7 @@ const ChatPrompt = {
|
|
|
499
590
|
hasAudio,
|
|
500
591
|
hasFile,
|
|
501
592
|
getTextContent,
|
|
593
|
+
sendUserMessage,
|
|
502
594
|
} = ctx.chat
|
|
503
595
|
|
|
504
596
|
const fileInput = ref(null)
|
|
@@ -640,8 +732,6 @@ const ChatPrompt = {
|
|
|
640
732
|
if (!messageText.value?.trim() && !hasImage() && !hasAudio() && !hasFile()) return
|
|
641
733
|
if (ctx.threads.isWatchingThread.value || !props.model) return
|
|
642
734
|
|
|
643
|
-
ctx.clearError()
|
|
644
|
-
|
|
645
735
|
// 1. Construct Structured Content (Text + Attachments)
|
|
646
736
|
let text = messageText.value.trim()
|
|
647
737
|
|
|
@@ -654,96 +744,8 @@ const ChatPrompt = {
|
|
|
654
744
|
}
|
|
655
745
|
|
|
656
746
|
messageText.value = ''
|
|
657
|
-
let content = ctx.chat.createContent({ text, files: ctx.chat.attachedFiles.value })
|
|
658
|
-
|
|
659
|
-
let thread
|
|
660
|
-
|
|
661
|
-
// Create thread if none exists
|
|
662
|
-
if (!ctx.threads.currentThread.value) {
|
|
663
|
-
thread = await ctx.threads.startNewThread({ model: props.model, redirect: true })
|
|
664
|
-
} else {
|
|
665
|
-
thread = ctx.threads.currentThread.value
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
let threadId = thread.id
|
|
669
|
-
let messages = thread.messages || []
|
|
670
|
-
if (!threadId) {
|
|
671
|
-
console.error('No thread ID found', thread, ctx.threads.currentThread.value)
|
|
672
|
-
return
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Handle Editing / Redo Logic
|
|
676
|
-
const editingMessage = ctx.chat.editingMessage.value
|
|
677
|
-
if (editingMessage) {
|
|
678
|
-
let messageIndex = messages.findIndex(m => m.timestamp === editingMessage)
|
|
679
|
-
if (messageIndex == -1) {
|
|
680
|
-
messageIndex = messages.findLastIndex(m => m.role === 'user')
|
|
681
|
-
}
|
|
682
|
-
console.log('Editing message', editingMessage, messageIndex, messages)
|
|
683
|
-
|
|
684
|
-
if (messageIndex >= 0) {
|
|
685
|
-
messages[messageIndex].content = content
|
|
686
|
-
// Truncate messages to only include up to the edited message
|
|
687
|
-
messages.length = messageIndex + 1
|
|
688
|
-
} else {
|
|
689
|
-
messages.push({
|
|
690
|
-
timestamp: new Date().valueOf(),
|
|
691
|
-
role: 'user',
|
|
692
|
-
content,
|
|
693
|
-
})
|
|
694
|
-
}
|
|
695
|
-
} else {
|
|
696
|
-
// Regular Send Logic
|
|
697
|
-
const lastMessage = messages[messages.length - 1]
|
|
698
|
-
|
|
699
|
-
// Check duplicate based on text content extracted from potential array
|
|
700
|
-
const getLastText = (msgContent) => {
|
|
701
|
-
if (typeof msgContent === 'string') return msgContent
|
|
702
|
-
if (Array.isArray(msgContent)) return msgContent.find(c => c.type === 'text')?.text || ''
|
|
703
|
-
return ''
|
|
704
|
-
}
|
|
705
|
-
const newText = text // content[0].text
|
|
706
|
-
const lastText = lastMessage && lastMessage.role === 'user' ? getLastText(lastMessage.content) : null
|
|
707
|
-
const isDuplicate = lastText === newText
|
|
708
|
-
|
|
709
|
-
// Add user message only if it's not a duplicate
|
|
710
|
-
// Note: We are saving the FULL STRUCTURED CONTENT array here
|
|
711
|
-
if (!isDuplicate) {
|
|
712
|
-
messages.push({
|
|
713
|
-
timestamp: new Date().valueOf(),
|
|
714
|
-
role: 'user',
|
|
715
|
-
content,
|
|
716
|
-
})
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
const request = ctx.chat.createRequest({ model: props.model })
|
|
721
747
|
|
|
722
|
-
|
|
723
|
-
messages.forEach(m => {
|
|
724
|
-
request.messages.push(m)
|
|
725
|
-
})
|
|
726
|
-
|
|
727
|
-
// Update Thread Title if not set or is default
|
|
728
|
-
if (!thread.title || thread.title === 'New Chat' || request.title === 'New Chat') {
|
|
729
|
-
request.title = text.length > 100
|
|
730
|
-
? text.slice(0, 100) + '...'
|
|
731
|
-
: text
|
|
732
|
-
console.debug(`changing thread title from '${thread.title}' to '${request.title}'`)
|
|
733
|
-
} else {
|
|
734
|
-
console.debug(`thread title is '${thread.title}'`, request.title)
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const api = await ctx.threads.queueChat({ request, thread })
|
|
738
|
-
if (api.response) {
|
|
739
|
-
// success
|
|
740
|
-
ctx.chat.editingMessage.value = null
|
|
741
|
-
ctx.chat.attachedFiles.value = []
|
|
742
|
-
thread = api.response
|
|
743
|
-
ctx.threads.replaceThread(thread)
|
|
744
|
-
} else {
|
|
745
|
-
ctx.setError(api.error)
|
|
746
|
-
}
|
|
748
|
+
await sendUserMessage(text, { model: props.model })
|
|
747
749
|
|
|
748
750
|
// Restore focus to the textarea
|
|
749
751
|
nextTick(() => {
|
|
@@ -828,6 +830,7 @@ const ChatPrompt = {
|
|
|
828
830
|
addNewLine,
|
|
829
831
|
onKeyDown,
|
|
830
832
|
imageAspectRatios,
|
|
833
|
+
sendUserMessage,
|
|
831
834
|
}
|
|
832
835
|
}
|
|
833
836
|
}
|
|
@@ -935,6 +938,7 @@ export default {
|
|
|
935
938
|
ViewType,
|
|
936
939
|
ViewTypes,
|
|
937
940
|
ViewToolTypes,
|
|
941
|
+
TextViewer,
|
|
938
942
|
ToolArguments,
|
|
939
943
|
ToolOutput,
|
|
940
944
|
|
llms/ui/tailwind.input.css
CHANGED
|
@@ -166,6 +166,16 @@
|
|
|
166
166
|
overflow: auto !important;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
.prose .frontmatter {
|
|
170
|
+
white-space: pre-wrap;
|
|
171
|
+
word-break: break-all;
|
|
172
|
+
margin-bottom: 1em;
|
|
173
|
+
padding: 0.5rem;
|
|
174
|
+
font-size: 0.8rem !important;
|
|
175
|
+
background: #f9fafb;
|
|
176
|
+
border: 1px solid #e5e7eb;
|
|
177
|
+
}
|
|
178
|
+
|
|
169
179
|
/* highlight.js - vs.css */
|
|
170
180
|
.hljs {
|
|
171
181
|
background: white;
|