@hyperspaceng/neural-web-ui 0.60.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/CHANGELOG.md +329 -0
- package/README.md +601 -0
- package/example/README.md +61 -0
- package/example/index.html +13 -0
- package/example/package.json +25 -0
- package/example/src/app.css +1 -0
- package/example/src/custom-messages.ts +99 -0
- package/example/src/main.ts +421 -0
- package/example/tsconfig.json +23 -0
- package/example/vite.config.ts +6 -0
- package/package.json +51 -0
- package/scripts/count-prompt-tokens.ts +88 -0
- package/src/ChatPanel.ts +209 -0
- package/src/app.css +68 -0
- package/src/components/AgentInterface.ts +401 -0
- package/src/components/AttachmentTile.ts +107 -0
- package/src/components/ConsoleBlock.ts +72 -0
- package/src/components/CustomProviderCard.ts +100 -0
- package/src/components/ExpandableSection.ts +46 -0
- package/src/components/Input.ts +113 -0
- package/src/components/MessageEditor.ts +402 -0
- package/src/components/MessageList.ts +98 -0
- package/src/components/Messages.ts +383 -0
- package/src/components/ProviderKeyInput.ts +153 -0
- package/src/components/SandboxedIframe.ts +626 -0
- package/src/components/StreamingMessageContainer.ts +103 -0
- package/src/components/ThinkingBlock.ts +43 -0
- package/src/components/message-renderer-registry.ts +28 -0
- package/src/components/sandbox/ArtifactsRuntimeProvider.ts +219 -0
- package/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
- package/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
- package/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
- package/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
- package/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
- package/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
- package/src/dialogs/ApiKeyPromptDialog.ts +75 -0
- package/src/dialogs/AttachmentOverlay.ts +636 -0
- package/src/dialogs/CustomProviderDialog.ts +274 -0
- package/src/dialogs/ModelSelector.ts +367 -0
- package/src/dialogs/PersistentStorageDialog.ts +144 -0
- package/src/dialogs/ProvidersModelsTab.ts +212 -0
- package/src/dialogs/SessionListDialog.ts +150 -0
- package/src/dialogs/SettingsDialog.ts +218 -0
- package/src/index.ts +120 -0
- package/src/prompts/prompts.ts +282 -0
- package/src/storage/app-storage.ts +60 -0
- package/src/storage/backends/indexeddb-storage-backend.ts +193 -0
- package/src/storage/store.ts +33 -0
- package/src/storage/stores/custom-providers-store.ts +62 -0
- package/src/storage/stores/provider-keys-store.ts +33 -0
- package/src/storage/stores/sessions-store.ts +136 -0
- package/src/storage/stores/settings-store.ts +34 -0
- package/src/storage/types.ts +206 -0
- package/src/tools/artifacts/ArtifactElement.ts +14 -0
- package/src/tools/artifacts/ArtifactPill.ts +26 -0
- package/src/tools/artifacts/Console.ts +93 -0
- package/src/tools/artifacts/DocxArtifact.ts +213 -0
- package/src/tools/artifacts/ExcelArtifact.ts +231 -0
- package/src/tools/artifacts/GenericArtifact.ts +117 -0
- package/src/tools/artifacts/HtmlArtifact.ts +195 -0
- package/src/tools/artifacts/ImageArtifact.ts +116 -0
- package/src/tools/artifacts/MarkdownArtifact.ts +82 -0
- package/src/tools/artifacts/PdfArtifact.ts +201 -0
- package/src/tools/artifacts/SvgArtifact.ts +78 -0
- package/src/tools/artifacts/TextArtifact.ts +148 -0
- package/src/tools/artifacts/artifacts-tool-renderer.ts +310 -0
- package/src/tools/artifacts/artifacts.ts +713 -0
- package/src/tools/artifacts/index.ts +7 -0
- package/src/tools/extract-document.ts +275 -0
- package/src/tools/index.ts +46 -0
- package/src/tools/javascript-repl.ts +293 -0
- package/src/tools/renderer-registry.ts +130 -0
- package/src/tools/renderers/BashRenderer.ts +52 -0
- package/src/tools/renderers/CalculateRenderer.ts +58 -0
- package/src/tools/renderers/DefaultRenderer.ts +103 -0
- package/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
- package/src/tools/types.ts +15 -0
- package/src/utils/attachment-utils.ts +472 -0
- package/src/utils/auth-token.ts +22 -0
- package/src/utils/format.ts +42 -0
- package/src/utils/i18n.ts +653 -0
- package/src/utils/model-discovery.ts +277 -0
- package/src/utils/proxy-utils.ts +139 -0
- package/src/utils/test-sessions.ts +2357 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { icon } from "@mariozechner/mini-lit";
|
|
2
|
+
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
|
|
3
|
+
import { Select, type SelectOption } from "@mariozechner/mini-lit/dist/Select.js";
|
|
4
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
5
|
+
import { html, LitElement } from "lit";
|
|
6
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
7
|
+
import { createRef, ref } from "lit/directives/ref.js";
|
|
8
|
+
import { Brain, Loader2, Paperclip, Send, Sparkles, Square } from "lucide";
|
|
9
|
+
import { type Attachment, loadAttachment } from "../utils/attachment-utils.js";
|
|
10
|
+
import { i18n } from "../utils/i18n.js";
|
|
11
|
+
import "./AttachmentTile.js";
|
|
12
|
+
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
13
|
+
|
|
14
|
+
@customElement("message-editor")
|
|
15
|
+
export class MessageEditor extends LitElement {
|
|
16
|
+
private _value = "";
|
|
17
|
+
private textareaRef = createRef<HTMLTextAreaElement>();
|
|
18
|
+
|
|
19
|
+
@property()
|
|
20
|
+
get value() {
|
|
21
|
+
return this._value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
set value(val: string) {
|
|
25
|
+
const oldValue = this._value;
|
|
26
|
+
this._value = val;
|
|
27
|
+
this.requestUpdate("value", oldValue);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@property() isStreaming = false;
|
|
31
|
+
@property() currentModel?: Model<any>;
|
|
32
|
+
@property() thinkingLevel: ThinkingLevel = "off";
|
|
33
|
+
@property() showAttachmentButton = true;
|
|
34
|
+
@property() showModelSelector = true;
|
|
35
|
+
@property() showThinkingSelector = true;
|
|
36
|
+
@property() onInput?: (value: string) => void;
|
|
37
|
+
@property() onSend?: (input: string, attachments: Attachment[]) => void;
|
|
38
|
+
@property() onAbort?: () => void;
|
|
39
|
+
@property() onModelSelect?: () => void;
|
|
40
|
+
@property() onThinkingChange?: (level: "off" | "minimal" | "low" | "medium" | "high") => void;
|
|
41
|
+
@property() onFilesChange?: (files: Attachment[]) => void;
|
|
42
|
+
@property() attachments: Attachment[] = [];
|
|
43
|
+
@property() maxFiles = 10;
|
|
44
|
+
@property() maxFileSize = 20 * 1024 * 1024; // 20MB
|
|
45
|
+
@property() acceptedTypes =
|
|
46
|
+
"image/*,application/pdf,.docx,.pptx,.xlsx,.xls,.txt,.md,.json,.xml,.html,.css,.js,.ts,.jsx,.tsx,.yml,.yaml";
|
|
47
|
+
|
|
48
|
+
@state() processingFiles = false;
|
|
49
|
+
@state() isDragging = false;
|
|
50
|
+
private fileInputRef = createRef<HTMLInputElement>();
|
|
51
|
+
|
|
52
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private handleTextareaInput = (e: Event) => {
|
|
57
|
+
const textarea = e.target as HTMLTextAreaElement;
|
|
58
|
+
this.value = textarea.value;
|
|
59
|
+
this.onInput?.(this.value);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
private handleKeyDown = (e: KeyboardEvent) => {
|
|
63
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
if (!this.isStreaming && !this.processingFiles && (this.value.trim() || this.attachments.length > 0)) {
|
|
66
|
+
this.handleSend();
|
|
67
|
+
}
|
|
68
|
+
} else if (e.key === "Escape" && this.isStreaming) {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
this.onAbort?.();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
private handlePaste = async (e: ClipboardEvent) => {
|
|
75
|
+
const items = e.clipboardData?.items;
|
|
76
|
+
if (!items) return;
|
|
77
|
+
|
|
78
|
+
const imageFiles: File[] = [];
|
|
79
|
+
|
|
80
|
+
// Check for image items in clipboard
|
|
81
|
+
for (let i = 0; i < items.length; i++) {
|
|
82
|
+
const item = items[i];
|
|
83
|
+
if (item.type.startsWith("image/")) {
|
|
84
|
+
const file = item.getAsFile();
|
|
85
|
+
if (file) {
|
|
86
|
+
imageFiles.push(file);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// If we found images, process them
|
|
92
|
+
if (imageFiles.length > 0) {
|
|
93
|
+
e.preventDefault(); // Prevent default paste behavior
|
|
94
|
+
|
|
95
|
+
if (imageFiles.length + this.attachments.length > this.maxFiles) {
|
|
96
|
+
alert(`Maximum ${this.maxFiles} files allowed`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.processingFiles = true;
|
|
101
|
+
const newAttachments: Attachment[] = [];
|
|
102
|
+
|
|
103
|
+
for (const file of imageFiles) {
|
|
104
|
+
try {
|
|
105
|
+
if (file.size > this.maxFileSize) {
|
|
106
|
+
alert(`Image exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const attachment = await loadAttachment(file);
|
|
111
|
+
newAttachments.push(attachment);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error("Error processing pasted image:", error);
|
|
114
|
+
alert(`Failed to process pasted image: ${String(error)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.attachments = [...this.attachments, ...newAttachments];
|
|
119
|
+
this.onFilesChange?.(this.attachments);
|
|
120
|
+
this.processingFiles = false;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
private handleSend = () => {
|
|
125
|
+
this.onSend?.(this.value, this.attachments);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
private handleAttachmentClick = () => {
|
|
129
|
+
this.fileInputRef.value?.click();
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
private async handleFilesSelected(e: Event) {
|
|
133
|
+
const input = e.target as HTMLInputElement;
|
|
134
|
+
const files = Array.from(input.files || []);
|
|
135
|
+
if (files.length === 0) return;
|
|
136
|
+
|
|
137
|
+
if (files.length + this.attachments.length > this.maxFiles) {
|
|
138
|
+
alert(`Maximum ${this.maxFiles} files allowed`);
|
|
139
|
+
input.value = "";
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.processingFiles = true;
|
|
144
|
+
const newAttachments: Attachment[] = [];
|
|
145
|
+
|
|
146
|
+
for (const file of files) {
|
|
147
|
+
try {
|
|
148
|
+
if (file.size > this.maxFileSize) {
|
|
149
|
+
alert(`${file.name} exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const attachment = await loadAttachment(file);
|
|
154
|
+
newAttachments.push(attachment);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(`Error processing ${file.name}:`, error);
|
|
157
|
+
alert(`Failed to process ${file.name}: ${String(error)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.attachments = [...this.attachments, ...newAttachments];
|
|
162
|
+
this.onFilesChange?.(this.attachments);
|
|
163
|
+
this.processingFiles = false;
|
|
164
|
+
input.value = ""; // Reset input
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private removeFile(fileId: string) {
|
|
168
|
+
this.attachments = this.attachments.filter((f) => f.id !== fileId);
|
|
169
|
+
this.onFilesChange?.(this.attachments);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private handleDragOver = (e: DragEvent) => {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
e.stopPropagation();
|
|
175
|
+
if (!this.isDragging) {
|
|
176
|
+
this.isDragging = true;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
private handleDragLeave = (e: DragEvent) => {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
e.stopPropagation();
|
|
183
|
+
// Only set isDragging to false if we're leaving the entire component
|
|
184
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
185
|
+
const x = e.clientX;
|
|
186
|
+
const y = e.clientY;
|
|
187
|
+
if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
|
|
188
|
+
this.isDragging = false;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
private handleDrop = async (e: DragEvent) => {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
e.stopPropagation();
|
|
195
|
+
this.isDragging = false;
|
|
196
|
+
|
|
197
|
+
const files = Array.from(e.dataTransfer?.files || []);
|
|
198
|
+
if (files.length === 0) return;
|
|
199
|
+
|
|
200
|
+
if (files.length + this.attachments.length > this.maxFiles) {
|
|
201
|
+
alert(`Maximum ${this.maxFiles} files allowed`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.processingFiles = true;
|
|
206
|
+
const newAttachments: Attachment[] = [];
|
|
207
|
+
|
|
208
|
+
for (const file of files) {
|
|
209
|
+
try {
|
|
210
|
+
if (file.size > this.maxFileSize) {
|
|
211
|
+
alert(`${file.name} exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const attachment = await loadAttachment(file);
|
|
216
|
+
newAttachments.push(attachment);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(`Error processing ${file.name}:`, error);
|
|
219
|
+
alert(`Failed to process ${file.name}: ${String(error)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.attachments = [...this.attachments, ...newAttachments];
|
|
224
|
+
this.onFilesChange?.(this.attachments);
|
|
225
|
+
this.processingFiles = false;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
override firstUpdated() {
|
|
229
|
+
const textarea = this.textareaRef.value;
|
|
230
|
+
if (textarea) {
|
|
231
|
+
textarea.focus();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
override render() {
|
|
236
|
+
// Check if current model supports thinking/reasoning
|
|
237
|
+
const model = this.currentModel;
|
|
238
|
+
const supportsThinking = model?.reasoning === true; // Models with reasoning:true support thinking
|
|
239
|
+
|
|
240
|
+
return html`
|
|
241
|
+
<div
|
|
242
|
+
class="bg-card rounded-xl border shadow-sm relative ${this.isDragging ? "border-primary border-2 bg-primary/5" : "border-border"}"
|
|
243
|
+
@dragover=${this.handleDragOver}
|
|
244
|
+
@dragleave=${this.handleDragLeave}
|
|
245
|
+
@drop=${this.handleDrop}
|
|
246
|
+
>
|
|
247
|
+
<!-- Drag overlay -->
|
|
248
|
+
${
|
|
249
|
+
this.isDragging
|
|
250
|
+
? html`
|
|
251
|
+
<div class="absolute inset-0 bg-primary/10 rounded-xl pointer-events-none z-10 flex items-center justify-center">
|
|
252
|
+
<div class="text-primary font-medium">${i18n("Drop files here")}</div>
|
|
253
|
+
</div>
|
|
254
|
+
`
|
|
255
|
+
: ""
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
<!-- Attachments -->
|
|
259
|
+
${
|
|
260
|
+
this.attachments.length > 0
|
|
261
|
+
? html`
|
|
262
|
+
<div class="px-4 pt-3 pb-2 flex flex-wrap gap-2">
|
|
263
|
+
${this.attachments.map(
|
|
264
|
+
(attachment) => html`
|
|
265
|
+
<attachment-tile
|
|
266
|
+
.attachment=${attachment}
|
|
267
|
+
.showDelete=${true}
|
|
268
|
+
.onDelete=${() => this.removeFile(attachment.id)}
|
|
269
|
+
></attachment-tile>
|
|
270
|
+
`,
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
`
|
|
274
|
+
: ""
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
<textarea
|
|
278
|
+
class="w-full bg-transparent p-4 text-foreground placeholder-muted-foreground outline-none resize-none overflow-y-auto"
|
|
279
|
+
placeholder=${i18n("Type a message...")}
|
|
280
|
+
rows="1"
|
|
281
|
+
style="max-height: 200px; field-sizing: content; min-height: 1lh; height: auto;"
|
|
282
|
+
.value=${this.value}
|
|
283
|
+
@input=${this.handleTextareaInput}
|
|
284
|
+
@keydown=${this.handleKeyDown}
|
|
285
|
+
@paste=${this.handlePaste}
|
|
286
|
+
${ref(this.textareaRef)}
|
|
287
|
+
></textarea>
|
|
288
|
+
|
|
289
|
+
<!-- Hidden file input -->
|
|
290
|
+
<input
|
|
291
|
+
type="file"
|
|
292
|
+
${ref(this.fileInputRef)}
|
|
293
|
+
@change=${this.handleFilesSelected}
|
|
294
|
+
accept=${this.acceptedTypes}
|
|
295
|
+
multiple
|
|
296
|
+
style="display: none;"
|
|
297
|
+
/>
|
|
298
|
+
|
|
299
|
+
<!-- Button Row -->
|
|
300
|
+
<div class="px-2 pb-2 flex items-center justify-between">
|
|
301
|
+
<!-- Left side - attachment and thinking selector -->
|
|
302
|
+
<div class="flex gap-2 items-center">
|
|
303
|
+
${
|
|
304
|
+
this.showAttachmentButton
|
|
305
|
+
? this.processingFiles
|
|
306
|
+
? html`
|
|
307
|
+
<div class="h-8 w-8 flex items-center justify-center">
|
|
308
|
+
${icon(Loader2, "sm", "animate-spin text-muted-foreground")}
|
|
309
|
+
</div>
|
|
310
|
+
`
|
|
311
|
+
: html`
|
|
312
|
+
${Button({
|
|
313
|
+
variant: "ghost",
|
|
314
|
+
size: "icon",
|
|
315
|
+
className: "h-8 w-8",
|
|
316
|
+
onClick: this.handleAttachmentClick,
|
|
317
|
+
children: icon(Paperclip, "sm"),
|
|
318
|
+
})}
|
|
319
|
+
`
|
|
320
|
+
: ""
|
|
321
|
+
}
|
|
322
|
+
${
|
|
323
|
+
supportsThinking && this.showThinkingSelector
|
|
324
|
+
? html`
|
|
325
|
+
${Select({
|
|
326
|
+
value: this.thinkingLevel,
|
|
327
|
+
placeholder: i18n("Off"),
|
|
328
|
+
options: [
|
|
329
|
+
{ value: "off", label: i18n("Off"), icon: icon(Brain, "sm") },
|
|
330
|
+
{ value: "minimal", label: i18n("Minimal"), icon: icon(Brain, "sm") },
|
|
331
|
+
{ value: "low", label: i18n("Low"), icon: icon(Brain, "sm") },
|
|
332
|
+
{ value: "medium", label: i18n("Medium"), icon: icon(Brain, "sm") },
|
|
333
|
+
{ value: "high", label: i18n("High"), icon: icon(Brain, "sm") },
|
|
334
|
+
] as SelectOption[],
|
|
335
|
+
onChange: (value: string) => {
|
|
336
|
+
const level = value as "off" | "minimal" | "low" | "medium" | "high";
|
|
337
|
+
this.thinkingLevel = level;
|
|
338
|
+
this.onThinkingChange?.(level);
|
|
339
|
+
},
|
|
340
|
+
width: "80px",
|
|
341
|
+
size: "sm",
|
|
342
|
+
variant: "ghost",
|
|
343
|
+
fitContent: true,
|
|
344
|
+
})}
|
|
345
|
+
`
|
|
346
|
+
: ""
|
|
347
|
+
}
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<!-- Model selector and send on the right -->
|
|
351
|
+
<div class="flex gap-2 items-center">
|
|
352
|
+
${
|
|
353
|
+
this.showModelSelector && this.currentModel
|
|
354
|
+
? html`
|
|
355
|
+
${Button({
|
|
356
|
+
variant: "ghost",
|
|
357
|
+
size: "sm",
|
|
358
|
+
onClick: () => {
|
|
359
|
+
// Focus textarea before opening model selector so focus returns there
|
|
360
|
+
this.textareaRef.value?.focus();
|
|
361
|
+
// Wait for next frame to ensure focus takes effect before dialog captures it
|
|
362
|
+
requestAnimationFrame(() => {
|
|
363
|
+
this.onModelSelect?.();
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
children: html`
|
|
367
|
+
${icon(Sparkles, "sm")}
|
|
368
|
+
<span class="ml-1">${this.currentModel.id}</span>
|
|
369
|
+
`,
|
|
370
|
+
className: "h-8 text-xs truncate",
|
|
371
|
+
})}
|
|
372
|
+
`
|
|
373
|
+
: ""
|
|
374
|
+
}
|
|
375
|
+
${
|
|
376
|
+
this.isStreaming
|
|
377
|
+
? html`
|
|
378
|
+
${Button({
|
|
379
|
+
variant: "ghost",
|
|
380
|
+
size: "icon",
|
|
381
|
+
onClick: this.onAbort,
|
|
382
|
+
children: icon(Square, "sm"),
|
|
383
|
+
className: "h-8 w-8",
|
|
384
|
+
})}
|
|
385
|
+
`
|
|
386
|
+
: html`
|
|
387
|
+
${Button({
|
|
388
|
+
variant: "ghost",
|
|
389
|
+
size: "icon",
|
|
390
|
+
onClick: this.handleSend,
|
|
391
|
+
disabled: (!this.value.trim() && this.attachments.length === 0) || this.processingFiles,
|
|
392
|
+
children: html`<div style="transform: rotate(-45deg)">${icon(Send, "sm")}</div>`,
|
|
393
|
+
className: "h-8 w-8",
|
|
394
|
+
})}
|
|
395
|
+
`
|
|
396
|
+
}
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
`;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { AgentMessage, AgentTool } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import type {
|
|
3
|
+
AssistantMessage as AssistantMessageType,
|
|
4
|
+
ToolResultMessage as ToolResultMessageType,
|
|
5
|
+
} from "@mariozechner/pi-ai";
|
|
6
|
+
import { html, LitElement, type TemplateResult } from "lit";
|
|
7
|
+
import { property } from "lit/decorators.js";
|
|
8
|
+
import { repeat } from "lit/directives/repeat.js";
|
|
9
|
+
import { renderMessage } from "./message-renderer-registry.js";
|
|
10
|
+
|
|
11
|
+
export class MessageList extends LitElement {
|
|
12
|
+
@property({ type: Array }) messages: AgentMessage[] = [];
|
|
13
|
+
@property({ type: Array }) tools: AgentTool[] = [];
|
|
14
|
+
@property({ type: Object }) pendingToolCalls?: Set<string>;
|
|
15
|
+
@property({ type: Boolean }) isStreaming: boolean = false;
|
|
16
|
+
@property({ attribute: false }) onCostClick?: () => void;
|
|
17
|
+
|
|
18
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override connectedCallback(): void {
|
|
23
|
+
super.connectedCallback();
|
|
24
|
+
this.style.display = "block";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private buildRenderItems() {
|
|
28
|
+
// Map tool results by call id for quick lookup
|
|
29
|
+
const resultByCallId = new Map<string, ToolResultMessageType>();
|
|
30
|
+
for (const message of this.messages) {
|
|
31
|
+
if (message.role === "toolResult") {
|
|
32
|
+
resultByCallId.set(message.toolCallId, message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const items: Array<{ key: string; template: TemplateResult }> = [];
|
|
37
|
+
let index = 0;
|
|
38
|
+
for (const msg of this.messages) {
|
|
39
|
+
// Skip artifact messages - they're for session persistence only, not UI display
|
|
40
|
+
if (msg.role === "artifact") {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Try custom renderer first
|
|
45
|
+
const customTemplate = renderMessage(msg);
|
|
46
|
+
if (customTemplate) {
|
|
47
|
+
items.push({ key: `msg:${index}`, template: customTemplate });
|
|
48
|
+
index++;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fall back to built-in renderers
|
|
53
|
+
if (msg.role === "user" || msg.role === "user-with-attachments") {
|
|
54
|
+
items.push({
|
|
55
|
+
key: `msg:${index}`,
|
|
56
|
+
template: html`<user-message .message=${msg}></user-message>`,
|
|
57
|
+
});
|
|
58
|
+
index++;
|
|
59
|
+
} else if (msg.role === "assistant") {
|
|
60
|
+
const amsg = msg as AssistantMessageType;
|
|
61
|
+
items.push({
|
|
62
|
+
key: `msg:${index}`,
|
|
63
|
+
template: html`<assistant-message
|
|
64
|
+
.message=${amsg}
|
|
65
|
+
.tools=${this.tools}
|
|
66
|
+
.isStreaming=${false}
|
|
67
|
+
.pendingToolCalls=${this.pendingToolCalls}
|
|
68
|
+
.toolResultsById=${resultByCallId}
|
|
69
|
+
.hideToolCalls=${false}
|
|
70
|
+
.hidePendingToolCalls=${this.isStreaming}
|
|
71
|
+
.onCostClick=${this.onCostClick}
|
|
72
|
+
></assistant-message>`,
|
|
73
|
+
});
|
|
74
|
+
index++;
|
|
75
|
+
} else {
|
|
76
|
+
// Skip standalone toolResult messages; they are rendered via paired tool-message above
|
|
77
|
+
// Skip unknown roles
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return items;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override render() {
|
|
84
|
+
const items = this.buildRenderItems();
|
|
85
|
+
return html`<div class="flex flex-col gap-3">
|
|
86
|
+
${repeat(
|
|
87
|
+
items,
|
|
88
|
+
(it) => it.key,
|
|
89
|
+
(it) => it.template,
|
|
90
|
+
)}
|
|
91
|
+
</div>`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Register custom element
|
|
96
|
+
if (!customElements.get("message-list")) {
|
|
97
|
+
customElements.define("message-list", MessageList);
|
|
98
|
+
}
|