update
This commit is contained in:
1 parent
9779c4d1ca
commit
1b578c111a
719 files changed
+191593
-98
No files matched your search
@@ -0,0 +1,472 @@
|
||||
import { parseAsync } from "docx-preview";
|
||||
import JSZip from "jszip";
|
||||
import type { PDFDocumentProxy } from "pdfjs-dist";
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
import * as XLSX from "xlsx";
|
||||
import { i18n } from "./i18n.js";
|
||||
|
||||
// Configure PDF.js worker - we'll need to bundle this
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString();
|
||||
|
||||
export interface Attachment {
|
||||
id: string;
|
||||
type: "image" | "document";
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
content: string; // base64 encoded original data (without data URL prefix)
|
||||
extractedText?: string; // For documents: <pdf filename="..."><page number="1">text</page></pdf>
|
||||
preview?: string; // base64 image preview (first page for PDFs, or same as content for images)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an attachment from various sources
|
||||
* @param source - URL string, File, Blob, or ArrayBuffer
|
||||
* @param fileName - Optional filename override
|
||||
* @returns Promise<Attachment>
|
||||
* @throws Error if loading fails
|
||||
*/
|
||||
export async function loadAttachment(
|
||||
source: string | File | Blob | ArrayBuffer,
|
||||
fileName?: string,
|
||||
): Promise<Attachment> {
|
||||
let arrayBuffer: ArrayBuffer;
|
||||
let detectedFileName = fileName || "unnamed";
|
||||
let mimeType = "application/octet-stream";
|
||||
let size = 0;
|
||||
|
||||
// Convert source to ArrayBuffer
|
||||
if (typeof source === "string") {
|
||||
// It's a URL - fetch it
|
||||
const response = await fetch(source);
|
||||
if (!response.ok) {
|
||||
throw new Error(i18n("Failed to fetch file"));
|
||||
}
|
||||
arrayBuffer = await response.arrayBuffer();
|
||||
size = arrayBuffer.byteLength;
|
||||
mimeType = response.headers.get("content-type") || mimeType;
|
||||
if (!fileName) {
|
||||
// Try to extract filename from URL
|
||||
const urlParts = source.split("/");
|
||||
detectedFileName = urlParts[urlParts.length - 1] || "document";
|
||||
}
|
||||
} else if (source instanceof File) {
|
||||
arrayBuffer = await source.arrayBuffer();
|
||||
size = source.size;
|
||||
mimeType = source.type || mimeType;
|
||||
detectedFileName = fileName || source.name;
|
||||
} else if (source instanceof Blob) {
|
||||
arrayBuffer = await source.arrayBuffer();
|
||||
size = source.size;
|
||||
mimeType = source.type || mimeType;
|
||||
} else if (source instanceof ArrayBuffer) {
|
||||
arrayBuffer = source;
|
||||
size = source.byteLength;
|
||||
} else {
|
||||
throw new Error(i18n("Invalid source type"));
|
||||
}
|
||||
|
||||
// Convert ArrayBuffer to base64 - handle large files properly
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
let binary = "";
|
||||
const chunkSize = 0x8000; // Process in 32KB chunks to avoid stack overflow
|
||||
for (let i = 0; i < uint8Array.length; i += chunkSize) {
|
||||
const chunk = uint8Array.slice(i, i + chunkSize);
|
||||
binary += String.fromCharCode(...chunk);
|
||||
}
|
||||
const base64Content = btoa(binary);
|
||||
|
||||
// Detect type and process accordingly
|
||||
const id = `${detectedFileName}_${Date.now()}_${Math.random()}`;
|
||||
|
||||
// Check if it's a PDF
|
||||
if (mimeType === "application/pdf" || detectedFileName.toLowerCase().endsWith(".pdf")) {
|
||||
const { extractedText, preview } = await processPdf(arrayBuffer, detectedFileName);
|
||||
return {
|
||||
id,
|
||||
type: "document",
|
||||
fileName: detectedFileName,
|
||||
mimeType: "application/pdf",
|
||||
size,
|
||||
content: base64Content,
|
||||
extractedText,
|
||||
preview,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's a DOCX file
|
||||
if (
|
||||
mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
|
||||
detectedFileName.toLowerCase().endsWith(".docx")
|
||||
) {
|
||||
const { extractedText } = await processDocx(arrayBuffer, detectedFileName);
|
||||
return {
|
||||
id,
|
||||
type: "document",
|
||||
fileName: detectedFileName,
|
||||
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
size,
|
||||
content: base64Content,
|
||||
extractedText,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's a PPTX file
|
||||
if (
|
||||
mimeType === "application/vnd.openxmlformats-officedocument.presentationml.presentation" ||
|
||||
detectedFileName.toLowerCase().endsWith(".pptx")
|
||||
) {
|
||||
const { extractedText } = await processPptx(arrayBuffer, detectedFileName);
|
||||
return {
|
||||
id,
|
||||
type: "document",
|
||||
fileName: detectedFileName,
|
||||
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
size,
|
||||
content: base64Content,
|
||||
extractedText,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's an Excel file (XLSX/XLS)
|
||||
const excelMimeTypes = [
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-excel",
|
||||
];
|
||||
if (
|
||||
excelMimeTypes.includes(mimeType) ||
|
||||
detectedFileName.toLowerCase().endsWith(".xlsx") ||
|
||||
detectedFileName.toLowerCase().endsWith(".xls")
|
||||
) {
|
||||
const { extractedText } = await processExcel(arrayBuffer, detectedFileName);
|
||||
return {
|
||||
id,
|
||||
type: "document",
|
||||
fileName: detectedFileName,
|
||||
mimeType: mimeType.startsWith("application/vnd")
|
||||
? mimeType
|
||||
: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
size,
|
||||
content: base64Content,
|
||||
extractedText,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's an image
|
||||
if (mimeType.startsWith("image/")) {
|
||||
return {
|
||||
id,
|
||||
type: "image",
|
||||
fileName: detectedFileName,
|
||||
mimeType,
|
||||
size,
|
||||
content: base64Content,
|
||||
preview: base64Content, // For images, preview is the same as content
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's a text document
|
||||
const textExtensions = [
|
||||
".txt",
|
||||
".md",
|
||||
".json",
|
||||
".xml",
|
||||
".html",
|
||||
".css",
|
||||
".js",
|
||||
".ts",
|
||||
".jsx",
|
||||
".tsx",
|
||||
".yml",
|
||||
".yaml",
|
||||
];
|
||||
const isTextFile =
|
||||
mimeType.startsWith("text/") || textExtensions.some((ext) => detectedFileName.toLowerCase().endsWith(ext));
|
||||
|
||||
if (isTextFile) {
|
||||
const decoder = new TextDecoder();
|
||||
const text = decoder.decode(arrayBuffer);
|
||||
return {
|
||||
id,
|
||||
type: "document",
|
||||
fileName: detectedFileName,
|
||||
mimeType: mimeType.startsWith("text/") ? mimeType : "text/plain",
|
||||
size,
|
||||
content: base64Content,
|
||||
extractedText: text,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported file type: ${mimeType}`);
|
||||
}
|
||||
|
||||
async function processPdf(
|
||||
arrayBuffer: ArrayBuffer,
|
||||
fileName: string,
|
||||
): Promise<{ extractedText: string; preview?: string }> {
|
||||
let pdf: PDFDocumentProxy | null = null;
|
||||
try {
|
||||
pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
|
||||
// Extract text with page structure
|
||||
let extractedText = `<pdf filename="${fileName}">`;
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const textContent = await page.getTextContent();
|
||||
const pageText = textContent.items
|
||||
.map((item: any) => item.str)
|
||||
.filter((str: string) => str.trim())
|
||||
.join(" ");
|
||||
extractedText += `\n<page number="${i}">\n${pageText}\n</page>`;
|
||||
}
|
||||
extractedText += "\n</pdf>";
|
||||
|
||||
// Generate preview from first page
|
||||
const preview = await generatePdfPreview(pdf);
|
||||
|
||||
return { extractedText, preview };
|
||||
} catch (error) {
|
||||
console.error("Error processing PDF:", error);
|
||||
throw new Error(`Failed to process PDF: ${String(error)}`);
|
||||
} finally {
|
||||
// Clean up PDF resources
|
||||
if (pdf) {
|
||||
pdf.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePdfPreview(pdf: PDFDocumentProxy): Promise<string | undefined> {
|
||||
try {
|
||||
const page = await pdf.getPage(1);
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
|
||||
// Create canvas with reasonable size for thumbnail (160x160 max)
|
||||
const scale = Math.min(160 / viewport.width, 160 / viewport.height);
|
||||
const scaledViewport = page.getViewport({ scale });
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
canvas.height = scaledViewport.height;
|
||||
canvas.width = scaledViewport.width;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: scaledViewport,
|
||||
canvas: canvas,
|
||||
};
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
// Return base64 without data URL prefix
|
||||
return canvas.toDataURL("image/png").split(",")[1];
|
||||
} catch (error) {
|
||||
console.error("Error generating PDF preview:", error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function processDocx(arrayBuffer: ArrayBuffer, fileName: string): Promise<{ extractedText: string }> {
|
||||
try {
|
||||
// Parse document structure
|
||||
const wordDoc = await parseAsync(arrayBuffer);
|
||||
|
||||
// Extract structured text from document body
|
||||
let extractedText = `<docx filename="${fileName}">\n<page number="1">\n`;
|
||||
|
||||
const body = wordDoc.documentPart?.body;
|
||||
if (body?.children) {
|
||||
// Walk through document elements and extract text
|
||||
const texts: string[] = [];
|
||||
for (const element of body.children) {
|
||||
const text = extractTextFromElement(element);
|
||||
if (text) {
|
||||
texts.push(text);
|
||||
}
|
||||
}
|
||||
extractedText += texts.join("\n");
|
||||
}
|
||||
|
||||
extractedText += `\n</page>\n</docx>`;
|
||||
return { extractedText };
|
||||
} catch (error) {
|
||||
console.error("Error processing DOCX:", error);
|
||||
throw new Error(`Failed to process DOCX: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function extractTextFromElement(element: any): string {
|
||||
let text = "";
|
||||
|
||||
// Check type with lowercase
|
||||
const elementType = element.type?.toLowerCase() || "";
|
||||
|
||||
// Handle paragraphs
|
||||
if (elementType === "paragraph" && element.children) {
|
||||
for (const child of element.children) {
|
||||
const childType = child.type?.toLowerCase() || "";
|
||||
if (childType === "run" && child.children) {
|
||||
for (const textChild of child.children) {
|
||||
const textType = textChild.type?.toLowerCase() || "";
|
||||
if (textType === "text") {
|
||||
text += textChild.text || "";
|
||||
}
|
||||
}
|
||||
} else if (childType === "text") {
|
||||
text += child.text || "";
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle tables
|
||||
else if (elementType === "table") {
|
||||
if (element.children) {
|
||||
const tableTexts: string[] = [];
|
||||
for (const row of element.children) {
|
||||
const rowType = row.type?.toLowerCase() || "";
|
||||
if (rowType === "tablerow" && row.children) {
|
||||
const rowTexts: string[] = [];
|
||||
for (const cell of row.children) {
|
||||
const cellType = cell.type?.toLowerCase() || "";
|
||||
if (cellType === "tablecell" && cell.children) {
|
||||
const cellTexts: string[] = [];
|
||||
for (const cellElement of cell.children) {
|
||||
const cellText = extractTextFromElement(cellElement);
|
||||
if (cellText) cellTexts.push(cellText);
|
||||
}
|
||||
if (cellTexts.length > 0) rowTexts.push(cellTexts.join(" "));
|
||||
}
|
||||
}
|
||||
if (rowTexts.length > 0) tableTexts.push(rowTexts.join(" | "));
|
||||
}
|
||||
}
|
||||
if (tableTexts.length > 0) {
|
||||
text = `\n[Table]\n${tableTexts.join("\n")}\n[/Table]\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Recursively handle other container elements
|
||||
else if (element.children && Array.isArray(element.children)) {
|
||||
const childTexts: string[] = [];
|
||||
for (const child of element.children) {
|
||||
const childText = extractTextFromElement(child);
|
||||
if (childText) childTexts.push(childText);
|
||||
}
|
||||
text = childTexts.join(" ");
|
||||
}
|
||||
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
async function processPptx(arrayBuffer: ArrayBuffer, fileName: string): Promise<{ extractedText: string }> {
|
||||
try {
|
||||
// Load the PPTX file as a ZIP
|
||||
const zip = await JSZip.loadAsync(arrayBuffer);
|
||||
|
||||
// PPTX slides are stored in ppt/slides/slide[n].xml
|
||||
let extractedText = `<pptx filename="${fileName}">`;
|
||||
|
||||
// Get all slide files and sort them numerically
|
||||
const slideFiles = Object.keys(zip.files)
|
||||
.filter((name) => name.match(/ppt\/slides\/slide\d+\.xml$/))
|
||||
.sort((a, b) => {
|
||||
const numA = Number.parseInt(a.match(/slide(\d+)\.xml$/)?.[1] || "0", 10);
|
||||
const numB = Number.parseInt(b.match(/slide(\d+)\.xml$/)?.[1] || "0", 10);
|
||||
return numA - numB;
|
||||
});
|
||||
|
||||
// Extract text from each slide
|
||||
for (let i = 0; i < slideFiles.length; i++) {
|
||||
const slideFile = zip.file(slideFiles[i]);
|
||||
if (slideFile) {
|
||||
const slideXml = await slideFile.async("text");
|
||||
|
||||
// Extract text from XML (simple regex approach)
|
||||
// Looking for <a:t> tags which contain text in PPTX
|
||||
const textMatches = slideXml.match(/<a:t[^>]*>([^<]+)<\/a:t>/g);
|
||||
|
||||
if (textMatches) {
|
||||
extractedText += `\n<slide number="${i + 1}">`;
|
||||
const slideTexts = textMatches
|
||||
.map((match) => {
|
||||
const textMatch = match.match(/<a:t[^>]*>([^<]+)<\/a:t>/);
|
||||
return textMatch ? textMatch[1] : "";
|
||||
})
|
||||
.filter((t) => t.trim());
|
||||
|
||||
if (slideTexts.length > 0) {
|
||||
extractedText += `\n${slideTexts.join("\n")}`;
|
||||
}
|
||||
extractedText += "\n</slide>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also try to extract text from notes
|
||||
const notesFiles = Object.keys(zip.files)
|
||||
.filter((name) => name.match(/ppt\/notesSlides\/notesSlide\d+\.xml$/))
|
||||
.sort((a, b) => {
|
||||
const numA = Number.parseInt(a.match(/notesSlide(\d+)\.xml$/)?.[1] || "0", 10);
|
||||
const numB = Number.parseInt(b.match(/notesSlide(\d+)\.xml$/)?.[1] || "0", 10);
|
||||
return numA - numB;
|
||||
});
|
||||
|
||||
if (notesFiles.length > 0) {
|
||||
extractedText += "\n<notes>";
|
||||
for (const noteFile of notesFiles) {
|
||||
const file = zip.file(noteFile);
|
||||
if (file) {
|
||||
const noteXml = await file.async("text");
|
||||
const textMatches = noteXml.match(/<a:t[^>]*>([^<]+)<\/a:t>/g);
|
||||
if (textMatches) {
|
||||
const noteTexts = textMatches
|
||||
.map((match) => {
|
||||
const textMatch = match.match(/<a:t[^>]*>([^<]+)<\/a:t>/);
|
||||
return textMatch ? textMatch[1] : "";
|
||||
})
|
||||
.filter((t) => t.trim());
|
||||
|
||||
if (noteTexts.length > 0) {
|
||||
const slideNum = noteFile.match(/notesSlide(\d+)\.xml$/)?.[1];
|
||||
extractedText += `\n[Slide ${slideNum} notes]: ${noteTexts.join(" ")}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
extractedText += "\n</notes>";
|
||||
}
|
||||
|
||||
extractedText += "\n</pptx>";
|
||||
return { extractedText };
|
||||
} catch (error) {
|
||||
console.error("Error processing PPTX:", error);
|
||||
throw new Error(`Failed to process PPTX: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function processExcel(arrayBuffer: ArrayBuffer, fileName: string): Promise<{ extractedText: string }> {
|
||||
try {
|
||||
// Read the workbook
|
||||
const workbook = XLSX.read(arrayBuffer, { type: "array" });
|
||||
|
||||
let extractedText = `<excel filename="${fileName}">`;
|
||||
|
||||
// Process each sheet
|
||||
for (const [index, sheetName] of workbook.SheetNames.entries()) {
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
|
||||
// Extract text as CSV for the extractedText field
|
||||
const csvText = XLSX.utils.sheet_to_csv(worksheet);
|
||||
extractedText += `\n<sheet name="${sheetName}" index="${index + 1}">\n${csvText}\n</sheet>`;
|
||||
}
|
||||
|
||||
extractedText += "\n</excel>";
|
||||
|
||||
return { extractedText };
|
||||
} catch (error) {
|
||||
console.error("Error processing Excel:", error);
|
||||
throw new Error(`Failed to process Excel: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import PromptDialog from "@mariozechner/mini-lit/dist/PromptDialog.js";
|
||||
import { i18n } from "./i18n.js";
|
||||
|
||||
export async function getAuthToken(): Promise<string | undefined> {
|
||||
let authToken: string | undefined = localStorage.getItem(`auth-token`) || "";
|
||||
if (authToken) return authToken;
|
||||
|
||||
while (true) {
|
||||
authToken = (
|
||||
await PromptDialog.ask(i18n("Enter Auth Token"), i18n("Please enter your auth token."), "", true)
|
||||
)?.trim();
|
||||
if (authToken) {
|
||||
localStorage.setItem(`auth-token`, authToken);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return authToken?.trim() || undefined;
|
||||
}
|
||||
|
||||
export async function clearAuthToken() {
|
||||
localStorage.removeItem(`auth-token`);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { i18n } from "@mariozechner/mini-lit";
|
||||
import type { Usage } from "@mariozechner/pi-ai";
|
||||
|
||||
export function formatCost(cost: number): string {
|
||||
return `$${cost.toFixed(4)}`;
|
||||
}
|
||||
|
||||
export function formatModelCost(cost: any): string {
|
||||
if (!cost) return i18n("Free");
|
||||
const input = cost.input || 0;
|
||||
const output = cost.output || 0;
|
||||
if (input === 0 && output === 0) return i18n("Free");
|
||||
|
||||
// Format numbers with appropriate precision
|
||||
const formatNum = (num: number): string => {
|
||||
if (num >= 100) return num.toFixed(0);
|
||||
if (num >= 10) return num.toFixed(1).replace(/\.0$/, "");
|
||||
if (num >= 1) return num.toFixed(2).replace(/\.?0+$/, "");
|
||||
return num.toFixed(3).replace(/\.?0+$/, "");
|
||||
};
|
||||
|
||||
return `$${formatNum(input)}/$${formatNum(output)}`;
|
||||
}
|
||||
|
||||
export function formatUsage(usage: Usage) {
|
||||
if (!usage) return "";
|
||||
|
||||
const parts = [];
|
||||
if (usage.input) parts.push(`↑${formatTokenCount(usage.input)}`);
|
||||
if (usage.output) parts.push(`↓${formatTokenCount(usage.output)}`);
|
||||
if (usage.cacheRead) parts.push(`R${formatTokenCount(usage.cacheRead)}`);
|
||||
if (usage.cacheWrite) parts.push(`W${formatTokenCount(usage.cacheWrite)}`);
|
||||
if (usage.cost?.total) parts.push(formatCost(usage.cost.total));
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
export function formatTokenCount(count: number): string {
|
||||
if (count < 1000) return count.toString();
|
||||
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
||||
return `${Math.round(count / 1000)}k`;
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
import { defaultEnglish, defaultGerman, type MiniLitRequiredMessages, setTranslations } from "@mariozechner/mini-lit";
|
||||
|
||||
declare module "@mariozechner/mini-lit" {
|
||||
interface i18nMessages extends MiniLitRequiredMessages {
|
||||
Free: string;
|
||||
"Input Required": string;
|
||||
Cancel: string;
|
||||
Confirm: string;
|
||||
"Select Model": string;
|
||||
"Search models...": string;
|
||||
Format: string;
|
||||
Thinking: string;
|
||||
Vision: string;
|
||||
You: string;
|
||||
Assistant: string;
|
||||
"Thinking...": string;
|
||||
"Type your message...": string;
|
||||
"API Keys Configuration": string;
|
||||
"Configure API keys for LLM providers. Keys are stored locally in your browser.": string;
|
||||
Configured: string;
|
||||
"Not configured": string;
|
||||
"✓ Valid": string;
|
||||
"✗ Invalid": string;
|
||||
"Testing...": string;
|
||||
Update: string;
|
||||
Test: string;
|
||||
Remove: string;
|
||||
Save: string;
|
||||
"Update API key": string;
|
||||
"Enter API key": string;
|
||||
"Type a message...": string;
|
||||
"Failed to fetch file": string;
|
||||
"Invalid source type": string;
|
||||
PDF: string;
|
||||
Document: string;
|
||||
Presentation: string;
|
||||
Spreadsheet: string;
|
||||
Text: string;
|
||||
"Error loading file": string;
|
||||
"No text content available": string;
|
||||
"Failed to load PDF": string;
|
||||
"Failed to load document": string;
|
||||
"Failed to load spreadsheet": string;
|
||||
"Error loading PDF": string;
|
||||
"Error loading document": string;
|
||||
"Error loading spreadsheet": string;
|
||||
"Preview not available for this file type.": string;
|
||||
"Click the download button above to view it on your computer.": string;
|
||||
"No content available": string;
|
||||
"Failed to display text content": string;
|
||||
"API keys are required to use AI models. Get your keys from the provider's website.": string;
|
||||
console: string;
|
||||
"Copy output": string;
|
||||
"Copied!": string;
|
||||
"Error:": string;
|
||||
"Request aborted": string;
|
||||
Call: string;
|
||||
Result: string;
|
||||
"(no result)": string;
|
||||
"Waiting for tool result…": string;
|
||||
"Call was aborted; no result.": string;
|
||||
"No session available": string;
|
||||
"No session set": string;
|
||||
"Preparing tool parameters...": string;
|
||||
"(no output)": string;
|
||||
Input: string;
|
||||
Output: string;
|
||||
"Writing expression...": string;
|
||||
"Waiting for expression...": string;
|
||||
Calculating: string;
|
||||
"Getting current time in": string;
|
||||
"Getting current date and time": string;
|
||||
"Waiting for command...": string;
|
||||
"Writing command...": string;
|
||||
"Running command...": string;
|
||||
"Command failed:": string;
|
||||
"Enter Auth Token": string;
|
||||
"Please enter your auth token.": string;
|
||||
"Auth token is required for proxy transport": string;
|
||||
// JavaScript REPL strings
|
||||
"Execution aborted": string;
|
||||
"Code parameter is required": string;
|
||||
"Unknown error": string;
|
||||
"Code executed successfully (no output)": string;
|
||||
"Execution failed": string;
|
||||
"JavaScript REPL": string;
|
||||
"JavaScript code to execute": string;
|
||||
"Writing JavaScript code...": string;
|
||||
"Executing JavaScript": string;
|
||||
"Preparing JavaScript...": string;
|
||||
"Preparing command...": string;
|
||||
"Preparing calculation...": string;
|
||||
"Preparing tool...": string;
|
||||
"Getting time...": string;
|
||||
// Artifacts strings
|
||||
"Processing artifact...": string;
|
||||
"Preparing artifact...": string;
|
||||
"Processing artifact": string;
|
||||
"Processed artifact": string;
|
||||
"Creating artifact": string;
|
||||
"Created artifact": string;
|
||||
"Updating artifact": string;
|
||||
"Updated artifact": string;
|
||||
"Rewriting artifact": string;
|
||||
"Rewrote artifact": string;
|
||||
"Getting artifact": string;
|
||||
"Got artifact": string;
|
||||
"Deleting artifact": string;
|
||||
"Deleted artifact": string;
|
||||
"Getting logs": string;
|
||||
"Got logs": string;
|
||||
"An error occurred": string;
|
||||
"Copy logs": string;
|
||||
"Autoscroll enabled": string;
|
||||
"Autoscroll disabled": string;
|
||||
Processing: string;
|
||||
Create: string;
|
||||
Rewrite: string;
|
||||
Get: string;
|
||||
Delete: string;
|
||||
"Get logs": string;
|
||||
"Show artifacts": string;
|
||||
"Close artifacts": string;
|
||||
Artifacts: string;
|
||||
"Copy HTML": string;
|
||||
"Download HTML": string;
|
||||
"Reload HTML": string;
|
||||
"Copy SVG": string;
|
||||
"Download SVG": string;
|
||||
"Copy Markdown": string;
|
||||
"Download Markdown": string;
|
||||
Download: string;
|
||||
"No logs for {filename}": string;
|
||||
"API Keys Settings": string;
|
||||
Settings: string;
|
||||
"API Keys": string;
|
||||
Proxy: string;
|
||||
"Use CORS Proxy": string;
|
||||
"Proxy URL": string;
|
||||
"Format: The proxy must accept requests as <proxy-url>/?url=<target-url>": string;
|
||||
"Settings are stored locally in your browser": string;
|
||||
Clear: string;
|
||||
"API Key Required": string;
|
||||
"Enter your API key for {provider}": string;
|
||||
"Allows browser-based apps to bypass CORS restrictions when calling LLM providers. Required for Z-AI and Anthropic with OAuth token.": string;
|
||||
Off: string;
|
||||
Minimal: string;
|
||||
Low: string;
|
||||
Medium: string;
|
||||
High: string;
|
||||
"Storage Permission Required": string;
|
||||
"This app needs persistent storage to save your conversations": string;
|
||||
"Why is this needed?": string;
|
||||
"Without persistent storage, your browser may delete saved conversations when it needs disk space. Granting this permission ensures your chat history is preserved.": string;
|
||||
"What this means:": string;
|
||||
"Your conversations will be saved locally in your browser": string;
|
||||
"Data will not be deleted automatically to free up space": string;
|
||||
"You can still manually clear data at any time": string;
|
||||
"No data is sent to external servers": string;
|
||||
"Continue Anyway": string;
|
||||
"Requesting...": string;
|
||||
"Grant Permission": string;
|
||||
Sessions: string;
|
||||
"Load a previous conversation": string;
|
||||
"No sessions yet": string;
|
||||
"Delete this session?": string;
|
||||
Today: string;
|
||||
Yesterday: string;
|
||||
"{days} days ago": string;
|
||||
messages: string;
|
||||
tokens: string;
|
||||
"Drop files here": string;
|
||||
// Providers & Models
|
||||
"Providers & Models": string;
|
||||
"Cloud Providers": string;
|
||||
"Cloud LLM providers with predefined models. API keys are stored locally in your browser.": string;
|
||||
"Custom Providers": string;
|
||||
"User-configured servers with auto-discovered or manually defined models.": string;
|
||||
"Add Provider": string;
|
||||
"No custom providers configured. Click 'Add Provider' to get started.": string;
|
||||
Models: string;
|
||||
"auto-discovered": string;
|
||||
Refresh: string;
|
||||
Edit: string;
|
||||
"Are you sure you want to delete this provider?": string;
|
||||
"Edit Provider": string;
|
||||
"Provider Name": string;
|
||||
"e.g., My Ollama Server": string;
|
||||
"Provider Type": string;
|
||||
"Base URL": string;
|
||||
"e.g., http://localhost:11434": string;
|
||||
"API Key (Optional)": string;
|
||||
"Leave empty if not required": string;
|
||||
"Test Connection": string;
|
||||
Discovered: string;
|
||||
models: string;
|
||||
and: string;
|
||||
more: string;
|
||||
"For manual provider types, add models after saving the provider.": string;
|
||||
"Please fill in all required fields": string;
|
||||
"Failed to save provider": string;
|
||||
"OpenAI Completions Compatible": string;
|
||||
"OpenAI Responses Compatible": string;
|
||||
"Anthropic Messages Compatible": string;
|
||||
"Checking...": string;
|
||||
Disconnected: string;
|
||||
}
|
||||
}
|
||||
|
||||
export const translations = {
|
||||
en: {
|
||||
...defaultEnglish,
|
||||
Free: "Free",
|
||||
"Input Required": "Input Required",
|
||||
Cancel: "Cancel",
|
||||
Confirm: "Confirm",
|
||||
"Select Model": "Select Model",
|
||||
"Search models...": "Search models...",
|
||||
Format: "Format",
|
||||
Thinking: "Thinking",
|
||||
Vision: "Vision",
|
||||
You: "You",
|
||||
Assistant: "Assistant",
|
||||
"Thinking...": "Thinking...",
|
||||
"Type your message...": "Type your message...",
|
||||
"API Keys Configuration": "API Keys Configuration",
|
||||
"Configure API keys for LLM providers. Keys are stored locally in your browser.":
|
||||
"Configure API keys for LLM providers. Keys are stored locally in your browser.",
|
||||
Configured: "Configured",
|
||||
"Not configured": "Not configured",
|
||||
"✓ Valid": "✓ Valid",
|
||||
"✗ Invalid": "✗ Invalid",
|
||||
"Testing...": "Testing...",
|
||||
Update: "Update",
|
||||
Test: "Test",
|
||||
Remove: "Remove",
|
||||
Save: "Save",
|
||||
"Update API key": "Update API key",
|
||||
"Enter API key": "Enter API key",
|
||||
"Type a message...": "Type a message...",
|
||||
"Failed to fetch file": "Failed to fetch file",
|
||||
"Invalid source type": "Invalid source type",
|
||||
PDF: "PDF",
|
||||
Document: "Document",
|
||||
Presentation: "Presentation",
|
||||
Spreadsheet: "Spreadsheet",
|
||||
Text: "Text",
|
||||
"Error loading file": "Error loading file",
|
||||
"No text content available": "No text content available",
|
||||
"Failed to load PDF": "Failed to load PDF",
|
||||
"Failed to load document": "Failed to load document",
|
||||
"Failed to load spreadsheet": "Failed to load spreadsheet",
|
||||
"Error loading PDF": "Error loading PDF",
|
||||
"Error loading document": "Error loading document",
|
||||
"Error loading spreadsheet": "Error loading spreadsheet",
|
||||
"Preview not available for this file type.": "Preview not available for this file type.",
|
||||
"Click the download button above to view it on your computer.":
|
||||
"Click the download button above to view it on your computer.",
|
||||
"No content available": "No content available",
|
||||
"Failed to display text content": "Failed to display text content",
|
||||
"API keys are required to use AI models. Get your keys from the provider's website.":
|
||||
"API keys are required to use AI models. Get your keys from the provider's website.",
|
||||
console: "console",
|
||||
"Copy output": "Copy output",
|
||||
"Copied!": "Copied!",
|
||||
"Error:": "Error:",
|
||||
"Request aborted": "Request aborted",
|
||||
Call: "Call",
|
||||
Result: "Result",
|
||||
"(no result)": "(no result)",
|
||||
"Waiting for tool result…": "Waiting for tool result…",
|
||||
"Call was aborted; no result.": "Call was aborted; no result.",
|
||||
"No session available": "No session available",
|
||||
"No session set": "No session set",
|
||||
"Preparing tool parameters...": "Preparing tool parameters...",
|
||||
"(no output)": "(no output)",
|
||||
Input: "Input",
|
||||
Output: "Output",
|
||||
"Waiting for expression...": "Waiting for expression...",
|
||||
"Writing expression...": "Writing expression...",
|
||||
Calculating: "Calculating",
|
||||
"Getting current time in": "Getting current time in",
|
||||
"Getting current date and time": "Getting current date and time",
|
||||
"Waiting for command...": "Waiting for command...",
|
||||
"Writing command...": "Writing command...",
|
||||
"Running command...": "Running command...",
|
||||
"Command failed": "Command failed",
|
||||
"Enter Auth Token": "Enter Auth Token",
|
||||
"Please enter your auth token.": "Please enter your auth token.",
|
||||
"Auth token is required for proxy transport": "Auth token is required for proxy transport",
|
||||
// JavaScript REPL strings
|
||||
"Execution aborted": "Execution aborted",
|
||||
"Code parameter is required": "Code parameter is required",
|
||||
"Unknown error": "Unknown error",
|
||||
"Code executed successfully (no output)": "Code executed successfully (no output)",
|
||||
"Execution failed": "Execution failed",
|
||||
"JavaScript REPL": "JavaScript REPL",
|
||||
"JavaScript code to execute": "JavaScript code to execute",
|
||||
"Writing JavaScript code...": "Writing JavaScript code...",
|
||||
"Executing JavaScript": "Executing JavaScript",
|
||||
"Preparing JavaScript...": "Preparing JavaScript...",
|
||||
"Preparing command...": "Preparing command...",
|
||||
"Preparing calculation...": "Preparing calculation...",
|
||||
"Preparing tool...": "Preparing tool...",
|
||||
"Getting time...": "Getting time...",
|
||||
// Artifacts strings
|
||||
"Processing artifact...": "Processing artifact...",
|
||||
"Preparing artifact...": "Preparing artifact...",
|
||||
"Processing artifact": "Processing artifact",
|
||||
"Processed artifact": "Processed artifact",
|
||||
"Creating artifact": "Creating artifact",
|
||||
"Created artifact": "Created artifact",
|
||||
"Updating artifact": "Updating artifact",
|
||||
"Updated artifact": "Updated artifact",
|
||||
"Rewriting artifact": "Rewriting artifact",
|
||||
"Rewrote artifact": "Rewrote artifact",
|
||||
"Getting artifact": "Getting artifact",
|
||||
"Got artifact": "Got artifact",
|
||||
"Deleting artifact": "Deleting artifact",
|
||||
"Deleted artifact": "Deleted artifact",
|
||||
"Getting logs": "Getting logs",
|
||||
"Got logs": "Got logs",
|
||||
"An error occurred": "An error occurred",
|
||||
"Copy logs": "Copy logs",
|
||||
"Autoscroll enabled": "Autoscroll enabled",
|
||||
"Autoscroll disabled": "Autoscroll disabled",
|
||||
Processing: "Processing",
|
||||
Create: "Create",
|
||||
Rewrite: "Rewrite",
|
||||
Get: "Get",
|
||||
"Get logs": "Get logs",
|
||||
"Show artifacts": "Show artifacts",
|
||||
"Close artifacts": "Close artifacts",
|
||||
Artifacts: "Artifacts",
|
||||
"Copy HTML": "Copy HTML",
|
||||
"Download HTML": "Download HTML",
|
||||
"Reload HTML": "Reload HTML",
|
||||
"Copy SVG": "Copy SVG",
|
||||
"Download SVG": "Download SVG",
|
||||
"Copy Markdown": "Copy Markdown",
|
||||
"Download Markdown": "Download Markdown",
|
||||
Download: "Download",
|
||||
"No logs for {filename}": "No logs for {filename}",
|
||||
"API Keys Settings": "API Keys Settings",
|
||||
Settings: "Settings",
|
||||
"API Keys": "API Keys",
|
||||
Proxy: "Proxy",
|
||||
"Use CORS Proxy": "Use CORS Proxy",
|
||||
"Proxy URL": "Proxy URL",
|
||||
"Format: The proxy must accept requests as <proxy-url>/?url=<target-url>":
|
||||
"Format: The proxy must accept requests as <proxy-url>/?url=<target-url>",
|
||||
"Settings are stored locally in your browser": "Settings are stored locally in your browser",
|
||||
Clear: "Clear",
|
||||
"API Key Required": "API Key Required",
|
||||
"Enter your API key for {provider}": "Enter your API key for {provider}",
|
||||
"Allows browser-based apps to bypass CORS restrictions when calling LLM providers. Required for Z-AI and Anthropic with OAuth token.":
|
||||
"Allows browser-based apps to bypass CORS restrictions when calling LLM providers. Required for Z-AI and Anthropic with OAuth token.",
|
||||
Off: "Off",
|
||||
Minimal: "Minimal",
|
||||
Low: "Low",
|
||||
Medium: "Medium",
|
||||
High: "High",
|
||||
"Storage Permission Required": "Storage Permission Required",
|
||||
"This app needs persistent storage to save your conversations":
|
||||
"This app needs persistent storage to save your conversations",
|
||||
"Why is this needed?": "Why is this needed?",
|
||||
"Without persistent storage, your browser may delete saved conversations when it needs disk space. Granting this permission ensures your chat history is preserved.":
|
||||
"Without persistent storage, your browser may delete saved conversations when it needs disk space. Granting this permission ensures your chat history is preserved.",
|
||||
"What this means:": "What this means:",
|
||||
"Your conversations will be saved locally in your browser":
|
||||
"Your conversations will be saved locally in your browser",
|
||||
"Data will not be deleted automatically to free up space":
|
||||
"Data will not be deleted automatically to free up space",
|
||||
"You can still manually clear data at any time": "You can still manually clear data at any time",
|
||||
"No data is sent to external servers": "No data is sent to external servers",
|
||||
"Continue Anyway": "Continue Anyway",
|
||||
"Requesting...": "Requesting...",
|
||||
"Grant Permission": "Grant Permission",
|
||||
Sessions: "Sessions",
|
||||
"Load a previous conversation": "Load a previous conversation",
|
||||
"No sessions yet": "No sessions yet",
|
||||
"Delete this session?": "Delete this session?",
|
||||
Today: "Today",
|
||||
Yesterday: "Yesterday",
|
||||
"{days} days ago": "{days} days ago",
|
||||
messages: "messages",
|
||||
tokens: "tokens",
|
||||
Delete: "Delete",
|
||||
"Drop files here": "Drop files here",
|
||||
"Command failed:": "Command failed:",
|
||||
// Providers & Models
|
||||
"Providers & Models": "Providers & Models",
|
||||
"Cloud Providers": "Cloud Providers",
|
||||
"Cloud LLM providers with predefined models. API keys are stored locally in your browser.":
|
||||
"Cloud LLM providers with predefined models. API keys are stored locally in your browser.",
|
||||
"Custom Providers": "Custom Providers",
|
||||
"User-configured servers with auto-discovered or manually defined models.":
|
||||
"User-configured servers with auto-discovered or manually defined models.",
|
||||
"Add Provider": "Add Provider",
|
||||
"No custom providers configured. Click 'Add Provider' to get started.":
|
||||
"No custom providers configured. Click 'Add Provider' to get started.",
|
||||
"auto-discovered": "auto-discovered",
|
||||
Refresh: "Refresh",
|
||||
Edit: "Edit",
|
||||
"Are you sure you want to delete this provider?": "Are you sure you want to delete this provider?",
|
||||
"Edit Provider": "Edit Provider",
|
||||
"Provider Name": "Provider Name",
|
||||
"e.g., My Ollama Server": "e.g., My Ollama Server",
|
||||
"Provider Type": "Provider Type",
|
||||
"Base URL": "Base URL",
|
||||
"e.g., http://localhost:11434": "e.g., http://localhost:11434",
|
||||
"API Key (Optional)": "API Key (Optional)",
|
||||
"Leave empty if not required": "Leave empty if not required",
|
||||
"Test Connection": "Test Connection",
|
||||
Discovered: "Discovered",
|
||||
Models: "Models",
|
||||
models: "models",
|
||||
and: "and",
|
||||
more: "more",
|
||||
"For manual provider types, add models after saving the provider.":
|
||||
"For manual provider types, add models after saving the provider.",
|
||||
"Please fill in all required fields": "Please fill in all required fields",
|
||||
"Failed to save provider": "Failed to save provider",
|
||||
"OpenAI Completions Compatible": "OpenAI Completions Compatible",
|
||||
"OpenAI Responses Compatible": "OpenAI Responses Compatible",
|
||||
"Anthropic Messages Compatible": "Anthropic Messages Compatible",
|
||||
"Checking...": "Checking...",
|
||||
Disconnected: "Disconnected",
|
||||
},
|
||||
de: {
|
||||
...defaultGerman,
|
||||
Free: "Kostenlos",
|
||||
"Input Required": "Eingabe erforderlich",
|
||||
Cancel: "Abbrechen",
|
||||
Confirm: "Bestätigen",
|
||||
"Select Model": "Modell auswählen",
|
||||
"Search models...": "Modelle suchen...",
|
||||
Format: "Formatieren",
|
||||
Thinking: "Thinking",
|
||||
Vision: "Vision",
|
||||
You: "Sie",
|
||||
Assistant: "Assistent",
|
||||
"Thinking...": "Denkt nach...",
|
||||
"Type your message...": "Geben Sie Ihre Nachricht ein...",
|
||||
"API Keys Configuration": "API-Schlüssel-Konfiguration",
|
||||
"Configure API keys for LLM providers. Keys are stored locally in your browser.":
|
||||
"Konfigurieren Sie API-Schlüssel für LLM-Anbieter. Schlüssel werden lokal in Ihrem Browser gespeichert.",
|
||||
Configured: "Konfiguriert",
|
||||
"Not configured": "Nicht konfiguriert",
|
||||
"✓ Valid": "✓ Gültig",
|
||||
"✗ Invalid": "✗ Ungültig",
|
||||
"Testing...": "Teste...",
|
||||
Update: "Aktualisieren",
|
||||
Test: "Testen",
|
||||
Remove: "Entfernen",
|
||||
Save: "Speichern",
|
||||
"Update API key": "API-Schlüssel aktualisieren",
|
||||
"Enter API key": "API-Schlüssel eingeben",
|
||||
"Type a message...": "Nachricht eingeben...",
|
||||
"Failed to fetch file": "Datei konnte nicht abgerufen werden",
|
||||
"Invalid source type": "Ungültiger Quellentyp",
|
||||
PDF: "PDF",
|
||||
Document: "Dokument",
|
||||
Presentation: "Präsentation",
|
||||
Spreadsheet: "Tabelle",
|
||||
Text: "Text",
|
||||
"Error loading file": "Fehler beim Laden der Datei",
|
||||
"No text content available": "Kein Textinhalt verfügbar",
|
||||
"Failed to load PDF": "PDF konnte nicht geladen werden",
|
||||
"Failed to load document": "Dokument konnte nicht geladen werden",
|
||||
"Failed to load spreadsheet": "Tabelle konnte nicht geladen werden",
|
||||
"Error loading PDF": "Fehler beim Laden des PDFs",
|
||||
"Error loading document": "Fehler beim Laden des Dokuments",
|
||||
"Error loading spreadsheet": "Fehler beim Laden der Tabelle",
|
||||
"Preview not available for this file type.": "Vorschau für diesen Dateityp nicht verfügbar.",
|
||||
"Click the download button above to view it on your computer.":
|
||||
"Klicken Sie oben auf die Download-Schaltfläche, um die Datei auf Ihrem Computer anzuzeigen.",
|
||||
"No content available": "Kein Inhalt verfügbar",
|
||||
"Failed to display text content": "Textinhalt konnte nicht angezeigt werden",
|
||||
"API keys are required to use AI models. Get your keys from the provider's website.":
|
||||
"API-Schlüssel sind erforderlich, um KI-Modelle zu verwenden. Holen Sie sich Ihre Schlüssel von der Website des Anbieters.",
|
||||
console: "Konsole",
|
||||
"Copy output": "Ausgabe kopieren",
|
||||
"Copied!": "Kopiert!",
|
||||
"Error:": "Fehler:",
|
||||
"Request aborted": "Anfrage abgebrochen",
|
||||
Call: "Aufruf",
|
||||
Result: "Ergebnis",
|
||||
"(no result)": "(kein Ergebnis)",
|
||||
"Waiting for tool result…": "Warte auf Tool-Ergebnis…",
|
||||
"Call was aborted; no result.": "Aufruf wurde abgebrochen; kein Ergebnis.",
|
||||
"No session available": "Keine Sitzung verfügbar",
|
||||
"No session set": "Keine Sitzung gesetzt",
|
||||
"Preparing tool parameters...": "Bereite Tool-Parameter vor...",
|
||||
"(no output)": "(keine Ausgabe)",
|
||||
Input: "Eingabe",
|
||||
Output: "Ausgabe",
|
||||
"Waiting for expression...": "Warte auf Ausdruck",
|
||||
"Writing expression...": "Schreibe Ausdruck...",
|
||||
Calculating: "Berechne",
|
||||
"Getting current time in": "Hole aktuelle Zeit in",
|
||||
"Getting current date and time": "Hole aktuelles Datum und Uhrzeit",
|
||||
"Waiting for command...": "Warte auf Befehl...",
|
||||
"Writing command...": "Schreibe Befehl...",
|
||||
"Running command...": "Führe Befehl aus...",
|
||||
"Command failed": "Befehl fehlgeschlagen",
|
||||
"Enter Auth Token": "Auth-Token eingeben",
|
||||
"Please enter your auth token.": "Bitte geben Sie Ihr Auth-Token ein.",
|
||||
"Auth token is required for proxy transport": "Auth-Token ist für Proxy-Transport erforderlich",
|
||||
// JavaScript REPL strings
|
||||
"Execution aborted": "Ausführung abgebrochen",
|
||||
"Code parameter is required": "Code-Parameter ist erforderlich",
|
||||
"Unknown error": "Unbekannter Fehler",
|
||||
"Code executed successfully (no output)": "Code erfolgreich ausgeführt (keine Ausgabe)",
|
||||
"Execution failed": "Ausführung fehlgeschlagen",
|
||||
"JavaScript REPL": "JavaScript REPL",
|
||||
"JavaScript code to execute": "Auszuführender JavaScript-Code",
|
||||
"Writing JavaScript code...": "Schreibe JavaScript-Code...",
|
||||
"Executing JavaScript": "Führe JavaScript aus",
|
||||
"Preparing JavaScript...": "Bereite JavaScript vor...",
|
||||
"Preparing command...": "Bereite Befehl vor...",
|
||||
"Preparing calculation...": "Bereite Berechnung vor...",
|
||||
"Preparing tool...": "Bereite Tool vor...",
|
||||
"Getting time...": "Hole Zeit...",
|
||||
// Artifacts strings
|
||||
"Processing artifact...": "Verarbeite Artefakt...",
|
||||
"Preparing artifact...": "Bereite Artefakt vor...",
|
||||
"Processing artifact": "Verarbeite Artefakt",
|
||||
"Processed artifact": "Artefakt verarbeitet",
|
||||
"Creating artifact": "Erstelle Artefakt",
|
||||
"Created artifact": "Artefakt erstellt",
|
||||
"Updating artifact": "Aktualisiere Artefakt",
|
||||
"Updated artifact": "Artefakt aktualisiert",
|
||||
"Rewriting artifact": "Überschreibe Artefakt",
|
||||
"Rewrote artifact": "Artefakt überschrieben",
|
||||
"Getting artifact": "Hole Artefakt",
|
||||
"Got artifact": "Artefakt geholt",
|
||||
"Deleting artifact": "Lösche Artefakt",
|
||||
"Deleted artifact": "Artefakt gelöscht",
|
||||
"Getting logs": "Hole Logs",
|
||||
"Got logs": "Logs geholt",
|
||||
"An error occurred": "Ein Fehler ist aufgetreten",
|
||||
"Copy logs": "Logs kopieren",
|
||||
"Autoscroll enabled": "Automatisches Scrollen aktiviert",
|
||||
"Autoscroll disabled": "Automatisches Scrollen deaktiviert",
|
||||
Processing: "Verarbeitung",
|
||||
Create: "Erstellen",
|
||||
Rewrite: "Überschreiben",
|
||||
Get: "Abrufen",
|
||||
"Get logs": "Logs abrufen",
|
||||
"Show artifacts": "Artefakte anzeigen",
|
||||
"Close artifacts": "Artefakte schließen",
|
||||
Artifacts: "Artefakte",
|
||||
"Copy HTML": "HTML kopieren",
|
||||
"Download HTML": "HTML herunterladen",
|
||||
"Reload HTML": "HTML neu laden",
|
||||
"Copy SVG": "SVG kopieren",
|
||||
"Download SVG": "SVG herunterladen",
|
||||
"Copy Markdown": "Markdown kopieren",
|
||||
"Download Markdown": "Markdown herunterladen",
|
||||
Download: "Herunterladen",
|
||||
"No logs for {filename}": "Keine Logs für {filename}",
|
||||
"API Keys Settings": "API-Schlüssel Einstellungen",
|
||||
Settings: "Einstellungen",
|
||||
"API Keys": "API-Schlüssel",
|
||||
Proxy: "Proxy",
|
||||
"Use CORS Proxy": "CORS-Proxy verwenden",
|
||||
"Proxy URL": "Proxy-URL",
|
||||
"Format: The proxy must accept requests as <proxy-url>/?url=<target-url>":
|
||||
"Format: Der Proxy muss Anfragen als <proxy-url>/?url=<ziel-url> akzeptieren",
|
||||
"Settings are stored locally in your browser": "Einstellungen werden lokal in Ihrem Browser gespeichert",
|
||||
Clear: "Löschen",
|
||||
"API Key Required": "API-Schlüssel erforderlich",
|
||||
"Enter your API key for {provider}": "Geben Sie Ihren API-Schlüssel für {provider} ein",
|
||||
"Allows browser-based apps to bypass CORS restrictions when calling LLM providers. Required for Z-AI and Anthropic with OAuth token.":
|
||||
"Ermöglicht browserbasierten Anwendungen, CORS-Einschränkungen beim Aufruf von LLM-Anbietern zu umgehen. Erforderlich für Z-AI und Anthropic mit OAuth-Token.",
|
||||
Off: "Aus",
|
||||
Minimal: "Minimal",
|
||||
Low: "Niedrig",
|
||||
Medium: "Mittel",
|
||||
High: "Hoch",
|
||||
"Storage Permission Required": "Speicherberechtigung erforderlich",
|
||||
"This app needs persistent storage to save your conversations":
|
||||
"Diese App benötigt dauerhaften Speicher, um Ihre Konversationen zu speichern",
|
||||
"Why is this needed?": "Warum wird das benötigt?",
|
||||
"Without persistent storage, your browser may delete saved conversations when it needs disk space. Granting this permission ensures your chat history is preserved.":
|
||||
"Ohne dauerhaften Speicher kann Ihr Browser gespeicherte Konversationen löschen, wenn Speicherplatz benötigt wird. Diese Berechtigung stellt sicher, dass Ihr Chatverlauf erhalten bleibt.",
|
||||
"What this means:": "Was das bedeutet:",
|
||||
"Your conversations will be saved locally in your browser":
|
||||
"Ihre Konversationen werden lokal in Ihrem Browser gespeichert",
|
||||
"Data will not be deleted automatically to free up space":
|
||||
"Daten werden nicht automatisch gelöscht, um Speicherplatz freizugeben",
|
||||
"You can still manually clear data at any time": "Sie können Daten jederzeit manuell löschen",
|
||||
"No data is sent to external servers": "Keine Daten werden an externe Server gesendet",
|
||||
"Continue Anyway": "Trotzdem fortfahren",
|
||||
"Requesting...": "Anfrage läuft...",
|
||||
"Grant Permission": "Berechtigung erteilen",
|
||||
Sessions: "Sitzungen",
|
||||
"Load a previous conversation": "Frühere Konversation laden",
|
||||
"No sessions yet": "Noch keine Sitzungen",
|
||||
"Delete this session?": "Diese Sitzung löschen?",
|
||||
Today: "Heute",
|
||||
Yesterday: "Gestern",
|
||||
"{days} days ago": "vor {days} Tagen",
|
||||
messages: "Nachrichten",
|
||||
tokens: "Tokens",
|
||||
Delete: "Löschen",
|
||||
"Drop files here": "Dateien hier ablegen",
|
||||
"Command failed:": "Befehl fehlgeschlagen:",
|
||||
// Providers & Models
|
||||
"Providers & Models": "Anbieter & Modelle",
|
||||
"Cloud Providers": "Cloud-Anbieter",
|
||||
"Cloud LLM providers with predefined models. API keys are stored locally in your browser.":
|
||||
"Cloud-LLM-Anbieter mit vordefinierten Modellen. API-Schlüssel werden lokal in Ihrem Browser gespeichert.",
|
||||
"Custom Providers": "Benutzerdefinierte Anbieter",
|
||||
"User-configured servers with auto-discovered or manually defined models.":
|
||||
"Benutzerkonfigurierte Server mit automatisch erkannten oder manuell definierten Modellen.",
|
||||
"Add Provider": "Anbieter hinzufügen",
|
||||
"No custom providers configured. Click 'Add Provider' to get started.":
|
||||
"Keine benutzerdefinierten Anbieter konfiguriert. Klicken Sie auf 'Anbieter hinzufügen', um zu beginnen.",
|
||||
"auto-discovered": "automatisch erkannt",
|
||||
Refresh: "Aktualisieren",
|
||||
Edit: "Bearbeiten",
|
||||
"Are you sure you want to delete this provider?": "Sind Sie sicher, dass Sie diesen Anbieter löschen möchten?",
|
||||
"Edit Provider": "Anbieter bearbeiten",
|
||||
"Provider Name": "Anbietername",
|
||||
"e.g., My Ollama Server": "z.B. Mein Ollama Server",
|
||||
"Provider Type": "Anbietertyp",
|
||||
"Base URL": "Basis-URL",
|
||||
"e.g., http://localhost:11434": "z.B. http://localhost:11434",
|
||||
"API Key (Optional)": "API-Schlüssel (Optional)",
|
||||
"Leave empty if not required": "Leer lassen, falls nicht erforderlich",
|
||||
"Test Connection": "Verbindung testen",
|
||||
Discovered: "Erkannt",
|
||||
Models: "Modelle",
|
||||
models: "Modelle",
|
||||
and: "und",
|
||||
more: "mehr",
|
||||
"For manual provider types, add models after saving the provider.":
|
||||
"Für manuelle Anbietertypen fügen Sie Modelle nach dem Speichern des Anbieters hinzu.",
|
||||
"Please fill in all required fields": "Bitte füllen Sie alle erforderlichen Felder aus",
|
||||
"Failed to save provider": "Fehler beim Speichern des Anbieters",
|
||||
"OpenAI Completions Compatible": "OpenAI Completions Kompatibel",
|
||||
"OpenAI Responses Compatible": "OpenAI Responses Kompatibel",
|
||||
"Anthropic Messages Compatible": "Anthropic Messages Kompatibel",
|
||||
"Checking...": "Überprüfe...",
|
||||
Disconnected: "Getrennt",
|
||||
},
|
||||
};
|
||||
|
||||
setTranslations(translations);
|
||||
|
||||
export * from "@mariozechner/mini-lit/dist/i18n.js";
|
||||
@@ -0,0 +1,277 @@
|
||||
import { LMStudioClient } from "@lmstudio/sdk";
|
||||
import type { Model } from "@mariozechner/pi-ai";
|
||||
import { Ollama } from "ollama/browser";
|
||||
|
||||
/**
|
||||
* Discover models from an Ollama server.
|
||||
* @param baseUrl - Base URL of the Ollama server (e.g., "http://localhost:11434")
|
||||
* @param apiKey - Optional API key (currently unused by Ollama)
|
||||
* @returns Array of discovered models
|
||||
*/
|
||||
export async function discoverOllamaModels(baseUrl: string, _apiKey?: string): Promise<Model<any>[]> {
|
||||
try {
|
||||
// Create Ollama client
|
||||
const ollama = new Ollama({ host: baseUrl });
|
||||
|
||||
// Get list of available models
|
||||
const { models } = await ollama.list();
|
||||
|
||||
// Fetch details for each model and convert to Model format
|
||||
const ollamaModelPromises: Promise<Model<any> | null>[] = models.map(async (model: any) => {
|
||||
try {
|
||||
// Get model details
|
||||
const details = await ollama.show({
|
||||
model: model.name,
|
||||
});
|
||||
|
||||
// Check capabilities - filter out models that don't support tools
|
||||
const capabilities: string[] = (details as any).capabilities || [];
|
||||
if (!capabilities.includes("tools")) {
|
||||
console.debug(`Skipping model ${model.name}: does not support tools`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract model info
|
||||
const modelInfo: any = details.model_info || {};
|
||||
|
||||
// Get context window size - look for architecture-specific keys
|
||||
const architecture = modelInfo["general.architecture"] || "";
|
||||
const contextKey = `${architecture}.context_length`;
|
||||
const contextWindow = parseInt(modelInfo[contextKey] || "8192", 10);
|
||||
|
||||
// Ollama caps max tokens at 10x context length
|
||||
const maxTokens = contextWindow * 10;
|
||||
|
||||
// Ollama only supports completions API
|
||||
const ollamaModel: Model<any> = {
|
||||
id: model.name,
|
||||
name: model.name,
|
||||
api: "openai-completions" as any,
|
||||
provider: "", // Will be set by caller
|
||||
baseUrl: `${baseUrl}/v1`,
|
||||
reasoning: capabilities.includes("thinking"),
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: contextWindow,
|
||||
maxTokens: maxTokens,
|
||||
};
|
||||
|
||||
return ollamaModel;
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch details for model ${model.name}:`, err);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(ollamaModelPromises);
|
||||
return results.filter((m): m is Model<any> => m !== null);
|
||||
} catch (err) {
|
||||
console.error("Failed to discover Ollama models:", err);
|
||||
throw new Error(`Ollama discovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover models from a llama.cpp server via OpenAI-compatible /v1/models endpoint.
|
||||
* @param baseUrl - Base URL of the llama.cpp server (e.g., "http://localhost:8080")
|
||||
* @param apiKey - Optional API key
|
||||
* @returns Array of discovered models
|
||||
*/
|
||||
export async function discoverLlamaCppModels(baseUrl: string, apiKey?: string): Promise<Model<any>[]> {
|
||||
try {
|
||||
const headers: HeadersInit = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (apiKey) {
|
||||
headers.Authorization = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/v1/models`, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.data || !Array.isArray(data.data)) {
|
||||
throw new Error("Invalid response format from llama.cpp server");
|
||||
}
|
||||
|
||||
return data.data.map((model: any) => {
|
||||
// llama.cpp doesn't always provide context window info
|
||||
const contextWindow = model.context_length || 8192;
|
||||
const maxTokens = model.max_tokens || 4096;
|
||||
|
||||
const llamaModel: Model<any> = {
|
||||
id: model.id,
|
||||
name: model.id,
|
||||
api: "openai-completions" as any,
|
||||
provider: "", // Will be set by caller
|
||||
baseUrl: `${baseUrl}/v1`,
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: contextWindow,
|
||||
maxTokens: maxTokens,
|
||||
};
|
||||
|
||||
return llamaModel;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to discover llama.cpp models:", err);
|
||||
throw new Error(`llama.cpp discovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover models from a vLLM server via OpenAI-compatible /v1/models endpoint.
|
||||
* @param baseUrl - Base URL of the vLLM server (e.g., "http://localhost:8000")
|
||||
* @param apiKey - Optional API key
|
||||
* @returns Array of discovered models
|
||||
*/
|
||||
export async function discoverVLLMModels(baseUrl: string, apiKey?: string): Promise<Model<any>[]> {
|
||||
try {
|
||||
const headers: HeadersInit = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (apiKey) {
|
||||
headers.Authorization = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/v1/models`, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.data || !Array.isArray(data.data)) {
|
||||
throw new Error("Invalid response format from vLLM server");
|
||||
}
|
||||
|
||||
return data.data.map((model: any) => {
|
||||
// vLLM provides max_model_len which is the context window
|
||||
const contextWindow = model.max_model_len || 8192;
|
||||
const maxTokens = Math.min(contextWindow, 4096); // Cap max tokens
|
||||
|
||||
const vllmModel: Model<any> = {
|
||||
id: model.id,
|
||||
name: model.id,
|
||||
api: "openai-completions" as any,
|
||||
provider: "", // Will be set by caller
|
||||
baseUrl: `${baseUrl}/v1`,
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: contextWindow,
|
||||
maxTokens: maxTokens,
|
||||
};
|
||||
|
||||
return vllmModel;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to discover vLLM models:", err);
|
||||
throw new Error(`vLLM discovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover models from an LM Studio server using the LM Studio SDK.
|
||||
* @param baseUrl - Base URL of the LM Studio server (e.g., "http://localhost:1234")
|
||||
* @param apiKey - Optional API key (unused for LM Studio SDK)
|
||||
* @returns Array of discovered models
|
||||
*/
|
||||
export async function discoverLMStudioModels(baseUrl: string, _apiKey?: string): Promise<Model<any>[]> {
|
||||
try {
|
||||
// Extract host and port from baseUrl
|
||||
const url = new URL(baseUrl);
|
||||
const port = url.port ? parseInt(url.port, 10) : 1234;
|
||||
|
||||
// Create LM Studio client
|
||||
const client = new LMStudioClient({ baseUrl: `ws://${url.hostname}:${port}` });
|
||||
|
||||
// List all downloaded models
|
||||
const models = await client.system.listDownloadedModels();
|
||||
|
||||
// Filter to only LLM models and map to our Model format
|
||||
return models
|
||||
.filter((model) => model.type === "llm")
|
||||
.map((model) => {
|
||||
const contextWindow = model.maxContextLength;
|
||||
// Use 10x context length like Ollama does
|
||||
const maxTokens = contextWindow;
|
||||
|
||||
const lmStudioModel: Model<any> = {
|
||||
id: model.path,
|
||||
name: model.displayName || model.path,
|
||||
api: "openai-completions" as any,
|
||||
provider: "", // Will be set by caller
|
||||
baseUrl: `${baseUrl}/v1`,
|
||||
reasoning: model.trainedForToolUse || false,
|
||||
input: model.vision ? ["text", "image"] : ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: contextWindow,
|
||||
maxTokens: maxTokens,
|
||||
};
|
||||
|
||||
return lmStudioModel;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to discover LM Studio models:", err);
|
||||
throw new Error(`LM Studio discovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to discover models based on provider type.
|
||||
* @param type - Provider type
|
||||
* @param baseUrl - Base URL of the server
|
||||
* @param apiKey - Optional API key
|
||||
* @returns Array of discovered models
|
||||
*/
|
||||
export async function discoverModels(
|
||||
type: "ollama" | "llama.cpp" | "vllm" | "lmstudio",
|
||||
baseUrl: string,
|
||||
apiKey?: string,
|
||||
): Promise<Model<any>[]> {
|
||||
switch (type) {
|
||||
case "ollama":
|
||||
return discoverOllamaModels(baseUrl, apiKey);
|
||||
case "llama.cpp":
|
||||
return discoverLlamaCppModels(baseUrl, apiKey);
|
||||
case "vllm":
|
||||
return discoverVLLMModels(baseUrl, apiKey);
|
||||
case "lmstudio":
|
||||
return discoverLMStudioModels(baseUrl, apiKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { Api, Context, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
|
||||
import { streamSimple } from "@mariozechner/pi-ai";
|
||||
|
||||
/**
|
||||
* Centralized proxy decision logic.
|
||||
*
|
||||
* Determines whether to use a CORS proxy for LLM API requests based on:
|
||||
* - Provider name
|
||||
* - API key pattern (for providers where it matters)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if a provider/API key combination requires a CORS proxy.
|
||||
*
|
||||
* @param provider - Provider name (e.g., "anthropic", "openai", "zai")
|
||||
* @param apiKey - API key for the provider
|
||||
* @returns true if proxy is required, false otherwise
|
||||
*/
|
||||
export function shouldUseProxyForProvider(provider: string, apiKey: string): boolean {
|
||||
switch (provider.toLowerCase()) {
|
||||
case "zai":
|
||||
// Z-AI always requires proxy
|
||||
return true;
|
||||
|
||||
case "anthropic":
|
||||
// Anthropic OAuth tokens (sk-ant-oat-*) require proxy
|
||||
// Regular API keys (sk-ant-api-*) do NOT require proxy
|
||||
return apiKey.startsWith("sk-ant-oat") || apiKey.startsWith("{");
|
||||
|
||||
case "openai-codex":
|
||||
// Codex uses chatgpt.com/backend-api which has no CORS
|
||||
return true;
|
||||
|
||||
// These providers work without proxy
|
||||
case "openai":
|
||||
case "google":
|
||||
case "groq":
|
||||
case "openrouter":
|
||||
case "cerebras":
|
||||
case "xai":
|
||||
case "ollama":
|
||||
case "lmstudio":
|
||||
case "github-copilot":
|
||||
return false;
|
||||
|
||||
// Unknown providers - assume no proxy needed
|
||||
// This allows new providers to work by default
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply CORS proxy to a model's baseUrl if needed.
|
||||
*
|
||||
* @param model - The model to potentially proxy
|
||||
* @param apiKey - API key for the provider
|
||||
* @param proxyUrl - CORS proxy URL (e.g., "https://proxy.mariozechner.at/proxy")
|
||||
* @returns Model with modified baseUrl if proxy is needed, otherwise original model
|
||||
*/
|
||||
export function applyProxyIfNeeded<T extends Api>(model: Model<T>, apiKey: string, proxyUrl?: string): Model<T> {
|
||||
// If no proxy URL configured, return original model
|
||||
if (!proxyUrl) {
|
||||
return model;
|
||||
}
|
||||
|
||||
// If model has no baseUrl, can't proxy it
|
||||
if (!model.baseUrl) {
|
||||
return model;
|
||||
}
|
||||
|
||||
// Check if this provider/key needs proxy
|
||||
if (!shouldUseProxyForProvider(model.provider, apiKey)) {
|
||||
return model;
|
||||
}
|
||||
|
||||
// Apply proxy to baseUrl
|
||||
return {
|
||||
...model,
|
||||
baseUrl: `${proxyUrl}/?url=${encodeURIComponent(model.baseUrl)}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is likely a CORS error.
|
||||
*
|
||||
* CORS errors in browsers typically manifest as:
|
||||
* - TypeError with message "Failed to fetch"
|
||||
* - NetworkError
|
||||
*
|
||||
* @param error - The error to check
|
||||
* @returns true if error is likely a CORS error
|
||||
*/
|
||||
export function isCorsError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for common CORS error patterns
|
||||
const message = error.message.toLowerCase();
|
||||
|
||||
// "Failed to fetch" is the standard CORS error in most browsers
|
||||
if (error.name === "TypeError" && message.includes("failed to fetch")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Some browsers report "NetworkError"
|
||||
if (error.name === "NetworkError") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// CORS-specific messages
|
||||
if (message.includes("cors") || message.includes("cross-origin")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a streamFn that applies CORS proxy when needed.
|
||||
* Reads proxy settings from storage on each call.
|
||||
*
|
||||
* @param getProxyUrl - Async function to get current proxy URL (or undefined if disabled)
|
||||
* @returns A streamFn compatible with Agent's streamFn option
|
||||
*/
|
||||
export function createStreamFn(getProxyUrl: () => Promise<string | undefined>) {
|
||||
return async (model: Model<any>, context: Context, options?: SimpleStreamOptions) => {
|
||||
const apiKey = options?.apiKey;
|
||||
const proxyUrl = await getProxyUrl();
|
||||
|
||||
if (!apiKey || !proxyUrl) {
|
||||
return streamSimple(model, context, options);
|
||||
}
|
||||
|
||||
const proxiedModel = applyProxyIfNeeded(model, apiKey, proxyUrl);
|
||||
return streamSimple(proxiedModel, context, options);
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user