mac: bundle web chat assets

This commit is contained in:
Peter Steinberger
2025-12-06 05:01:28 +01:00
parent 15cdeeddaf
commit 42d843297d
315 changed files with 16618 additions and 20 deletions

View File

@@ -0,0 +1,15 @@
import "../components/ProviderKeyInput.js";
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
export declare class ApiKeyPromptDialog extends DialogBase {
private provider;
private resolvePromise?;
private unsubscribe?;
protected modalWidth: string;
protected modalHeight: string;
static prompt(provider: string): Promise<boolean>;
connectedCallback(): Promise<void>;
disconnectedCallback(): void;
close(): void;
protected renderContent(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=ApiKeyPromptDialog.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ApiKeyPromptDialog.d.ts","sourceRoot":"","sources":["../../src/dialogs/ApiKeyPromptDialog.ts"],"names":[],"mappings":"AACA,OAAO,mCAAmC,CAAC;AAE3C,OAAO,EAAE,UAAU,EAAE,MAAM,2CAA2C,CAAC;AAKvE,qBACa,kBAAmB,SAAQ,UAAU;IACxC,OAAO,CAAC,QAAQ,CAAM;IAE/B,OAAO,CAAC,cAAc,CAAC,CAA6B;IACpD,OAAO,CAAC,WAAW,CAAC,CAAa;IAEjC,SAAS,CAAC,UAAU,SAAsB;IAC1C,SAAS,CAAC,WAAW,SAAU;WAElB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUxC,iBAAiB;IAmBvB,oBAAoB;IAQpB,KAAK;cAOK,aAAa;CAYhC"}

View File

@@ -0,0 +1,79 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var ApiKeyPromptDialog_1;
import { customElement, state } from "lit/decorators.js";
import "../components/ProviderKeyInput.js";
import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js";
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
import { html } from "lit";
import { getAppStorage } from "../storage/app-storage.js";
import { i18n } from "../utils/i18n.js";
let ApiKeyPromptDialog = ApiKeyPromptDialog_1 = class ApiKeyPromptDialog extends DialogBase {
constructor() {
super(...arguments);
this.provider = "";
this.modalWidth = "min(500px, 90vw)";
this.modalHeight = "auto";
}
static async prompt(provider) {
const dialog = new ApiKeyPromptDialog_1();
dialog.provider = provider;
dialog.open();
return new Promise((resolve) => {
dialog.resolvePromise = resolve;
});
}
async connectedCallback() {
super.connectedCallback();
// Poll for key existence - when key is added, resolve and close
const checkInterval = setInterval(async () => {
const hasKey = !!(await getAppStorage().providerKeys.get(this.provider));
if (hasKey) {
clearInterval(checkInterval);
if (this.resolvePromise) {
this.resolvePromise(true);
this.resolvePromise = undefined;
}
this.close();
}
}, 500);
this.unsubscribe = () => clearInterval(checkInterval);
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.unsubscribe) {
this.unsubscribe();
this.unsubscribe = undefined;
}
}
close() {
super.close();
if (this.resolvePromise) {
this.resolvePromise(false);
}
}
renderContent() {
return html `
${DialogContent({
children: html `
${DialogHeader({
title: i18n("API Key Required"),
})}
<provider-key-input .provider=${this.provider}></provider-key-input>
`,
})}
`;
}
};
__decorate([
state()
], ApiKeyPromptDialog.prototype, "provider", void 0);
ApiKeyPromptDialog = ApiKeyPromptDialog_1 = __decorate([
customElement("api-key-prompt-dialog")
], ApiKeyPromptDialog);
export { ApiKeyPromptDialog };
//# sourceMappingURL=ApiKeyPromptDialog.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ApiKeyPromptDialog.js","sourceRoot":"","sources":["../../src/dialogs/ApiKeyPromptDialog.ts"],"names":[],"mappings":";;;;;;;AAAA,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,mCAAmC,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACpF,OAAO,EAAE,UAAU,EAAE,MAAM,2CAA2C,CAAC;AACvE,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAGjC,IAAM,kBAAkB,0BAAxB,MAAM,kBAAmB,SAAQ,UAAU;IAA3C;;QACW,aAAQ,GAAG,EAAE,CAAC;QAKrB,eAAU,GAAG,kBAAkB,CAAC;QAChC,gBAAW,GAAG,MAAM,CAAC;IA0DhC,CAAC;IAxDA,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,QAAgB;QACnC,MAAM,MAAM,GAAG,IAAI,oBAAkB,EAAE,CAAC;QACxC,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAC3B,MAAM,CAAC,IAAI,EAAE,CAAC;QAEd,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC9B,MAAM,CAAC,cAAc,GAAG,OAAO,CAAC;QACjC,CAAC,CAAC,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,iBAAiB;QAC/B,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAE1B,gEAAgE;QAChE,MAAM,aAAa,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;YAC5C,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,aAAa,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;YACzE,IAAI,MAAM,EAAE,CAAC;gBACZ,aAAa,CAAC,aAAa,CAAC,CAAC;gBAC7B,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;oBACzB,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;oBAC1B,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;gBACjC,CAAC;gBACD,IAAI,CAAC,KAAK,EAAE,CAAC;YACd,CAAC;QACF,CAAC,EAAE,GAAG,CAAC,CAAC;QAER,IAAI,CAAC,WAAW,GAAG,GAAG,EAAE,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IACvD,CAAC;IAEQ,oBAAoB;QAC5B,KAAK,CAAC,oBAAoB,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,WAAW,EAAE,CAAC;YACnB,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;QAC9B,CAAC;IACF,CAAC;IAEQ,KAAK;QACb,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACzB,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QAC5B,CAAC;IACF,CAAC;IAEkB,aAAa;QAC/B,OAAO,IAAI,CAAA;KACR,aAAa,CAAC;YACf,QAAQ,EAAE,IAAI,CAAA;OACX,YAAY,CAAC;gBACd,KAAK,EAAE,IAAI,CAAC,kBAAkB,CAAC;aAC/B,CAAC;qCAC8B,IAAI,CAAC,QAAQ;KAC7C;SACD,CAAC;GACF,CAAC;IACH,CAAC;CACD,CAAA;AAhEiB;IAAhB,KAAK,EAAE;oDAAuB;AADnB,kBAAkB;IAD9B,aAAa,CAAC,uBAAuB,CAAC;GAC1B,kBAAkB,CAiE9B"}

View File

@@ -0,0 +1,32 @@
import "@mariozechner/mini-lit/dist/ModeToggle.js";
import { LitElement } from "lit";
import type { Attachment } from "../utils/attachment-utils.js";
export declare class AttachmentOverlay extends LitElement {
private attachment?;
private showExtractedText;
private error;
private currentLoadingTask;
private onCloseCallback?;
private boundHandleKeyDown?;
protected createRenderRoot(): HTMLElement | DocumentFragment;
static open(attachment: Attachment, onClose?: () => void): void;
private setupEventListeners;
private close;
private getFileType;
private getFileTypeLabel;
private handleBackdropClick;
private handleDownload;
private cleanup;
render(): import("lit-html").TemplateResult<1>;
private renderToggle;
private renderContent;
private renderFileContent;
updated(changedProperties: Map<string, any>): Promise<void>;
private renderPdf;
private renderDocx;
private renderExcel;
private renderExcelSheet;
private base64ToArrayBuffer;
private renderExtractedText;
}
//# sourceMappingURL=AttachmentOverlay.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"AttachmentOverlay.d.ts","sourceRoot":"","sources":["../../src/dialogs/AttachmentOverlay.ts"],"names":[],"mappings":"AAAA,OAAO,2CAA2C,CAAC;AAInD,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAKvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAK/D,qBAAa,iBAAkB,SAAQ,UAAU;IACvC,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,KAAK,CAAuB;IAG7C,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,eAAe,CAAC,CAAa;IACrC,OAAO,CAAC,kBAAkB,CAAC,CAA6B;cAErC,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAIrE,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,IAAI;IAQxD,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,KAAK;IASb,OAAO,CAAC,WAAW;IAsBnB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,mBAAmB,CAEzB;IAEF,OAAO,CAAC,cAAc,CAqBpB;IAEF,OAAO,CAAC,OAAO;IAUN,MAAM;IAwCf,OAAO,CAAC,YAAY;IAwBpB,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,iBAAiB;IA+DV,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC;YA6B5C,SAAS;YAgFT,UAAU;YAqGV,WAAW;IA2EzB,OAAO,CAAC,gBAAgB;IAyCxB,OAAO,CAAC,mBAAmB;YASb,mBAAmB;CAsBjC"}

View File

@@ -0,0 +1,576 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import "@mariozechner/mini-lit/dist/ModeToggle.js";
import { icon } from "@mariozechner/mini-lit";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { renderAsync } from "docx-preview";
import { html, LitElement } from "lit";
import { state } from "lit/decorators.js";
import { Download, X } from "lucide";
import * as pdfjsLib from "pdfjs-dist";
import * as XLSX from "xlsx";
import { i18n } from "../utils/i18n.js";
export class AttachmentOverlay extends LitElement {
constructor() {
super(...arguments);
this.showExtractedText = false;
this.error = null;
// Track current loading task to cancel if needed
this.currentLoadingTask = null;
this.handleBackdropClick = () => {
this.close();
};
this.handleDownload = () => {
if (!this.attachment)
return;
// Create a blob from the base64 content
const byteCharacters = atob(this.attachment.content);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: this.attachment.mimeType });
// Create download link
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = this.attachment.fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
}
createRenderRoot() {
return this;
}
static open(attachment, onClose) {
const overlay = new AttachmentOverlay();
overlay.attachment = attachment;
overlay.onCloseCallback = onClose;
document.body.appendChild(overlay);
overlay.setupEventListeners();
}
setupEventListeners() {
this.boundHandleKeyDown = (e) => {
if (e.key === "Escape") {
this.close();
}
};
window.addEventListener("keydown", this.boundHandleKeyDown);
}
close() {
this.cleanup();
if (this.boundHandleKeyDown) {
window.removeEventListener("keydown", this.boundHandleKeyDown);
}
this.onCloseCallback?.();
this.remove();
}
getFileType() {
if (!this.attachment)
return "text";
if (this.attachment.type === "image")
return "image";
if (this.attachment.mimeType === "application/pdf")
return "pdf";
if (this.attachment.mimeType?.includes("wordprocessingml"))
return "docx";
if (this.attachment.mimeType?.includes("presentationml") ||
this.attachment.fileName.toLowerCase().endsWith(".pptx"))
return "pptx";
if (this.attachment.mimeType?.includes("spreadsheetml") ||
this.attachment.mimeType?.includes("ms-excel") ||
this.attachment.fileName.toLowerCase().endsWith(".xlsx") ||
this.attachment.fileName.toLowerCase().endsWith(".xls"))
return "excel";
return "text";
}
getFileTypeLabel() {
const type = this.getFileType();
switch (type) {
case "pdf":
return i18n("PDF");
case "docx":
return i18n("Document");
case "pptx":
return i18n("Presentation");
case "excel":
return i18n("Spreadsheet");
default:
return "";
}
}
cleanup() {
this.showExtractedText = false;
this.error = null;
// Cancel any loading PDF task when closing
if (this.currentLoadingTask) {
this.currentLoadingTask.destroy();
this.currentLoadingTask = null;
}
}
render() {
if (!this.attachment)
return html ``;
return html `
<!-- Full screen overlay -->
<div class="fixed inset-0 bg-black/90 z-50 flex flex-col" @click=${this.handleBackdropClick}>
<!-- Compact header bar -->
<div class="bg-background/95 backdrop-blur border-b border-border" @click=${(e) => e.stopPropagation()}>
<div class="px-4 py-2 flex items-center justify-between">
<div class="flex items-center gap-3 min-w-0">
<span class="text-sm font-medium text-foreground truncate">${this.attachment.fileName}</span>
</div>
<div class="flex items-center gap-2">
${this.renderToggle()}
${Button({
variant: "ghost",
size: "icon",
onClick: this.handleDownload,
children: icon(Download, "sm"),
className: "h-8 w-8",
})}
${Button({
variant: "ghost",
size: "icon",
onClick: () => this.close(),
children: icon(X, "sm"),
className: "h-8 w-8",
})}
</div>
</div>
</div>
<!-- Content container -->
<div class="flex-1 flex items-center justify-center overflow-auto" @click=${(e) => e.stopPropagation()}>
${this.renderContent()}
</div>
</div>
`;
}
renderToggle() {
if (!this.attachment)
return html ``;
const fileType = this.getFileType();
const hasExtractedText = !!this.attachment.extractedText;
const showToggle = fileType !== "image" && fileType !== "text" && fileType !== "pptx" && hasExtractedText;
if (!showToggle)
return html ``;
const fileTypeLabel = this.getFileTypeLabel();
return html `
<mode-toggle
.modes=${[fileTypeLabel, i18n("Text")]}
.selectedIndex=${this.showExtractedText ? 1 : 0}
@mode-change=${(e) => {
e.stopPropagation();
this.showExtractedText = e.detail.index === 1;
this.error = null;
}}
></mode-toggle>
`;
}
renderContent() {
if (!this.attachment)
return html ``;
// Error state
if (this.error) {
return html `
<div class="bg-destructive/10 border border-destructive text-destructive p-4 rounded-lg max-w-2xl">
<div class="font-medium mb-1">${i18n("Error loading file")}</div>
<div class="text-sm opacity-90">${this.error}</div>
</div>
`;
}
// Content based on file type
return this.renderFileContent();
}
renderFileContent() {
if (!this.attachment)
return html ``;
const fileType = this.getFileType();
// Show extracted text if toggled
if (this.showExtractedText && fileType !== "image") {
return html `
<div class="bg-card border border-border text-foreground p-6 w-full h-full max-w-4xl overflow-auto">
<pre class="whitespace-pre-wrap font-mono text-xs leading-relaxed">${this.attachment.extractedText || i18n("No text content available")}</pre>
</div>
`;
}
// Render based on file type
switch (fileType) {
case "image": {
const imageUrl = `data:${this.attachment.mimeType};base64,${this.attachment.content}`;
return html `
<img src="${imageUrl}" class="max-w-full max-h-full object-contain rounded-lg shadow-lg" alt="${this.attachment.fileName}" />
`;
}
case "pdf":
return html `
<div
id="pdf-container"
class="bg-card text-foreground overflow-auto shadow-lg border border-border w-full h-full max-w-[1000px]"
></div>
`;
case "docx":
return html `
<div
id="docx-container"
class="bg-card text-foreground overflow-auto shadow-lg border border-border w-full h-full max-w-[1000px]"
></div>
`;
case "excel":
return html ` <div id="excel-container" class="bg-card text-foreground overflow-auto w-full h-full"></div> `;
case "pptx":
return html `
<div
id="pptx-container"
class="bg-card text-foreground overflow-auto shadow-lg border border-border w-full h-full max-w-[1000px]"
></div>
`;
default:
return html `
<div class="bg-card border border-border text-foreground p-6 w-full h-full max-w-4xl overflow-auto">
<pre class="whitespace-pre-wrap font-mono text-sm">${this.attachment.extractedText || i18n("No content available")}</pre>
</div>
`;
}
}
async updated(changedProperties) {
super.updated(changedProperties);
// Only process if we need to render the actual file (not extracted text)
if ((changedProperties.has("attachment") || changedProperties.has("showExtractedText")) &&
this.attachment &&
!this.showExtractedText &&
!this.error) {
const fileType = this.getFileType();
switch (fileType) {
case "pdf":
await this.renderPdf();
break;
case "docx":
await this.renderDocx();
break;
case "excel":
await this.renderExcel();
break;
case "pptx":
await this.renderExtractedText();
break;
}
}
}
async renderPdf() {
const container = this.querySelector("#pdf-container");
if (!container || !this.attachment)
return;
let pdf = null;
try {
// Convert base64 to ArrayBuffer
const arrayBuffer = this.base64ToArrayBuffer(this.attachment.content);
// Cancel any existing loading task
if (this.currentLoadingTask) {
this.currentLoadingTask.destroy();
}
// Load the PDF
this.currentLoadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
pdf = await this.currentLoadingTask.promise;
this.currentLoadingTask = null;
// Clear container and add wrapper
container.innerHTML = "";
const wrapper = document.createElement("div");
wrapper.className = "";
container.appendChild(wrapper);
// Render all pages
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
// Create a container for each page
const pageContainer = document.createElement("div");
pageContainer.className = "mb-4 last:mb-0";
// Create canvas for this page
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
// Set scale for reasonable resolution
const viewport = page.getViewport({ scale: 1.5 });
canvas.height = viewport.height;
canvas.width = viewport.width;
// Style the canvas
canvas.className = "w-full max-w-full h-auto block mx-auto bg-white rounded shadow-sm border border-border";
// Fill white background for proper PDF rendering
if (context) {
context.fillStyle = "white";
context.fillRect(0, 0, canvas.width, canvas.height);
}
// Render page
await page.render({
canvasContext: context,
viewport: viewport,
canvas: canvas,
}).promise;
pageContainer.appendChild(canvas);
// Add page separator for multi-page documents
if (pageNum < pdf.numPages) {
const separator = document.createElement("div");
separator.className = "h-px bg-border my-4";
pageContainer.appendChild(separator);
}
wrapper.appendChild(pageContainer);
}
}
catch (error) {
console.error("Error rendering PDF:", error);
this.error = error?.message || i18n("Failed to load PDF");
}
finally {
if (pdf) {
pdf.destroy();
}
}
}
async renderDocx() {
const container = this.querySelector("#docx-container");
if (!container || !this.attachment)
return;
try {
// Convert base64 to ArrayBuffer
const arrayBuffer = this.base64ToArrayBuffer(this.attachment.content);
// Clear container first
container.innerHTML = "";
// Create a wrapper div for the document
const wrapper = document.createElement("div");
wrapper.className = "docx-wrapper-custom";
container.appendChild(wrapper);
// Render the DOCX file into the wrapper
await renderAsync(arrayBuffer, wrapper, undefined, {
className: "docx",
inWrapper: true,
ignoreWidth: true, // Let it be responsive
ignoreHeight: false,
ignoreFonts: false,
breakPages: true,
ignoreLastRenderedPageBreak: true,
experimental: false,
trimXmlDeclaration: true,
useBase64URL: false,
renderHeaders: true,
renderFooters: true,
renderFootnotes: true,
renderEndnotes: true,
});
// Apply custom styles to match theme and fix sizing
const style = document.createElement("style");
style.textContent = `
#docx-container {
padding: 0;
}
#docx-container .docx-wrapper-custom {
max-width: 100%;
overflow-x: auto;
}
#docx-container .docx-wrapper {
max-width: 100% !important;
margin: 0 !important;
background: transparent !important;
padding: 0em !important;
}
#docx-container .docx-wrapper > section.docx {
box-shadow: none !important;
border: none !important;
border-radius: 0 !important;
margin: 0 !important;
padding: 2em !important;
background: white !important;
color: black !important;
max-width: 100% !important;
width: 100% !important;
min-width: 0 !important;
overflow-x: auto !important;
}
/* Fix tables and wide content */
#docx-container table {
max-width: 100% !important;
width: auto !important;
overflow-x: auto !important;
display: block !important;
}
#docx-container img {
max-width: 100% !important;
height: auto !important;
}
/* Fix paragraphs and text */
#docx-container p,
#docx-container span,
#docx-container div {
max-width: 100% !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
/* Hide page breaks in web view */
#docx-container .docx-page-break {
display: none !important;
}
`;
container.appendChild(style);
}
catch (error) {
console.error("Error rendering DOCX:", error);
this.error = error?.message || i18n("Failed to load document");
}
}
async renderExcel() {
const container = this.querySelector("#excel-container");
if (!container || !this.attachment)
return;
try {
// Convert base64 to ArrayBuffer
const arrayBuffer = this.base64ToArrayBuffer(this.attachment.content);
// Read the workbook
const workbook = XLSX.read(arrayBuffer, { type: "array" });
// Clear container
container.innerHTML = "";
const wrapper = document.createElement("div");
wrapper.className = "overflow-auto h-full flex flex-col";
container.appendChild(wrapper);
// Create tabs for multiple sheets
if (workbook.SheetNames.length > 1) {
const tabContainer = document.createElement("div");
tabContainer.className = "flex gap-2 mb-4 border-b border-border sticky top-0 bg-card z-10";
const sheetContents = [];
workbook.SheetNames.forEach((sheetName, index) => {
// Create tab button
const tab = document.createElement("button");
tab.textContent = sheetName;
tab.className =
index === 0
? "px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary"
: "px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:border-b-2 hover:border-border transition-colors";
// Create sheet content
const sheetDiv = document.createElement("div");
sheetDiv.style.display = index === 0 ? "flex" : "none";
sheetDiv.className = "flex-1 overflow-auto";
sheetDiv.appendChild(this.renderExcelSheet(workbook.Sheets[sheetName], sheetName));
sheetContents.push(sheetDiv);
// Tab click handler
tab.onclick = () => {
// Update tab styles
tabContainer.querySelectorAll("button").forEach((btn, btnIndex) => {
if (btnIndex === index) {
btn.className = "px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary";
}
else {
btn.className =
"px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:border-b-2 hover:border-border transition-colors";
}
});
// Show/hide sheets
sheetContents.forEach((content, contentIndex) => {
content.style.display = contentIndex === index ? "flex" : "none";
});
};
tabContainer.appendChild(tab);
});
wrapper.appendChild(tabContainer);
sheetContents.forEach((content) => {
wrapper.appendChild(content);
});
}
else {
// Single sheet
const sheetName = workbook.SheetNames[0];
wrapper.appendChild(this.renderExcelSheet(workbook.Sheets[sheetName], sheetName));
}
}
catch (error) {
console.error("Error rendering Excel:", error);
this.error = error?.message || i18n("Failed to load spreadsheet");
}
}
renderExcelSheet(worksheet, sheetName) {
const sheetDiv = document.createElement("div");
// Generate HTML table
const htmlTable = XLSX.utils.sheet_to_html(worksheet, { id: `sheet-${sheetName}` });
const tempDiv = document.createElement("div");
tempDiv.innerHTML = htmlTable;
// Find and style the table
const table = tempDiv.querySelector("table");
if (table) {
table.className = "w-full border-collapse text-foreground";
// Style all cells
table.querySelectorAll("td, th").forEach((cell) => {
const cellEl = cell;
cellEl.className = "border border-border px-3 py-2 text-sm text-left";
});
// Style header row
const headerCells = table.querySelectorAll("thead th, tr:first-child td");
if (headerCells.length > 0) {
headerCells.forEach((th) => {
const thEl = th;
thEl.className =
"border border-border px-3 py-2 text-sm font-semibold bg-muted text-foreground sticky top-0";
});
}
// Alternate row colors
table.querySelectorAll("tbody tr:nth-child(even)").forEach((row) => {
const rowEl = row;
rowEl.className = "bg-muted/30";
});
sheetDiv.appendChild(table);
}
return sheetDiv;
}
base64ToArrayBuffer(base64) {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
async renderExtractedText() {
const container = this.querySelector("#pptx-container");
if (!container || !this.attachment)
return;
try {
// Display the extracted text content
container.innerHTML = "";
const wrapper = document.createElement("div");
wrapper.className = "p-6 overflow-auto";
// Create a pre element to preserve formatting
const pre = document.createElement("pre");
pre.className = "whitespace-pre-wrap text-sm text-foreground font-mono";
pre.textContent = this.attachment.extractedText || i18n("No text content available");
wrapper.appendChild(pre);
container.appendChild(wrapper);
}
catch (error) {
console.error("Error rendering extracted text:", error);
this.error = error?.message || i18n("Failed to display text content");
}
}
}
__decorate([
state()
], AttachmentOverlay.prototype, "attachment", void 0);
__decorate([
state()
], AttachmentOverlay.prototype, "showExtractedText", void 0);
__decorate([
state()
], AttachmentOverlay.prototype, "error", void 0);
// Register the custom element only once
if (!customElements.get("attachment-overlay")) {
customElements.define("attachment-overlay", AttachmentOverlay);
}
//# sourceMappingURL=AttachmentOverlay.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
import { type TemplateResult } from "lit";
import type { CustomProvider, CustomProviderType } from "../storage/stores/custom-providers-store.js";
export declare class CustomProviderDialog extends DialogBase {
private provider?;
private initialType?;
private onSaveCallback?;
private name;
private type;
private baseUrl;
private apiKey;
private testing;
private testError;
private discoveredModels;
protected modalWidth: string;
protected modalHeight: string;
static open(provider: CustomProvider | undefined, initialType: CustomProviderType | undefined, onSave?: () => void): Promise<void>;
private initializeFromProvider;
private updateDefaultBaseUrl;
private isAutoDiscoveryType;
private testConnection;
private save;
protected renderContent(): TemplateResult;
}
//# sourceMappingURL=CustomProviderDialog.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CustomProviderDialog.d.ts","sourceRoot":"","sources":["../../src/dialogs/CustomProviderDialog.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,2CAA2C,CAAC;AAKvE,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAGhD,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AAGtG,qBAAa,oBAAqB,SAAQ,UAAU;IACnD,OAAO,CAAC,QAAQ,CAAC,CAAiB;IAClC,OAAO,CAAC,WAAW,CAAC,CAAqB;IACzC,OAAO,CAAC,cAAc,CAAC,CAAa;IAE3B,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,IAAI,CAA4C;IACxD,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,gBAAgB,CAAoB;IAErD,SAAS,CAAC,UAAU,SAAsB;IAC1C,SAAS,CAAC,WAAW,SAAsB;WAE9B,IAAI,CAChB,QAAQ,EAAE,cAAc,GAAG,SAAS,EACpC,WAAW,EAAE,kBAAkB,GAAG,SAAS,EAC3C,MAAM,CAAC,EAAE,MAAM,IAAI;IAYpB,OAAO,CAAC,sBAAsB;IAmB9B,OAAO,CAAC,oBAAoB;IAgB5B,OAAO,CAAC,mBAAmB;YAIb,cAAc;YA6Bd,IAAI;cA8BC,aAAa,IAAI,cAAc;CAiIlD"}

