@mariozechner/pi-web-ui 0.5.44
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/README.md +252 -0
- package/dist/ChatPanel.d.ts +23 -0
- package/dist/ChatPanel.d.ts.map +1 -0
- package/dist/ChatPanel.js +224 -0
- package/dist/ChatPanel.js.map +1 -0
- package/dist/app.css +2 -0
- package/dist/components/AgentInterface.d.ts +35 -0
- package/dist/components/AgentInterface.d.ts.map +1 -0
- package/dist/components/AgentInterface.js +308 -0
- package/dist/components/AgentInterface.js.map +1 -0
- package/dist/components/AttachmentTile.d.ts +12 -0
- package/dist/components/AttachmentTile.d.ts.map +1 -0
- package/dist/components/AttachmentTile.js +114 -0
- package/dist/components/AttachmentTile.js.map +1 -0
- package/dist/components/ConsoleBlock.d.ts +11 -0
- package/dist/components/ConsoleBlock.d.ts.map +1 -0
- package/dist/components/ConsoleBlock.js +77 -0
- package/dist/components/ConsoleBlock.js.map +1 -0
- package/dist/components/Input.d.ts +26 -0
- package/dist/components/Input.d.ts.map +1 -0
- package/dist/components/Input.js +56 -0
- package/dist/components/Input.js.map +1 -0
- package/dist/components/MessageEditor.d.ts +38 -0
- package/dist/components/MessageEditor.d.ts.map +1 -0
- package/dist/components/MessageEditor.js +296 -0
- package/dist/components/MessageEditor.js.map +1 -0
- package/dist/components/MessageList.d.ts +13 -0
- package/dist/components/MessageList.d.ts.map +1 -0
- package/dist/components/MessageList.js +88 -0
- package/dist/components/MessageList.js.map +1 -0
- package/dist/components/Messages.d.ts +53 -0
- package/dist/components/Messages.d.ts.map +1 -0
- package/dist/components/Messages.js +323 -0
- package/dist/components/Messages.js.map +1 -0
- package/dist/components/ProviderKeyInput.d.ts +16 -0
- package/dist/components/ProviderKeyInput.d.ts.map +1 -0
- package/dist/components/ProviderKeyInput.js +183 -0
- package/dist/components/ProviderKeyInput.js.map +1 -0
- package/dist/components/SandboxedIframe.d.ts +63 -0
- package/dist/components/SandboxedIframe.d.ts.map +1 -0
- package/dist/components/SandboxedIframe.js +435 -0
- package/dist/components/SandboxedIframe.js.map +1 -0
- package/dist/components/StreamingMessageContainer.d.ts +17 -0
- package/dist/components/StreamingMessageContainer.d.ts.map +1 -0
- package/dist/components/StreamingMessageContainer.js +114 -0
- package/dist/components/StreamingMessageContainer.js.map +1 -0
- package/dist/dialogs/ApiKeyPromptDialog.d.ts +15 -0
- package/dist/dialogs/ApiKeyPromptDialog.d.ts.map +1 -0
- package/dist/dialogs/ApiKeyPromptDialog.js +80 -0
- package/dist/dialogs/ApiKeyPromptDialog.js.map +1 -0
- package/dist/dialogs/AttachmentOverlay.d.ts +32 -0
- package/dist/dialogs/AttachmentOverlay.d.ts.map +1 -0
- package/dist/dialogs/AttachmentOverlay.js +575 -0
- package/dist/dialogs/AttachmentOverlay.js.map +1 -0
- package/dist/dialogs/ModelSelector.d.ts +27 -0
- package/dist/dialogs/ModelSelector.d.ts.map +1 -0
- package/dist/dialogs/ModelSelector.js +334 -0
- package/dist/dialogs/ModelSelector.js.map +1 -0
- package/dist/dialogs/SettingsDialog.d.ts +31 -0
- package/dist/dialogs/SettingsDialog.d.ts.map +1 -0
- package/dist/dialogs/SettingsDialog.js +228 -0
- package/dist/dialogs/SettingsDialog.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/state/agent-session.d.ts +58 -0
- package/dist/state/agent-session.d.ts.map +1 -0
- package/dist/state/agent-session.js +252 -0
- package/dist/state/agent-session.js.map +1 -0
- package/dist/state/transports/AppTransport.d.ts +13 -0
- package/dist/state/transports/AppTransport.d.ts.map +1 -0
- package/dist/state/transports/AppTransport.js +316 -0
- package/dist/state/transports/AppTransport.js.map +1 -0
- package/dist/state/transports/ProviderTransport.d.ts +12 -0
- package/dist/state/transports/ProviderTransport.d.ts.map +1 -0
- package/dist/state/transports/ProviderTransport.js +44 -0
- package/dist/state/transports/ProviderTransport.js.map +1 -0
- package/dist/state/transports/index.d.ts +4 -0
- package/dist/state/transports/index.d.ts.map +1 -0
- package/dist/state/transports/index.js +4 -0
- package/dist/state/transports/index.js.map +1 -0
- package/dist/state/transports/proxy-types.d.ts +48 -0
- package/dist/state/transports/proxy-types.d.ts.map +1 -0
- package/dist/state/transports/proxy-types.js +2 -0
- package/dist/state/transports/proxy-types.js.map +1 -0
- package/dist/state/transports/types.d.ts +11 -0
- package/dist/state/transports/types.d.ts.map +1 -0
- package/dist/state/transports/types.js +2 -0
- package/dist/state/transports/types.js.map +1 -0
- package/dist/state/types.d.ts +15 -0
- package/dist/state/types.d.ts.map +1 -0
- package/dist/state/types.js +2 -0
- package/dist/state/types.js.map +1 -0
- package/dist/storage/app-storage.d.ts +26 -0
- package/dist/storage/app-storage.d.ts.map +1 -0
- package/dist/storage/app-storage.js +44 -0
- package/dist/storage/app-storage.js.map +1 -0
- package/dist/storage/backends/chrome-storage-backend.d.ts +18 -0
- package/dist/storage/backends/chrome-storage-backend.d.ts.map +1 -0
- package/dist/storage/backends/chrome-storage-backend.js +67 -0
- package/dist/storage/backends/chrome-storage-backend.js.map +1 -0
- package/dist/storage/backends/indexeddb-backend.d.ts +20 -0
- package/dist/storage/backends/indexeddb-backend.d.ts.map +1 -0
- package/dist/storage/backends/indexeddb-backend.js +89 -0
- package/dist/storage/backends/indexeddb-backend.js.map +1 -0
- package/dist/storage/backends/local-storage-backend.d.ts +18 -0
- package/dist/storage/backends/local-storage-backend.d.ts.map +1 -0
- package/dist/storage/backends/local-storage-backend.js +69 -0
- package/dist/storage/backends/local-storage-backend.js.map +1 -0
- package/dist/storage/repositories/provider-keys-repository.d.ts +34 -0
- package/dist/storage/repositories/provider-keys-repository.d.ts.map +1 -0
- package/dist/storage/repositories/provider-keys-repository.js +50 -0
- package/dist/storage/repositories/provider-keys-repository.js.map +1 -0
- package/dist/storage/repositories/settings-repository.d.ts +34 -0
- package/dist/storage/repositories/settings-repository.d.ts.map +1 -0
- package/dist/storage/repositories/settings-repository.js +46 -0
- package/dist/storage/repositories/settings-repository.js.map +1 -0
- package/dist/storage/types.d.ts +43 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/tools/artifacts/ArtifactElement.d.ts +10 -0
- package/dist/tools/artifacts/ArtifactElement.d.ts.map +1 -0
- package/dist/tools/artifacts/ArtifactElement.js +12 -0
- package/dist/tools/artifacts/ArtifactElement.js.map +1 -0
- package/dist/tools/artifacts/HtmlArtifact.d.ts +30 -0
- package/dist/tools/artifacts/HtmlArtifact.d.ts.map +1 -0
- package/dist/tools/artifacts/HtmlArtifact.js +217 -0
- package/dist/tools/artifacts/HtmlArtifact.js.map +1 -0
- package/dist/tools/artifacts/MarkdownArtifact.d.ts +20 -0
- package/dist/tools/artifacts/MarkdownArtifact.d.ts.map +1 -0
- package/dist/tools/artifacts/MarkdownArtifact.js +84 -0
- package/dist/tools/artifacts/MarkdownArtifact.js.map +1 -0
- package/dist/tools/artifacts/SvgArtifact.d.ts +19 -0
- package/dist/tools/artifacts/SvgArtifact.d.ts.map +1 -0
- package/dist/tools/artifacts/SvgArtifact.js +80 -0
- package/dist/tools/artifacts/SvgArtifact.js.map +1 -0
- package/dist/tools/artifacts/TextArtifact.d.ts +20 -0
- package/dist/tools/artifacts/TextArtifact.d.ts.map +1 -0
- package/dist/tools/artifacts/TextArtifact.js +147 -0
- package/dist/tools/artifacts/TextArtifact.js.map +1 -0
- package/dist/tools/artifacts/artifacts.d.ts +67 -0
- package/dist/tools/artifacts/artifacts.d.ts.map +1 -0
- package/dist/tools/artifacts/artifacts.js +836 -0
- package/dist/tools/artifacts/artifacts.js.map +1 -0
- package/dist/tools/artifacts/index.d.ts +7 -0
- package/dist/tools/artifacts/index.d.ts.map +1 -0
- package/dist/tools/artifacts/index.js +7 -0
- package/dist/tools/artifacts/index.js.map +1 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +29 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/javascript-repl.d.ts +43 -0
- package/dist/tools/javascript-repl.d.ts.map +1 -0
- package/dist/tools/javascript-repl.js +252 -0
- package/dist/tools/javascript-repl.js.map +1 -0
- package/dist/tools/renderer-registry.d.ts +11 -0
- package/dist/tools/renderer-registry.d.ts.map +1 -0
- package/dist/tools/renderer-registry.js +15 -0
- package/dist/tools/renderer-registry.js.map +1 -0
- package/dist/tools/renderers/BashRenderer.d.ts +12 -0
- package/dist/tools/renderers/BashRenderer.d.ts.map +1 -0
- package/dist/tools/renderers/BashRenderer.js +35 -0
- package/dist/tools/renderers/BashRenderer.js.map +1 -0
- package/dist/tools/renderers/CalculateRenderer.d.ts +12 -0
- package/dist/tools/renderers/CalculateRenderer.d.ts.map +1 -0
- package/dist/tools/renderers/CalculateRenderer.js +38 -0
- package/dist/tools/renderers/CalculateRenderer.js.map +1 -0
- package/dist/tools/renderers/DefaultRenderer.d.ts +8 -0
- package/dist/tools/renderers/DefaultRenderer.d.ts.map +1 -0
- package/dist/tools/renderers/DefaultRenderer.js +31 -0
- package/dist/tools/renderers/DefaultRenderer.js.map +1 -0
- package/dist/tools/renderers/GetCurrentTimeRenderer.d.ts +12 -0
- package/dist/tools/renderers/GetCurrentTimeRenderer.d.ts.map +1 -0
- package/dist/tools/renderers/GetCurrentTimeRenderer.js +30 -0
- package/dist/tools/renderers/GetCurrentTimeRenderer.js.map +1 -0
- package/dist/tools/types.d.ts +7 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/utils/attachment-utils.d.ts +19 -0
- package/dist/utils/attachment-utils.d.ts.map +1 -0
- package/dist/utils/attachment-utils.js +415 -0
- package/dist/utils/attachment-utils.js.map +1 -0
- package/dist/utils/auth-token.d.ts +3 -0
- package/dist/utils/auth-token.d.ts.map +1 -0
- package/dist/utils/auth-token.js +19 -0
- package/dist/utils/auth-token.js.map +1 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +47 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/i18n.d.ts +111 -0
- package/dist/utils/i18n.d.ts.map +1 -0
- package/dist/utils/i18n.js +224 -0
- package/dist/utils/i18n.js.map +1 -0
- package/dist/utils/test-sessions.d.ts +347 -0
- package/dist/utils/test-sessions.d.ts.map +1 -0
- package/dist/utils/test-sessions.js +2215 -0
- package/dist/utils/test-sessions.js.map +1 -0
- package/example/README.md +61 -0
- package/example/index.html +13 -0
- package/example/package-lock.json +1965 -0
- package/example/package.json +22 -0
- package/example/src/app.css +1 -0
- package/example/src/main.ts +57 -0
- package/example/src/test-sessions.ts +104 -0
- package/example/tsconfig.json +15 -0
- package/example/vite.config.ts +6 -0
- package/package.json +45 -0
- package/src/ChatPanel.ts +214 -0
- package/src/app.css +44 -0
- package/src/components/AgentInterface.ts +316 -0
- package/src/components/AttachmentTile.ts +112 -0
- package/src/components/ConsoleBlock.ts +67 -0
- package/src/components/Input.ts +112 -0
- package/src/components/MessageEditor.ts +272 -0
- package/src/components/MessageList.ts +82 -0
- package/src/components/Messages.ts +310 -0
- package/src/components/ProviderKeyInput.ts +170 -0
- package/src/components/SandboxedIframe.ts +525 -0
- package/src/components/StreamingMessageContainer.ts +101 -0
- package/src/dialogs/ApiKeyPromptDialog.ts +76 -0
- package/src/dialogs/AttachmentOverlay.ts +635 -0
- package/src/dialogs/ModelSelector.ts +324 -0
- package/src/dialogs/SettingsDialog.ts +223 -0
- package/src/index.ts +63 -0
- package/src/state/agent-session.ts +311 -0
- package/src/state/transports/AppTransport.ts +363 -0
- package/src/state/transports/ProviderTransport.ts +49 -0
- package/src/state/transports/index.ts +3 -0
- package/src/state/transports/proxy-types.ts +15 -0
- package/src/state/transports/types.ts +16 -0
- package/src/state/types.ts +11 -0
- package/src/storage/app-storage.ts +53 -0
- package/src/storage/backends/chrome-storage-backend.ts +82 -0
- package/src/storage/backends/indexeddb-backend.ts +107 -0
- package/src/storage/backends/local-storage-backend.ts +74 -0
- package/src/storage/repositories/provider-keys-repository.ts +55 -0
- package/src/storage/repositories/settings-repository.ts +51 -0
- package/src/storage/types.ts +48 -0
- package/src/tools/artifacts/ArtifactElement.ts +15 -0
- package/src/tools/artifacts/HtmlArtifact.ts +221 -0
- package/src/tools/artifacts/MarkdownArtifact.ts +81 -0
- package/src/tools/artifacts/SvgArtifact.ts +77 -0
- package/src/tools/artifacts/TextArtifact.ts +148 -0
- package/src/tools/artifacts/artifacts.ts +888 -0
- package/src/tools/artifacts/index.ts +6 -0
- package/src/tools/index.ts +35 -0
- package/src/tools/javascript-repl.ts +309 -0
- package/src/tools/renderer-registry.ts +18 -0
- package/src/tools/renderers/BashRenderer.ts +45 -0
- package/src/tools/renderers/CalculateRenderer.ts +49 -0
- package/src/tools/renderers/DefaultRenderer.ts +36 -0
- package/src/tools/renderers/GetCurrentTimeRenderer.ts +39 -0
- package/src/tools/types.ts +7 -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 +343 -0
- package/src/utils/test-sessions.ts +2247 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { html } from "@mariozechner/mini-lit";
|
|
2
|
+
import type { ToolResultMessage, Usage } from "@mariozechner/pi-ai";
|
|
3
|
+
import { LitElement } from "lit";
|
|
4
|
+
import { customElement, property, query } from "lit/decorators.js";
|
|
5
|
+
import { ModelSelector } from "../dialogs/ModelSelector.js";
|
|
6
|
+
import type { MessageEditor } from "./MessageEditor.js";
|
|
7
|
+
import "./MessageEditor.js";
|
|
8
|
+
import "./MessageList.js";
|
|
9
|
+
import "./Messages.js"; // Import for side effects to register the custom elements
|
|
10
|
+
import type { AgentSession, AgentSessionEvent } from "../state/agent-session.js";
|
|
11
|
+
import { getAppStorage } from "../storage/app-storage.js";
|
|
12
|
+
import "./StreamingMessageContainer.js";
|
|
13
|
+
import type { Attachment } from "../utils/attachment-utils.js";
|
|
14
|
+
import { formatUsage } from "../utils/format.js";
|
|
15
|
+
import { i18n } from "../utils/i18n.js";
|
|
16
|
+
import type { StreamingMessageContainer } from "./StreamingMessageContainer.js";
|
|
17
|
+
|
|
18
|
+
@customElement("agent-interface")
|
|
19
|
+
export class AgentInterface extends LitElement {
|
|
20
|
+
// Optional external session: when provided, this component becomes a view over the session
|
|
21
|
+
@property({ attribute: false }) session?: AgentSession;
|
|
22
|
+
@property() enableAttachments = true;
|
|
23
|
+
@property() enableModelSelector = true;
|
|
24
|
+
@property() enableThinking = true;
|
|
25
|
+
@property() showThemeToggle = false;
|
|
26
|
+
@property() showDebugToggle = false;
|
|
27
|
+
// Optional custom API key prompt handler - if not provided, uses default dialog
|
|
28
|
+
@property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise<boolean>;
|
|
29
|
+
|
|
30
|
+
// References
|
|
31
|
+
@query("message-editor") private _messageEditor!: MessageEditor;
|
|
32
|
+
@query("streaming-message-container") private _streamingContainer!: StreamingMessageContainer;
|
|
33
|
+
|
|
34
|
+
private _autoScroll = true;
|
|
35
|
+
private _lastScrollTop = 0;
|
|
36
|
+
private _lastClientHeight = 0;
|
|
37
|
+
private _scrollContainer?: HTMLElement;
|
|
38
|
+
private _resizeObserver?: ResizeObserver;
|
|
39
|
+
private _unsubscribeSession?: () => void;
|
|
40
|
+
|
|
41
|
+
public setInput(text: string, attachments?: Attachment[]) {
|
|
42
|
+
const update = () => {
|
|
43
|
+
if (!this._messageEditor) requestAnimationFrame(update);
|
|
44
|
+
else {
|
|
45
|
+
this._messageEditor.value = text;
|
|
46
|
+
this._messageEditor.attachments = attachments || [];
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
update();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override async connectedCallback() {
|
|
57
|
+
super.connectedCallback();
|
|
58
|
+
|
|
59
|
+
this.style.display = "flex";
|
|
60
|
+
this.style.flexDirection = "column";
|
|
61
|
+
this.style.height = "100%";
|
|
62
|
+
this.style.minHeight = "0";
|
|
63
|
+
|
|
64
|
+
// Wait for first render to get scroll container
|
|
65
|
+
await this.updateComplete;
|
|
66
|
+
this._scrollContainer = this.querySelector(".overflow-y-auto") as HTMLElement;
|
|
67
|
+
|
|
68
|
+
if (this._scrollContainer) {
|
|
69
|
+
// Set up ResizeObserver to detect content changes
|
|
70
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
71
|
+
if (this._autoScroll && this._scrollContainer) {
|
|
72
|
+
this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Observe the content container inside the scroll container
|
|
77
|
+
const contentContainer = this._scrollContainer.querySelector(".max-w-3xl");
|
|
78
|
+
if (contentContainer) {
|
|
79
|
+
this._resizeObserver.observe(contentContainer);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Set up scroll listener with better detection
|
|
83
|
+
this._scrollContainer.addEventListener("scroll", this._handleScroll);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Subscribe to external session if provided
|
|
87
|
+
this.setupSessionSubscription();
|
|
88
|
+
|
|
89
|
+
// Attach debug listener if session provided
|
|
90
|
+
if (this.session) {
|
|
91
|
+
this.session = this.session; // explicitly set to trigger subscription
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override disconnectedCallback() {
|
|
96
|
+
super.disconnectedCallback();
|
|
97
|
+
|
|
98
|
+
// Clean up observers and listeners
|
|
99
|
+
if (this._resizeObserver) {
|
|
100
|
+
this._resizeObserver.disconnect();
|
|
101
|
+
this._resizeObserver = undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this._scrollContainer) {
|
|
105
|
+
this._scrollContainer.removeEventListener("scroll", this._handleScroll);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this._unsubscribeSession) {
|
|
109
|
+
this._unsubscribeSession();
|
|
110
|
+
this._unsubscribeSession = undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private setupSessionSubscription() {
|
|
115
|
+
if (this._unsubscribeSession) {
|
|
116
|
+
this._unsubscribeSession();
|
|
117
|
+
this._unsubscribeSession = undefined;
|
|
118
|
+
}
|
|
119
|
+
if (!this.session) return;
|
|
120
|
+
this._unsubscribeSession = this.session.subscribe(async (ev: AgentSessionEvent) => {
|
|
121
|
+
if (ev.type === "state-update") {
|
|
122
|
+
if (this._streamingContainer) {
|
|
123
|
+
this._streamingContainer.isStreaming = ev.state.isStreaming;
|
|
124
|
+
this._streamingContainer.setMessage(ev.state.streamMessage, !ev.state.isStreaming);
|
|
125
|
+
}
|
|
126
|
+
this.requestUpdate();
|
|
127
|
+
} else if (ev.type === "error-no-model") {
|
|
128
|
+
// TODO show some UI feedback
|
|
129
|
+
} else if (ev.type === "error-no-api-key") {
|
|
130
|
+
// Handled by onApiKeyRequired callback
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private _handleScroll = (_ev: any) => {
|
|
136
|
+
if (!this._scrollContainer) return;
|
|
137
|
+
|
|
138
|
+
const currentScrollTop = this._scrollContainer.scrollTop;
|
|
139
|
+
const scrollHeight = this._scrollContainer.scrollHeight;
|
|
140
|
+
const clientHeight = this._scrollContainer.clientHeight;
|
|
141
|
+
const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight;
|
|
142
|
+
|
|
143
|
+
// Ignore relayout due to message editor getting pushed up by stats
|
|
144
|
+
if (clientHeight < this._lastClientHeight) {
|
|
145
|
+
this._lastClientHeight = clientHeight;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Only disable auto-scroll if user scrolled UP or is far from bottom
|
|
150
|
+
if (currentScrollTop !== 0 && currentScrollTop < this._lastScrollTop && distanceFromBottom > 50) {
|
|
151
|
+
this._autoScroll = false;
|
|
152
|
+
} else if (distanceFromBottom < 10) {
|
|
153
|
+
// Re-enable if very close to bottom
|
|
154
|
+
this._autoScroll = true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this._lastScrollTop = currentScrollTop;
|
|
158
|
+
this._lastClientHeight = clientHeight;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
public async sendMessage(input: string, attachments?: Attachment[]) {
|
|
162
|
+
if ((!input.trim() && attachments?.length === 0) || this.session?.state.isStreaming) return;
|
|
163
|
+
const session = this.session;
|
|
164
|
+
if (!session) throw new Error("No session set on AgentInterface");
|
|
165
|
+
if (!session.state.model) throw new Error("No model set on AgentInterface");
|
|
166
|
+
|
|
167
|
+
// Check if API key exists for the provider (only needed in direct mode)
|
|
168
|
+
const provider = session.state.model.provider;
|
|
169
|
+
const apiKey = await getAppStorage().providerKeys.getKey(provider);
|
|
170
|
+
|
|
171
|
+
// If no API key, prompt for it
|
|
172
|
+
if (!apiKey) {
|
|
173
|
+
if (!this.onApiKeyRequired) {
|
|
174
|
+
console.error("No API key configured and no onApiKeyRequired handler set");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const success = await this.onApiKeyRequired(provider);
|
|
179
|
+
|
|
180
|
+
// If still no API key, abort the send
|
|
181
|
+
if (!success) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Only clear editor after we know we can send
|
|
187
|
+
this._messageEditor.value = "";
|
|
188
|
+
this._messageEditor.attachments = [];
|
|
189
|
+
this._autoScroll = true; // Enable auto-scroll when sending a message
|
|
190
|
+
|
|
191
|
+
await this.session?.prompt(input, attachments);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private renderMessages() {
|
|
195
|
+
if (!this.session)
|
|
196
|
+
return html`<div class="p-4 text-center text-muted-foreground">${i18n("No session available")}</div>`;
|
|
197
|
+
const state = this.session.state;
|
|
198
|
+
// Build a map of tool results to allow inline rendering in assistant messages
|
|
199
|
+
const toolResultsById = new Map<string, ToolResultMessage<any>>();
|
|
200
|
+
for (const message of state.messages) {
|
|
201
|
+
if (message.role === "toolResult") {
|
|
202
|
+
toolResultsById.set(message.toolCallId, message);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return html`
|
|
206
|
+
<div class="flex flex-col gap-3">
|
|
207
|
+
<!-- Stable messages list - won't re-render during streaming -->
|
|
208
|
+
<message-list
|
|
209
|
+
.messages=${this.session.state.messages}
|
|
210
|
+
.tools=${state.tools}
|
|
211
|
+
.pendingToolCalls=${this.session ? this.session.state.pendingToolCalls : new Set<string>()}
|
|
212
|
+
.isStreaming=${state.isStreaming}
|
|
213
|
+
></message-list>
|
|
214
|
+
|
|
215
|
+
<!-- Streaming message container - manages its own updates -->
|
|
216
|
+
<streaming-message-container
|
|
217
|
+
class="${state.isStreaming ? "" : "hidden"}"
|
|
218
|
+
.tools=${state.tools}
|
|
219
|
+
.isStreaming=${state.isStreaming}
|
|
220
|
+
.pendingToolCalls=${state.pendingToolCalls}
|
|
221
|
+
.toolResultsById=${toolResultsById}
|
|
222
|
+
></streaming-message-container>
|
|
223
|
+
</div>
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private renderStats() {
|
|
228
|
+
if (!this.session) return html`<div class="text-xs h-5"></div>`;
|
|
229
|
+
|
|
230
|
+
const state = this.session.state;
|
|
231
|
+
const totals = state.messages
|
|
232
|
+
.filter((m) => m.role === "assistant")
|
|
233
|
+
.reduce(
|
|
234
|
+
(acc, msg: any) => {
|
|
235
|
+
const usage = msg.usage;
|
|
236
|
+
if (usage) {
|
|
237
|
+
acc.input += usage.input;
|
|
238
|
+
acc.output += usage.output;
|
|
239
|
+
acc.cacheRead += usage.cacheRead;
|
|
240
|
+
acc.cacheWrite += usage.cacheWrite;
|
|
241
|
+
acc.cost.total += usage.cost.total;
|
|
242
|
+
}
|
|
243
|
+
return acc;
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
input: 0,
|
|
247
|
+
output: 0,
|
|
248
|
+
cacheRead: 0,
|
|
249
|
+
cacheWrite: 0,
|
|
250
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
251
|
+
} satisfies Usage,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const hasTotals = totals.input || totals.output || totals.cacheRead || totals.cacheWrite;
|
|
255
|
+
const totalsText = hasTotals ? formatUsage(totals) : "";
|
|
256
|
+
|
|
257
|
+
return html`
|
|
258
|
+
<div class="text-xs text-muted-foreground flex justify-between items-center h-5">
|
|
259
|
+
<div class="flex items-center gap-1">
|
|
260
|
+
${this.showThemeToggle ? html`<theme-toggle></theme-toggle>` : html``}
|
|
261
|
+
</div>
|
|
262
|
+
<div class="flex ml-auto items-center gap-3">${totalsText ? html`<span>${totalsText}</span>` : ""}</div>
|
|
263
|
+
</div>
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
override render() {
|
|
268
|
+
if (!this.session)
|
|
269
|
+
return html`<div class="p-4 text-center text-muted-foreground">${i18n("No session set")}</div>`;
|
|
270
|
+
|
|
271
|
+
const session = this.session;
|
|
272
|
+
const state = this.session.state;
|
|
273
|
+
return html`
|
|
274
|
+
<div class="flex flex-col h-full bg-background text-foreground">
|
|
275
|
+
<!-- Messages Area -->
|
|
276
|
+
<div class="flex-1 overflow-y-auto">
|
|
277
|
+
<div class="max-w-3xl mx-auto p-4 pb-0">${this.renderMessages()}</div>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<!-- Input Area -->
|
|
281
|
+
<div class="shrink-0">
|
|
282
|
+
<div class="max-w-3xl mx-auto px-2">
|
|
283
|
+
<message-editor
|
|
284
|
+
.isStreaming=${state.isStreaming}
|
|
285
|
+
.currentModel=${state.model}
|
|
286
|
+
.thinkingLevel=${state.thinkingLevel}
|
|
287
|
+
.showAttachmentButton=${this.enableAttachments}
|
|
288
|
+
.showModelSelector=${this.enableModelSelector}
|
|
289
|
+
.showThinking=${this.enableThinking}
|
|
290
|
+
.onSend=${(input: string, attachments: Attachment[]) => {
|
|
291
|
+
this.sendMessage(input, attachments);
|
|
292
|
+
}}
|
|
293
|
+
.onAbort=${() => session.abort()}
|
|
294
|
+
.onModelSelect=${() => {
|
|
295
|
+
ModelSelector.open(state.model, (model) => session.setModel(model));
|
|
296
|
+
}}
|
|
297
|
+
.onThinkingChange=${
|
|
298
|
+
this.enableThinking
|
|
299
|
+
? (level: "off" | "minimal" | "low" | "medium" | "high") => {
|
|
300
|
+
session.setThinkingLevel(level);
|
|
301
|
+
}
|
|
302
|
+
: undefined
|
|
303
|
+
}
|
|
304
|
+
></message-editor>
|
|
305
|
+
${this.renderStats()}
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Register custom element with guard
|
|
314
|
+
if (!customElements.get("agent-interface")) {
|
|
315
|
+
customElements.define("agent-interface", AgentInterface);
|
|
316
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { html, icon } from "@mariozechner/mini-lit";
|
|
2
|
+
import { LitElement } from "lit";
|
|
3
|
+
import { customElement, property } from "lit/decorators.js";
|
|
4
|
+
import { FileSpreadsheet, FileText, X } from "lucide";
|
|
5
|
+
import { AttachmentOverlay } from "../dialogs/AttachmentOverlay.js";
|
|
6
|
+
import type { Attachment } from "../utils/attachment-utils.js";
|
|
7
|
+
import { i18n } from "../utils/i18n.js";
|
|
8
|
+
|
|
9
|
+
@customElement("attachment-tile")
|
|
10
|
+
export class AttachmentTile extends LitElement {
|
|
11
|
+
@property({ type: Object }) attachment!: Attachment;
|
|
12
|
+
@property({ type: Boolean }) showDelete = false;
|
|
13
|
+
@property() onDelete?: () => void;
|
|
14
|
+
|
|
15
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override connectedCallback(): void {
|
|
20
|
+
super.connectedCallback();
|
|
21
|
+
this.style.display = "block";
|
|
22
|
+
this.classList.add("max-h-16");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private handleClick = () => {
|
|
26
|
+
AttachmentOverlay.open(this.attachment);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
override render() {
|
|
30
|
+
const hasPreview = !!this.attachment.preview;
|
|
31
|
+
const isImage = this.attachment.type === "image";
|
|
32
|
+
const isPdf = this.attachment.mimeType === "application/pdf";
|
|
33
|
+
const isDocx =
|
|
34
|
+
this.attachment.mimeType?.includes("wordprocessingml") ||
|
|
35
|
+
this.attachment.fileName.toLowerCase().endsWith(".docx");
|
|
36
|
+
const isPptx =
|
|
37
|
+
this.attachment.mimeType?.includes("presentationml") ||
|
|
38
|
+
this.attachment.fileName.toLowerCase().endsWith(".pptx");
|
|
39
|
+
const isExcel =
|
|
40
|
+
this.attachment.mimeType?.includes("spreadsheetml") ||
|
|
41
|
+
this.attachment.fileName.toLowerCase().endsWith(".xlsx") ||
|
|
42
|
+
this.attachment.fileName.toLowerCase().endsWith(".xls");
|
|
43
|
+
|
|
44
|
+
// Choose the appropriate icon
|
|
45
|
+
const getDocumentIcon = () => {
|
|
46
|
+
if (isExcel) return icon(FileSpreadsheet, "md");
|
|
47
|
+
return icon(FileText, "md");
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return html`
|
|
51
|
+
<div class="relative group inline-block">
|
|
52
|
+
${
|
|
53
|
+
hasPreview
|
|
54
|
+
? html`
|
|
55
|
+
<div class="relative">
|
|
56
|
+
<img
|
|
57
|
+
src="data:${isImage ? this.attachment.mimeType : "image/png"};base64,${this.attachment.preview}"
|
|
58
|
+
class="w-16 h-16 object-cover rounded-lg border border-input cursor-pointer hover:opacity-80 transition-opacity"
|
|
59
|
+
alt="${this.attachment.fileName}"
|
|
60
|
+
title="${this.attachment.fileName}"
|
|
61
|
+
@click=${this.handleClick}
|
|
62
|
+
/>
|
|
63
|
+
${
|
|
64
|
+
isPdf
|
|
65
|
+
? html`
|
|
66
|
+
<!-- PDF badge overlay -->
|
|
67
|
+
<div class="absolute bottom-0 left-0 right-0 bg-background/90 px-1 py-0.5 rounded-b-lg">
|
|
68
|
+
<div class="text-[10px] text-muted-foreground text-center font-medium">${i18n("PDF")}</div>
|
|
69
|
+
</div>
|
|
70
|
+
`
|
|
71
|
+
: ""
|
|
72
|
+
}
|
|
73
|
+
</div>
|
|
74
|
+
`
|
|
75
|
+
: html`
|
|
76
|
+
<!-- Fallback: document icon + filename -->
|
|
77
|
+
<div
|
|
78
|
+
class="w-16 h-16 rounded-lg border border-input cursor-pointer hover:opacity-80 transition-opacity bg-muted text-muted-foreground flex flex-col items-center justify-center p-2"
|
|
79
|
+
@click=${this.handleClick}
|
|
80
|
+
title="${this.attachment.fileName}"
|
|
81
|
+
>
|
|
82
|
+
${getDocumentIcon()}
|
|
83
|
+
<div class="text-[10px] text-center truncate w-full">
|
|
84
|
+
${
|
|
85
|
+
this.attachment.fileName.length > 10
|
|
86
|
+
? this.attachment.fileName.substring(0, 8) + "..."
|
|
87
|
+
: this.attachment.fileName
|
|
88
|
+
}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
`
|
|
92
|
+
}
|
|
93
|
+
${
|
|
94
|
+
this.showDelete
|
|
95
|
+
? html`
|
|
96
|
+
<button
|
|
97
|
+
@click=${(e: Event) => {
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
this.onDelete?.();
|
|
100
|
+
}}
|
|
101
|
+
class="absolute -top-1 -right-1 w-5 h-5 bg-background hover:bg-muted text-muted-foreground hover:text-foreground rounded-full flex items-center justify-center opacity-100 hover:opacity-100 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100 transition-opacity border border-input shadow-sm"
|
|
102
|
+
title="${i18n("Remove")}"
|
|
103
|
+
>
|
|
104
|
+
${icon(X, "xs")}
|
|
105
|
+
</button>
|
|
106
|
+
`
|
|
107
|
+
: ""
|
|
108
|
+
}
|
|
109
|
+
</div>
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { html, icon } from "@mariozechner/mini-lit";
|
|
2
|
+
import { LitElement } from "lit";
|
|
3
|
+
import { property, state } from "lit/decorators.js";
|
|
4
|
+
import { Check, Copy } from "lucide";
|
|
5
|
+
import { i18n } from "../utils/i18n.js";
|
|
6
|
+
|
|
7
|
+
export class ConsoleBlock extends LitElement {
|
|
8
|
+
@property() content: string = "";
|
|
9
|
+
@state() private copied = false;
|
|
10
|
+
|
|
11
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
12
|
+
return this;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
override connectedCallback(): void {
|
|
16
|
+
super.connectedCallback();
|
|
17
|
+
this.style.display = "block";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private async copy() {
|
|
21
|
+
try {
|
|
22
|
+
await navigator.clipboard.writeText(this.content || "");
|
|
23
|
+
this.copied = true;
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
this.copied = false;
|
|
26
|
+
}, 1500);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.error("Copy failed", e);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override updated() {
|
|
33
|
+
// Auto-scroll to bottom on content changes
|
|
34
|
+
const container = this.querySelector(".console-scroll") as HTMLElement | null;
|
|
35
|
+
if (container) {
|
|
36
|
+
container.scrollTop = container.scrollHeight;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override render() {
|
|
41
|
+
return html`
|
|
42
|
+
<div class="border border-border rounded-lg overflow-hidden">
|
|
43
|
+
<div class="flex items-center justify-between px-3 py-1.5 bg-muted border-b border-border">
|
|
44
|
+
<span class="text-xs text-muted-foreground font-mono">${i18n("console")}</span>
|
|
45
|
+
<button
|
|
46
|
+
@click=${() => this.copy()}
|
|
47
|
+
class="flex items-center gap-1 px-2 py-0.5 text-xs rounded hover:bg-accent text-muted-foreground hover:text-accent-foreground transition-colors"
|
|
48
|
+
title="${i18n("Copy output")}"
|
|
49
|
+
>
|
|
50
|
+
${this.copied ? icon(Check, "sm") : icon(Copy, "sm")}
|
|
51
|
+
${this.copied ? html`<span>${i18n("Copied!")}</span>` : ""}
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="console-scroll overflow-auto max-h-64">
|
|
55
|
+
<pre class="!bg-background !border-0 !rounded-none m-0 p-3 text-xs text-foreground font-mono whitespace-pre-wrap">
|
|
56
|
+
${this.content || ""}</pre
|
|
57
|
+
>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Register custom element
|
|
65
|
+
if (!customElements.get("console-block")) {
|
|
66
|
+
customElements.define("console-block", ConsoleBlock);
|
|
67
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { type BaseComponentProps, fc, html } from "@mariozechner/mini-lit";
|
|
2
|
+
import { type Ref, ref } from "lit/directives/ref.js";
|
|
3
|
+
import { i18n } from "../utils/i18n.js";
|
|
4
|
+
|
|
5
|
+
export type InputType = "text" | "email" | "password" | "number" | "url" | "tel" | "search";
|
|
6
|
+
export type InputSize = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
export interface InputProps extends BaseComponentProps {
|
|
9
|
+
type?: InputType;
|
|
10
|
+
size?: InputSize;
|
|
11
|
+
value?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
name?: string;
|
|
18
|
+
autocomplete?: string;
|
|
19
|
+
min?: number;
|
|
20
|
+
max?: number;
|
|
21
|
+
step?: number;
|
|
22
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
23
|
+
onInput?: (e: Event) => void;
|
|
24
|
+
onChange?: (e: Event) => void;
|
|
25
|
+
onKeyDown?: (e: KeyboardEvent) => void;
|
|
26
|
+
onKeyUp?: (e: KeyboardEvent) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const Input = fc<InputProps>(
|
|
30
|
+
({
|
|
31
|
+
type = "text",
|
|
32
|
+
size = "md",
|
|
33
|
+
value = "",
|
|
34
|
+
placeholder = "",
|
|
35
|
+
label = "",
|
|
36
|
+
error = "",
|
|
37
|
+
disabled = false,
|
|
38
|
+
required = false,
|
|
39
|
+
name = "",
|
|
40
|
+
autocomplete = "",
|
|
41
|
+
min,
|
|
42
|
+
max,
|
|
43
|
+
step,
|
|
44
|
+
inputRef,
|
|
45
|
+
onInput,
|
|
46
|
+
onChange,
|
|
47
|
+
onKeyDown,
|
|
48
|
+
onKeyUp,
|
|
49
|
+
className = "",
|
|
50
|
+
}) => {
|
|
51
|
+
const sizeClasses = {
|
|
52
|
+
sm: "h-8 px-3 py-1 text-sm",
|
|
53
|
+
md: "h-9 px-3 py-1 text-sm md:text-sm",
|
|
54
|
+
lg: "h-10 px-4 py-1 text-base",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const baseClasses =
|
|
58
|
+
"flex w-full min-w-0 rounded-md border bg-transparent text-foreground shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium";
|
|
59
|
+
const interactionClasses =
|
|
60
|
+
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground";
|
|
61
|
+
const focusClasses = "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]";
|
|
62
|
+
const darkClasses = "dark:bg-input/30";
|
|
63
|
+
const stateClasses = error
|
|
64
|
+
? "border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40"
|
|
65
|
+
: "border-input";
|
|
66
|
+
const disabledClasses = "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50";
|
|
67
|
+
|
|
68
|
+
const handleInput = (e: Event) => {
|
|
69
|
+
onInput?.(e);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleChange = (e: Event) => {
|
|
73
|
+
onChange?.(e);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return html`
|
|
77
|
+
<div class="flex flex-col gap-1.5 ${className}">
|
|
78
|
+
${
|
|
79
|
+
label
|
|
80
|
+
? html`
|
|
81
|
+
<label class="text-sm font-medium text-foreground">
|
|
82
|
+
${label} ${required ? html`<span class="text-destructive">${i18n("*")}</span>` : ""}
|
|
83
|
+
</label>
|
|
84
|
+
`
|
|
85
|
+
: ""
|
|
86
|
+
}
|
|
87
|
+
<input
|
|
88
|
+
type="${type}"
|
|
89
|
+
class="${baseClasses} ${
|
|
90
|
+
sizeClasses[size]
|
|
91
|
+
} ${interactionClasses} ${focusClasses} ${darkClasses} ${stateClasses} ${disabledClasses}"
|
|
92
|
+
.value=${value}
|
|
93
|
+
placeholder="${placeholder}"
|
|
94
|
+
?disabled=${disabled}
|
|
95
|
+
?required=${required}
|
|
96
|
+
?aria-invalid=${!!error}
|
|
97
|
+
name="${name}"
|
|
98
|
+
autocomplete="${autocomplete}"
|
|
99
|
+
min="${min ?? ""}"
|
|
100
|
+
max="${max ?? ""}"
|
|
101
|
+
step="${step ?? ""}"
|
|
102
|
+
@input=${handleInput}
|
|
103
|
+
@change=${handleChange}
|
|
104
|
+
@keydown=${onKeyDown}
|
|
105
|
+
@keyup=${onKeyUp}
|
|
106
|
+
${inputRef ? ref(inputRef) : ""}
|
|
107
|
+
/>
|
|
108
|
+
${error ? html`<span class="text-sm text-destructive">${error}</span>` : ""}
|
|
109
|
+
</div>
|
|
110
|
+
`;
|
|
111
|
+
},
|
|
112
|
+
);
|