llms-py 3.0.13__py3-none-any.whl → 3.0.15__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_use/README.md +96 -0
- llms/extensions/computer_use/__init__.py +27 -0
- llms/extensions/computer_use/base.py +80 -0
- llms/extensions/computer_use/bash.py +185 -0
- llms/extensions/computer_use/computer.py +523 -0
- llms/extensions/computer_use/edit.py +303 -0
- llms/extensions/computer_use/platform.py +461 -0
- llms/extensions/computer_use/run.py +37 -0
- llms/extensions/providers/anthropic.py +22 -3
- 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 +335 -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/ui/index.mjs +1 -1
- llms/main.py +88 -11
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +39 -0
- llms/ui/ctx.mjs +53 -6
- llms/ui/modules/chat/ChatBody.mjs +138 -13
- llms/ui/modules/chat/index.mjs +4 -12
- llms/ui/tailwind.input.css +10 -0
- llms/ui/utils.mjs +25 -1
- {llms_py-3.0.13.dist-info → llms_py-3.0.15.dist-info}/METADATA +1 -1
- {llms_py-3.0.13.dist-info → llms_py-3.0.15.dist-info}/RECORD +34 -18
- {llms_py-3.0.13.dist-info → llms_py-3.0.15.dist-info}/WHEEL +0 -0
- {llms_py-3.0.13.dist-info → llms_py-3.0.15.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.13.dist-info → llms_py-3.0.15.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.13.dist-info → llms_py-3.0.15.dist-info}/top_level.txt +0 -0
llms/main.py
CHANGED
|
@@ -26,11 +26,24 @@ import sys
|
|
|
26
26
|
import time
|
|
27
27
|
import traceback
|
|
28
28
|
from datetime import datetime
|
|
29
|
-
from enum import IntEnum
|
|
29
|
+
from enum import Enum, IntEnum
|
|
30
30
|
from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
|
|
31
31
|
from io import BytesIO
|
|
32
32
|
from pathlib import Path
|
|
33
|
-
from typing import
|
|
33
|
+
from typing import (
|
|
34
|
+
Annotated,
|
|
35
|
+
Any,
|
|
36
|
+
Callable,
|
|
37
|
+
Dict,
|
|
38
|
+
List,
|
|
39
|
+
Literal,
|
|
40
|
+
Optional,
|
|
41
|
+
Tuple,
|
|
42
|
+
Union,
|
|
43
|
+
get_args,
|
|
44
|
+
get_origin,
|
|
45
|
+
get_type_hints,
|
|
46
|
+
)
|
|
34
47
|
from urllib.parse import parse_qs, urlencode, urljoin
|
|
35
48
|
|
|
36
49
|
import aiohttp
|
|
@@ -43,7 +56,7 @@ try:
|
|
|
43
56
|
except ImportError:
|
|
44
57
|
HAS_PIL = False
|
|
45
58
|
|
|
46
|
-
VERSION = "3.0.
|
|
59
|
+
VERSION = "3.0.15"
|
|
47
60
|
_ROOT = None
|
|
48
61
|
DEBUG = os.getenv("DEBUG") == "1"
|
|
49
62
|
MOCK = os.getenv("MOCK") == "1"
|
|
@@ -348,22 +361,75 @@ def to_content(result):
|
|
|
348
361
|
return str(result)
|
|
349
362
|
|
|
350
363
|
|
|
364
|
+
def get_literal_values(typ):
|
|
365
|
+
"""Recursively extract values from Literal and Union types."""
|
|
366
|
+
origin = get_origin(typ)
|
|
367
|
+
if origin is Literal:
|
|
368
|
+
return list(get_args(typ))
|
|
369
|
+
elif origin is Union:
|
|
370
|
+
values = []
|
|
371
|
+
for arg in get_args(typ):
|
|
372
|
+
# Recurse for nested Unions or Literals
|
|
373
|
+
nested_values = get_literal_values(arg)
|
|
374
|
+
if nested_values:
|
|
375
|
+
for v in nested_values:
|
|
376
|
+
if v not in values:
|
|
377
|
+
values.append(v)
|
|
378
|
+
return values
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
351
382
|
def function_to_tool_definition(func):
|
|
352
|
-
type_hints = get_type_hints(func)
|
|
383
|
+
type_hints = get_type_hints(func, include_extras=True)
|
|
353
384
|
signature = inspect.signature(func)
|
|
354
385
|
parameters = {"type": "object", "properties": {}, "required": []}
|
|
355
386
|
|
|
356
387
|
for name, param in signature.parameters.items():
|
|
357
388
|
param_type = type_hints.get(name, str)
|
|
358
389
|
param_type_name = "string"
|
|
359
|
-
|
|
390
|
+
enum_values = None
|
|
391
|
+
description = None
|
|
392
|
+
|
|
393
|
+
# Check for Annotated (for description)
|
|
394
|
+
if get_origin(param_type) is Annotated:
|
|
395
|
+
args = get_args(param_type)
|
|
396
|
+
param_type = args[0]
|
|
397
|
+
for arg in args[1:]:
|
|
398
|
+
if isinstance(arg, str):
|
|
399
|
+
description = arg
|
|
400
|
+
break
|
|
401
|
+
|
|
402
|
+
# Check for Enum
|
|
403
|
+
if inspect.isclass(param_type) and issubclass(param_type, Enum):
|
|
404
|
+
enum_values = [e.value for e in param_type]
|
|
405
|
+
else:
|
|
406
|
+
# Check for Literal / Union[Literal]
|
|
407
|
+
enum_values = get_literal_values(param_type)
|
|
408
|
+
|
|
409
|
+
if enum_values:
|
|
410
|
+
# Infer type from the first value
|
|
411
|
+
value_type = type(enum_values[0])
|
|
412
|
+
if value_type is int:
|
|
413
|
+
param_type_name = "integer"
|
|
414
|
+
elif value_type is float:
|
|
415
|
+
param_type_name = "number"
|
|
416
|
+
elif value_type is bool:
|
|
417
|
+
param_type_name = "boolean"
|
|
418
|
+
|
|
419
|
+
elif param_type is int:
|
|
360
420
|
param_type_name = "integer"
|
|
361
421
|
elif param_type is float:
|
|
362
422
|
param_type_name = "number"
|
|
363
423
|
elif param_type is bool:
|
|
364
424
|
param_type_name = "boolean"
|
|
365
425
|
|
|
366
|
-
|
|
426
|
+
prop = {"type": param_type_name}
|
|
427
|
+
if description:
|
|
428
|
+
prop["description"] = description
|
|
429
|
+
if enum_values:
|
|
430
|
+
prop["enum"] = enum_values
|
|
431
|
+
parameters["properties"][name] = prop
|
|
432
|
+
|
|
367
433
|
if param.default == inspect.Parameter.empty:
|
|
368
434
|
parameters["required"].append(name)
|
|
369
435
|
|
|
@@ -371,7 +437,7 @@ def function_to_tool_definition(func):
|
|
|
371
437
|
"type": "function",
|
|
372
438
|
"function": {
|
|
373
439
|
"name": func.__name__,
|
|
374
|
-
"description": func.__doc__ or "",
|
|
440
|
+
"description": (func.__doc__ or "").strip(),
|
|
375
441
|
"parameters": parameters,
|
|
376
442
|
},
|
|
377
443
|
}
|
|
@@ -791,7 +857,7 @@ def chat_to_prompt(chat):
|
|
|
791
857
|
prompt = ""
|
|
792
858
|
if "messages" in chat:
|
|
793
859
|
for message in chat["messages"]:
|
|
794
|
-
if message
|
|
860
|
+
if message.get("role") == "user":
|
|
795
861
|
# if content is string
|
|
796
862
|
if isinstance(message["content"], str):
|
|
797
863
|
if prompt:
|
|
@@ -810,7 +876,7 @@ def chat_to_prompt(chat):
|
|
|
810
876
|
def chat_to_system_prompt(chat):
|
|
811
877
|
if "messages" in chat:
|
|
812
878
|
for message in chat["messages"]:
|
|
813
|
-
if message
|
|
879
|
+
if message.get("role") == "system":
|
|
814
880
|
# if content is string
|
|
815
881
|
if isinstance(message["content"], str):
|
|
816
882
|
return message["content"]
|
|
@@ -838,7 +904,7 @@ def last_user_prompt(chat):
|
|
|
838
904
|
prompt = ""
|
|
839
905
|
if "messages" in chat:
|
|
840
906
|
for message in chat["messages"]:
|
|
841
|
-
if message
|
|
907
|
+
if message.get("role") == "user":
|
|
842
908
|
# if content is string
|
|
843
909
|
if isinstance(message["content"], str):
|
|
844
910
|
prompt = message["content"]
|
|
@@ -1585,6 +1651,9 @@ async def g_chat_completion(chat, context=None):
|
|
|
1585
1651
|
if context is None:
|
|
1586
1652
|
context = {"chat": chat, "tools": "all"}
|
|
1587
1653
|
|
|
1654
|
+
if "request_id" not in context:
|
|
1655
|
+
context["request_id"] = str(int(time.time() * 1000))
|
|
1656
|
+
|
|
1588
1657
|
# get first provider that has the model
|
|
1589
1658
|
candidate_providers = [name for name, provider in g_handlers.items() if provider.provider_model(model)]
|
|
1590
1659
|
if len(candidate_providers) == 0:
|
|
@@ -1736,7 +1805,12 @@ async def g_chat_completion(chat, context=None):
|
|
|
1736
1805
|
continue
|
|
1737
1806
|
|
|
1738
1807
|
# If we get here, all providers failed
|
|
1739
|
-
|
|
1808
|
+
if first_exception:
|
|
1809
|
+
raise first_exception
|
|
1810
|
+
|
|
1811
|
+
e = Exception("All providers failed")
|
|
1812
|
+
await g_app.on_chat_error(e, context or {"chat": chat})
|
|
1813
|
+
raise e
|
|
1740
1814
|
|
|
1741
1815
|
|
|
1742
1816
|
async def cli_chat(chat, tools=None, image=None, audio=None, file=None, args=None, raw=False):
|
|
@@ -2918,6 +2992,9 @@ class ExtensionContext:
|
|
|
2918
2992
|
def add_index_footer(self, html: str):
|
|
2919
2993
|
self.app.index_footers.append(html)
|
|
2920
2994
|
|
|
2995
|
+
def get_home_path(self, name: str = "") -> str:
|
|
2996
|
+
return home_llms_path(name)
|
|
2997
|
+
|
|
2921
2998
|
def get_config(self) -> Optional[Dict[str, Any]]:
|
|
2922
2999
|
return g_config
|
|
2923
3000
|
|
llms/ui/ai.mjs
CHANGED
llms/ui/app.css
CHANGED
|
@@ -427,6 +427,9 @@
|
|
|
427
427
|
.right-2 {
|
|
428
428
|
right: calc(var(--spacing) * 2);
|
|
429
429
|
}
|
|
430
|
+
.right-3 {
|
|
431
|
+
right: calc(var(--spacing) * 3);
|
|
432
|
+
}
|
|
430
433
|
.right-4 {
|
|
431
434
|
right: calc(var(--spacing) * 4);
|
|
432
435
|
}
|
|
@@ -622,6 +625,12 @@
|
|
|
622
625
|
.-mb-px {
|
|
623
626
|
margin-bottom: -1px;
|
|
624
627
|
}
|
|
628
|
+
.mb-0 {
|
|
629
|
+
margin-bottom: calc(var(--spacing) * 0);
|
|
630
|
+
}
|
|
631
|
+
.mb-0\.5 {
|
|
632
|
+
margin-bottom: calc(var(--spacing) * 0.5);
|
|
633
|
+
}
|
|
625
634
|
.mb-1 {
|
|
626
635
|
margin-bottom: calc(var(--spacing) * 1);
|
|
627
636
|
}
|
|
@@ -899,6 +908,9 @@
|
|
|
899
908
|
.w-16 {
|
|
900
909
|
width: calc(var(--spacing) * 16);
|
|
901
910
|
}
|
|
911
|
+
.w-28 {
|
|
912
|
+
width: calc(var(--spacing) * 28);
|
|
913
|
+
}
|
|
902
914
|
.w-32 {
|
|
903
915
|
width: calc(var(--spacing) * 32);
|
|
904
916
|
}
|
|
@@ -1667,6 +1679,12 @@
|
|
|
1667
1679
|
background-color: color-mix(in oklab, var(--color-gray-50) 50%, transparent);
|
|
1668
1680
|
}
|
|
1669
1681
|
}
|
|
1682
|
+
.bg-gray-50\/90 {
|
|
1683
|
+
background-color: color-mix(in srgb, oklch(98.5% 0.002 247.839) 90%, transparent);
|
|
1684
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1685
|
+
background-color: color-mix(in oklab, var(--color-gray-50) 90%, transparent);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1670
1688
|
.bg-gray-100 {
|
|
1671
1689
|
background-color: var(--color-gray-100);
|
|
1672
1690
|
}
|
|
@@ -2506,6 +2524,10 @@
|
|
|
2506
2524
|
--tw-ordinal: ordinal;
|
|
2507
2525
|
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
|
2508
2526
|
}
|
|
2527
|
+
.tabular-nums {
|
|
2528
|
+
--tw-numeric-spacing: tabular-nums;
|
|
2529
|
+
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
|
2530
|
+
}
|
|
2509
2531
|
.overline {
|
|
2510
2532
|
text-decoration-line: overline;
|
|
2511
2533
|
}
|
|
@@ -4888,6 +4910,14 @@
|
|
|
4888
4910
|
}
|
|
4889
4911
|
}
|
|
4890
4912
|
}
|
|
4913
|
+
.dark\:bg-gray-800\/90 {
|
|
4914
|
+
&:where(.dark, .dark *) {
|
|
4915
|
+
background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 90%, transparent);
|
|
4916
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
4917
|
+
background-color: color-mix(in oklab, var(--color-gray-800) 90%, transparent);
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4891
4921
|
.dark\:bg-gray-900 {
|
|
4892
4922
|
&:where(.dark, .dark *) {
|
|
4893
4923
|
background-color: var(--color-gray-900);
|
|
@@ -6282,6 +6312,15 @@
|
|
|
6282
6312
|
border-radius: 0 !important;
|
|
6283
6313
|
overflow: auto !important;
|
|
6284
6314
|
}
|
|
6315
|
+
.prose .frontmatter {
|
|
6316
|
+
white-space: pre-wrap;
|
|
6317
|
+
word-break: break-all;
|
|
6318
|
+
margin-bottom: 1em;
|
|
6319
|
+
padding: 0.5rem;
|
|
6320
|
+
font-size: 0.8rem !important;
|
|
6321
|
+
background: #f9fafb;
|
|
6322
|
+
border: 1px solid #e5e7eb;
|
|
6323
|
+
}
|
|
6285
6324
|
.hljs {
|
|
6286
6325
|
background: white;
|
|
6287
6326
|
color: black;
|
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
|
}
|
|
@@ -55,8 +55,8 @@ function embedHtml(html) {
|
|
|
55
55
|
|
|
56
56
|
export const TypeText = {
|
|
57
57
|
template: `
|
|
58
|
-
<div v-if="text.type === 'text'">
|
|
59
|
-
|
|
58
|
+
<div data-type="text" v-if="text.type === 'text'">
|
|
59
|
+
!<div v-html="html?.trim()" class="whitespace-pre-wrap"></div>
|
|
60
60
|
</div>
|
|
61
61
|
`,
|
|
62
62
|
props: {
|
|
@@ -161,7 +161,7 @@ export const LightboxImage = {
|
|
|
161
161
|
|
|
162
162
|
export const TypeImage = {
|
|
163
163
|
template: `
|
|
164
|
-
<div v-if="image.type === 'image_url'">
|
|
164
|
+
<div data-type="image" v-if="image.type === 'image_url'">
|
|
165
165
|
<LightboxImage :src="$ctx.resolveUrl(image.image_url.url)" />
|
|
166
166
|
</div>
|
|
167
167
|
`,
|
|
@@ -175,7 +175,7 @@ export const TypeImage = {
|
|
|
175
175
|
|
|
176
176
|
export const TypeAudio = {
|
|
177
177
|
template: `
|
|
178
|
-
<div v-if="audio.type === 'audio_url'">
|
|
178
|
+
<div data-type="audio" v-if="audio.type === 'audio_url'">
|
|
179
179
|
<slot></slot>
|
|
180
180
|
<audio controls :src="$ctx.resolveUrl(audio.audio_url.url)" class="h-8 w-64"></audio>
|
|
181
181
|
</div>
|
|
@@ -190,7 +190,7 @@ export const TypeAudio = {
|
|
|
190
190
|
|
|
191
191
|
export const TypeFile = {
|
|
192
192
|
template: `
|
|
193
|
-
<a v-if="file.type === 'file'" :href="$ctx.resolveUrl(file.file.file_data)" target="_blank"
|
|
193
|
+
<a data-type="file" v-if="file.type === 'file'" :href="$ctx.resolveUrl(file.file.file_data)" target="_blank"
|
|
194
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
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
196
|
<span class="max-w-xs truncate">{{ file.file.filename || 'Attachment' }}</span>
|
|
@@ -211,7 +211,7 @@ export const ViewType = {
|
|
|
211
211
|
<TypeImage v-else-if="result.type === 'image_url'" :image="result" />
|
|
212
212
|
<TypeAudio v-else-if="result.type === 'audio_url'" :audio="result" />
|
|
213
213
|
<TypeFile v-else-if="result.type === 'file'" :file="result" />
|
|
214
|
-
<div v-else>
|
|
214
|
+
<div data-type="other" v-else>
|
|
215
215
|
<HtmlFormat :value="result" :classes="$utils.htmlFormatClasses" />
|
|
216
216
|
</div>
|
|
217
217
|
</div>
|
|
@@ -346,17 +346,138 @@ export const MessageReasoning = {
|
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
export const TextViewer = {
|
|
350
|
+
template: `
|
|
351
|
+
<div v-if="text.length > 200" class="relative group">
|
|
352
|
+
<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">
|
|
353
|
+
<!-- Style Selector -->
|
|
354
|
+
<div class="relative flex items-center">
|
|
355
|
+
<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">
|
|
356
|
+
<span>{{ prefs || 'pre' }}</span>
|
|
357
|
+
<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>
|
|
358
|
+
</button>
|
|
359
|
+
<!-- Popover -->
|
|
360
|
+
<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">
|
|
361
|
+
<button
|
|
362
|
+
v-for="style in textStyles"
|
|
363
|
+
:key="style"
|
|
364
|
+
@click="setStyle(style)"
|
|
365
|
+
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"
|
|
366
|
+
:class="{ 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20': prefs === style }"
|
|
367
|
+
>
|
|
368
|
+
{{ style }}
|
|
369
|
+
</button>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<div class="w-px h-3 bg-gray-300 dark:bg-gray-600"></div>
|
|
374
|
+
|
|
375
|
+
<!-- Text Length -->
|
|
376
|
+
<span class="text-xs text-gray-500 dark:text-gray-400 tabular-nums" :title="text.length + ' characters'">
|
|
377
|
+
{{ $fmt.humanifyNumber(text.length) }}
|
|
378
|
+
</span>
|
|
379
|
+
|
|
380
|
+
<!-- Maximize Toggle -->
|
|
381
|
+
<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'">
|
|
382
|
+
<svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
383
|
+
<path v-if="isMaximized" fill="currentColor" d="M9 9H3V7h4V3h2zm0 6H3v2h4v4h2zm12 0h-6v6h2v-4h4zm-6-6h6V7h-4V3h-2z"/>
|
|
384
|
+
<path v-else fill="currentColor" d="M3 3h6v2H5v4H3zm0 18h6v-2H5v-4H3zm12 0h6v-6h-2v4h-4zm6-18h-6v2h4v4h2z"/>
|
|
385
|
+
</svg>
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<!-- Content -->
|
|
390
|
+
<div :class="containerClass">
|
|
391
|
+
<div v-if="prefs === 'markdown'" class="prose prose-sm max-w-none dark:prose-invert">
|
|
392
|
+
<div v-html="$fmt.markdown(text)"></div>
|
|
393
|
+
</div>
|
|
394
|
+
<div v-else :class="['p-0.5', contentClass]">{{ text }}</div>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
<div v-else class="whitespace-pre-wrap">{{ text }}</div>
|
|
398
|
+
`,
|
|
399
|
+
props: {
|
|
400
|
+
prefsName: String,
|
|
401
|
+
text: String,
|
|
402
|
+
},
|
|
403
|
+
setup(props) {
|
|
404
|
+
const ctx = inject('ctx')
|
|
405
|
+
const textStyles = ['pre', 'normal', 'markdown']
|
|
406
|
+
const prefs = ref('pre')
|
|
407
|
+
const maximized = ref({})
|
|
408
|
+
const dropdownOpen = ref(false)
|
|
409
|
+
const hash = computed(() => ctx.utils.hashString(props.text))
|
|
410
|
+
|
|
411
|
+
const toggleDropdown = () => {
|
|
412
|
+
dropdownOpen.value = !dropdownOpen.value
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const setStyle = (style) => {
|
|
416
|
+
prefs.value = style
|
|
417
|
+
dropdownOpen.value = false
|
|
418
|
+
const key = props.prefsName || 'default'
|
|
419
|
+
const currentPrefs = ctx.getPrefs().textStyle || {}
|
|
420
|
+
ctx.setPrefs({
|
|
421
|
+
textStyle: {
|
|
422
|
+
...currentPrefs,
|
|
423
|
+
[key]: style
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
onMounted(() => {
|
|
429
|
+
const current = ctx.getPrefs()
|
|
430
|
+
const key = props.prefsName || 'default'
|
|
431
|
+
if (current.textStyle && current.textStyle[key]) {
|
|
432
|
+
prefs.value = current.textStyle[key]
|
|
433
|
+
}
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
const isMaximized = computed(() => maximized.value[hash.value])
|
|
437
|
+
|
|
438
|
+
const toggleMaximized = () => {
|
|
439
|
+
maximized.value[hash.value] = !maximized.value[hash.value]
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const containerClass = computed(() => {
|
|
443
|
+
return isMaximized.value ? 'w-full h-full' : 'max-h-60 overflow-y-auto'
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
const contentClass = computed(() => {
|
|
447
|
+
if (prefs.value === 'pre') return 'whitespace-pre-wrap font-mono text-xs'
|
|
448
|
+
if (prefs.value === 'normal') return 'font-sans text-sm'
|
|
449
|
+
return ''
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
hash,
|
|
454
|
+
textStyles,
|
|
455
|
+
prefs,
|
|
456
|
+
dropdownOpen,
|
|
457
|
+
toggleDropdown,
|
|
458
|
+
setStyle,
|
|
459
|
+
isMaximized,
|
|
460
|
+
toggleMaximized,
|
|
461
|
+
containerClass,
|
|
462
|
+
contentClass
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
349
467
|
export const ToolArguments = {
|
|
350
468
|
template: `
|
|
351
469
|
<div ref="refArgs" v-if="dict" class="not-prose">
|
|
352
470
|
<div class="prose html-format">
|
|
353
471
|
<table class="table-object border-none">
|
|
354
472
|
<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%">
|
|
473
|
+
<td data-arg="name" class="align-top py-2 px-4 text-left text-sm font-medium tracking-wider whitespace-nowrap lowercase">{{ k }}</td>
|
|
474
|
+
<td data-arg="html" v-if="$utils.isHtml(v)" style="margin:0;padding:0;width:100%">
|
|
357
475
|
<div v-html="embedHtml(v)" class="w-full h-full"></div>
|
|
358
476
|
</td>
|
|
359
|
-
<td v-else class="align-top py-2 px-4 text-sm
|
|
477
|
+
<td data-arg="string" v-else-if="typeof v === 'string'" class="align-top py-2 px-4 text-sm">
|
|
478
|
+
<TextViewer prefsName="toolArgs" :text="v" />
|
|
479
|
+
</td>
|
|
480
|
+
<td data-arg="value" v-else class="align-top py-2 px-4 text-sm whitespace-pre-wrap">
|
|
360
481
|
<HtmlFormat :value="v" :classes="$utils.htmlFormatClasses" />
|
|
361
482
|
</td>
|
|
362
483
|
</tr>
|
|
@@ -373,6 +494,7 @@ export const ToolArguments = {
|
|
|
373
494
|
},
|
|
374
495
|
setup(props) {
|
|
375
496
|
const refArgs = ref()
|
|
497
|
+
const maximized = ref({})
|
|
376
498
|
const dict = computed(() => {
|
|
377
499
|
if (isEmpty(props.value)) return null
|
|
378
500
|
const ret = tryParseJson(props.value)
|
|
@@ -409,6 +531,7 @@ export const ToolArguments = {
|
|
|
409
531
|
|
|
410
532
|
return {
|
|
411
533
|
refArgs,
|
|
534
|
+
maximized,
|
|
412
535
|
dict,
|
|
413
536
|
list,
|
|
414
537
|
isEmpty,
|
|
@@ -439,9 +562,11 @@ export const ToolOutput = {
|
|
|
439
562
|
</span>
|
|
440
563
|
</div>
|
|
441
564
|
</div>
|
|
442
|
-
<div class="
|
|
443
|
-
<
|
|
444
|
-
|
|
565
|
+
<div class="px-3 py-2">
|
|
566
|
+
<div v-if="$ctx.prefs.toolFormat !== 'preview' || !hasJsonStructure(output.content)">
|
|
567
|
+
<TextViewer prefsName="toolOutput" :text="output.content" />
|
|
568
|
+
</div>
|
|
569
|
+
<div v-else class="not-prose text-xs">
|
|
445
570
|
<HtmlFormat v-if="tryParseJson(output.content)" :value="tryParseJson(output.content)" :classes="$utils.htmlFormatClasses" />
|
|
446
571
|
<div v-else class="text-gray-500 italic p-2">Invalid JSON content</div>
|
|
447
572
|
</div>
|
|
@@ -524,7 +649,7 @@ export const ChatBody = {
|
|
|
524
649
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700'"
|
|
525
650
|
>
|
|
526
651
|
<!-- Copy button in top right corner -->
|
|
527
|
-
<button
|
|
652
|
+
<button v-if="message.content"
|
|
528
653
|
type="button"
|
|
529
654
|
@click="copyMessageContent(message)"
|
|
530
655
|
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"
|
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()
|
|
@@ -935,6 +926,7 @@ export default {
|
|
|
935
926
|
ViewType,
|
|
936
927
|
ViewTypes,
|
|
937
928
|
ViewToolTypes,
|
|
929
|
+
TextViewer,
|
|
938
930
|
ToolArguments,
|
|
939
931
|
ToolOutput,
|
|
940
932
|
|
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;
|
llms/ui/utils.mjs
CHANGED
|
@@ -211,6 +211,19 @@ export function isHtml(s) {
|
|
|
211
211
|
return isHtml
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
const htmlEntities = {
|
|
215
|
+
'&': '&',
|
|
216
|
+
'<': '<',
|
|
217
|
+
'>': '>',
|
|
218
|
+
'"': '"',
|
|
219
|
+
"'": '''
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function encodeHtml(str) {
|
|
223
|
+
if (!str) return ''
|
|
224
|
+
return str.replace(/[&<>"']/g, m => htmlEntities[m]);
|
|
225
|
+
}
|
|
226
|
+
|
|
214
227
|
/**
|
|
215
228
|
* @param {object|array} type
|
|
216
229
|
* @param {'div'|'table'|'thead'|'th'|'tr'|'td'} tag
|
|
@@ -241,6 +254,16 @@ export const nextId = (() => {
|
|
|
241
254
|
}
|
|
242
255
|
})();
|
|
243
256
|
|
|
257
|
+
export function fnv1a(str) {
|
|
258
|
+
let hash = 0x811c9dc5
|
|
259
|
+
for (let i = 0; i < str.length; i++) {
|
|
260
|
+
hash ^= str.charCodeAt(i)
|
|
261
|
+
hash = Math.imul(hash, 0x01000193)
|
|
262
|
+
}
|
|
263
|
+
return hash >>> 0
|
|
264
|
+
}
|
|
265
|
+
export const hashString = fnv1a
|
|
266
|
+
|
|
244
267
|
export function utilsFunctions() {
|
|
245
268
|
return {
|
|
246
269
|
nextId,
|
|
@@ -256,6 +279,7 @@ export function utilsFunctions() {
|
|
|
256
279
|
pluralize,
|
|
257
280
|
isHtml,
|
|
258
281
|
htmlFormatClasses,
|
|
282
|
+
encodeHtml,
|
|
283
|
+
hashString,
|
|
259
284
|
}
|
|
260
285
|
}
|
|
261
|
-
|