View File

@@ -0,0 +1,270 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { i18n } from "@mariozechner/mini-lit";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
import { Input } from "@mariozechner/mini-lit/dist/Input.js";
import { Label } from "@mariozechner/mini-lit/dist/Label.js";
import { Select } from "@mariozechner/mini-lit/dist/Select.js";
import { html } from "lit";
import { state } from "lit/decorators.js";
import { getAppStorage } from "../storage/app-storage.js";
import { discoverModels } from "../utils/model-discovery.js";
export class CustomProviderDialog extends DialogBase {
constructor() {
super(...arguments);
this.name = "";
this.type = "openai-completions";
this.baseUrl = "";
this.apiKey = "";
this.testing = false;
this.testError = "";
this.discoveredModels = [];
this.modalWidth = "min(800px, 90vw)";
this.modalHeight = "min(700px, 90vh)";
}
static async open(provider, initialType, onSave) {
const dialog = new CustomProviderDialog();
dialog.provider = provider;
dialog.initialType = initialType;
dialog.onSaveCallback = onSave;
document.body.appendChild(dialog);
dialog.initializeFromProvider();
dialog.open();
dialog.requestUpdate();
}
initializeFromProvider() {
if (this.provider) {
this.name = this.provider.name;
this.type = this.provider.type;
this.baseUrl = this.provider.baseUrl;
this.apiKey = this.provider.apiKey || "";
this.discoveredModels = this.provider.models || [];
}
else {
this.name = "";
this.type = this.initialType || "openai-completions";
this.baseUrl = "";
this.updateDefaultBaseUrl();
this.apiKey = "";
this.discoveredModels = [];
}
this.testError = "";
this.testing = false;
}
updateDefaultBaseUrl() {
if (this.baseUrl)
return;
const defaults = {
ollama: "http://localhost:11434",
"llama.cpp": "http://localhost:8080",
vllm: "http://localhost:8000",
lmstudio: "http://localhost:1234",
"openai-completions": "",
"openai-responses": "",
"anthropic-messages": "",
};
this.baseUrl = defaults[this.type] || "";
}
isAutoDiscoveryType() {
return this.type === "ollama" || this.type === "llama.cpp" || this.type === "vllm" || this.type === "lmstudio";
}
async testConnection() {
if (!this.isAutoDiscoveryType())
return;
this.testing = true;
this.testError = "";
this.discoveredModels = [];
try {
const models = await discoverModels(this.type, this.baseUrl, this.apiKey || undefined);
this.discoveredModels = models.map((model) => ({
...model,
provider: this.name || this.type,
}));
this.testError = "";
}
catch (error) {
this.testError = error instanceof Error ? error.message : String(error);
this.discoveredModels = [];
}
finally {
this.testing = false;
this.requestUpdate();
}
}
async save() {
if (!this.name || !this.baseUrl) {
alert(i18n("Please fill in all required fields"));
return;
}
try {
const storage = getAppStorage();
const provider = {
id: this.provider?.id || crypto.randomUUID(),
name: this.name,
type: this.type,
baseUrl: this.baseUrl,
apiKey: this.apiKey || undefined,
models: this.isAutoDiscoveryType() ? undefined : this.provider?.models || [],
};
await storage.customProviders.set(provider);
if (this.onSaveCallback) {
this.onSaveCallback();
}
this.close();
}
catch (error) {
console.error("Failed to save provider:", error);
alert(i18n("Failed to save provider"));
}
}
renderContent() {
const providerTypes = [
{ value: "ollama", label: "Ollama (auto-discovery)" },
{ value: "llama.cpp", label: "llama.cpp (auto-discovery)" },
{ value: "vllm", label: "vLLM (auto-discovery)" },
{ value: "lmstudio", label: "LM Studio (auto-discovery)" },
{ value: "openai-completions", label: "OpenAI Completions Compatible" },
{ value: "openai-responses", label: "OpenAI Responses Compatible" },
{ value: "anthropic-messages", label: "Anthropic Messages Compatible" },
];
return html `
<div class="flex flex-col h-full overflow-hidden">
<div class="p-6 flex-shrink-0 border-b border-border">
<h2 class="text-lg font-semibold text-foreground">
${this.provider ? i18n("Edit Provider") : i18n("Add Provider")}
</h2>
</div>
<div class="flex-1 overflow-y-auto p-6">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
${Label({ htmlFor: "provider-name", children: i18n("Provider Name") })}
${Input({
value: this.name,
placeholder: i18n("e.g., My Ollama Server"),
onInput: (e) => {
this.name = e.target.value;
this.requestUpdate();
},
})}
</div>
<div class="flex flex-col gap-2">
${Label({ htmlFor: "provider-type", children: i18n("Provider Type") })}
${Select({
value: this.type,
options: providerTypes.map((pt) => ({
value: pt.value,
label: pt.label,
})),
onChange: (value) => {
this.type = value;
this.baseUrl = "";
this.updateDefaultBaseUrl();
this.requestUpdate();
},
width: "100%",
})}
</div>
<div class="flex flex-col gap-2">
${Label({ htmlFor: "base-url", children: i18n("Base URL") })}
${Input({
value: this.baseUrl,
placeholder: i18n("e.g., http://localhost:11434"),
onInput: (e) => {
this.baseUrl = e.target.value;
this.requestUpdate();
},
})}
</div>
<div class="flex flex-col gap-2">
${Label({ htmlFor: "api-key", children: i18n("API Key (Optional)") })}
${Input({
type: "password",
value: this.apiKey,
placeholder: i18n("Leave empty if not required"),
onInput: (e) => {
this.apiKey = e.target.value;
this.requestUpdate();
},
})}
</div>
${this.isAutoDiscoveryType()
? html `
<div class="flex flex-col gap-2">
${Button({
onClick: () => this.testConnection(),
variant: "outline",
disabled: this.testing || !this.baseUrl,
children: this.testing ? i18n("Testing...") : i18n("Test Connection"),
})}
${this.testError ? html ` <div class="text-sm text-destructive">${this.testError}</div> ` : ""}
${this.discoveredModels.length > 0
? html `
<div class="text-sm text-muted-foreground">
${i18n("Discovered")} ${this.discoveredModels.length} ${i18n("models")}:
<ul class="list-disc list-inside mt-2">
${this.discoveredModels.slice(0, 5).map((model) => html `<li>${model.name}</li>`)}
${this.discoveredModels.length > 5
? html `<li>...${i18n("and")} ${this.discoveredModels.length - 5} ${i18n("more")}</li>`
: ""}
</ul>
</div>
`
: ""}
</div>
`
: html ` <div class="text-sm text-muted-foreground">
${i18n("For manual provider types, add models after saving the provider.")}
</div>`}
</div>
</div>
<div class="p-6 flex-shrink-0 border-t border-border flex justify-end gap-2">
${Button({
onClick: () => this.close(),
variant: "ghost",
children: i18n("Cancel"),
})}
${Button({
onClick: () => this.save(),
variant: "default",
disabled: !this.name || !this.baseUrl,
children: i18n("Save"),
})}
</div>
</div>
`;
}
}
__decorate([
state()
], CustomProviderDialog.prototype, "name", void 0);
__decorate([
state()
], CustomProviderDialog.prototype, "type", void 0);
__decorate([
state()
], CustomProviderDialog.prototype, "baseUrl", void 0);
__decorate([
state()
], CustomProviderDialog.prototype, "apiKey", void 0);
__decorate([
state()
], CustomProviderDialog.prototype, "testing", void 0);
__decorate([
state()
], CustomProviderDialog.prototype, "testError", void 0);
__decorate([
state()
], CustomProviderDialog.prototype, "discoveredModels", void 0);
customElements.define("custom-provider-dialog", CustomProviderDialog);
//# sourceMappingURL=CustomProviderDialog.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,27 @@
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
import { type Model } from "@mariozechner/pi-ai";
import { type PropertyValues, type TemplateResult } from "lit";
export declare class ModelSelector extends DialogBase {
currentModel: Model<any> | null;
searchQuery: string;
filterThinking: boolean;
filterVision: boolean;
customProvidersLoading: boolean;
selectedIndex: number;
private navigationMode;
private customProviderModels;
private onSelectCallback?;
private scrollContainerRef;
private searchInputRef;
private lastMousePosition;
protected modalWidth: string;
static open(currentModel: Model<any> | null, onSelect: (model: Model<any>) => void): Promise<void>;
firstUpdated(changedProperties: PropertyValues): Promise<void>;
private loadCustomProviders;
private formatTokens;
private handleSelect;
private getFilteredModels;
private scrollToSelected;
protected renderContent(): TemplateResult;
}
//# sourceMappingURL=ModelSelector.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ModelSelector.d.ts","sourceRoot":"","sources":["../../src/dialogs/ModelSelector.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,2CAA2C,CAAC;AACvE,OAAO,EAA2B,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAQ,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAWrE,qBACa,aAAc,SAAQ,UAAU;IACnC,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAQ;IACvC,WAAW,SAAM;IACjB,cAAc,UAAS;IACvB,YAAY,UAAS;IACrB,sBAAsB,UAAS;IAC/B,aAAa,SAAK;IAClB,OAAO,CAAC,cAAc,CAAiC;IACvD,OAAO,CAAC,oBAAoB,CAAoB;IAEzD,OAAO,CAAC,gBAAgB,CAAC,CAA8B;IACvD,OAAO,CAAC,kBAAkB,CAA+B;IACzD,OAAO,CAAC,cAAc,CAAiC;IACvD,OAAO,CAAC,iBAAiB,CAAkB;IAE3C,UAAmB,UAAU,SAAsB;WAEtC,IAAI,CAAC,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI;IAQzE,YAAY,CAAC,iBAAiB,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;YAuD/D,mBAAmB;IA+CjC,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,iBAAiB;IAiDzB,OAAO,CAAC,gBAAgB;cAYL,aAAa,IAAI,cAAc;CA0FlD"}

