@oh-my-pi/pi-web-ui 1.337.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +609 -0
  3. package/example/README.md +61 -0
  4. package/example/index.html +13 -0
  5. package/example/package.json +24 -0
  6. package/example/src/app.css +1 -0
  7. package/example/src/custom-messages.ts +99 -0
  8. package/example/src/main.ts +420 -0
  9. package/example/tsconfig.json +23 -0
  10. package/example/vite.config.ts +6 -0
  11. package/package.json +57 -0
  12. package/scripts/count-prompt-tokens.ts +88 -0
  13. package/src/ChatPanel.ts +218 -0
  14. package/src/app.css +68 -0
  15. package/src/components/AgentInterface.ts +390 -0
  16. package/src/components/AttachmentTile.ts +107 -0
  17. package/src/components/ConsoleBlock.ts +74 -0
  18. package/src/components/CustomProviderCard.ts +96 -0
  19. package/src/components/ExpandableSection.ts +46 -0
  20. package/src/components/Input.ts +113 -0
  21. package/src/components/MessageEditor.ts +404 -0
  22. package/src/components/MessageList.ts +97 -0
  23. package/src/components/Messages.ts +384 -0
  24. package/src/components/ProviderKeyInput.ts +152 -0
  25. package/src/components/SandboxedIframe.ts +626 -0
  26. package/src/components/StreamingMessageContainer.ts +107 -0
  27. package/src/components/ThinkingBlock.ts +45 -0
  28. package/src/components/message-renderer-registry.ts +28 -0
  29. package/src/components/sandbox/ArtifactsRuntimeProvider.ts +219 -0
  30. package/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
  31. package/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
  32. package/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
  33. package/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
  34. package/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
  35. package/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
  36. package/src/dialogs/ApiKeyPromptDialog.ts +75 -0
  37. package/src/dialogs/AttachmentOverlay.ts +640 -0
  38. package/src/dialogs/CustomProviderDialog.ts +274 -0
  39. package/src/dialogs/ModelSelector.ts +314 -0
  40. package/src/dialogs/PersistentStorageDialog.ts +146 -0
  41. package/src/dialogs/ProvidersModelsTab.ts +212 -0
  42. package/src/dialogs/SessionListDialog.ts +157 -0
  43. package/src/dialogs/SettingsDialog.ts +216 -0
  44. package/src/index.ts +115 -0
  45. package/src/prompts/prompts.ts +282 -0
  46. package/src/storage/app-storage.ts +60 -0
  47. package/src/storage/backends/indexeddb-storage-backend.ts +193 -0
  48. package/src/storage/store.ts +33 -0
  49. package/src/storage/stores/custom-providers-store.ts +62 -0
  50. package/src/storage/stores/provider-keys-store.ts +33 -0
  51. package/src/storage/stores/sessions-store.ts +136 -0
  52. package/src/storage/stores/settings-store.ts +34 -0
  53. package/src/storage/types.ts +206 -0
  54. package/src/tools/artifacts/ArtifactElement.ts +14 -0
  55. package/src/tools/artifacts/ArtifactPill.ts +26 -0
  56. package/src/tools/artifacts/Console.ts +102 -0
  57. package/src/tools/artifacts/DocxArtifact.ts +213 -0
  58. package/src/tools/artifacts/ExcelArtifact.ts +231 -0
  59. package/src/tools/artifacts/GenericArtifact.ts +118 -0
  60. package/src/tools/artifacts/HtmlArtifact.ts +203 -0
  61. package/src/tools/artifacts/ImageArtifact.ts +116 -0
  62. package/src/tools/artifacts/MarkdownArtifact.ts +83 -0
  63. package/src/tools/artifacts/PdfArtifact.ts +201 -0
  64. package/src/tools/artifacts/SvgArtifact.ts +82 -0
  65. package/src/tools/artifacts/TextArtifact.ts +148 -0
  66. package/src/tools/artifacts/artifacts-tool-renderer.ts +371 -0
  67. package/src/tools/artifacts/artifacts.ts +713 -0
  68. package/src/tools/artifacts/index.ts +7 -0
  69. package/src/tools/extract-document.ts +271 -0
  70. package/src/tools/index.ts +46 -0
  71. package/src/tools/javascript-repl.ts +316 -0
  72. package/src/tools/renderer-registry.ts +127 -0
  73. package/src/tools/renderers/BashRenderer.ts +52 -0
  74. package/src/tools/renderers/CalculateRenderer.ts +58 -0
  75. package/src/tools/renderers/DefaultRenderer.ts +95 -0
  76. package/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
  77. package/src/tools/types.ts +15 -0
  78. package/src/utils/attachment-utils.ts +472 -0
  79. package/src/utils/auth-token.ts +22 -0
  80. package/src/utils/format.ts +42 -0
  81. package/src/utils/i18n.ts +653 -0
  82. package/src/utils/model-discovery.ts +277 -0
  83. package/src/utils/proxy-utils.ts +134 -0
  84. package/src/utils/test-sessions.ts +2357 -0
  85. package/tsconfig.build.json +20 -0
  86. package/tsconfig.json +7 -0
