@cianfrani/ai-ui 0.1.0-alpha.0
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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/ai-ui.js +3218 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/lib/has-slot-controller.d.ts +21 -0
- package/dist/types/lib/tool-tone.d.ts +2 -0
- package/dist/types/native-styles.d.ts +5 -0
- package/dist/types/semantic/ai-conversation.d.ts +28 -0
- package/dist/types/semantic/ai-event.d.ts +91 -0
- package/dist/types/semantic/ai-message.d.ts +45 -0
- package/dist/types/semantic/ai-thinking.d.ts +41 -0
- package/dist/types/semantic/ai-tool-call.d.ts +59 -0
- package/dist/types/semantic/ai-tool-result.d.ts +44 -0
- package/dist/types/semantic/index.d.ts +6 -0
- package/dist/types/visual/avatar.d.ts +34 -0
- package/dist/types/visual/badge.d.ts +32 -0
- package/dist/types/visual/divider.d.ts +26 -0
- package/dist/types/visual/icon.d.ts +24 -0
- package/dist/types/visual/index.d.ts +9 -0
- package/dist/types/visual/markdown.d.ts +52 -0
- package/dist/types/visual/stack.d.ts +32 -0
- package/dist/types/visual/status.d.ts +33 -0
- package/dist/types/visual/surface.d.ts +34 -0
- package/dist/types/visual/text.d.ts +37 -0
- package/package.json +67 -0
- package/src/custom-elements.json +3741 -0
- package/src/index.ts +8 -0
- package/src/lib/has-slot-controller.ts +61 -0
- package/src/lib/tool-tone.ts +18 -0
- package/src/native-styles.ts +29 -0
- package/src/semantic/ai-conversation.ts +84 -0
- package/src/semantic/ai-event.ts +452 -0
- package/src/semantic/ai-message.ts +235 -0
- package/src/semantic/ai-thinking.ts +190 -0
- package/src/semantic/ai-tool-call.ts +513 -0
- package/src/semantic/ai-tool-result.ts +239 -0
- package/src/semantic/index.ts +6 -0
- package/src/visual/avatar.ts +163 -0
- package/src/visual/badge.ts +141 -0
- package/src/visual/divider.ts +97 -0
- package/src/visual/icon.ts +97 -0
- package/src/visual/index.ts +9 -0
- package/src/visual/markdown.ts +888 -0
- package/src/visual/stack.ts +115 -0
- package/src/visual/status.ts +170 -0
- package/src/visual/surface.ts +150 -0
- package/src/visual/text.ts +141 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { LitElement, css, html, nothing } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
|
|
4
|
+
export type AiMessageRole = "user" | "assistant" | "system" | "tool";
|
|
5
|
+
export type AiMessageStatus = "pending" | "running" | "success" | "error" | "cancelled" | "unknown";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Message-first transcript record with role-specific pi-ui presentation.
|
|
9
|
+
*
|
|
10
|
+
* @slot - Content blocks, usually ai-markdown, ai-thinking, ai-tool-call, ai-tool-result, or ai-event.
|
|
11
|
+
* @slot meta - Timestamp, model, or other metadata. Prefer native <time>.
|
|
12
|
+
* @slot avatar - Optional actor avatar.
|
|
13
|
+
* @slot actor - Optional actor label.
|
|
14
|
+
*
|
|
15
|
+
* @cssprop --ai-message-background - Message body background. Default: transparent
|
|
16
|
+
* @cssprop --ai-message-color - Message text color. Default: inherit
|
|
17
|
+
* @cssprop --ai-message-border-color - Optional message border color. Default: transparent
|
|
18
|
+
* @cssprop --ai-message-border-width - Optional border width. Default: 0
|
|
19
|
+
* @cssprop --ai-message-radius - Optional message radius. Default: 0
|
|
20
|
+
* @cssprop --ai-message-gap - Gap between slotted content blocks. Default: var(--spacing-xs)
|
|
21
|
+
* @cssprop --ai-message-user-background - User bubble background. Default: var(--accent)
|
|
22
|
+
* @cssprop --ai-message-user-color - User bubble color. Default: var(--text-on-accent)
|
|
23
|
+
* @cssprop --ai-message-user-max-width - User bubble max width. Default: min(90%, 640px)
|
|
24
|
+
* @cssprop --ai-message-assistant-rail-color - Assistant rail color.
|
|
25
|
+
* @cssprop --ai-message-assistant-label-color - Assistant label color. Default: var(--text-muted)
|
|
26
|
+
* @cssprop --ai-message-meta-color - Meta text color. Default: var(--text-muted)
|
|
27
|
+
*/
|
|
28
|
+
@customElement("ai-message")
|
|
29
|
+
export class AiMessage extends LitElement {
|
|
30
|
+
/** Message author/source role. Unknown values render as system-style rows. */
|
|
31
|
+
@property({ reflect: true })
|
|
32
|
+
override role: "user" | "assistant" | "system" | "tool" = "assistant";
|
|
33
|
+
|
|
34
|
+
/** Optional associated record id; maps to native `for` attribute. */
|
|
35
|
+
@property({ reflect: true, attribute: "for" })
|
|
36
|
+
htmlFor = "";
|
|
37
|
+
|
|
38
|
+
/** Message lifecycle status. */
|
|
39
|
+
@property({ reflect: true })
|
|
40
|
+
status: "pending" | "running" | "success" | "error" | "cancelled" | "unknown" = "unknown";
|
|
41
|
+
|
|
42
|
+
/** ISO-ish timestamp for metadata/association; visual formatting is left to slotted meta. */
|
|
43
|
+
@property({ reflect: true })
|
|
44
|
+
timestamp = "";
|
|
45
|
+
|
|
46
|
+
/** Display label for role chrome. */
|
|
47
|
+
@property({ reflect: true })
|
|
48
|
+
label = "";
|
|
49
|
+
|
|
50
|
+
static override styles = css`
|
|
51
|
+
:host {
|
|
52
|
+
box-sizing: border-box;
|
|
53
|
+
display: block;
|
|
54
|
+
margin: 0;
|
|
55
|
+
padding: 0;
|
|
56
|
+
max-width: 100%;
|
|
57
|
+
min-width: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
*,
|
|
61
|
+
*::before,
|
|
62
|
+
*::after {
|
|
63
|
+
box-sizing: inherit;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.message {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
min-width: 0;
|
|
70
|
+
max-width: 100%;
|
|
71
|
+
background-color: var(--ai-message-background, transparent);
|
|
72
|
+
color: var(--ai-message-color, inherit);
|
|
73
|
+
border: var(--ai-message-border-width, 0) solid var(--ai-message-border-color, transparent);
|
|
74
|
+
border-radius: var(--ai-message-radius, 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.content {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: var(--ai-message-gap, 3px);
|
|
81
|
+
min-width: 0;
|
|
82
|
+
max-width: 100%;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.content ::slotted(*) {
|
|
86
|
+
min-width: 0;
|
|
87
|
+
max-width: 100%;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
::slotted([slot="meta"]) {
|
|
91
|
+
color: var(--ai-message-meta-color, var(--text-muted, var(--ai-color-text-muted, #888)));
|
|
92
|
+
font-size: var(--font-size-caption, var(--ai-font-size-caption, 0.75rem));
|
|
93
|
+
line-height: var(--line-height-tight, var(--ai-line-height-tight, 1.1));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.meta-fallback {
|
|
97
|
+
color: var(--ai-message-meta-color, var(--text-muted, var(--ai-color-text-muted, #888)));
|
|
98
|
+
font-size: var(--font-size-caption, var(--ai-font-size-caption, 0.75rem));
|
|
99
|
+
line-height: var(--line-height-tight, var(--ai-line-height-tight, 1.1));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* User bubble treatment. */
|
|
103
|
+
:host([role="user"]) .message {
|
|
104
|
+
align-items: flex-end;
|
|
105
|
+
gap: calc(var(--spacing-xs, 4px) / 2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
:host([role="user"]) .content {
|
|
109
|
+
width: auto;
|
|
110
|
+
max-width: var(--ai-message-user-max-width, min(90%, 640px));
|
|
111
|
+
padding: var(--spacing-sm, 8px) var(--spacing-md, 12px);
|
|
112
|
+
background: var(--ai-message-user-background, var(--accent, Highlight));
|
|
113
|
+
color: var(--ai-message-user-color, var(--text-on-accent, HighlightText));
|
|
114
|
+
border-radius: var(
|
|
115
|
+
--ai-message-radius,
|
|
116
|
+
var(--radius, 8px) var(--radius, 8px) 4px var(--radius, 8px)
|
|
117
|
+
);
|
|
118
|
+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
|
119
|
+
overflow-wrap: anywhere;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Assistant rail/header treatment. */
|
|
123
|
+
:host([role="assistant"]) .message {
|
|
124
|
+
align-items: stretch;
|
|
125
|
+
gap: 2px;
|
|
126
|
+
width: 100%;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.header {
|
|
130
|
+
display: none;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
:host([role="assistant"]) .header {
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: var(--spacing-sm, 8px);
|
|
137
|
+
padding-left: calc(var(--spacing-md, 12px) + 2px);
|
|
138
|
+
line-height: var(--line-height-tight, 1.1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.actor-label {
|
|
142
|
+
color: var(--ai-message-assistant-label-color, var(--text-muted, #888));
|
|
143
|
+
font-size: var(--font-size-caption, 0.75rem);
|
|
144
|
+
font-weight: var(--font-weight-bold, 700);
|
|
145
|
+
letter-spacing: var(--tracking-overline, 0.08em);
|
|
146
|
+
line-height: var(--line-height-tight, 1.1);
|
|
147
|
+
text-transform: uppercase;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
:host([role="assistant"]) .content {
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
width: 100%;
|
|
154
|
+
max-width: 100%;
|
|
155
|
+
min-width: 0;
|
|
156
|
+
padding-left: calc(var(--spacing-md, 12px) + 2px);
|
|
157
|
+
border-left: 1px solid var(--ai-message-assistant-rail-color, rgba(255, 255, 255, 0.08));
|
|
158
|
+
background: transparent;
|
|
159
|
+
color: var(--ai-message-color, var(--text, inherit));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
:host([role="tool"]) .message,
|
|
163
|
+
:host([role="system"]) .message {
|
|
164
|
+
gap: var(--ai-message-gap, 3px);
|
|
165
|
+
color: var(--ai-message-color, var(--text, inherit));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
:host([role="tool"]) .content {
|
|
169
|
+
width: 100%;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
:host([role="system"]) .content {
|
|
173
|
+
color: color-mix(in oklch, var(--text-muted, currentColor) 92%, transparent);
|
|
174
|
+
font-size: var(--font-size-meta, 0.8125rem);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@media (max-width: 420px) {
|
|
178
|
+
:host([role="assistant"]) .header,
|
|
179
|
+
:host([role="assistant"]) .content {
|
|
180
|
+
padding-left: calc(var(--spacing-sm, 8px) + 2px);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
private get normalizedRole(): AiMessageRole {
|
|
186
|
+
if (
|
|
187
|
+
this.role === "user" ||
|
|
188
|
+
this.role === "assistant" ||
|
|
189
|
+
this.role === "system" ||
|
|
190
|
+
this.role === "tool"
|
|
191
|
+
) {
|
|
192
|
+
return this.role;
|
|
193
|
+
}
|
|
194
|
+
return "system";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private get actorLabel(): string {
|
|
198
|
+
return this.label || (this.normalizedRole === "assistant" ? "Assistant" : this.normalizedRole);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
override render() {
|
|
202
|
+
const role = this.normalizedRole;
|
|
203
|
+
return html`
|
|
204
|
+
<article class="message" data-role=${role} aria-busy=${this.status === "running"}>
|
|
205
|
+
${
|
|
206
|
+
role === "assistant"
|
|
207
|
+
? html`
|
|
208
|
+
<div class="header">
|
|
209
|
+
<slot name="avatar"></slot>
|
|
210
|
+
<slot name="actor"><span class="actor-label">${this.actorLabel}</span></slot>
|
|
211
|
+
<slot name="meta"></slot>
|
|
212
|
+
</div>
|
|
213
|
+
`
|
|
214
|
+
: nothing
|
|
215
|
+
}
|
|
216
|
+
<div class="content">
|
|
217
|
+
<slot></slot>
|
|
218
|
+
</div>
|
|
219
|
+
${
|
|
220
|
+
role !== "assistant"
|
|
221
|
+
? html`
|
|
222
|
+
<slot name="meta"></slot>
|
|
223
|
+
`
|
|
224
|
+
: nothing
|
|
225
|
+
}
|
|
226
|
+
</article>
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
declare global {
|
|
232
|
+
interface HTMLElementTagNameMap {
|
|
233
|
+
"ai-message": AiMessage;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { LitElement, css, html } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
import "./ai-tool-call";
|
|
4
|
+
import type { AiShowHideDetail } from "./ai-event";
|
|
5
|
+
|
|
6
|
+
function summarizeThinking(content: string): string {
|
|
7
|
+
const normalized = content.replace(/\s+/g, " ").trim();
|
|
8
|
+
if (!normalized) {
|
|
9
|
+
return "Reasoning";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const sentence = normalized.match(/.*?[.!?](?:\s|$)/)?.[0]?.trim() ?? normalized;
|
|
13
|
+
if (sentence.length <= 88) {
|
|
14
|
+
return sentence;
|
|
15
|
+
}
|
|
16
|
+
return `${sentence.slice(0, 85).trimEnd()}…`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Specialized assistant content block for model reasoning.
|
|
21
|
+
*
|
|
22
|
+
* @slot - Thinking content if content property is not set.
|
|
23
|
+
* @slot meta - Optional metadata.
|
|
24
|
+
*
|
|
25
|
+
* @fires ai-show - Emitted when disclosure opens. Bubbles, composed.
|
|
26
|
+
* @fires ai-hide - Emitted when disclosure closes. Bubbles, composed.
|
|
27
|
+
*
|
|
28
|
+
* @cssprop --ai-thinking-color - Thinking content color.
|
|
29
|
+
* @cssprop --ai-thinking-muted-color - Muted/placeholder color.
|
|
30
|
+
* @cssprop --ai-thinking-max-height - Max scroll height. Default: 140px
|
|
31
|
+
* @cssprop --ai-thinking-summary-color - Summary color.
|
|
32
|
+
*/
|
|
33
|
+
@customElement("ai-thinking")
|
|
34
|
+
export class AiThinking extends LitElement {
|
|
35
|
+
/** Thinking text to render. Prefer property binding for streaming content. */
|
|
36
|
+
@property({ type: String })
|
|
37
|
+
content = "";
|
|
38
|
+
|
|
39
|
+
/** Origin of the thinking block. */
|
|
40
|
+
@property({ reflect: true })
|
|
41
|
+
source: "model" | "assistant" | "unknown" = "unknown";
|
|
42
|
+
|
|
43
|
+
/** Thinking exists but is not available. */
|
|
44
|
+
@property({ reflect: true, type: Boolean })
|
|
45
|
+
redacted = false;
|
|
46
|
+
|
|
47
|
+
/** Whether the disclosure is expanded. */
|
|
48
|
+
@property({ reflect: true, type: Boolean })
|
|
49
|
+
open = false;
|
|
50
|
+
|
|
51
|
+
/** Optional summary headline. Defaults to a first-sentence summary of content. */
|
|
52
|
+
@property({ reflect: true })
|
|
53
|
+
headline = "";
|
|
54
|
+
|
|
55
|
+
static override styles = css`
|
|
56
|
+
*,
|
|
57
|
+
*::before,
|
|
58
|
+
*::after {
|
|
59
|
+
box-sizing: border-box;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:host {
|
|
63
|
+
display: block;
|
|
64
|
+
margin: 0;
|
|
65
|
+
padding: 0;
|
|
66
|
+
width: 100%;
|
|
67
|
+
max-width: 100%;
|
|
68
|
+
min-width: 0;
|
|
69
|
+
color: var(
|
|
70
|
+
--ai-thinking-color,
|
|
71
|
+
color-mix(in oklch, var(--text-muted, currentColor) 90%, transparent)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.thinking-content {
|
|
76
|
+
color: var(
|
|
77
|
+
--ai-thinking-color,
|
|
78
|
+
color-mix(in oklch, var(--text-muted, currentColor) 90%, transparent)
|
|
79
|
+
);
|
|
80
|
+
font-size: var(--font-size-caption, 0.75rem);
|
|
81
|
+
line-height: var(--line-height-body, 1.5);
|
|
82
|
+
white-space: pre-wrap;
|
|
83
|
+
word-break: break-word;
|
|
84
|
+
max-height: var(--ai-thinking-max-height, 140px);
|
|
85
|
+
overflow-x: auto;
|
|
86
|
+
overflow-y: auto;
|
|
87
|
+
-webkit-overflow-scrolling: touch;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.redacted {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 6px;
|
|
94
|
+
padding: 2px 4px;
|
|
95
|
+
color: var(--ai-thinking-muted-color, var(--text-muted, var(--ai-color-text-muted, #888)));
|
|
96
|
+
font-size: var(--font-size-caption, 0.75rem);
|
|
97
|
+
line-height: var(--line-height-snug, 1.25);
|
|
98
|
+
font-style: italic;
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
show(): void {
|
|
103
|
+
if (this.redacted) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.open = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
hide(): void {
|
|
110
|
+
if (this.redacted) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.open = false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
toggle(force?: boolean): void {
|
|
117
|
+
if (this.redacted) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.open = force ?? !this.open;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private handleShow() {
|
|
124
|
+
if (this.redacted) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.open = true;
|
|
128
|
+
this.dispatchEvent(
|
|
129
|
+
new CustomEvent<AiShowHideDetail>("ai-show", {
|
|
130
|
+
detail: { source: "thinking", redacted: this.redacted },
|
|
131
|
+
bubbles: true,
|
|
132
|
+
composed: true,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private handleHide() {
|
|
138
|
+
if (this.redacted) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.open = false;
|
|
142
|
+
this.dispatchEvent(
|
|
143
|
+
new CustomEvent<AiShowHideDetail>("ai-hide", {
|
|
144
|
+
detail: { source: "thinking", redacted: this.redacted },
|
|
145
|
+
bubbles: true,
|
|
146
|
+
composed: true,
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
protected override updated(changed: Map<string, unknown>) {
|
|
152
|
+
if (changed.has("redacted") && this.redacted && this.open) {
|
|
153
|
+
this.open = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
override render() {
|
|
158
|
+
if (this.redacted) {
|
|
159
|
+
return html`
|
|
160
|
+
<div class="redacted">Reasoning redacted<slot name="meta"></slot></div>
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const content = this.content.trim();
|
|
165
|
+
if (!content) {
|
|
166
|
+
return html`
|
|
167
|
+
<slot></slot>
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return html`
|
|
172
|
+
<ai-tool-call
|
|
173
|
+
name="Reasoning"
|
|
174
|
+
.headline=${this.headline || summarizeThinking(content)}
|
|
175
|
+
.open=${this.open}
|
|
176
|
+
@ai-show=${this.handleShow}
|
|
177
|
+
@ai-hide=${this.handleHide}
|
|
178
|
+
>
|
|
179
|
+
<div class="thinking-content">${content}</div>
|
|
180
|
+
<slot name="meta"></slot>
|
|
181
|
+
</ai-tool-call>
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
declare global {
|
|
187
|
+
interface HTMLElementTagNameMap {
|
|
188
|
+
"ai-thinking": AiThinking;
|
|
189
|
+
}
|
|
190
|
+
}
|