@adia-ai/web-modules 0.3.4 → 0.3.6
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.
- package/CHANGELOG.md +50 -0
- package/chat/chat-composer/chat-composer.a2ui.json +94 -0
- package/chat/chat-composer/chat-composer.examples.html +28 -0
- package/chat/chat-composer/chat-composer.html +43 -0
- package/chat/chat-composer/chat-composer.js +107 -0
- package/chat/chat-composer/chat-composer.test.js +112 -0
- package/chat/chat-composer/chat-composer.yaml +91 -0
- package/chat/chat-empty/chat-empty.a2ui.json +68 -0
- package/chat/chat-empty/chat-empty.examples.html +34 -0
- package/chat/chat-empty/chat-empty.html +42 -0
- package/chat/chat-empty/chat-empty.yaml +58 -0
- package/chat/chat-header/chat-header.a2ui.json +77 -0
- package/chat/chat-header/chat-header.examples.html +30 -0
- package/chat/chat-header/chat-header.html +42 -0
- package/chat/chat-header/chat-header.yaml +68 -0
- package/chat/chat-shell/chat-shell.css +1 -0
- package/chat/chat-shell/chat-shell.examples.html +43 -2
- package/chat/chat-shell/chat-shell.js +35 -7
- package/chat/chat-shell/css/chat-shell.bespoke.css +196 -0
- package/chat/chat-sidebar/chat-sidebar.a2ui.json +136 -0
- package/chat/chat-sidebar/chat-sidebar.examples.html +36 -0
- package/chat/chat-sidebar/chat-sidebar.html +43 -0
- package/chat/chat-sidebar/chat-sidebar.js +227 -0
- package/chat/chat-sidebar/chat-sidebar.test.js +110 -0
- package/chat/chat-sidebar/chat-sidebar.yaml +140 -0
- package/chat/chat-status/chat-status.a2ui.json +63 -0
- package/chat/chat-status/chat-status.examples.html +29 -0
- package/chat/chat-status/chat-status.html +42 -0
- package/chat/chat-status/chat-status.yaml +52 -0
- package/chat/chat-thread/chat-thread.a2ui.json +91 -0
- package/chat/chat-thread/chat-thread.examples.html +36 -0
- package/chat/chat-thread/chat-thread.html +43 -0
- package/chat/chat-thread/chat-thread.js +106 -0
- package/chat/chat-thread/chat-thread.test.js +82 -0
- package/chat/chat-thread/chat-thread.yaml +89 -0
- package/chat/index.js +3 -0
- package/editor/editor-canvas/editor-canvas.a2ui.json +87 -0
- package/editor/editor-canvas/editor-canvas.js +103 -0
- package/editor/editor-canvas/editor-canvas.test.js +100 -0
- package/editor/editor-canvas/editor-canvas.yaml +88 -0
- package/editor/editor-canvas-empty/editor-canvas-empty.a2ui.json +69 -0
- package/editor/editor-canvas-empty/editor-canvas-empty.yaml +56 -0
- package/editor/editor-shell/css/editor-shell.bespoke.css +172 -0
- package/editor/editor-shell/editor-shell.css +1 -0
- package/editor/editor-shell/editor-shell.js +85 -30
- package/editor/editor-sidebar/editor-sidebar.a2ui.json +88 -0
- package/editor/editor-sidebar/editor-sidebar.js +173 -0
- package/editor/editor-sidebar/editor-sidebar.test.js +126 -0
- package/editor/editor-sidebar/editor-sidebar.yaml +83 -0
- package/editor/editor-statusbar/editor-statusbar.a2ui.json +76 -0
- package/editor/editor-statusbar/editor-statusbar.yaml +57 -0
- package/editor/editor-toolbar/editor-toolbar.a2ui.json +96 -0
- package/editor/editor-toolbar/editor-toolbar.js +58 -0
- package/editor/editor-toolbar/editor-toolbar.test.js +99 -0
- package/editor/editor-toolbar/editor-toolbar.yaml +81 -0
- package/editor/index.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/ChatStatus.json",
|
|
4
|
+
"title": "ChatStatus",
|
|
5
|
+
"description": "Module-tier chat status indicator. CSS-only — no behavior, no JS.\nConveys connection / streaming state via inline content. Typically\nslotted into <chat-header slot=\"status\"> or used inline.\n\nReplaces the legacy <span data-chat-status> shape per ADR-0023.\n",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"component": {
|
|
17
|
+
"const": "ChatStatus"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"required": [
|
|
21
|
+
"component"
|
|
22
|
+
],
|
|
23
|
+
"unevaluatedProperties": false,
|
|
24
|
+
"x-adiaui": {
|
|
25
|
+
"anti_patterns": [],
|
|
26
|
+
"category": "feedback",
|
|
27
|
+
"events": {},
|
|
28
|
+
"examples": [],
|
|
29
|
+
"keywords": [
|
|
30
|
+
"chat-status",
|
|
31
|
+
"status-indicator",
|
|
32
|
+
"connection-status",
|
|
33
|
+
"streaming-indicator"
|
|
34
|
+
],
|
|
35
|
+
"name": "ChatStatus",
|
|
36
|
+
"related": [
|
|
37
|
+
"ChatShell",
|
|
38
|
+
"ChatHeader",
|
|
39
|
+
"ChatThread"
|
|
40
|
+
],
|
|
41
|
+
"slots": {
|
|
42
|
+
"default": {
|
|
43
|
+
"description": "Status content — text, icon, dot indicator, etc. Authors set content directly; the host (chat-shell) updates innerText during streaming via legacy [data-chat-status] selector OR via querySelector('chat-status') for the bespoke shape."
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"states": [
|
|
47
|
+
{
|
|
48
|
+
"description": "Default, the only state.",
|
|
49
|
+
"name": "idle"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"synonyms": {
|
|
53
|
+
"chat-status": [
|
|
54
|
+
"connection-indicator",
|
|
55
|
+
"streaming-status"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"tag": "chat-status",
|
|
59
|
+
"tokens": {},
|
|
60
|
+
"traits": [],
|
|
61
|
+
"version": 1
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<header>
|
|
2
|
+
<div>
|
|
3
|
+
<h1>Chat Status</h1>
|
|
4
|
+
<div data-actions>
|
|
5
|
+
<tag-ui size="sm">chat-status</tag-ui>
|
|
6
|
+
<tag-ui size="sm" variant="ghost">CSS-only</tag-ui>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
<p>Module-tier chat status indicator. CSS-only. Inline content for connection/streaming state.</p>
|
|
10
|
+
</header>
|
|
11
|
+
|
|
12
|
+
<section data-section>
|
|
13
|
+
<h2 variant="section">Role</h2>
|
|
14
|
+
<p>Inline status indicator. Pure CSS-only stub. Authors set content directly; the host's streaming pipeline updates innerText.</p>
|
|
15
|
+
</section>
|
|
16
|
+
|
|
17
|
+
<section data-section>
|
|
18
|
+
<h2 variant="section">Composition</h2>
|
|
19
|
+
<p>Typical placement inside <code><chat-shell></code>:</p>
|
|
20
|
+
<code-ui language="html"><chat-status>
|
|
21
|
+
<icon-ui name="circle-fill" style="color: var(--a-success)"></icon-ui>
|
|
22
|
+
<span>Connected</span>
|
|
23
|
+
</chat-status></code-ui>
|
|
24
|
+
</section>
|
|
25
|
+
|
|
26
|
+
<section data-section>
|
|
27
|
+
<h2 variant="section">Family</h2>
|
|
28
|
+
<p>Per <a href="../../../../.brain/adrs/0023-bespoke-shell-tier-children.md">ADR-0023</a>, the chat cluster's bespoke family — <code><chat-shell></code> (host), <code><chat-thread></code>, <code><chat-composer></code>, <code><chat-sidebar></code> (JS-bearing) + <code><chat-header></code>, <code><chat-status></code>, <code><chat-empty></code> (CSS-only). Mirrors the admin cluster's pattern.</p>
|
|
29
|
+
</section>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="auto">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Chat Status — AdiaUI</title>
|
|
7
|
+
|
|
8
|
+
<link rel="stylesheet" href="../../../web-components/styles/resets.css">
|
|
9
|
+
<link rel="stylesheet" href="../../../web-components/styles/tokens.css">
|
|
10
|
+
<link rel="stylesheet" href="../chat-shell/chat-shell.css">
|
|
11
|
+
<link rel="stylesheet" href="../../../web-components/components/code/code.css">
|
|
12
|
+
<link rel="stylesheet" href="../../../web-components/components/tag/tag.css">
|
|
13
|
+
|
|
14
|
+
<script type="module" src="../chat-shell/chat-shell.js"></script>
|
|
15
|
+
<script type="module" src="../../../web-components/components/code/code.js"></script>
|
|
16
|
+
<script type="module" src="../../../web-components/components/tag/tag.js"></script>
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
:where(html, body) { margin: 0; min-height: 100vh; background: var(--a-bg); color: var(--a-fg); font-family: var(--a-font); }
|
|
20
|
+
main { max-width: 960px; margin-inline: auto; padding: var(--a-space-6) var(--a-space-5); }
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
|
|
25
|
+
<main id="demo-root">
|
|
26
|
+
<p>Loading examples…</p>
|
|
27
|
+
</main>
|
|
28
|
+
|
|
29
|
+
<script type="module">
|
|
30
|
+
const root = document.getElementById('demo-root');
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch('./chat-status.examples.html');
|
|
33
|
+
if (!res.ok) throw new Error(`fetch failed (${res.status})`);
|
|
34
|
+
root.innerHTML = await res.text();
|
|
35
|
+
} catch (err) {
|
|
36
|
+
root.innerHTML = `<p style="color:var(--a-danger-strong);">Failed to load chat-status.examples.html — ${err.message}</p>`;
|
|
37
|
+
console.error('[chat-status.html]', err);
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: ChatStatus
|
|
4
|
+
tag: chat-status
|
|
5
|
+
component: ChatStatus
|
|
6
|
+
category: feedback
|
|
7
|
+
version: 1
|
|
8
|
+
description: |
|
|
9
|
+
Module-tier chat status indicator. CSS-only — no behavior, no JS.
|
|
10
|
+
Conveys connection / streaming state via inline content. Typically
|
|
11
|
+
slotted into <chat-header slot="status"> or used inline.
|
|
12
|
+
|
|
13
|
+
Replaces the legacy <span data-chat-status> shape per ADR-0023.
|
|
14
|
+
|
|
15
|
+
props: {}
|
|
16
|
+
|
|
17
|
+
events: {}
|
|
18
|
+
|
|
19
|
+
slots:
|
|
20
|
+
default:
|
|
21
|
+
description: >-
|
|
22
|
+
Status content — text, icon, dot indicator, etc. Authors set
|
|
23
|
+
content directly; the host (chat-shell) updates innerText
|
|
24
|
+
during streaming via legacy [data-chat-status] selector OR
|
|
25
|
+
via querySelector('chat-status') for the bespoke shape.
|
|
26
|
+
|
|
27
|
+
states:
|
|
28
|
+
- name: idle
|
|
29
|
+
description: Default, the only state.
|
|
30
|
+
|
|
31
|
+
traits: []
|
|
32
|
+
|
|
33
|
+
a2ui:
|
|
34
|
+
rules:
|
|
35
|
+
- >-
|
|
36
|
+
chat-status replaces legacy <span data-chat-status> for the
|
|
37
|
+
streaming/connection indicator. Place inside <chat-header
|
|
38
|
+
slot="status">.
|
|
39
|
+
|
|
40
|
+
keywords:
|
|
41
|
+
- chat-status
|
|
42
|
+
- status-indicator
|
|
43
|
+
- connection-status
|
|
44
|
+
- streaming-indicator
|
|
45
|
+
|
|
46
|
+
synonyms:
|
|
47
|
+
chat-status: [connection-indicator, streaming-status]
|
|
48
|
+
|
|
49
|
+
related:
|
|
50
|
+
- ChatShell
|
|
51
|
+
- ChatHeader
|
|
52
|
+
- ChatThread
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/ChatThread.json",
|
|
4
|
+
"title": "ChatThread",
|
|
5
|
+
"description": "Module-tier chat message thread container — replaces legacy\n<section data-chat-messages> per ADR-0023. Owns scroll-to-bottom\non new message (with user-scroll-up suspension), [streaming] and\n[empty] reflected attributes, and a stable target for the host's\nmessage rendering pipeline.\n\nSits inside <chat-shell> as the central scroll surface. Authors\ncompose <chat-empty> as an optional first child for the empty\nstate; message children are appended dynamically by the host.\n\nBackwards compat — <chat-shell> still recognizes the legacy\n<section data-chat-messages> shape via :is() selector. New code\nshould prefer <chat-thread>.\n",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"component": {
|
|
17
|
+
"const": "ChatThread"
|
|
18
|
+
},
|
|
19
|
+
"empty": {
|
|
20
|
+
"description": "Reflected — set when zero message children. Drives the\n<chat-empty> visibility via CSS — no JS toggling needed.\n",
|
|
21
|
+
"type": "boolean",
|
|
22
|
+
"default": true
|
|
23
|
+
},
|
|
24
|
+
"streaming": {
|
|
25
|
+
"description": "Reflected — set by the host while an LLM response is streaming.\nConsumers can style streaming-mode (e.g. cursor blink) via\n:has(chat-thread[streaming]) or attribute selectors.\n",
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"default": false
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"required": [
|
|
31
|
+
"component"
|
|
32
|
+
],
|
|
33
|
+
"unevaluatedProperties": false,
|
|
34
|
+
"x-adiaui": {
|
|
35
|
+
"anti_patterns": [],
|
|
36
|
+
"category": "container",
|
|
37
|
+
"events": {},
|
|
38
|
+
"examples": [],
|
|
39
|
+
"keywords": [
|
|
40
|
+
"chat-thread",
|
|
41
|
+
"message-list",
|
|
42
|
+
"conversation",
|
|
43
|
+
"thread",
|
|
44
|
+
"scroll-surface"
|
|
45
|
+
],
|
|
46
|
+
"name": "ChatThread",
|
|
47
|
+
"related": [
|
|
48
|
+
"ChatShell",
|
|
49
|
+
"ChatEmpty",
|
|
50
|
+
"ChatComposer",
|
|
51
|
+
"ChatSidebar"
|
|
52
|
+
],
|
|
53
|
+
"slots": {
|
|
54
|
+
"default": {
|
|
55
|
+
"description": "Default — message children (typically appended dynamically by <chat-shell>'s rendering pipeline) plus an optional first <chat-empty> sibling for the empty state."
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"states": [
|
|
59
|
+
{
|
|
60
|
+
"description": "Default, no streaming.",
|
|
61
|
+
"name": "idle"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"description": "Host is actively streaming an LLM response.",
|
|
65
|
+
"attribute": "streaming",
|
|
66
|
+
"name": "streaming"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"description": "Zero message children — empty state visible.",
|
|
70
|
+
"attribute": "empty",
|
|
71
|
+
"name": "empty"
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"synonyms": {
|
|
75
|
+
"message-list": [
|
|
76
|
+
"messages",
|
|
77
|
+
"message-stream"
|
|
78
|
+
],
|
|
79
|
+
"thread": [
|
|
80
|
+
"conversation",
|
|
81
|
+
"dialogue",
|
|
82
|
+
"chat-log",
|
|
83
|
+
"transcript"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"tag": "chat-thread",
|
|
87
|
+
"tokens": {},
|
|
88
|
+
"traits": [],
|
|
89
|
+
"version": 1
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<header>
|
|
2
|
+
<div>
|
|
3
|
+
<h1>Chat Thread</h1>
|
|
4
|
+
<div data-actions>
|
|
5
|
+
<tag-ui size="sm">chat-thread</tag-ui>
|
|
6
|
+
<tag-ui size="sm" variant="ghost">JS-bearing</tag-ui>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
<p>Module-tier chat message thread container — owns scroll-to-bottom, [streaming] and [empty] reflected attributes.</p>
|
|
10
|
+
</header>
|
|
11
|
+
|
|
12
|
+
<section data-section>
|
|
13
|
+
<h2 variant="section">Role</h2>
|
|
14
|
+
<p>Owns scroll-to-bottom on new message (with user-scroll-up suspension), [streaming] and [empty] reflected attributes, and a stable target for the host's message rendering pipeline. JS-bearing.</p>
|
|
15
|
+
</section>
|
|
16
|
+
|
|
17
|
+
<section data-section>
|
|
18
|
+
<h2 variant="section">Composition</h2>
|
|
19
|
+
<p>Typical placement inside <code><chat-shell></code>:</p>
|
|
20
|
+
<code-ui language="html"><chat-shell>
|
|
21
|
+
<chat-header>…</chat-header>
|
|
22
|
+
<chat-thread>
|
|
23
|
+
<chat-empty>
|
|
24
|
+
<empty-state-ui icon="chat-circle" heading="Hello!" description="Ask me anything."></empty-state-ui>
|
|
25
|
+
</chat-empty>
|
|
26
|
+
</chat-thread>
|
|
27
|
+
<chat-composer>
|
|
28
|
+
<chat-input-ui placeholder="Message…"></chat-input-ui>
|
|
29
|
+
</chat-composer>
|
|
30
|
+
</chat-shell></code-ui>
|
|
31
|
+
</section>
|
|
32
|
+
|
|
33
|
+
<section data-section>
|
|
34
|
+
<h2 variant="section">Family</h2>
|
|
35
|
+
<p>Per <a href="../../../../.brain/adrs/0023-bespoke-shell-tier-children.md">ADR-0023</a>, the chat cluster's bespoke family — <code><chat-shell></code> (host), <code><chat-thread></code>, <code><chat-composer></code>, <code><chat-sidebar></code> (JS-bearing) + <code><chat-header></code>, <code><chat-status></code>, <code><chat-empty></code> (CSS-only). Mirrors the admin cluster's pattern.</p>
|
|
36
|
+
</section>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="auto">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Chat Thread — AdiaUI</title>
|
|
7
|
+
|
|
8
|
+
<link rel="stylesheet" href="../../../web-components/styles/resets.css">
|
|
9
|
+
<link rel="stylesheet" href="../../../web-components/styles/tokens.css">
|
|
10
|
+
<link rel="stylesheet" href="../chat-shell/chat-shell.css">
|
|
11
|
+
<link rel="stylesheet" href="../../../web-components/components/code/code.css">
|
|
12
|
+
<link rel="stylesheet" href="../../../web-components/components/tag/tag.css">
|
|
13
|
+
|
|
14
|
+
<script type="module" src="../chat-shell/chat-shell.js"></script>
|
|
15
|
+
<script type="module" src="./chat-thread.js"></script>
|
|
16
|
+
<script type="module" src="../../../web-components/components/code/code.js"></script>
|
|
17
|
+
<script type="module" src="../../../web-components/components/tag/tag.js"></script>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
:where(html, body) { margin: 0; min-height: 100vh; background: var(--a-bg); color: var(--a-fg); font-family: var(--a-font); }
|
|
21
|
+
main { max-width: 960px; margin-inline: auto; padding: var(--a-space-6) var(--a-space-5); }
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
|
|
26
|
+
<main id="demo-root">
|
|
27
|
+
<p>Loading examples…</p>
|
|
28
|
+
</main>
|
|
29
|
+
|
|
30
|
+
<script type="module">
|
|
31
|
+
const root = document.getElementById('demo-root');
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch('./chat-thread.examples.html');
|
|
34
|
+
if (!res.ok) throw new Error(`fetch failed (${res.status})`);
|
|
35
|
+
root.innerHTML = await res.text();
|
|
36
|
+
} catch (err) {
|
|
37
|
+
root.innerHTML = `<p style="color:var(--a-danger-strong);">Failed to load chat-thread.examples.html — ${err.message}</p>`;
|
|
38
|
+
console.error('[chat-thread.html]', err);
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <chat-thread streaming?>
|
|
3
|
+
* <chat-empty>...</chat-empty> (optional, hidden when messages present)
|
|
4
|
+
* <!-- assistant/user message divs appended dynamically by chat-shell -->
|
|
5
|
+
* </chat-thread>
|
|
6
|
+
*
|
|
7
|
+
* Module-tier message thread container — replaces legacy
|
|
8
|
+
* <section data-chat-messages> per ADR-0023. Owns:
|
|
9
|
+
*
|
|
10
|
+
* - Scroll-to-bottom on new message (idempotent, respects user
|
|
11
|
+
* scroll-up to read history; resumes auto-scroll when scrolled
|
|
12
|
+
* to bottom)
|
|
13
|
+
* - [streaming] reflected attribute (set by host while LLM streams)
|
|
14
|
+
* - [empty] reflected attribute (set when no message children)
|
|
15
|
+
* - Stable target for chat-shell's #messagesEl reference (host
|
|
16
|
+
* queries this OR the legacy [data-chat-messages] selector)
|
|
17
|
+
*
|
|
18
|
+
* Reflected attributes:
|
|
19
|
+
* [streaming] — true while LLM is streaming a response
|
|
20
|
+
* [empty] — true when zero message children (drives empty-state)
|
|
21
|
+
*
|
|
22
|
+
* Public methods:
|
|
23
|
+
* .scrollToBottom() — programmatic scroll (smooth)
|
|
24
|
+
* .scrollToBottomInstant() — immediate (no animation)
|
|
25
|
+
*
|
|
26
|
+
* The host (<chat-shell>) reads either <chat-thread> or
|
|
27
|
+
* [data-chat-messages] via :is() selector for backwards compat.
|
|
28
|
+
* Message rendering lives in the host (one source of truth for
|
|
29
|
+
* markdown + escape rules); the thread just owns the scroll surface.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { UIElement } from '../../../web-components/core/element.js';
|
|
33
|
+
|
|
34
|
+
const SCROLL_BOTTOM_TOLERANCE = 40; // px from bottom that counts as "at bottom"
|
|
35
|
+
|
|
36
|
+
class ChatThread extends UIElement {
|
|
37
|
+
static properties = {
|
|
38
|
+
streaming: { type: Boolean, default: false, reflect: true },
|
|
39
|
+
empty: { type: Boolean, default: true, reflect: true },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
static template = () => null;
|
|
43
|
+
|
|
44
|
+
#childObserver = null;
|
|
45
|
+
#autoScroll = true;
|
|
46
|
+
#scrollHandler = null;
|
|
47
|
+
|
|
48
|
+
connected() {
|
|
49
|
+
this.#syncEmptyFromChildren();
|
|
50
|
+
this.#setupChildObserver();
|
|
51
|
+
this.#setupScrollListener();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
disconnected() {
|
|
55
|
+
this.#childObserver?.disconnect();
|
|
56
|
+
this.#childObserver = null;
|
|
57
|
+
if (this.#scrollHandler) {
|
|
58
|
+
this.removeEventListener('scroll', this.#scrollHandler);
|
|
59
|
+
this.#scrollHandler = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Public API ──
|
|
64
|
+
|
|
65
|
+
scrollToBottom() {
|
|
66
|
+
this.scrollTo({ top: this.scrollHeight, behavior: 'smooth' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
scrollToBottomInstant() {
|
|
70
|
+
this.scrollTop = this.scrollHeight;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Internal: empty state from message children ──
|
|
74
|
+
|
|
75
|
+
#syncEmptyFromChildren() {
|
|
76
|
+
// Children that count as "messages" — anything except <chat-empty> stub
|
|
77
|
+
const messageChildren = Array.from(this.children).filter(
|
|
78
|
+
(c) => c.tagName.toLowerCase() !== 'chat-empty'
|
|
79
|
+
);
|
|
80
|
+
this.empty = messageChildren.length === 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#setupChildObserver() {
|
|
84
|
+
this.#childObserver = new MutationObserver(() => {
|
|
85
|
+
this.#syncEmptyFromChildren();
|
|
86
|
+
// New message added — scroll to bottom if user is at bottom
|
|
87
|
+
if (this.#autoScroll) {
|
|
88
|
+
this.scrollToBottomInstant();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
this.#childObserver.observe(this, { childList: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Internal: track user scroll to suspend auto-scroll ──
|
|
95
|
+
|
|
96
|
+
#setupScrollListener() {
|
|
97
|
+
this.#scrollHandler = () => {
|
|
98
|
+
const distanceFromBottom = this.scrollHeight - (this.scrollTop + this.clientHeight);
|
|
99
|
+
this.#autoScroll = distanceFromBottom <= SCROLL_BOTTOM_TOLERANCE;
|
|
100
|
+
};
|
|
101
|
+
this.addEventListener('scroll', this.#scrollHandler, { passive: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
customElements.define('chat-thread', ChatThread);
|
|
106
|
+
export { ChatThread };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import '../../../web-components/core/element.js';
|
|
3
|
+
import './chat-thread.js';
|
|
4
|
+
|
|
5
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
+
|
|
7
|
+
function mount(html) {
|
|
8
|
+
const wrap = document.createElement('div');
|
|
9
|
+
wrap.innerHTML = html;
|
|
10
|
+
document.body.appendChild(wrap);
|
|
11
|
+
return wrap.firstElementChild;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let originalRect;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
document.body.innerHTML = '';
|
|
17
|
+
globalThis.MutationObserver = class {
|
|
18
|
+
observe() {} disconnect() {}
|
|
19
|
+
};
|
|
20
|
+
originalRect = HTMLElement.prototype.getBoundingClientRect;
|
|
21
|
+
HTMLElement.prototype.getBoundingClientRect = function () {
|
|
22
|
+
return { width: 480, height: 600, top: 0, left: 0, right: 480, bottom: 600, x: 0, y: 0 };
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
if (originalRect) HTMLElement.prototype.getBoundingClientRect = originalRect;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('chat-thread', () => {
|
|
31
|
+
it('registers chat-thread as a custom element', () => {
|
|
32
|
+
expect(customElements.get('chat-thread')).toBeDefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('defaults to streaming=false on connect', () => {
|
|
36
|
+
const t = mount('<chat-thread></chat-thread>');
|
|
37
|
+
expect(t.streaming).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('defaults to empty=true when no children', async () => {
|
|
41
|
+
const t = mount('<chat-thread></chat-thread>');
|
|
42
|
+
await tick();
|
|
43
|
+
expect(t.empty).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('reflects [streaming] via property assignment', async () => {
|
|
47
|
+
const t = mount('<chat-thread></chat-thread>');
|
|
48
|
+
t.streaming = true;
|
|
49
|
+
await tick();
|
|
50
|
+
expect(t.hasAttribute('streaming')).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('reflects [empty] via property assignment', async () => {
|
|
54
|
+
const t = mount('<chat-thread></chat-thread>');
|
|
55
|
+
t.empty = false;
|
|
56
|
+
await tick();
|
|
57
|
+
expect(t.hasAttribute('empty')).toBe(false);
|
|
58
|
+
t.empty = true;
|
|
59
|
+
await tick();
|
|
60
|
+
expect(t.hasAttribute('empty')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('exposes .scrollToBottom() and .scrollToBottomInstant() methods', () => {
|
|
64
|
+
const t = mount('<chat-thread></chat-thread>');
|
|
65
|
+
expect(typeof t.scrollToBottom).toBe('function');
|
|
66
|
+
expect(typeof t.scrollToBottomInstant).toBe('function');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not count <chat-empty> stub as a message child for [empty] calc', () => {
|
|
70
|
+
const t = mount('<chat-thread><chat-empty></chat-empty></chat-thread>');
|
|
71
|
+
// chat-empty alone — empty should still be true
|
|
72
|
+
expect(t.empty).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('cleanup on disconnect — removes scroll listener', () => {
|
|
76
|
+
const t = mount('<chat-thread></chat-thread>');
|
|
77
|
+
const removeSpy = vi.spyOn(t, 'removeEventListener');
|
|
78
|
+
t.remove();
|
|
79
|
+
const removedTypes = removeSpy.mock.calls.map((args) => args[0]);
|
|
80
|
+
expect(removedTypes).toContain('scroll');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: ChatThread
|
|
4
|
+
tag: chat-thread
|
|
5
|
+
component: ChatThread
|
|
6
|
+
category: container
|
|
7
|
+
version: 1
|
|
8
|
+
description: |
|
|
9
|
+
Module-tier chat message thread container — replaces legacy
|
|
10
|
+
<section data-chat-messages> per ADR-0023. Owns scroll-to-bottom
|
|
11
|
+
on new message (with user-scroll-up suspension), [streaming] and
|
|
12
|
+
[empty] reflected attributes, and a stable target for the host's
|
|
13
|
+
message rendering pipeline.
|
|
14
|
+
|
|
15
|
+
Sits inside <chat-shell> as the central scroll surface. Authors
|
|
16
|
+
compose <chat-empty> as an optional first child for the empty
|
|
17
|
+
state; message children are appended dynamically by the host.
|
|
18
|
+
|
|
19
|
+
Backwards compat — <chat-shell> still recognizes the legacy
|
|
20
|
+
<section data-chat-messages> shape via :is() selector. New code
|
|
21
|
+
should prefer <chat-thread>.
|
|
22
|
+
|
|
23
|
+
props:
|
|
24
|
+
streaming:
|
|
25
|
+
description: |
|
|
26
|
+
Reflected — set by the host while an LLM response is streaming.
|
|
27
|
+
Consumers can style streaming-mode (e.g. cursor blink) via
|
|
28
|
+
:has(chat-thread[streaming]) or attribute selectors.
|
|
29
|
+
type: boolean
|
|
30
|
+
default: false
|
|
31
|
+
reflect: true
|
|
32
|
+
|
|
33
|
+
empty:
|
|
34
|
+
description: |
|
|
35
|
+
Reflected — set when zero message children. Drives the
|
|
36
|
+
<chat-empty> visibility via CSS — no JS toggling needed.
|
|
37
|
+
type: boolean
|
|
38
|
+
default: true
|
|
39
|
+
reflect: true
|
|
40
|
+
|
|
41
|
+
events: {}
|
|
42
|
+
|
|
43
|
+
slots:
|
|
44
|
+
default:
|
|
45
|
+
description: >-
|
|
46
|
+
Default — message children (typically appended dynamically by
|
|
47
|
+
<chat-shell>'s rendering pipeline) plus an optional first
|
|
48
|
+
<chat-empty> sibling for the empty state.
|
|
49
|
+
|
|
50
|
+
states:
|
|
51
|
+
- name: idle
|
|
52
|
+
description: Default, no streaming.
|
|
53
|
+
- name: streaming
|
|
54
|
+
attribute: streaming
|
|
55
|
+
description: Host is actively streaming an LLM response.
|
|
56
|
+
- name: empty
|
|
57
|
+
attribute: empty
|
|
58
|
+
description: Zero message children — empty state visible.
|
|
59
|
+
|
|
60
|
+
traits: []
|
|
61
|
+
|
|
62
|
+
a2ui:
|
|
63
|
+
rules:
|
|
64
|
+
- >-
|
|
65
|
+
chat-thread is the bespoke replacement for legacy
|
|
66
|
+
<section data-chat-messages> inside <chat-shell>. Use it for
|
|
67
|
+
the message scroll surface; the host appends dynamic message
|
|
68
|
+
divs as children.
|
|
69
|
+
- >-
|
|
70
|
+
Place <chat-empty> as an optional first child for the empty
|
|
71
|
+
state; the [empty] reflected attribute drives its visibility
|
|
72
|
+
via CSS (no JS toggling).
|
|
73
|
+
|
|
74
|
+
keywords:
|
|
75
|
+
- chat-thread
|
|
76
|
+
- message-list
|
|
77
|
+
- conversation
|
|
78
|
+
- thread
|
|
79
|
+
- scroll-surface
|
|
80
|
+
|
|
81
|
+
synonyms:
|
|
82
|
+
thread: [conversation, dialogue, chat-log, transcript]
|
|
83
|
+
message-list: [messages, message-stream]
|
|
84
|
+
|
|
85
|
+
related:
|
|
86
|
+
- ChatShell
|
|
87
|
+
- ChatEmpty
|
|
88
|
+
- ChatComposer
|
|
89
|
+
- ChatSidebar
|
package/chat/index.js
CHANGED