@@ -0,0 +1,146 @@
1
+ import { Button } from "@mariozechner/mini-lit/dist/Button.js";
2
+ import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js";
3
+ import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
4
+ import { html } from "lit";
5
+ import { customElement, state } from "lit/decorators.js";
6
+ import { i18n } from "../utils/i18n.js";
7
+
8
+ @customElement("persistent-storage-dialog")
9
+ export class PersistentStorageDialog extends DialogBase {
10
+ @state() private requesting = false;
11
+
12
+ private resolvePromise?: (userApproved: boolean) => void;
13
+
14
+ protected modalWidth = "min(500px, 90vw)";
15
+ protected modalHeight = "auto";
16
+
17
+ /**
18
+ * Request persistent storage permission.
19
+ * Returns true if browser granted persistent storage, false otherwise.
20
+ */
21
+ static async request(): Promise<boolean> {
22
+ // Check if already persisted
23
+ if (navigator.storage?.persisted) {
24
+ const alreadyPersisted = await navigator.storage.persisted();
25
+ if (alreadyPersisted) {
26
+ console.log("✓ Persistent storage already granted");
27
+ return true;
28
+ }
29
+ }
30
+
31
+ // Show dialog and wait for user response
32
+ const dialog = new PersistentStorageDialog();
33
+ dialog.open();
34
+
35
+ const userApproved = await new Promise<boolean>((resolve) => {
36
+ dialog.resolvePromise = resolve;
37
+ });
38
+
39
+ if (!userApproved) {
40
+ console.warn("⚠ User declined persistent storage - sessions may be lost");
41
+ return false;
42
+ }
43
+
44
+ // User approved, request from browser
45
+ if (!navigator.storage?.persist) {
46
+ console.warn("⚠ Persistent storage API not available");
47
+ return false;
48
+ }
49
+
50
+ try {
51
+ const granted = await navigator.storage.persist();
52
+ if (granted) {
53
+ console.log("✓ Persistent storage granted - sessions will be preserved");
54
+ } else {
55
+ console.warn("⚠ Browser denied persistent storage - sessions may be lost under storage pressure");
56
+ }
57
+ return granted;
58
+ } catch (error) {
59
+ console.error("Failed to request persistent storage:", error);
60
+ return false;
61
+ }
62
+ }
63
+
64
+ private handleGrant() {
65
+ if (this.resolvePromise) {
66
+ this.resolvePromise(true);
67
+ this.resolvePromise = undefined;
68
+ }
69
+ this.close();
70
+ }
71
+
72
+ private handleDeny() {
73
+ if (this.resolvePromise) {
74
+ this.resolvePromise(false);
75
+ this.resolvePromise = undefined;
76
+ }
77
+ this.close();
78
+ }
79
+
80
+ override close() {
81
+ super.close();
82
+ if (this.resolvePromise) {
83
+ this.resolvePromise(false);
84
+ }
85
+ }
86
+
87
+ protected override renderContent() {
88
+ return html`
89
+ ${DialogContent({
90
+ children: html`
91
+ ${DialogHeader({
92
+ title: i18n("Storage Permission Required"),
93
+ description: i18n("This app needs persistent storage to save your conversations"),
94
+ })}
95
+
96
+ <div class="mt-4 flex flex-col gap-4">
97
+ <div class="flex gap-3 p-4 bg-warning/10 border border-warning/20 rounded-lg">
98
+ <div class="flex-shrink-0 text-warning">
99
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
100
+ <path
101
+ d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
102
+ ></path>
103
+ <line x1="12" y1="9" x2="12" y2="13"></line>
104
+ <line x1="12" y1="17" x2="12.01" y2="17"></line>
105
+ </svg>
106
+ </div>
107
+ <div class="text-sm">
108
+ <p class="font-medium text-foreground mb-1">${i18n("Why is this needed?")}</p>
109
+ <p class="text-muted-foreground">
110
+ ${i18n(
111
+ "Without persistent storage, your browser may delete saved conversations when it needs disk space. Granting this permission ensures your chat history is preserved.",
112
+ )}
113
+ </p>
114
+ </div>
115
+ </div>
116
+
117
+ <div class="text-sm text-muted-foreground">
118
+ <p class="mb-2">${i18n("What this means:")}</p>
119
+ <ul class="list-disc list-inside space-y-1 ml-2">
120
+ <li>${i18n("Your conversations will be saved locally in your browser")}</li>
121
+ <li>${i18n("Data will not be deleted automatically to free up space")}</li>
122
+ <li>${i18n("You can still manually clear data at any time")}</li>
123
+ <li>${i18n("No data is sent to external servers")}</li>
124
+ </ul>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="mt-6 flex gap-3 justify-end">
129
+ ${Button({
130
+ variant: "outline",
131
+ onClick: () => this.handleDeny(),
132
+ disabled: this.requesting,
133
+ children: i18n("Continue Anyway"),
134
+ })}
135
+ ${Button({
136
+ variant: "default",
137
+ onClick: () => this.handleGrant(),
138
+ disabled: this.requesting,
139
+ children: this.requesting ? i18n("Requesting...") : i18n("Grant Permission"),
140
+ })}
141
+ </div>
142
+ `,
143
+ })}
144
+ `;
145
+ }
146
+ }
@@ -0,0 +1,212 @@
1
+ import { i18n } from "@mariozechner/mini-lit";
2
+ import { Select } from "@mariozechner/mini-lit/dist/Select.js";
3
+ import { getProviders } from "@oh-my-pi/pi-ai";
4
+ import { html, type TemplateResult } from "lit";
5
+ import { customElement, state } from "lit/decorators.js";
6
+ import "../components/CustomProviderCard.js";
7
+ import "../components/ProviderKeyInput.js";
8
+ import { getAppStorage } from "../storage/app-storage.js";
9
+ import type {
10
+ AutoDiscoveryProviderType,
11
+ CustomProvider,
12
+ CustomProviderType,
13
+ } from "../storage/stores/custom-providers-store.js";
14
+ import { discoverModels } from "../utils/model-discovery.js";
15
+ import { CustomProviderDialog } from "./CustomProviderDialog.js";
16
+ import { SettingsTab } from "./SettingsDialog.js";
17
+
18
+ @customElement("providers-models-tab")
19
+ export class ProvidersModelsTab extends SettingsTab {
20
+ @state() private customProviders: CustomProvider[] = [];
21
+ @state() private providerStatus: Map<
22
+ string,
23
+ { modelCount: number; status: "connected" | "disconnected" | "checking" }
24
+ > = new Map();
25
+
26
+ override async connectedCallback() {
27
+ super.connectedCallback();
28
+ await this.loadCustomProviders();
29
+ }
30
+
31
+ private async loadCustomProviders() {
32
+ try {
33
+ const storage = getAppStorage();
34
+ this.customProviders = await storage.customProviders.getAll();
35
+
36
+ // Check status for auto-discovery providers
37
+ for (const provider of this.customProviders) {
38
+ const isAutoDiscovery =
39
+ provider.type === "ollama" ||
40
+ provider.type === "llama.cpp" ||
41
+ provider.type === "vllm" ||
42
+ provider.type === "lmstudio";
43
+ if (isAutoDiscovery) {
44
+ this.checkProviderStatus(provider);
45
+ }
46
+ }
47
+ } catch (error) {
48
+ console.error("Failed to load custom providers:", error);
49
+ }
50
+ }
51
+
52
+ getTabName(): string {
53
+ return "Providers & Models";
54
+ }
55
+
56
+ private async checkProviderStatus(provider: CustomProvider) {
57
+ this.providerStatus.set(provider.id, { modelCount: 0, status: "checking" });
58
+ this.requestUpdate();
59
+
60
+ try {
61
+ const models = await discoverModels(
62
+ provider.type as AutoDiscoveryProviderType,
63
+ provider.baseUrl,
64
+ provider.apiKey,
65
+ );
66
+
67
+ this.providerStatus.set(provider.id, { modelCount: models.length, status: "connected" });
68
+ } catch (_error) {
69
+ this.providerStatus.set(provider.id, { modelCount: 0, status: "disconnected" });
70
+ }
71
+ this.requestUpdate();
72
+ }
73
+
74
+ private renderKnownProviders(): TemplateResult {
75
+ const providers = getProviders();
76
+
77
+ return html`
78
+ <div class="flex flex-col gap-6">
79
+ <div>
80
+ <h3 class="text-sm font-semibold text-foreground mb-2">Cloud Providers</h3>
81
+ <p class="text-sm text-muted-foreground mb-4">
82
+ Cloud LLM providers with predefined models. API keys are stored locally in your browser.
83
+ </p>
84
+ </div>
85
+ <div class="flex flex-col gap-6">
86
+ ${providers.map((provider) => html` <provider-key-input .provider=${provider}></provider-key-input> `)}
87
+ </div>
88
+ </div>
89
+ `;
90
+ }
91
+
92
+ private renderCustomProviders(): TemplateResult {
93
+ const isAutoDiscovery = (type: string) =>
94
+ type === "ollama" || type === "llama.cpp" || type === "vllm" || type === "lmstudio";
95
+
96
+ return html`
97
+ <div class="flex flex-col gap-6">
98
+ <div class="flex items-center justify-between">
99
+ <div>
100
+ <h3 class="text-sm font-semibold text-foreground mb-2">Custom Providers</h3>
101
+ <p class="text-sm text-muted-foreground">
102
+ User-configured servers with auto-discovered or manually defined models.
103
+ </p>
104
+ </div>
105
+ ${Select({
106
+ placeholder: i18n("Add Provider"),
107
+ options: [
108
+ { value: "ollama", label: "Ollama" },
109
+ { value: "llama.cpp", label: "llama.cpp" },
110
+ { value: "vllm", label: "vLLM" },
111
+ { value: "lmstudio", label: "LM Studio" },
112
+ { value: "openai-completions", label: i18n("OpenAI Completions Compatible") },
113
+ { value: "openai-responses", label: i18n("OpenAI Responses Compatible") },
114
+ { value: "anthropic-messages", label: i18n("Anthropic Messages Compatible") },
115
+ ],
116
+ onChange: (value: string) => this.addCustomProvider(value as CustomProviderType),
117
+ variant: "outline",
118
+ size: "sm",
119
+ })}
120
+ </div>
121
+
122
+ ${
123
+ this.customProviders.length === 0
124
+ ? html`
125
+ <div class="text-sm text-muted-foreground text-center py-8">
126
+ No custom providers configured. Click 'Add Provider' to get started.
127
+ </div>
128
+ `
129
+ : html`
130
+ <div class="flex flex-col gap-4">
131
+ ${this.customProviders.map(
132
+ (provider) => html`
133
+ <custom-provider-card
134
+ .provider=${provider}
135
+ .isAutoDiscovery=${isAutoDiscovery(provider.type)}
136
+ .status=${this.providerStatus.get(provider.id)}
137
+ .onRefresh=${(p: CustomProvider) => this.refreshProvider(p)}
138
+ .onEdit=${(p: CustomProvider) => this.editProvider(p)}
139
+ .onDelete=${(p: CustomProvider) => this.deleteProvider(p)}
140
+ ></custom-provider-card>
141
+ `,
142
+ )}
143
+ </div>
144
+ `
145
+ }
146
+ </div>
147
+ `;
148
+ }
149
+
150
+ private async addCustomProvider(type: CustomProviderType) {
151
+ await CustomProviderDialog.open(undefined, type, async () => {
152
+ await this.loadCustomProviders();
153
+ this.requestUpdate();
154
+ });
155
+ }
156
+
157
+ private async editProvider(provider: CustomProvider) {
158
+ await CustomProviderDialog.open(provider, undefined, async () => {
159
+ await this.loadCustomProviders();
160
+ this.requestUpdate();
161
+ });
162
+ }
163
+
164
+ private async refreshProvider(provider: CustomProvider) {
165
+ this.providerStatus.set(provider.id, { modelCount: 0, status: "checking" });
166
+ this.requestUpdate();
167
+
168
+ try {
169
+ const models = await discoverModels(
170
+ provider.type as AutoDiscoveryProviderType,
171
+ provider.baseUrl,
172
+ provider.apiKey,
173
+ );
174
+
175
+ this.providerStatus.set(provider.id, { modelCount: models.length, status: "connected" });
176
+ this.requestUpdate();
177
+
178
+ console.log(`Refreshed ${models.length} models from ${provider.name}`);
179
+ } catch (error) {
180
+ this.providerStatus.set(provider.id, { modelCount: 0, status: "disconnected" });
181
+ this.requestUpdate();
182
+
183
+ console.error(`Failed to refresh provider ${provider.name}:`, error);
184
+ alert(`Failed to refresh provider: ${error instanceof Error ? error.message : String(error)}`);
185
+ }
186
+ }
187
+
188
+ private async deleteProvider(provider: CustomProvider) {
189
+ if (!confirm("Are you sure you want to delete this provider?")) {
190
+ return;
191
+ }
192
+
193
+ try {
194
+ const storage = getAppStorage();
195
+ await storage.customProviders.delete(provider.id);
196
+ await this.loadCustomProviders();
197
+ this.requestUpdate();
198
+ } catch (error) {
199
+ console.error("Failed to delete provider:", error);
200
+ }
201
+ }
202
+
203
+ render(): TemplateResult {
204
+ return html`
205
+ <div class="flex flex-col gap-8">
206
+ ${this.renderKnownProviders()}
207
+ <div class="border-t border-border"></div>
208
+ ${this.renderCustomProviders()}
209
+ </div>
210
+ `;
211
+ }
212
+ }
@@ -0,0 +1,157 @@
1
+ import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js";
2
+ import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
3
+ import { html } from "lit";
4
+ import { customElement, state } from "lit/decorators.js";
5
+ import { getAppStorage } from "../storage/app-storage.js";
6
+ import type { SessionMetadata } from "../storage/types.js";
7
+ import { formatUsage } from "../utils/format.js";
8
+ import { i18n } from "../utils/i18n.js";
9
+
10
+ @customElement("session-list-dialog")
11
+ export class SessionListDialog extends DialogBase {
12
+ @state() private sessions: SessionMetadata[] = [];
13
+ @state() private loading = true;
14
+
15
+ private onSelectCallback?: (sessionId: string) => void;
16
+ private onDeleteCallback?: (sessionId: string) => void;
17
+ private deletedSessions = new Set<string>();
18
+ private closedViaSelection = false;
19
+
20
+ protected modalWidth = "min(600px, 90vw)";
21
+ protected modalHeight = "min(700px, 90vh)";
22
+
23
+ static async open(onSelect: (sessionId: string) => void, onDelete?: (sessionId: string) => void) {
24
+ const dialog = new SessionListDialog();
25
+ dialog.onSelectCallback = onSelect;
26
+ dialog.onDeleteCallback = onDelete;
27
+ dialog.open();
28
+ await dialog.loadSessions();
29
+ }
30
+
31
+ private async loadSessions() {
32
+ this.loading = true;
33
+ try {
34
+ const storage = getAppStorage();
35
+ this.sessions = await storage.sessions.getAllMetadata();
36
+ } catch (err) {
37
+ console.error("Failed to load sessions:", err);
38
+ this.sessions = [];
39
+ } finally {
40
+ this.loading = false;
41
+ }
42
+ }
43
+
44
+ private async handleDelete(sessionId: string, event: Event) {
45
+ event.stopPropagation();
46
+
47
+ if (!confirm(i18n("Delete this session?"))) {
48
+ return;
49
+ }
50
+
51
+ try {
52
+ const storage = getAppStorage();
53
+ if (!storage.sessions) return;
54
+
55
+ await storage.sessions.deleteSession(sessionId);
56
+ await this.loadSessions();
57
+
58
+ // Track deleted session
59
+ this.deletedSessions.add(sessionId);
60
+ } catch (err) {
61
+ console.error("Failed to delete session:", err);
62
+ }
63
+ }
64
+
65
+ override close() {
66
+ super.close();
67
+
68
+ // Only notify about deleted sessions if dialog wasn't closed via selection
69
+ if (!this.closedViaSelection && this.onDeleteCallback && this.deletedSessions.size > 0) {
70
+ for (const sessionId of this.deletedSessions) {
71
+ this.onDeleteCallback(sessionId);
72
+ }
73
+ }
74
+ }
75
+
76
+ private handleSelect(sessionId: string) {
77
+ this.closedViaSelection = true;
78
+ if (this.onSelectCallback) {
79
+ this.onSelectCallback(sessionId);
80
+ }
81
+ this.close();
82
+ }
83
+
84
+ private formatDate(isoString: string): string {
85
+ const date = new Date(isoString);
86
+ const now = new Date();
87
+ const diff = now.getTime() - date.getTime();
88
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
89
+
90
+ if (days === 0) {
91
+ return i18n("Today");
92
+ } else if (days === 1) {
93
+ return i18n("Yesterday");
94
+ } else if (days < 7) {
95
+ return i18n("{days} days ago").replace("{days}", days.toString());
96
+ } else {
97
+ return date.toLocaleDateString();
98
+ }
99
+ }
100
+
101
+ protected override renderContent() {
102
+ return html`
103
+ ${DialogContent({
104
+ className: "h-full flex flex-col",
105
+ children: html`
106
+ ${DialogHeader({
107
+ title: i18n("Sessions"),
108
+ description: i18n("Load a previous conversation"),
109
+ })}
110
+
111
+ <div class="flex-1 overflow-y-auto mt-4 space-y-2">
112
+ ${
113
+ this.loading
114
+ ? html`<div class="text-center py-8 text-muted-foreground">${i18n("Loading...")}</div>`
115
+ : this.sessions.length === 0
116
+ ? html`<div class="text-center py-8 text-muted-foreground">${i18n("No sessions yet")}</div>`
117
+ : this.sessions.map(
118
+ (session) => html`
119
+ <div
120
+ class="group flex items-start gap-3 p-3 rounded-lg border border-border hover:bg-secondary/50 cursor-pointer transition-colors"
121
+ @click=${() => this.handleSelect(session.id)}
122
+ >
123
+ <div class="flex-1 min-w-0">
124
+ <div class="font-medium text-sm text-foreground truncate">${session.title}</div>
125
+ <div class="text-xs text-muted-foreground mt-1">${this.formatDate(session.lastModified)}</div>
126
+ <div class="text-xs text-muted-foreground mt-1">
127
+ ${session.messageCount} ${i18n("messages")} · ${formatUsage(session.usage)}
128
+ </div>
129
+ </div>
130
+ <button
131
+ class="opacity-0 group-hover:opacity-100 p-1 rounded hover:bg-destructive/10 text-destructive transition-opacity"
132
+ @click=${(e: Event) => this.handleDelete(session.id, e)}
133
+ title=${i18n("Delete")}
134
+ >
135
+ <svg
136
+ width="16"
137
+ height="16"
138
+ viewBox="0 0 24 24"
139
+ fill="none"
140
+ stroke="currentColor"
141
+ stroke-width="2"
142
+ >
143
+ <path d="M3 6h18"></path>
144
+ <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
145
+ <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
146
+ </svg>
147
+ </button>
148
+ </div>
149
+ `,
150
+ )
151
+ }
152
+ </div>
153
+ `,
154
+ })}
155
+ `;
156
+ }
157
+ }