View File

@@ -0,0 +1,320 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var ModelSelector_1;
import { icon } from "@mariozechner/mini-lit";
import { Badge } from "@mariozechner/mini-lit/dist/Badge.js";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js";
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
import { getModels, getProviders } from "@mariozechner/pi-ai";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import { Brain, Image as ImageIcon } from "lucide";
import { Input } from "../components/Input.js";
import { getAppStorage } from "../storage/app-storage.js";
import { formatModelCost } from "../utils/format.js";
import { i18n } from "../utils/i18n.js";
import { discoverModels } from "../utils/model-discovery.js";
let ModelSelector = ModelSelector_1 = class ModelSelector extends DialogBase {
constructor() {
super(...arguments);
this.currentModel = null;
this.searchQuery = "";
this.filterThinking = false;
this.filterVision = false;
this.customProvidersLoading = false;
this.selectedIndex = 0;
this.navigationMode = "mouse";
this.customProviderModels = [];
this.scrollContainerRef = createRef();
this.searchInputRef = createRef();
this.lastMousePosition = { x: 0, y: 0 };
this.modalWidth = "min(400px, 90vw)";
}
static async open(currentModel, onSelect) {
const selector = new ModelSelector_1();
selector.currentModel = currentModel;
selector.onSelectCallback = onSelect;
selector.open();
selector.loadCustomProviders();
}
async firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
// Wait for dialog to be fully rendered
await this.updateComplete;
// Focus the search input when dialog opens
this.searchInputRef.value?.focus();
// Track actual mouse movement
this.addEventListener("mousemove", (e) => {
// Check if mouse actually moved
if (e.clientX !== this.lastMousePosition.x || e.clientY !== this.lastMousePosition.y) {
this.lastMousePosition = { x: e.clientX, y: e.clientY };
// Only switch to mouse mode on actual mouse movement
if (this.navigationMode === "keyboard") {
this.navigationMode = "mouse";
// Update selection to the item under the mouse
const target = e.target;
const modelItem = target.closest("[data-model-item]");
if (modelItem) {
const allItems = this.scrollContainerRef.value?.querySelectorAll("[data-model-item]");
if (allItems) {
const index = Array.from(allItems).indexOf(modelItem);
if (index !== -1) {
this.selectedIndex = index;
}
}
}
}
}
});
// Add global keyboard handler for the dialog
this.addEventListener("keydown", (e) => {
// Get filtered models to know the bounds
const filteredModels = this.getFilteredModels();
if (e.key === "ArrowDown") {
e.preventDefault();
this.navigationMode = "keyboard";
this.selectedIndex = Math.min(this.selectedIndex + 1, filteredModels.length - 1);
this.scrollToSelected();
}
else if (e.key === "ArrowUp") {
e.preventDefault();
this.navigationMode = "keyboard";
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
this.scrollToSelected();
}
else if (e.key === "Enter") {
e.preventDefault();
if (filteredModels[this.selectedIndex]) {
this.handleSelect(filteredModels[this.selectedIndex].model);
}
}
});
}
async loadCustomProviders() {
this.customProvidersLoading = true;
const allCustomModels = [];
try {
const storage = getAppStorage();
const customProviders = await storage.customProviders.getAll();
// Load models from custom providers
for (const provider of customProviders) {
const isAutoDiscovery = provider.type === "ollama" ||
provider.type === "llama.cpp" ||
provider.type === "vllm" ||
provider.type === "lmstudio";
if (isAutoDiscovery) {
try {
const models = await discoverModels(provider.type, provider.baseUrl, provider.apiKey);
const modelsWithProvider = models.map((model) => ({
...model,
provider: provider.name,
}));
allCustomModels.push(...modelsWithProvider);
}
catch (error) {
console.debug(`Failed to load models from ${provider.name}:`, error);
}
}
else if (provider.models) {
// Manual provider - models already defined
allCustomModels.push(...provider.models);
}
}
}
catch (error) {
console.error("Failed to load custom providers:", error);
}
finally {
this.customProviderModels = allCustomModels;
this.customProvidersLoading = false;
this.requestUpdate();
}
}
formatTokens(tokens) {
if (tokens >= 1000000)
return `${(tokens / 1000000).toFixed(0)}M`;
if (tokens >= 1000)
return `${(tokens / 1000).toFixed(0)}`;
return String(tokens);
}
handleSelect(model) {
if (model) {
this.onSelectCallback?.(model);
this.close();
}
}
getFilteredModels() {
// Collect all models from known providers
const allModels = [];
const knownProviders = getProviders();
for (const provider of knownProviders) {
const models = getModels(provider);
for (const model of models) {
allModels.push({ provider, id: model.id, model });
}
}
// Add custom provider models
for (const model of this.customProviderModels) {
allModels.push({ provider: model.provider, id: model.id, model });
}
// Filter models based on search and capability filters
let filteredModels = allModels;
// Apply search filter
if (this.searchQuery) {
filteredModels = filteredModels.filter(({ provider, id, model }) => {
const searchTokens = this.searchQuery.split(/\s+/).filter((t) => t);
const searchText = `${provider} ${id} ${model.name}`.toLowerCase();
return searchTokens.every((token) => searchText.includes(token));
});
}
// Apply capability filters
if (this.filterThinking) {
filteredModels = filteredModels.filter(({ model }) => model.reasoning);
}
if (this.filterVision) {
filteredModels = filteredModels.filter(({ model }) => model.input.includes("image"));
}
// Sort: current model first, then by provider
filteredModels.sort((a, b) => {
const aIsCurrent = this.currentModel?.id === a.model.id;
const bIsCurrent = this.currentModel?.id === b.model.id;
if (aIsCurrent && !bIsCurrent)
return -1;
if (!aIsCurrent && bIsCurrent)
return 1;
return a.provider.localeCompare(b.provider);
});
return filteredModels;
}
scrollToSelected() {
requestAnimationFrame(() => {
const scrollContainer = this.scrollContainerRef.value;
const selectedElement = scrollContainer?.querySelectorAll("[data-model-item]")[this.selectedIndex];
if (selectedElement) {
selectedElement.scrollIntoView({ block: "nearest", behavior: "smooth" });
}
});
}
renderContent() {
const filteredModels = this.getFilteredModels();
return html `
<!-- Header and Search -->
<div class="p-6 pb-4 flex flex-col gap-4 border-b border-border flex-shrink-0">
${DialogHeader({ title: i18n("Select Model") })}
${Input({
placeholder: i18n("Search models..."),
value: this.searchQuery,
inputRef: this.searchInputRef,
onInput: (e) => {
this.searchQuery = e.target.value;
this.selectedIndex = 0;
// Reset scroll position when search changes
if (this.scrollContainerRef.value) {
this.scrollContainerRef.value.scrollTop = 0;
}
},
})}
<div class="flex gap-2">
${Button({
variant: this.filterThinking ? "default" : "secondary",
size: "sm",
onClick: () => {
this.filterThinking = !this.filterThinking;
this.selectedIndex = 0;
if (this.scrollContainerRef.value) {
this.scrollContainerRef.value.scrollTop = 0;
}
},
className: "rounded-full",
children: html `<span class="inline-flex items-center gap-1">${icon(Brain, "sm")} ${i18n("Thinking")}</span>`,
})}
${Button({
variant: this.filterVision ? "default" : "secondary",
size: "sm",
onClick: () => {
this.filterVision = !this.filterVision;
this.selectedIndex = 0;
if (this.scrollContainerRef.value) {
this.scrollContainerRef.value.scrollTop = 0;
}
},
className: "rounded-full",
children: html `<span class="inline-flex items-center gap-1">${icon(ImageIcon, "sm")} ${i18n("Vision")}</span>`,
})}
</div>
</div>
<!-- Scrollable model list -->
<div class="flex-1 overflow-y-auto" ${ref(this.scrollContainerRef)}>
${filteredModels.map(({ provider, id, model }, index) => {
const isCurrent = this.currentModel?.id === model.id && this.currentModel?.provider === model.provider;
const isSelected = index === this.selectedIndex;
return html `
<div
data-model-item
class="px-4 py-3 ${this.navigationMode === "mouse" ? "hover:bg-muted" : ""} cursor-pointer border-b border-border ${isSelected ? "bg-accent" : ""}"
@click=${() => this.handleSelect(model)}
@mouseenter=${() => {
// Only update selection in mouse mode
if (this.navigationMode === "mouse") {
this.selectedIndex = index;
}
}}
>
<div class="flex items-center justify-between gap-2 mb-1">
<div class="flex items-center gap-2 flex-1 min-w-0">
<span class="text-sm font-medium text-foreground truncate">${id}</span>
${isCurrent ? html `<span class="text-green-500">✓</span>` : ""}
</div>
${Badge(provider, "outline")}
</div>
<div class="flex items-center justify-between text-xs text-muted-foreground">
<div class="flex items-center gap-2">
<span class="${model.reasoning ? "" : "opacity-30"}">${icon(Brain, "sm")}</span>
<span class="${model.input.includes("image") ? "" : "opacity-30"}">${icon(ImageIcon, "sm")}</span>
<span>${this.formatTokens(model.contextWindow)}K/${this.formatTokens(model.maxTokens)}K</span>
</div>
<span>${formatModelCost(model.cost)}</span>
</div>
</div>
`;
})}
</div>
`;
}
};
__decorate([
state()
], ModelSelector.prototype, "currentModel", void 0);
__decorate([
state()
], ModelSelector.prototype, "searchQuery", void 0);
__decorate([
state()
], ModelSelector.prototype, "filterThinking", void 0);
__decorate([
state()
], ModelSelector.prototype, "filterVision", void 0);
__decorate([
state()
], ModelSelector.prototype, "customProvidersLoading", void 0);
__decorate([
state()
], ModelSelector.prototype, "selectedIndex", void 0);
__decorate([
state()
], ModelSelector.prototype, "navigationMode", void 0);
__decorate([
state()
], ModelSelector.prototype, "customProviderModels", void 0);
ModelSelector = ModelSelector_1 = __decorate([
customElement("agent-model-selector")
], ModelSelector);
export { ModelSelector };
//# sourceMappingURL=ModelSelector.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,17 @@
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
export declare class PersistentStorageDialog extends DialogBase {
private requesting;
private resolvePromise?;
protected modalWidth: string;
protected modalHeight: string;
/**
* Request persistent storage permission.
* Returns true if browser granted persistent storage, false otherwise.
*/
static request(): Promise<boolean>;
private handleGrant;
private handleDeny;
close(): void;
protected renderContent(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=PersistentStorageDialog.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"PersistentStorageDialog.d.ts","sourceRoot":"","sources":["../../src/dialogs/PersistentStorageDialog.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,2CAA2C,CAAC;AAKvE,qBACa,uBAAwB,SAAQ,UAAU;IAC7C,OAAO,CAAC,UAAU,CAAS;IAEpC,OAAO,CAAC,cAAc,CAAC,CAAkC;IAEzD,SAAS,CAAC,UAAU,SAAsB;IAC1C,SAAS,CAAC,WAAW,SAAU;IAE/B;;;OAGG;WACU,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IA2CxC,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,UAAU;IAQT,KAAK;cAOK,aAAa;CAyDhC"}

View File

@@ -0,0 +1,147 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var PersistentStorageDialog_1;
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js";
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { i18n } from "../utils/i18n.js";
let PersistentStorageDialog = PersistentStorageDialog_1 = class PersistentStorageDialog extends DialogBase {
constructor() {
super(...arguments);
this.requesting = false;
this.modalWidth = "min(500px, 90vw)";
this.modalHeight = "auto";
}
/**
* Request persistent storage permission.
* Returns true if browser granted persistent storage, false otherwise.
*/
static async request() {
// Check if already persisted
if (navigator.storage?.persisted) {
const alreadyPersisted = await navigator.storage.persisted();
if (alreadyPersisted) {
console.log("✓ Persistent storage already granted");
return true;
}
}
// Show dialog and wait for user response
const dialog = new PersistentStorageDialog_1();
dialog.open();
const userApproved = await new Promise((resolve) => {
dialog.resolvePromise = resolve;
});
if (!userApproved) {
console.warn("⚠ User declined persistent storage - sessions may be lost");
return false;
}
// User approved, request from browser
if (!navigator.storage?.persist) {
console.warn("⚠ Persistent storage API not available");
return false;
}
try {
const granted = await navigator.storage.persist();
if (granted) {
console.log("✓ Persistent storage granted - sessions will be preserved");
}
else {
console.warn("⚠ Browser denied persistent storage - sessions may be lost under storage pressure");
}
return granted;
}
catch (error) {
console.error("Failed to request persistent storage:", error);
return false;
}
}
handleGrant() {
if (this.resolvePromise) {
this.resolvePromise(true);
this.resolvePromise = undefined;
}
this.close();
}
handleDeny() {
if (this.resolvePromise) {
this.resolvePromise(false);
this.resolvePromise = undefined;
}
this.close();
}
close() {
super.close();
if (this.resolvePromise) {
this.resolvePromise(false);
}
}
renderContent() {
return html `
${DialogContent({
children: html `
${DialogHeader({
title: i18n("Storage Permission Required"),
description: i18n("This app needs persistent storage to save your conversations"),
})}
<div class="mt-4 flex flex-col gap-4">
<div class="flex gap-3 p-4 bg-warning/10 border border-warning/20 rounded-lg">
<div class="flex-shrink-0 text-warning">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path 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"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</div>
<div class="text-sm">
<p class="font-medium text-foreground mb-1">${i18n("Why is this needed?")}</p>
<p class="text-muted-foreground">
${i18n("Without persistent storage, your browser may delete saved conversations when it needs disk space. Granting this permission ensures your chat history is preserved.")}
</p>
</div>
</div>
<div class="text-sm text-muted-foreground">
<p class="mb-2">${i18n("What this means:")}</p>
<ul class="list-disc list-inside space-y-1 ml-2">
<li>${i18n("Your conversations will be saved locally in your browser")}</li>
<li>${i18n("Data will not be deleted automatically to free up space")}</li>
<li>${i18n("You can still manually clear data at any time")}</li>
<li>${i18n("No data is sent to external servers")}</li>
</ul>
</div>
</div>
<div class="mt-6 flex gap-3 justify-end">
${Button({
variant: "outline",
onClick: () => this.handleDeny(),
disabled: this.requesting,
children: i18n("Continue Anyway"),
})}
${Button({
variant: "default",
onClick: () => this.handleGrant(),
disabled: this.requesting,
children: this.requesting ? i18n("Requesting...") : i18n("Grant Permission"),
})}
</div>
`,
})}
`;
}
};
__decorate([
state()
], PersistentStorageDialog.prototype, "requesting", void 0);
PersistentStorageDialog = PersistentStorageDialog_1 = __decorate([
customElement("persistent-storage-dialog")
], PersistentStorageDialog);
export { PersistentStorageDialog };
//# sourceMappingURL=PersistentStorageDialog.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"PersistentStorageDialog.js","sourceRoot":"","sources":["../../src/dialogs/PersistentStorageDialog.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,20 @@
import { type TemplateResult } from "lit";
import "../components/CustomProviderCard.js";
import "../components/ProviderKeyInput.js";
import { SettingsTab } from "./SettingsDialog.js";
export declare class ProvidersModelsTab extends SettingsTab {
private customProviders;
private providerStatus;
connectedCallback(): Promise<void>;
private loadCustomProviders;
getTabName(): string;
private checkProviderStatus;
private renderKnownProviders;
private renderCustomProviders;
private addCustomProvider;
private editProvider;
private refreshProvider;
private deleteProvider;
render(): TemplateResult;
}
//# sourceMappingURL=ProvidersModelsTab.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ProvidersModelsTab.d.ts","sourceRoot":"","sources":["../../src/dialogs/ProvidersModelsTab.ts"],"names":[],"mappings":"AAGA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAEhD,OAAO,qCAAqC,CAAC;AAC7C,OAAO,mCAAmC,CAAC;AAS3C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,qBACa,kBAAmB,SAAQ,WAAW;IACzC,OAAO,CAAC,eAAe,CAAwB;IAC/C,OAAO,CAAC,cAAc,CAGjB;IAEC,iBAAiB;YAKlB,mBAAmB;IAqBjC,UAAU,IAAI,MAAM;YAIN,mBAAmB;IAkBjC,OAAO,CAAC,oBAAoB;IAkB5B,OAAO,CAAC,qBAAqB;YA0Df,iBAAiB;YAOjB,YAAY;YAOZ,eAAe;YAwBf,cAAc;IAe5B,MAAM,IAAI,cAAc;CASxB"}

View File

@@ -0,0 +1,191 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { i18n } from "@mariozechner/mini-lit";
import { Select } from "@mariozechner/mini-lit/dist/Select.js";
import { getProviders } from "@mariozechner/pi-ai";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import "../components/CustomProviderCard.js";
import "../components/ProviderKeyInput.js";
import { getAppStorage } from "../storage/app-storage.js";
import { discoverModels } from "../utils/model-discovery.js";
import { CustomProviderDialog } from "./CustomProviderDialog.js";
import { SettingsTab } from "./SettingsDialog.js";
let ProvidersModelsTab = class ProvidersModelsTab extends SettingsTab {
constructor() {
super(...arguments);
this.customProviders = [];
this.providerStatus = new Map();
}
async connectedCallback() {
super.connectedCallback();
await this.loadCustomProviders();
}
async loadCustomProviders() {
try {
const storage = getAppStorage();
this.customProviders = await storage.customProviders.getAll();
// Check status for auto-discovery providers
for (const provider of this.customProviders) {
const isAutoDiscovery = provider.type === "ollama" ||
provider.type === "llama.cpp" ||
provider.type === "vllm" ||
provider.type === "lmstudio";
if (isAutoDiscovery) {
this.checkProviderStatus(provider);
}
}
}
catch (error) {
console.error("Failed to load custom providers:", error);
}
}
getTabName() {
return "Providers & Models";
}
async checkProviderStatus(provider) {
this.providerStatus.set(provider.id, { modelCount: 0, status: "checking" });
this.requestUpdate();
try {
const models = await discoverModels(provider.type, provider.baseUrl, provider.apiKey);
this.providerStatus.set(provider.id, { modelCount: models.length, status: "connected" });
}
catch (error) {
this.providerStatus.set(provider.id, { modelCount: 0, status: "disconnected" });
}
this.requestUpdate();
}
renderKnownProviders() {
const providers = getProviders();
return html `
<div class="flex flex-col gap-6">
<div>
<h3 class="text-sm font-semibold text-foreground mb-2">Cloud Providers</h3>
<p class="text-sm text-muted-foreground mb-4">
Cloud LLM providers with predefined models. API keys are stored locally in your browser.
</p>
</div>
<div class="flex flex-col gap-6">
${providers.map((provider) => html ` <provider-key-input .provider=${provider}></provider-key-input> `)}
</div>
</div>
`;
}
renderCustomProviders() {
const isAutoDiscovery = (type) => type === "ollama" || type === "llama.cpp" || type === "vllm" || type === "lmstudio";
return html `
<div class="flex flex-col gap-6">
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-semibold text-foreground mb-2">Custom Providers</h3>
<p class="text-sm text-muted-foreground">
User-configured servers with auto-discovered or manually defined models.
</p>
</div>
${Select({
placeholder: i18n("Add Provider"),
options: [
{ value: "ollama", label: "Ollama" },
{ value: "llama.cpp", label: "llama.cpp" },
{ value: "vllm", label: "vLLM" },
{ value: "lmstudio", label: "LM Studio" },
{ value: "openai-completions", label: i18n("OpenAI Completions Compatible") },
{ value: "openai-responses", label: i18n("OpenAI Responses Compatible") },
{ value: "anthropic-messages", label: i18n("Anthropic Messages Compatible") },
],
onChange: (value) => this.addCustomProvider(value),
variant: "outline",
size: "sm",
})}
</div>
${this.customProviders.length === 0
? html `
<div class="text-sm text-muted-foreground text-center py-8">
No custom providers configured. Click 'Add Provider' to get started.
</div>
`
: html `
<div class="flex flex-col gap-4">
${this.customProviders.map((provider) => html `
<custom-provider-card
.provider=${provider}
.isAutoDiscovery=${isAutoDiscovery(provider.type)}
.status=${this.providerStatus.get(provider.id)}
.onRefresh=${(p) => this.refreshProvider(p)}
.onEdit=${(p) => this.editProvider(p)}
.onDelete=${(p) => this.deleteProvider(p)}
></custom-provider-card>
`)}
</div>
`}
</div>
`;
}
async addCustomProvider(type) {
await CustomProviderDialog.open(undefined, type, async () => {
await this.loadCustomProviders();
this.requestUpdate();
});
}
async editProvider(provider) {
await CustomProviderDialog.open(provider, undefined, async () => {
await this.loadCustomProviders();
this.requestUpdate();
});
}
async refreshProvider(provider) {
this.providerStatus.set(provider.id, { modelCount: 0, status: "checking" });
this.requestUpdate();
try {
const models = await discoverModels(provider.type, provider.baseUrl, provider.apiKey);
this.providerStatus.set(provider.id, { modelCount: models.length, status: "connected" });
this.requestUpdate();
console.log(`Refreshed ${models.length} models from ${provider.name}`);
}
catch (error) {
this.providerStatus.set(provider.id, { modelCount: 0, status: "disconnected" });
this.requestUpdate();
console.error(`Failed to refresh provider ${provider.name}:`, error);
alert(`Failed to refresh provider: ${error instanceof Error ? error.message : String(error)}`);
}
}
async deleteProvider(provider) {
if (!confirm("Are you sure you want to delete this provider?")) {
return;
}
try {
const storage = getAppStorage();
await storage.customProviders.delete(provider.id);
await this.loadCustomProviders();
this.requestUpdate();
}
catch (error) {
console.error("Failed to delete provider:", error);
}
}
render() {
return html `
<div class="flex flex-col gap-8">
${this.renderKnownProviders()}
<div class="border-t border-border"></div>
${this.renderCustomProviders()}
</div>
`;
}
};
__decorate([
state()
], ProvidersModelsTab.prototype, "customProviders", void 0);
__decorate([
state()
], ProvidersModelsTab.prototype, "providerStatus", void 0);
ProvidersModelsTab = __decorate([
customElement("providers-models-tab")
], ProvidersModelsTab);
export { ProvidersModelsTab };
//# sourceMappingURL=ProvidersModelsTab.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
export declare class SessionListDialog extends DialogBase {
private sessions;
private loading;
private onSelectCallback?;
private onDeleteCallback?;
private deletedSessions;
private closedViaSelection;
protected modalWidth: string;
protected modalHeight: string;
static open(onSelect: (sessionId: string) => void, onDelete?: (sessionId: string) => void): Promise<void>;
private loadSessions;
private handleDelete;
close(): void;
private handleSelect;
private formatDate;
protected renderContent(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=SessionListDialog.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SessionListDialog.d.ts","sourceRoot":"","sources":["../../src/dialogs/SessionListDialog.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,2CAA2C,CAAC;AAQvE,qBACa,iBAAkB,SAAQ,UAAU;IACvC,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,OAAO,CAAQ;IAEhC,OAAO,CAAC,gBAAgB,CAAC,CAA8B;IACvD,OAAO,CAAC,gBAAgB,CAAC,CAA8B;IACvD,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,kBAAkB,CAAS;IAEnC,SAAS,CAAC,UAAU,SAAsB;IAC1C,SAAS,CAAC,WAAW,SAAsB;WAE9B,IAAI,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI;YAQjF,YAAY;YAaZ,YAAY;IAqBjB,KAAK;IAWd,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,UAAU;cAiBC,aAAa;CAiDhC"}

View File

@@ -0,0 +1,154 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var SessionListDialog_1;
import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js";
import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { getAppStorage } from "../storage/app-storage.js";
import { formatUsage } from "../utils/format.js";
import { i18n } from "../utils/i18n.js";
let SessionListDialog = SessionListDialog_1 = class SessionListDialog extends DialogBase {
constructor() {
super(...arguments);
this.sessions = [];
this.loading = true;
this.deletedSessions = new Set();
this.closedViaSelection = false;
this.modalWidth = "min(600px, 90vw)";
this.modalHeight = "min(700px, 90vh)";
}
static async open(onSelect, onDelete) {
const dialog = new SessionListDialog_1();
dialog.onSelectCallback = onSelect;
dialog.onDeleteCallback = onDelete;
dialog.open();
await dialog.loadSessions();
}
async loadSessions() {
this.loading = true;
try {
const storage = getAppStorage();
this.sessions = await storage.sessions.getAllMetadata();
}
catch (err) {
console.error("Failed to load sessions:", err);
this.sessions = [];
}
finally {
this.loading = false;
}
}
async handleDelete(sessionId, event) {
event.stopPropagation();
if (!confirm(i18n("Delete this session?"))) {
return;
}
try {
const storage = getAppStorage();
if (!storage.sessions)
return;
await storage.sessions.deleteSession(sessionId);
await this.loadSessions();
// Track deleted session
this.deletedSessions.add(sessionId);
}
catch (err) {
console.error("Failed to delete session:", err);
}
}
close() {
super.close();
// Only notify about deleted sessions if dialog wasn't closed via selection
if (!this.closedViaSelection && this.onDeleteCallback && this.deletedSessions.size > 0) {
for (const sessionId of this.deletedSessions) {
this.onDeleteCallback(sessionId);
}
}
}
handleSelect(sessionId) {
this.closedViaSelection = true;
if (this.onSelectCallback) {
this.onSelectCallback(sessionId);
}
this.close();
}
formatDate(isoString) {
const date = new Date(isoString);
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) {
return i18n("Today");
}
else if (days === 1) {
return i18n("Yesterday");
}
else if (days < 7) {
return i18n("{days} days ago").replace("{days}", days.toString());
}
else {
return date.toLocaleDateString();
}
}
renderContent() {
return html `
${DialogContent({
className: "h-full flex flex-col",
children: html `
${DialogHeader({
title: i18n("Sessions"),
description: i18n("Load a previous conversation"),
})}
<div class="flex-1 overflow-y-auto mt-4 space-y-2">
${this.loading
? html `<div class="text-center py-8 text-muted-foreground">${i18n("Loading...")}</div>`
: this.sessions.length === 0
? html `<div class="text-center py-8 text-muted-foreground">${i18n("No sessions yet")}</div>`
: this.sessions.map((session) => html `
<div
class="group flex items-start gap-3 p-3 rounded-lg border border-border hover:bg-secondary/50 cursor-pointer transition-colors"
@click=${() => this.handleSelect(session.id)}
>
<div class="flex-1 min-w-0">
<div class="font-medium text-sm text-foreground truncate">${session.title}</div>
<div class="text-xs text-muted-foreground mt-1">${this.formatDate(session.lastModified)}</div>
<div class="text-xs text-muted-foreground mt-1">
${session.messageCount} ${i18n("messages")} · ${formatUsage(session.usage)}
</div>
</div>
<button
class="opacity-0 group-hover:opacity-100 p-1 rounded hover:bg-destructive/10 text-destructive transition-opacity"
@click=${(e) => this.handleDelete(session.id, e)}
title=${i18n("Delete")}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
</svg>
</button>
</div>
`)}
</div>
`,
})}
`;
}
};
__decorate([
state()
], SessionListDialog.prototype, "sessions", void 0);
__decorate([
state()
], SessionListDialog.prototype, "loading", void 0);
SessionListDialog = SessionListDialog_1 = __decorate([
customElement("session-list-dialog")
], SessionListDialog);
export { SessionListDialog };
//# sourceMappingURL=SessionListDialog.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SessionListDialog.js","sourceRoot":"","sources":["../../src/dialogs/SessionListDialog.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,30 @@
import { LitElement, type TemplateResult } from "lit";
import "../components/ProviderKeyInput.js";
export declare abstract class SettingsTab extends LitElement {
abstract getTabName(): string;
protected createRenderRoot(): this;
}
export declare class ApiKeysTab extends SettingsTab {
getTabName(): string;
render(): TemplateResult;
}
export declare class ProxyTab extends SettingsTab {
private proxyEnabled;
private proxyUrl;
connectedCallback(): Promise<void>;
private saveProxySettings;
getTabName(): string;
render(): TemplateResult;
}
export declare class SettingsDialog extends LitElement {
tabs: SettingsTab[];
private isOpen;
private activeTabIndex;
protected createRenderRoot(): this;
static open(tabs: SettingsTab[]): Promise<void>;
private setActiveTab;
private renderSidebarItem;
private renderMobileTab;
render(): TemplateResult;
}
//# sourceMappingURL=SettingsDialog.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SettingsDialog.d.ts","sourceRoot":"","sources":["../../src/dialogs/SettingsDialog.ts"],"names":[],"mappings":"AAMA,OAAO,EAAQ,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAE5D,OAAO,mCAAmC,CAAC;AAI3C,8BAAsB,WAAY,SAAQ,UAAU;IACnD,QAAQ,CAAC,UAAU,IAAI,MAAM;IAE7B,SAAS,CAAC,gBAAgB;CAG1B;AAGD,qBACa,UAAW,SAAQ,WAAW;IAC1C,UAAU,IAAI,MAAM;IAIpB,MAAM,IAAI,cAAc;CAYxB;AAGD,qBACa,QAAS,SAAQ,WAAW;IAC/B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAA2B;IAErC,iBAAiB;YAelB,iBAAiB;IAU/B,UAAU,IAAI,MAAM;IAIpB,MAAM,IAAI,cAAc;CAoCxB;AAED,qBACa,cAAe,SAAQ,UAAU;IACA,IAAI,EAAE,WAAW,EAAE,CAAM;IAC7D,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,cAAc,CAAK;IAEpC,SAAS,CAAC,gBAAgB;WAIb,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE;IAOrC,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,iBAAiB;IAgBzB,OAAO,CAAC,eAAe;IAcvB,MAAM;CAgDN"}

View File

@@ -0,0 +1,229 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var SettingsDialog_1;
import { i18n } from "@mariozechner/mini-lit";
import { Dialog, DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js";
import { Input } from "@mariozechner/mini-lit/dist/Input.js";
import { Label } from "@mariozechner/mini-lit/dist/Label.js";
import { Switch } from "@mariozechner/mini-lit/dist/Switch.js";
import { getProviders } from "@mariozechner/pi-ai";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import "../components/ProviderKeyInput.js";
import { getAppStorage } from "../storage/app-storage.js";
// Base class for settings tabs
export class SettingsTab extends LitElement {
createRenderRoot() {
return this;
}
}
// API Keys Tab
let ApiKeysTab = class ApiKeysTab extends SettingsTab {
getTabName() {
return i18n("API Keys");
}
render() {
const providers = getProviders();
return html `
<div class="flex flex-col gap-6">
<p class="text-sm text-muted-foreground">
${i18n("Configure API keys for LLM providers. Keys are stored locally in your browser.")}
</p>
${providers.map((provider) => html `<provider-key-input .provider=${provider}></provider-key-input>`)}
</div>
`;
}
};
ApiKeysTab = __decorate([
customElement("api-keys-tab")
], ApiKeysTab);
export { ApiKeysTab };
// Proxy Tab
let ProxyTab = class ProxyTab extends SettingsTab {
constructor() {
super(...arguments);
this.proxyEnabled = false;
this.proxyUrl = "http://localhost:3001";
}
async connectedCallback() {
super.connectedCallback();
// Load proxy settings when tab is connected
try {
const storage = getAppStorage();
const enabled = await storage.settings.get("proxy.enabled");
const url = await storage.settings.get("proxy.url");
if (enabled !== null)
this.proxyEnabled = enabled;
if (url !== null)
this.proxyUrl = url;
}
catch (error) {
console.error("Failed to load proxy settings:", error);
}
}
async saveProxySettings() {
try {
const storage = getAppStorage();
await storage.settings.set("proxy.enabled", this.proxyEnabled);
await storage.settings.set("proxy.url", this.proxyUrl);
}
catch (error) {
console.error("Failed to save proxy settings:", error);
}
}
getTabName() {
return i18n("Proxy");
}
render() {
return html `
<div class="flex flex-col gap-4">
<p class="text-sm text-muted-foreground">
${i18n("Allows browser-based apps to bypass CORS restrictions when calling LLM providers. Required for Z-AI and Anthropic with OAuth token.")}
</p>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-foreground">${i18n("Use CORS Proxy")}</span>
${Switch({
checked: this.proxyEnabled,
onChange: (checked) => {
this.proxyEnabled = checked;
this.saveProxySettings();
},
})}
</div>
<div class="space-y-2">
${Label({ children: i18n("Proxy URL") })}
${Input({
type: "text",
value: this.proxyUrl,
disabled: !this.proxyEnabled,
onInput: (e) => {
this.proxyUrl = e.target.value;
},
onChange: () => this.saveProxySettings(),
})}
<p class="text-xs text-muted-foreground">
${i18n("Format: The proxy must accept requests as <proxy-url>/?url=<target-url>")}
</p>
</div>
</div>
`;
}
};
__decorate([
state()
], ProxyTab.prototype, "proxyEnabled", void 0);
__decorate([
state()
], ProxyTab.prototype, "proxyUrl", void 0);
ProxyTab = __decorate([
customElement("proxy-tab")
], ProxyTab);
export { ProxyTab };
let SettingsDialog = SettingsDialog_1 = class SettingsDialog extends LitElement {
constructor() {
super(...arguments);
this.tabs = [];
this.isOpen = false;
this.activeTabIndex = 0;
}
createRenderRoot() {
return this;
}
static async open(tabs) {
const dialog = new SettingsDialog_1();
dialog.tabs = tabs;
dialog.isOpen = true;
document.body.appendChild(dialog);
}
setActiveTab(index) {
this.activeTabIndex = index;
}
renderSidebarItem(tab, index) {
const isActive = this.activeTabIndex === index;
return html `
<button
class="w-full text-left px-4 py-3 rounded-md transition-colors ${isActive
? "bg-secondary text-foreground font-medium"
: "text-muted-foreground hover:bg-secondary/50 hover:text-foreground"}"
@click=${() => this.setActiveTab(index)}
>
${tab.getTabName()}
</button>
`;
}
renderMobileTab(tab, index) {
const isActive = this.activeTabIndex === index;
return html `
<button
class="px-3 py-2 text-sm font-medium transition-colors ${isActive ? "border-b-2 border-primary text-foreground" : "text-muted-foreground hover:text-foreground"}"
@click=${() => this.setActiveTab(index)}
>
${tab.getTabName()}
</button>
`;
}
render() {
if (this.tabs.length === 0) {
return html ``;
}
return Dialog({
isOpen: this.isOpen,
onClose: () => {
this.isOpen = false;
this.remove();
},
width: "min(1000px, 90vw)",
height: "min(800px, 90vh)",
backdropClassName: "bg-black/50 backdrop-blur-sm",
children: html `
${DialogContent({
className: "h-full p-6",
children: html `
<div class="flex flex-col h-full overflow-hidden">
<!-- Header -->
<div class="pb-4 flex-shrink-0">${DialogHeader({ title: i18n("Settings") })}</div>
<!-- Mobile Tabs -->
<div class="md:hidden flex flex-shrink-0 pb-4">
${this.tabs.map((tab, index) => this.renderMobileTab(tab, index))}
</div>
<!-- Layout -->
<div class="flex flex-1 overflow-hidden">
<!-- Sidebar (desktop only) -->
<div class="hidden md:block w-64 flex-shrink-0 space-y-1">
${this.tabs.map((tab, index) => this.renderSidebarItem(tab, index))}
</div>
<!-- Content -->
<div class="flex-1 overflow-y-auto md:pl-6">
${this.tabs.map((tab, index) => html `<div style="display: ${this.activeTabIndex === index ? "block" : "none"}">${tab}</div>`)}
</div>
</div>
</div>
`,
})}
`,
});
}
};
__decorate([
property({ type: Array, attribute: false })
], SettingsDialog.prototype, "tabs", void 0);
__decorate([
state()
], SettingsDialog.prototype, "isOpen", void 0);
__decorate([
state()
], SettingsDialog.prototype, "activeTabIndex", void 0);
SettingsDialog = SettingsDialog_1 = __decorate([
customElement("settings-dialog")
], SettingsDialog);
export { SettingsDialog };
//# sourceMappingURL=SettingsDialog.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SettingsDialog.js","sourceRoot":"","sources":["../../src/dialogs/SettingsDialog.ts"],"names":[],"mappings":"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"}