From 58b6b195269253b8ac16554bb02a51df037e63d3 Mon Sep 17 00:00:00 2001 From: E66Crisp Date: Thu, 18 Dec 2025 20:14:10 +0800 Subject: [PATCH 001/349] fix: Prevent DrawIO remount and data loss when resizing window across 768px breakpoint (#306) * fix: Prevent DrawIO remount and data loss when resizing window across 768px breakpoint * fix: prevent DrawIO remount and data loss when resizing window - Move key from ResizablePanelGroup to chat-panel only - Save diagram to localStorage before breakpoint change - Restore defaultSize on drawio-panel to prevent layout flash - Keep save button functionality from main * fix: reset draw.io ready state on breakpoint change to restore diagram * fix: skip initial render save and remove console logs - Add isInitialRenderRef to skip unnecessary save/reset on first render - Remove console.log statements for production cleanliness - Add eslint-disable comment explaining loadDiagram dependency --------- Co-authored-by: dayuan.jiang --- app/page.tsx | 25 +++++++++-- components/chat-panel.tsx | 86 ++++++++---------------------------- contexts/diagram-context.tsx | 47 +++++++++++++++++++- 3 files changed, 86 insertions(+), 72 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 40c3ba97..f936fc54 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -34,6 +34,7 @@ export default function Home() { const chatPanelRef = useRef(null) const isSavingRef = useRef(false) const mouseOverDrawioRef = useRef(false) + const isMobileRef = useRef(false) // Reset saving flag when dialog closes (with delay to ignore lingering save events from draw.io) useEffect(() => { @@ -102,16 +103,32 @@ export default function Home() { resetDrawioReady() } - // Check mobile + // Check mobile - save diagram and reset draw.io before crossing breakpoint + const isInitialRenderRef = useRef(true) useEffect(() => { const checkMobile = () => { - setIsMobile(window.innerWidth < 768) + const newIsMobile = window.innerWidth < 768 + // If crossing the breakpoint (not initial render), save diagram and reset draw.io + if ( + !isInitialRenderRef.current && + newIsMobile !== isMobileRef.current + ) { + // Save diagram before remounting (fire and forget) + saveDiagramToStorage().catch(() => { + // Ignore timeout errors during resize + }) + // Reset draw.io ready state so onLoad triggers again after remount + resetDrawioReady() + } + isMobileRef.current = newIsMobile + isInitialRenderRef.current = false + setIsMobile(newIsMobile) } checkMobile() window.addEventListener("resize", checkMobile) return () => window.removeEventListener("resize", checkMobile) - }, []) + }, [saveDiagramToStorage, resetDrawioReady]) const toggleChatPanel = () => { const panel = chatPanelRef.current @@ -157,7 +174,6 @@ export default function Home() {
@@ -209,6 +225,7 @@ export default function Home() { {/* Chat Panel */} { @@ -148,6 +150,14 @@ export default function ChatPanel({ const [showNewChatDialog, setShowNewChatDialog] = useState(false) const [minimalStyle, setMinimalStyle] = useState(false) + // Restore input from sessionStorage on mount (when ChatPanel remounts due to key change) + useEffect(() => { + const savedInput = sessionStorage.getItem(SESSION_STORAGE_INPUT_KEY) + if (savedInput) { + setInput(savedInput) + } + }, []) + // Check config on mount useEffect(() => { fetch("/api/config") @@ -210,9 +220,6 @@ export default function ChatPanel({ const localStorageDebounceRef = useRef | null>(null) - const xmlStorageDebounceRef = useRef | null>( - null, - ) const LOCAL_STORAGE_DEBOUNCE_MS = 1000 // Save at most once per second const { @@ -725,47 +732,6 @@ Continue from EXACTLY where you stopped.`, } }, [setMessages]) - // Restore diagram XML when DrawIO becomes ready - const hasDiagramRestoredRef = useRef(false) - const [canSaveDiagram, setCanSaveDiagram] = useState(false) - useEffect(() => { - // Reset restore flag when DrawIO is not ready (e.g., theme/UI change remounts it) - if (!isDrawioReady) { - hasDiagramRestoredRef.current = false - setCanSaveDiagram(false) - return - } - if (hasDiagramRestoredRef.current) return - hasDiagramRestoredRef.current = true - - try { - const savedDiagramXml = localStorage.getItem( - STORAGE_DIAGRAM_XML_KEY, - ) - console.log( - "[ChatPanel] Restoring diagram, has saved XML:", - !!savedDiagramXml, - ) - if (savedDiagramXml) { - console.log( - "[ChatPanel] Loading saved diagram XML, length:", - savedDiagramXml.length, - ) - // Skip validation for trusted saved diagrams - onDisplayChart(savedDiagramXml, true) - chartXMLRef.current = savedDiagramXml - } - } catch (error) { - console.error("Failed to restore diagram from localStorage:", error) - } - - // Allow saving after restore is complete - setTimeout(() => { - console.log("[ChatPanel] Enabling diagram save") - setCanSaveDiagram(true) - }, 500) - }, [isDrawioReady, onDisplayChart]) - // Save messages to localStorage whenever they change (debounced to prevent blocking during streaming) useEffect(() => { if (!hasRestoredRef.current) return @@ -795,28 +761,6 @@ Continue from EXACTLY where you stopped.`, } }, [messages]) - // Save diagram XML to localStorage whenever it changes (debounced) - useEffect(() => { - if (!canSaveDiagram) return - if (!chartXML || chartXML.length <= 300) return - - // Clear any pending save - if (xmlStorageDebounceRef.current) { - clearTimeout(xmlStorageDebounceRef.current) - } - - // Debounce: save after 1 second of no changes - xmlStorageDebounceRef.current = setTimeout(() => { - localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML) - }, LOCAL_STORAGE_DEBOUNCE_MS) - - return () => { - if (xmlStorageDebounceRef.current) { - clearTimeout(xmlStorageDebounceRef.current) - } - } - }, [chartXML, canSaveDiagram]) - // Save XML snapshots to localStorage whenever they change const saveXmlSnapshots = useCallback(() => { try { @@ -916,6 +860,7 @@ Continue from EXACTLY where you stopped.`, }, ] as any) setInput("") + sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY) setFiles([]) return } @@ -963,6 +908,7 @@ Continue from EXACTLY where you stopped.`, // Token count is tracked in onFinish with actual server usage setInput("") + sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY) setFiles([]) } catch (error) { console.error("Error fetching chart data:", error) @@ -985,6 +931,7 @@ Continue from EXACTLY where you stopped.`, localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY) localStorage.removeItem(STORAGE_DIAGRAM_XML_KEY) localStorage.setItem(STORAGE_SESSION_ID_KEY, newSessionId) + sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY) toast.success("Started a fresh chat") } catch (error) { console.error("Failed to clear localStorage:", error) @@ -999,9 +946,14 @@ Continue from EXACTLY where you stopped.`, const handleInputChange = ( e: React.ChangeEvent, ) => { + saveInputToSessionStorage(e.target.value) setInput(e.target.value) } + const saveInputToSessionStorage = (input: string) => { + sessionStorage.setItem(SESSION_STORAGE_INPUT_KEY, input) + } + // Helper functions for message actions (regenerate/edit) // Extract previous XML snapshot before a given message index const getPreviousXml = (beforeIndex: number): string => { diff --git a/contexts/diagram-context.tsx b/contexts/diagram-context.tsx index a7cd4abd..5563e8bd 100644 --- a/contexts/diagram-context.tsx +++ b/contexts/diagram-context.tsx @@ -1,7 +1,7 @@ "use client" import type React from "react" -import { createContext, useContext, useRef, useState } from "react" +import { createContext, useContext, useEffect, useRef, useState } from "react" import type { DrawIoEmbedRef } from "react-drawio" import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel" import type { ExportFormat } from "@/components/save-dialog" @@ -40,12 +40,15 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) { { svg: string; xml: string }[] >([]) const [isDrawioReady, setIsDrawioReady] = useState(false) + const [canSaveDiagram, setCanSaveDiagram] = useState(false) const [showSaveDialog, setShowSaveDialog] = useState(false) const hasCalledOnLoadRef = useRef(false) const drawioRef = useRef(null) const resolverRef = useRef<((value: string) => void) | null>(null) // Track if we're expecting an export for history (user-initiated) const expectHistoryExportRef = useRef(false) + // Track if diagram has been restored from localStorage + const hasDiagramRestoredRef = useRef(false) const onDrawioLoad = () => { // Only set ready state once to prevent infinite loops @@ -61,6 +64,48 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) { setIsDrawioReady(false) } + // Restore diagram XML when DrawIO becomes ready + // eslint-disable-next-line react-hooks/exhaustive-deps -- loadDiagram uses refs internally and is stable + useEffect(() => { + // Reset restore flag when DrawIO is not ready (e.g., theme/UI change remounts it) + if (!isDrawioReady) { + hasDiagramRestoredRef.current = false + setCanSaveDiagram(false) + return + } + if (hasDiagramRestoredRef.current) return + hasDiagramRestoredRef.current = true + + try { + const savedDiagramXml = localStorage.getItem( + STORAGE_DIAGRAM_XML_KEY, + ) + if (savedDiagramXml) { + // Skip validation for trusted saved diagrams + loadDiagram(savedDiagramXml, true) + } + } catch (error) { + console.error("Failed to restore diagram from localStorage:", error) + } + + // Allow saving after restore is complete + setTimeout(() => { + setCanSaveDiagram(true) + }, 500) + }, [isDrawioReady]) + + // Save diagram XML to localStorage whenever it changes (debounced) + useEffect(() => { + if (!canSaveDiagram) return + if (!chartXML || chartXML.length <= 300) return + + const timeoutId = setTimeout(() => { + localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML) + }, 1000) + + return () => clearTimeout(timeoutId) + }, [chartXML, canSaveDiagram]) + // Track if we're expecting an export for file save (stores raw export data) const saveResolverRef = useRef<{ resolver: ((data: string) => void) | null From 81eb71e70406007ead630efc74cb8cb67e92c693 Mon Sep 17 00:00:00 2001 From: E66Crisp Date: Thu, 18 Dec 2025 20:16:32 +0800 Subject: [PATCH 002/349] fix(components): Send and sidebar buttons become inaccessible when chat-panel is resized (#309) --- components/chat-input.tsx | 4 ++-- components/chat-panel.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/chat-input.tsx b/components/chat-input.tsx index b63bbd12..8fe9d31d 100644 --- a/components/chat-input.tsx +++ b/components/chat-input.tsx @@ -335,7 +335,7 @@ export function ChatInput({ {/* Action bar */}
{/* Left actions */} -
+
{/* Right actions */} -
+
-
+
Next AI Drawio

)}

-
+
Date: Thu, 18 Dec 2025 21:00:21 +0800 Subject: [PATCH 003/349] feat: add support for custom AI Gateway base URL (#315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add support for custom AI Gateway base URL - Add createGateway support with configurable baseURL - Allow AI_GATEWAY_BASE_URL environment variable for: * Local development with custom Gateway * Self-hosted AI Gateway deployments * Enterprise proxy configurations - Maintain backward compatibility: defaults to Vercel Gateway when not set - Update documentation with usage examples and configuration notes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 * fix: remove errant character in error message --------- Co-authored-by: Claude Sonnet 4.5 Co-authored-by: dayuan.jiang --- docs/ai-providers.md | 19 +++++++++++++++++++ env.example | 2 ++ lib/ai-providers.ts | 17 +++++++++++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/docs/ai-providers.md b/docs/ai-providers.md index c06523a6..431d1bc2 100644 --- a/docs/ai-providers.md +++ b/docs/ai-providers.md @@ -140,17 +140,36 @@ OLLAMA_BASE_URL=http://localhost:11434 Vercel AI Gateway provides unified access to multiple AI providers through a single API key. This simplifies authentication and allows you to switch between providers without managing multiple API keys. +**Basic Usage (Vercel-hosted Gateway):** + ```bash AI_GATEWAY_API_KEY=your_gateway_api_key AI_MODEL=openai/gpt-4o ``` +**Custom Gateway URL (for local development or self-hosted Gateway):** + +```bash +AI_GATEWAY_API_KEY=your_custom_api_key +AI_GATEWAY_BASE_URL=https://your-custom-gateway.com/v1/ai +AI_MODEL=openai/gpt-4o +``` + Model format uses `provider/model` syntax: - `openai/gpt-4o` - OpenAI GPT-4o - `anthropic/claude-sonnet-4-5` - Anthropic Claude Sonnet 4.5 - `google/gemini-2.0-flash` - Google Gemini 2.0 Flash +**Configuration notes:** + +- If `AI_GATEWAY_BASE_URL` is not set, the default Vercel Gateway URL (`https://ai-gateway.vercel.sh/v1/ai`) is used +- Custom base URL is useful for: + - Local development with a custom Gateway instance + - Self-hosted AI Gateway deployments + - Enterprise proxy configurations +- When using a custom base URL, you must also provide `AI_GATEWAY_API_KEY` + Get your API key from the [Vercel AI Gateway dashboard](https://vercel.com/ai-gateway). ## Auto-Detection diff --git a/env.example b/env.example index 4992cf3e..36c3d35b 100644 --- a/env.example +++ b/env.example @@ -72,6 +72,8 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0 # Get your API key from: https://vercel.com/ai-gateway # Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5" # AI_GATEWAY_API_KEY=... +# AI_GATEWAY_BASE_URL=https://your-custom-gateway.com/v1/ai # Optional: Custom Gateway URL (for local dev or self-hosted Gateway) +# # If not set, uses Vercel default: https://ai-gateway.vercel.sh/v1/ai # Langfuse Observability (Optional) # Enable LLM tracing and analytics - https://langfuse.com diff --git a/lib/ai-providers.ts b/lib/ai-providers.ts index 9ac7675d..101ea706 100644 --- a/lib/ai-providers.ts +++ b/lib/ai-providers.ts @@ -2,7 +2,7 @@ import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock" import { createAnthropic } from "@ai-sdk/anthropic" import { azure, createAzure } from "@ai-sdk/azure" import { createDeepSeek, deepseek } from "@ai-sdk/deepseek" -import { gateway } from "@ai-sdk/gateway" +import { createGateway, gateway } from "@ai-sdk/gateway" import { createGoogleGenerativeAI, google } from "@ai-sdk/google" import { createOpenAI, openai } from "@ai-sdk/openai" import { fromNodeProviderChain } from "@aws-sdk/credential-providers" @@ -683,7 +683,20 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { // Vercel AI Gateway - unified access to multiple AI providers // Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5" // See: https://vercel.com/ai-gateway - model = gateway(modelId) + const apiKey = overrides?.apiKey || process.env.AI_GATEWAY_API_KEY + const baseURL = + overrides?.baseUrl || process.env.AI_GATEWAY_BASE_URL + // Only use custom configuration if explicitly set (local dev or custom Gateway) + // Otherwise undefined → AI SDK uses Vercel default (https://ai-gateway.vercel.sh/v1/ai) + OIDC + if (baseURL || overrides?.apiKey) { + const customGateway = createGateway({ + apiKey, + ...(baseURL && { baseURL }), + }) + model = customGateway(modelId) + } else { + model = gateway(modelId) + } break } From 6bb33eeda2d5c87ae41590800cc721ac054b0852 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Thu, 18 Dec 2025 23:03:08 +0900 Subject: [PATCH 004/349] chore: add auto-format workflow and fix formatting (#319) - Add GitHub Action to auto-format PRs with Biome - Fix formatting in app/manifest.ts and scripts/test-diagram-operations.mjs --- .github/workflows/auto-format.yml | 47 +++++++ app/manifest.ts | 49 +++---- scripts/test-diagram-operations.mjs | 202 ++++++++++++++++++++++------ 3 files changed, 235 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/auto-format.yml diff --git a/.github/workflows/auto-format.yml b/.github/workflows/auto-format.yml new file mode 100644 index 00000000..bbe997e7 --- /dev/null +++ b/.github/workflows/auto-format.yml @@ -0,0 +1,47 @@ +name: Auto Format + +on: + pull_request: + types: [opened, synchronize] + +permissions: + contents: write + +jobs: + format: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Biome + run: npm install --save-dev @biomejs/biome + + - name: Run Biome format + run: npx @biomejs/biome check --write --no-errors-on-unmatched . + + - name: Check for changes + id: changes + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Commit changes + if: steps.changes.outputs.has_changes == 'true' + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add . + git commit -m "style: auto-format with Biome" + git push diff --git a/app/manifest.ts b/app/manifest.ts index cb935764..47162004 100644 --- a/app/manifest.ts +++ b/app/manifest.ts @@ -1,27 +1,28 @@ -import type { MetadataRoute } from "next"; +import type { MetadataRoute } from "next" export default function manifest(): MetadataRoute.Manifest { - return { - name: 'Next AI Draw.io', - short_name: 'AIDraw.io', - description: 'Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.', - start_url: '/', - display: 'standalone', - background_color: '#f9fafb', - theme_color: '#171d26', - icons: [ - { - src: '/favicon-192x192.png', - sizes: '192x192', - type: 'image/png', - purpose: 'any', - }, - { - src: '/favicon-512x512.png', - sizes: '512x512', - type: 'image/png', - purpose: 'any', - }, - ], - } + return { + name: "Next AI Draw.io", + short_name: "AIDraw.io", + description: + "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.", + start_url: "/", + display: "standalone", + background_color: "#f9fafb", + theme_color: "#171d26", + icons: [ + { + src: "/favicon-192x192.png", + sizes: "192x192", + type: "image/png", + purpose: "any", + }, + { + src: "/favicon-512x512.png", + sizes: "512x512", + type: "image/png", + purpose: "any", + }, + ], + } } diff --git a/scripts/test-diagram-operations.mjs b/scripts/test-diagram-operations.mjs index 252660dc..f5e9113e 100644 --- a/scripts/test-diagram-operations.mjs +++ b/scripts/test-diagram-operations.mjs @@ -20,7 +20,13 @@ function applyDiagramOperations(xmlContent, operations) { if (parseError) { return { result: xmlContent, - errors: [{ type: "update", cellId: "", message: `XML parse error: ${parseError.textContent}` }], + errors: [ + { + type: "update", + cellId: "", + message: `XML parse error: ${parseError.textContent}`, + }, + ], } } @@ -28,7 +34,13 @@ function applyDiagramOperations(xmlContent, operations) { if (!root) { return { result: xmlContent, - errors: [{ type: "update", cellId: "", message: "Could not find element in XML" }], + errors: [ + { + type: "update", + cellId: "", + message: "Could not find element in XML", + }, + ], } } @@ -42,22 +54,41 @@ function applyDiagramOperations(xmlContent, operations) { if (op.type === "update") { const existingCell = cellMap.get(op.cell_id) if (!existingCell) { - errors.push({ type: "update", cellId: op.cell_id, message: `Cell with id="${op.cell_id}" not found` }) + errors.push({ + type: "update", + cellId: op.cell_id, + message: `Cell with id="${op.cell_id}" not found`, + }) continue } if (!op.new_xml) { - errors.push({ type: "update", cellId: op.cell_id, message: "new_xml is required for update operation" }) + errors.push({ + type: "update", + cellId: op.cell_id, + message: "new_xml is required for update operation", + }) continue } - const newDoc = parser.parseFromString(`${op.new_xml}`, "text/xml") + const newDoc = parser.parseFromString( + `${op.new_xml}`, + "text/xml", + ) const newCell = newDoc.querySelector("mxCell") if (!newCell) { - errors.push({ type: "update", cellId: op.cell_id, message: "new_xml must contain an mxCell element" }) + errors.push({ + type: "update", + cellId: op.cell_id, + message: "new_xml must contain an mxCell element", + }) continue } const newCellId = newCell.getAttribute("id") if (newCellId !== op.cell_id) { - errors.push({ type: "update", cellId: op.cell_id, message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"` }) + errors.push({ + type: "update", + cellId: op.cell_id, + message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`, + }) continue } const importedNode = doc.importNode(newCell, true) @@ -65,22 +96,41 @@ function applyDiagramOperations(xmlContent, operations) { cellMap.set(op.cell_id, importedNode) } else if (op.type === "add") { if (cellMap.has(op.cell_id)) { - errors.push({ type: "add", cellId: op.cell_id, message: `Cell with id="${op.cell_id}" already exists` }) + errors.push({ + type: "add", + cellId: op.cell_id, + message: `Cell with id="${op.cell_id}" already exists`, + }) continue } if (!op.new_xml) { - errors.push({ type: "add", cellId: op.cell_id, message: "new_xml is required for add operation" }) + errors.push({ + type: "add", + cellId: op.cell_id, + message: "new_xml is required for add operation", + }) continue } - const newDoc = parser.parseFromString(`${op.new_xml}`, "text/xml") + const newDoc = parser.parseFromString( + `${op.new_xml}`, + "text/xml", + ) const newCell = newDoc.querySelector("mxCell") if (!newCell) { - errors.push({ type: "add", cellId: op.cell_id, message: "new_xml must contain an mxCell element" }) + errors.push({ + type: "add", + cellId: op.cell_id, + message: "new_xml must contain an mxCell element", + }) continue } const newCellId = newCell.getAttribute("id") if (newCellId !== op.cell_id) { - errors.push({ type: "add", cellId: op.cell_id, message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"` }) + errors.push({ + type: "add", + cellId: op.cell_id, + message: `ID mismatch: cell_id is "${op.cell_id}" but new_xml has id="${newCellId}"`, + }) continue } const importedNode = doc.importNode(newCell, true) @@ -89,7 +139,11 @@ function applyDiagramOperations(xmlContent, operations) { } else if (op.type === "delete") { const existingCell = cellMap.get(op.cell_id) if (!existingCell) { - errors.push({ type: "delete", cellId: op.cell_id, message: `Cell with id="${op.cell_id}" not found` }) + errors.push({ + type: "delete", + cellId: op.cell_id, + message: `Cell with id="${op.cell_id}" not found`, + }) continue } existingCell.parentNode?.removeChild(existingCell) @@ -149,28 +203,52 @@ test("Update operation changes cell value", () => { { type: "update", cell_id: "2", - new_xml: '', + new_xml: + '', }, ]) - assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`) - assert(result.includes('value="Updated Box A"'), "Updated value should be in result") - assert(!result.includes('value="Box A"'), "Old value should not be in result") + assert( + errors.length === 0, + `Expected no errors, got: ${JSON.stringify(errors)}`, + ) + assert( + result.includes('value="Updated Box A"'), + "Updated value should be in result", + ) + assert( + !result.includes('value="Box A"'), + "Old value should not be in result", + ) }) test("Update operation fails for non-existent cell", () => { const { errors } = applyDiagramOperations(sampleXml, [ - { type: "update", cell_id: "999", new_xml: '' }, + { + type: "update", + cell_id: "999", + new_xml: '', + }, ]) assert(errors.length === 1, "Should have one error") - assert(errors[0].message.includes("not found"), "Error should mention not found") + assert( + errors[0].message.includes("not found"), + "Error should mention not found", + ) }) test("Update operation fails on ID mismatch", () => { const { errors } = applyDiagramOperations(sampleXml, [ - { type: "update", cell_id: "2", new_xml: '' }, + { + type: "update", + cell_id: "2", + new_xml: '', + }, ]) assert(errors.length === 1, "Should have one error") - assert(errors[0].message.includes("ID mismatch"), "Error should mention ID mismatch") + assert( + errors[0].message.includes("ID mismatch"), + "Error should mention ID mismatch", + ) }) test("Add operation creates new cell", () => { @@ -178,41 +256,72 @@ test("Add operation creates new cell", () => { { type: "add", cell_id: "new1", - new_xml: '', + new_xml: + '', }, ]) - assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`) + assert( + errors.length === 0, + `Expected no errors, got: ${JSON.stringify(errors)}`, + ) assert(result.includes('id="new1"'), "New cell should be in result") - assert(result.includes('value="New Box"'), "New cell value should be in result") + assert( + result.includes('value="New Box"'), + "New cell value should be in result", + ) }) test("Add operation fails for duplicate ID", () => { const { errors } = applyDiagramOperations(sampleXml, [ - { type: "add", cell_id: "2", new_xml: '' }, + { + type: "add", + cell_id: "2", + new_xml: '', + }, ]) assert(errors.length === 1, "Should have one error") - assert(errors[0].message.includes("already exists"), "Error should mention already exists") + assert( + errors[0].message.includes("already exists"), + "Error should mention already exists", + ) }) test("Add operation fails on ID mismatch", () => { const { errors } = applyDiagramOperations(sampleXml, [ - { type: "add", cell_id: "new1", new_xml: '' }, + { + type: "add", + cell_id: "new1", + new_xml: '', + }, ]) assert(errors.length === 1, "Should have one error") - assert(errors[0].message.includes("ID mismatch"), "Error should mention ID mismatch") + assert( + errors[0].message.includes("ID mismatch"), + "Error should mention ID mismatch", + ) }) test("Delete operation removes cell", () => { - const { result, errors } = applyDiagramOperations(sampleXml, [{ type: "delete", cell_id: "3" }]) - assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`) + const { result, errors } = applyDiagramOperations(sampleXml, [ + { type: "delete", cell_id: "3" }, + ]) + assert( + errors.length === 0, + `Expected no errors, got: ${JSON.stringify(errors)}`, + ) assert(!result.includes('id="3"'), "Deleted cell should not be in result") assert(result.includes('id="2"'), "Other cells should remain") }) test("Delete operation fails for non-existent cell", () => { - const { errors } = applyDiagramOperations(sampleXml, [{ type: "delete", cell_id: "999" }]) + const { errors } = applyDiagramOperations(sampleXml, [ + { type: "delete", cell_id: "999" }, + ]) assert(errors.length === 1, "Should have one error") - assert(errors[0].message.includes("not found"), "Error should mention not found") + assert( + errors[0].message.includes("not found"), + "Error should mention not found", + ) }) test("Multiple operations in sequence", () => { @@ -220,30 +329,45 @@ test("Multiple operations in sequence", () => { { type: "update", cell_id: "2", - new_xml: '', + new_xml: + '', }, { type: "add", cell_id: "new1", - new_xml: '', + new_xml: + '', }, { type: "delete", cell_id: "3" }, ]) - assert(errors.length === 0, `Expected no errors, got: ${JSON.stringify(errors)}`) - assert(result.includes('value="Updated"'), "Updated value should be present") + assert( + errors.length === 0, + `Expected no errors, got: ${JSON.stringify(errors)}`, + ) + assert( + result.includes('value="Updated"'), + "Updated value should be present", + ) assert(result.includes('id="new1"'), "Added cell should be present") assert(!result.includes('id="3"'), "Deleted cell should not be present") }) test("Invalid XML returns parse error", () => { - const { errors } = applyDiagramOperations(" { - const { errors } = applyDiagramOperations("", [{ type: "delete", cell_id: "1" }]) + const { errors } = applyDiagramOperations("", [ + { type: "delete", cell_id: "1" }, + ]) assert(errors.length === 1, "Should have one error") - assert(errors[0].message.includes("root"), "Error should mention root element") + assert( + errors[0].message.includes("root"), + "Error should mention root element", + ) }) // Summary From f087b54ee44dbcb95dfccfe36feebd235dbe3f59 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Sat, 20 Dec 2025 23:19:49 +0900 Subject: [PATCH 005/349] feat: add get_shape_library tool for AI icon discovery (#335) * feat: add get_shape_library tool for AI icon discovery - Add server-side tool that returns shape library documentation - AI can fetch icon/shape names on-demand before generating diagrams - Includes path traversal protection and input sanitization - Library index embedded in tool description for discoverability - Supports 33 libraries: AWS, Azure, GCP, Kubernetes, Cisco, etc. * fix: improve get_shape_library error handling and imports - Move fs/path imports to top of file (avoid dynamic imports per call) - Distinguish file-not-found vs other errors in catch block - Include invalid input in validation error message - Log unexpected errors for debugging * docs: add get_shape_library to system prompt tool list - Add Tool4 (get_shape_library) to available tools section - Add usage guidance in 'Choose the right tool' section - Update AWS icons note to reference get_shape_library for icon discovery * fix: display get_shape_library tool output in chat UI * fix: correct state check for get_shape_library output display * fix: make get_shape_library output respect fold state * style: auto-format with Biome --------- Co-authored-by: github-actions[bot] --- app/api/chat/route.ts | 65 ++ components/chat-message-display.tsx | 21 + docs/shape-libraries/README.md | 78 ++ docs/shape-libraries/alibaba_cloud.md | 328 ++++++++ docs/shape-libraries/android.md | 62 ++ docs/shape-libraries/arrows2.md | 33 + docs/shape-libraries/atlassian.md | 32 + docs/shape-libraries/aws4.md | 1049 +++++++++++++++++++++++++ docs/shape-libraries/azure2.md | 431 ++++++++++ docs/shape-libraries/basic.md | 48 ++ docs/shape-libraries/bpmn.md | 60 ++ docs/shape-libraries/cabinets.md | 71 ++ docs/shape-libraries/cisco19.md | 250 ++++++ docs/shape-libraries/citrix.md | 115 +++ docs/shape-libraries/electrical.md | 50 ++ docs/shape-libraries/floorplan.md | 62 ++ docs/shape-libraries/flowchart.md | 52 ++ docs/shape-libraries/fluidpower.md | 264 +++++++ docs/shape-libraries/gcp2.md | 315 ++++++++ docs/shape-libraries/infographic.md | 24 + docs/shape-libraries/kubernetes.md | 58 ++ docs/shape-libraries/lean_mapping.md | 31 + docs/shape-libraries/mscae.md | 22 + docs/shape-libraries/network.md | 72 ++ docs/shape-libraries/openstack.md | 36 + docs/shape-libraries/pid.md | 22 + docs/shape-libraries/rack.md | 57 ++ docs/shape-libraries/salesforce.md | 116 +++ docs/shape-libraries/sap.md | 179 +++++ docs/shape-libraries/sitemap.md | 68 ++ docs/shape-libraries/vvd.md | 112 +++ docs/shape-libraries/webicons.md | 194 +++++ lib/system-prompts.ts | 9 +- package-lock.json | 6 +- package.json | 2 +- 35 files changed, 4389 insertions(+), 5 deletions(-) create mode 100644 docs/shape-libraries/README.md create mode 100644 docs/shape-libraries/alibaba_cloud.md create mode 100644 docs/shape-libraries/android.md create mode 100644 docs/shape-libraries/arrows2.md create mode 100644 docs/shape-libraries/atlassian.md create mode 100644 docs/shape-libraries/aws4.md create mode 100644 docs/shape-libraries/azure2.md create mode 100644 docs/shape-libraries/basic.md create mode 100644 docs/shape-libraries/bpmn.md create mode 100644 docs/shape-libraries/cabinets.md create mode 100644 docs/shape-libraries/cisco19.md create mode 100644 docs/shape-libraries/citrix.md create mode 100644 docs/shape-libraries/electrical.md create mode 100644 docs/shape-libraries/floorplan.md create mode 100644 docs/shape-libraries/flowchart.md create mode 100644 docs/shape-libraries/fluidpower.md create mode 100644 docs/shape-libraries/gcp2.md create mode 100644 docs/shape-libraries/infographic.md create mode 100644 docs/shape-libraries/kubernetes.md create mode 100644 docs/shape-libraries/lean_mapping.md create mode 100644 docs/shape-libraries/mscae.md create mode 100644 docs/shape-libraries/network.md create mode 100644 docs/shape-libraries/openstack.md create mode 100644 docs/shape-libraries/pid.md create mode 100644 docs/shape-libraries/rack.md create mode 100644 docs/shape-libraries/salesforce.md create mode 100644 docs/shape-libraries/sap.md create mode 100644 docs/shape-libraries/sitemap.md create mode 100644 docs/shape-libraries/vvd.md create mode 100644 docs/shape-libraries/webicons.md diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index cc844658..5c446832 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -8,7 +8,9 @@ import { stepCountIs, streamText, } from "ai" +import fs from "fs/promises" import { jsonrepair } from "jsonrepair" +import path from "path" import { z } from "zod" import { getAIModel, supportsPromptCaching } from "@/lib/ai-providers" import { findCachedResponse } from "@/lib/cached-responses" @@ -596,6 +598,69 @@ Example: If previous output ended with ' { + // Sanitize input - prevent path traversal attacks + const sanitizedLibrary = library + .toLowerCase() + .replace(/[^a-z0-9_-]/g, "") + + if (sanitizedLibrary !== library.toLowerCase()) { + return `Invalid library name "${library}". Use only letters, numbers, underscores, and hyphens.` + } + + const baseDir = path.join( + process.cwd(), + "docs/shape-libraries", + ) + const filePath = path.join( + baseDir, + `${sanitizedLibrary}.md`, + ) + + // Verify path stays within expected directory + const resolvedPath = path.resolve(filePath) + if (!resolvedPath.startsWith(path.resolve(baseDir))) { + return `Invalid library path.` + } + + try { + const content = await fs.readFile(filePath, "utf-8") + return content + } catch (error) { + if ( + (error as NodeJS.ErrnoException).code === "ENOENT" + ) { + return `Library "${library}" not found. Available: aws4, azure2, gcp2, alibaba_cloud, cisco19, kubernetes, network, bpmn, flowchart, basic, arrows2, vvd, salesforce, citrix, sap, mscae, atlassian, fluidpower, electrical, pid, cabinets, floorplan, webicons, infographic, sitemap, android, lean_mapping, openstack, rack` + } + console.error( + `[get_shape_library] Error loading "${library}":`, + error, + ) + return `Error loading library "${library}". Please try again.` + } + }, + }, }, ...(process.env.TEMPERATURE !== undefined && { temperature: parseFloat(process.env.TEMPERATURE), diff --git a/components/chat-message-display.tsx b/components/chat-message-display.tsx index b2dafadb..58f2aaf7 100644 --- a/components/chat-message-display.tsx +++ b/components/chat-message-display.tsx @@ -634,6 +634,8 @@ export function ChatMessageDisplay({ return "Generate Diagram" case "edit_diagram": return "Edit Diagram" + case "get_shape_library": + return "Get Shape Library" default: return name } @@ -728,6 +730,25 @@ export function ChatMessageDisplay({
) })()} + {/* Show get_shape_library output on success */} + {output && + toolName === "get_shape_library" && + state === "output-available" && + isExpanded && ( +
+
+ Library loaded ( + {typeof output === "string" ? output.length : 0}{" "} + chars) +
+
+                                {typeof output === "string"
+                                    ? output.substring(0, 800) +
+                                      (output.length > 800 ? "\n..." : "")
+                                    : String(output)}
+                            
+
+ )}
) } diff --git a/docs/shape-libraries/README.md b/docs/shape-libraries/README.md new file mode 100644 index 00000000..44ef00e4 --- /dev/null +++ b/docs/shape-libraries/README.md @@ -0,0 +1,78 @@ +# Draw.io Shape Libraries + +Reference: `style="shape=mxgraph.."` + +## Cloud Providers + +| Library | Shapes | Prefix | Description | File | +|---------|--------|--------|-------------|------| +| aws4 | 1031 | `mxgraph.aws4` | Amazon Web Services (2025) - EC2, S3, Lambda, RDS, etc. | [aws4.md](./aws4.md) | +| azure2 | 608 | `img/lib/azure2/` | Microsoft Azure (2024) - VMs, Storage, AI, Networking, etc. | [azure2.md](./azure2.md) | +| gcp2 | 297 | `mxgraph.gcp2` | Google Cloud Platform - Compute Engine, BigQuery, GKE, etc. | [gcp2.md](./gcp2.md) | +| alibaba_cloud | 273 | `mxgraph.alibaba_cloud` | Alibaba Cloud - ECS, OSS, RDS, SLB, VPC, etc. | [alibaba_cloud.md](./alibaba_cloud.md) | +| openstack | 18 | `mxgraph.openstack` | OpenStack cloud platform icons | [openstack.md](./openstack.md) | +| digitalocean | 74 | `mxgraph.digitalocean` | DigitalOcean - Droplets, Spaces, Kubernetes, etc. | [digitalocean.md](./digitalocean.md) | +| salesforce | 96 | `mxgraph.salesforce` | Salesforce platform icons | [salesforce.md](./salesforce.md) | + +## Networking & Infrastructure + +| Library | Shapes | Prefix | Description | File | +|---------|--------|--------|-------------|------| +| cisco19 | 232 | `mxgraph.cisco19` | Cisco network equipment - routers, switches, firewalls | [cisco19.md](./cisco19.md) | +| network | 58 | `mxgraph.networks` | General network diagram symbols | [network.md](./network.md) | +| arista | 45 | `mxgraph.arista` | Arista network switches and equipment | [arista.md](./arista.md) | +| kubernetes | 40 | `mxgraph.kubernetes` | Kubernetes - pods, services, deployments, nodes | [kubernetes.md](./kubernetes.md) | +| vvd | 93 | `mxgraph.vvd` | VMware Validated Design icons | [vvd.md](./vvd.md) | +| rack | 11 | `mxgraph.rack` | Server rack and data center equipment | [rack.md](./rack.md) | + +## Business Process + +| Library | Shapes | Prefix | Description | File | +|---------|--------|--------|-------------|------| +| bpmn | 39 | `mxgraph.bpmn` | Business Process Model and Notation - events, gateways, tasks | [bpmn.md](./bpmn.md) | +| eip | 36 | `mxgraph.eip` | Enterprise Integration Patterns - messaging, routing | [eip.md](./eip.md) | +| lean_mapping | 13 | `mxgraph.lean_mapping` | Lean/Value Stream Mapping symbols | [lean_mapping.md](./lean_mapping.md) | + +## General Diagrams + +| Library | Shapes | Prefix | Description | File | +|---------|--------|--------|-------------|------| +| flowchart | 34 | `mxgraph.flowchart` | Standard flowchart symbols - process, decision, data | [flowchart.md](./flowchart.md) | +| basic | 30 | `mxgraph.basic` | Basic shapes - stars, banners, callouts, hearts | [basic.md](./basic.md) | +| arrows2 | 34 | `mxgraph.arrows2` | Arrow shapes and connectors | [arrows2.md](./arrows2.md) | +| infographic | 29 | `mxgraph.infographic` | Infographic elements - charts, icons, badges | [infographic.md](./infographic.md) | +| sitemap | 50 | `mxgraph.sitemap` | Website sitemap icons - pages, forms, navigation | [sitemap.md](./sitemap.md) | + +## UI/Mockups + +| Library | Shapes | Prefix | Description | File | +|---------|--------|--------|-------------|------| +| android | 17 | `mxgraph.android` | Android UI mockup components | [android.md](./android.md) | + +## Enterprise Software + +| Library | Shapes | Prefix | Description | File | +|---------|--------|--------|-------------|------| +| citrix | 97 | `mxgraph.citrix` | Citrix virtualization - XenApp, XenDesktop, NetScaler | [citrix.md](./citrix.md) | +| sap | 98 | `mxgraph.sap` | SAP enterprise software icons | [sap.md](./sap.md) | +| mscae | 73 | `mxgraph.mscae` | Microsoft Cloud and Enterprise symbols | [mscae.md](./mscae.md) | +| atlassian | 26 | `mxgraph.atlassian` | Atlassian - Jira, Confluence issue types | [atlassian.md](./atlassian.md) | + +## Engineering + +| Library | Shapes | Prefix | Description | File | +|---------|--------|--------|-------------|------| +| fluidpower | 246 | `mxgraph.fluid_power` | Hydraulic/pneumatic engineering symbols | [fluidpower.md](./fluidpower.md) | +| electrical | 50 | `mxgraph.electrical` | Electrical circuit symbols - resistors, capacitors | [electrical.md](./electrical.md) | +| pid | 18 | `mxgraph.pid2` | Piping and Instrumentation Diagram symbols | [pid.md](./pid.md) | +| cabinets | 53 | `mxgraph.cabinets` | Electrical cabinet components - breakers, terminals | [cabinets.md](./cabinets.md) | +| floorplan | 44 | `mxgraph.floorplan` | Floor plan furniture and fixtures | [floorplan.md](./floorplan.md) | + +## Icons & Graphics + +| Library | Shapes | Prefix | Description | File | +|---------|--------|--------|-------------|------| +| webicons | 176 | `mxgraph.webicons` | Web/social media logos - GitHub, Twitter, AWS, etc. | [webicons.md](./webicons.md) | +| un-ocha-icons | 242 | `mxgraph.un-ocha-icons` | UN OCHA humanitarian icons | [un-ocha-icons.md](./un-ocha-icons.md) | + +**Total: 33 libraries, 4,281 shapes** diff --git a/docs/shape-libraries/alibaba_cloud.md b/docs/shape-libraries/alibaba_cloud.md new file mode 100644 index 00000000..ffcb81e5 --- /dev/null +++ b/docs/shape-libraries/alibaba_cloud.md @@ -0,0 +1,328 @@ +# alibaba_cloud + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.alibaba_cloud` + +## Usage + +```xml + + + +``` + + + +## Shapes (311) + +- `abap_business_application_platform` +- `acms_application_configuration_manangement` +- `acr_cloud_container_registry` +- `actiontrail` +- `adam_advanced_database_and_application_migration` +- `adb_analyticdb_for_mysql` +- `address_purification` +- `afs_fraud_service` +- `agw_aligateway` +- `ahas_application_high_availability_service` +- `airec_artificial_intelligence_recommendation` +- `alb_application_load_balancer_01` +- `alb_application_load_balancer_02` +- `alibaba_cloud_logo` +- `alibaba_cloud_logo_chinese` +- `alibaba_cloud_logo_english` +- `alimail` +- `alimt_machine_translation` +- `aliyun_linux` +- `amqp_advanced_message_queuing_protocol` +- `amscloudapp` +- `analyticdb_for_postgresql` +- `antibot` +- `apigateway` +- `apsara_file_storage_for_hdfs` +- `apsaravideo_vod` +- `arms_application_real-time_monitoring_service` +- `ask_ack_container_service_for_kubernetes` +- `asm_service_mesh` +- `assettech` +- `avds_vulnerability_db_scanning` +- `baas_blockchain_as_a_service` +- `bandwidth_bag` +- `bastionhost` +- `batchcompute` +- `bccluster` +- `beebot` +- `beian` +- `bizdevops` +- `bizworks` +- `bpstudio` +- `cas_ssl_central_authentication_service` +- `cassandra_wide-column_database_01` +- `cassandra_wide-column_database_02` +- `ccc_cloud_call_center` +- `ccn_cloud_connect_network` +- `ccs_customer_service_01` +- `ccs_customer_service_02` +- `cddc_cloud_database_dedicated_cluster` +- `cdn_content_distribution_network` +- `cdp_cloudera_cdp` +- `cdt_cloud_datatransfer` +- `cen_cloud_enterprise_network` +- `cfw_cloud_firewall` +- `cityvisual` +- `clb_classic_load_balancer_01` +- `clb_classic_load_balancer_02` +- `clickhouse` +- `cloud_auth` +- `cloud_config` +- `cloud_display` +- `cloud_governance_center` +- `cloud_security_center` +- `cloud_shield` +- `cloudap` +- `cloudbox` +- `clouddesktop` +- `clouddev` +- `cloudphoto` +- `cloudproc` +- `cloudshell` +- `cmn_cloud_managed_network` +- `cmp_cloud_mobile_push` +- `cms_cloud_monitor_service` +- `codepipeline` +- `codestore` +- `companyreg` +- `computenest` +- `content_security` +- `coo` +- `cpns_cell_phone_number_service` +- `csas_cloud_security_access_service` +- `cvc_cloud_video_conferencing` +- `cwh_cloud_web_hosting` +- `das_database_autonomy_service` +- `databot` +- `datahub` +- `dataphin` +- `dataquotient` +- `datav` +- `dataworks_dataide` +- `dbaudit` +- `dbes_database_expert_service` +- `dbfs_database_file_system` +- `dbs_database_backup` +- `dcdn_dynamic_route_for_cdn` +- `ddh_dedicated_host` +- `ddos-bgp` +- `ddos-dip` +- `ddos-pro` +- `ddos_protection` +- `devops` +- `dg_database_gateway` +- `directmail` +- `disk_block_storage` +- `dlf_data_lake_formation` +- `dms_data_management_service` +- `dns_domain_name_system` +- `dns_privatezone_01` +- `dns_privatezone_02` +- `domain` +- `domain_and_website` +- `drds_distribute_relational_database_service` +- `dsi_data_security_insurance` +- `dts_data_transmission_service` +- `e-mapreduce` +- `eais_elastic_accelerated_computing_instances` +- `eci_elastic_container_instance` +- `ecs_elastic_compute_service` +- `edas_enterprise_distributed_application_service` +- `ehpc_elastic_high_performance_computing` +- `eip_elastic_ip_address` +- `elastic_web_hosting` +- `elasticsearch` +- `emas_enterprise_mobile_application_studio` +- `energyexpert` +- `ens_edge_node_service` +- `enterprise_website` +- `eprofile` +- `esign` +- `ess_elastic_scaling_service` +- `eventbridge` +- `express_connect` +- `face_recognition` +- `fc_function_compute` +- `flow_service` +- `flowbag` +- `fnf_serverless_function_flow` +- `fpga_field_programmable_gate_array` +- `fraud_detection` +- `ga_global_accelerator` +- `gameshield` +- `gdb_graph_database` +- `graphanalytics` +- `graphcompute` +- `gtm_global_traffic_manager` +- `gts_global_transaction_service` +- `gws_graphic_workstation` +- `havip_high-availability_virtual_ip_address` +- `hbase` +- `hbr_hybrid_backup_recovery` +- `hcs-hgw_hybrid_cloud_storage_array` +- `hcs-mgw_hybrid_cloud_storage_datatransport` +- `hcs-sgw_hybrid_cloud_storage_gateway` +- `hdr_hybrid_disaster_recovery` +- `hologres` +- `holowatcher` +- `hsm_hardware_security_module` +- `httpdns` +- `idrsservice` +- `image_recognition` +- `imagesearch` +- `imarketing` +- `imm_intelligent_media_management` +- `imp_intelligent_media_production` +- `imp_low_code_video_factory` +- `indvi_industrial_visual_intelligence` +- `intelligent_advisor` +- `iot_internet_of_things_platform` +- `iot_wireless_connection_service` +- `iotid_identity` +- `iov_iot_vehicle_cloud` +- `ipv6_gateway` +- `isoc_iot_security_operations_center` +- `isu_intelligent_semantic_understanding` +- `ivision` +- `ivpd_intelligent_visual_production` +- `kafka` +- `linkedmall` +- `linkwan` +- `live` +- `livinglink` +- `log_streaming` +- `logic_composer` +- `machine_learning` +- `man_mobile_analytics` +- `mariadb` +- `mas_mobile_acceleration_service` +- `maxcompute` +- `memcache` +- `miniappdev` +- `mns_message_service` +- `mobile_hotfix` +- `mobsec` +- `mongodb` +- `mps-ai` +- `mps-censor` +- `mps-cover` +- `mps-dna` +- `mps-multimod` +- `mps-produce` +- `mps_apsaravideo_media_processing` +- `mq_message_queue` +- `mqc_mobile_quality_center` +- `mse_microservices_engine` +- `multi-cloud_finops` +- `multi-mode_database_lindorm` +- `multimediaai` +- `mxgraph.alibaba_cloud` +- `mysql` +- `nas_network_attached_storage` +- `nat_gateway` +- `network_acl_access_control_list` +- `nlb_network_load_balancer_01` +- `nlb_network_load_balancer_02` +- `nlp-address` +- `nlp-automl` +- `nlp-ie_text_information_extraction` +- `nlp-ke_keyword_extraction` +- `nlp-ner_named_entity_recognition` +- `nlp-pos_part-of-speech_tagging` +- `nlp-ra_reflexive_anaphora` +- `nlp-sa_sentiment_analysis` +- `nlp-tc_text_categorization` +- `nlp-ws_word_segmentation` +- `nlp_natural_language_processing` +- `nls` +- `nls-asrbag` +- `nls-asrcustommodel` +- `nls-filebag` +- `nls-service` +- `nls-shortasrbag` +- `nls-ttsbag` +- `nodejs_performance_platform` +- `oceanbase` +- `ocr_optical_character_recognition` +- `onsmqtt_micro_message_queuing_telemetry_transport` +- `oos_operation_orchestration_service` +- `openanalytics` +- `openapi_explorer` +- `opensearch` +- `oss_object_storage_service` +- `ots_tablestore` +- `outboundbot` +- `pcdn_p2p_cdn` +- `petadata_hybriddb_for_mysql` +- `physical_connection` +- `pnvs_phone_number_verification_service` +- `polardb` +- `porana_portrait_analysis` +- `postgresql` +- `ppas_pay-as-you-go_database` +- `privatelink` +- `prometheus` +- `prophet` +- `pts_performance_test_service` +- `quickbi` +- `ram_resource_access_management` +- `re_recommendation_engine` +- `realtime_compute` +- `redis_kvstore` +- `region` +- `retailir` +- `ros_resource_orchestration_service` +- `route_table` +- `router` +- `rsimganalys` +- `rtc_real-time_communication` +- `sae_serverless_app_engine` +- `sag_smart_access_gateway_01` +- `sag_smart_access_gateway_02` +- `sas_situational_awareness` +- `sca_smart_conversation_analysis_01` +- `sca_smart_conversation_analysis_02` +- `scc_super_computing_cluster` +- `scdn_secure_cdn` +- `scu_storage_capacity_unit` +- `sddp_sensitive_data_protection` +- `shared_bandwidth` +- `shared_flow_bag` +- `shc_shield_hybrid_cloud` +- `slb_server_load_balancer_01` +- `slb_server_load_balancer_02` +- `slb_server_load_balancer_03` +- `sls_simple_log_service` +- `smc_server_migration_center` +- `sms_short_message_service` +- `sos` +- `spark_data_insights` +- `sppc` +- `sqlserver` +- `swas_simple_application_server` +- `tr_transit_router` +- `trademark_service` +- `uis_ultimate_internet_service` +- `user` +- `user_feedback_01` +- `user_feedback_02` +- `vbr_virtual_border_router` +- `vcs_visual_computing_service` +- `vms_voice_messaging_service` +- `voicebot_intelligent_voice_navigation` +- `vpc_virtual_private_cloud` +- `vpn_gateway` +- `vs_video_surveillance` +- `vswitch` +- `waf_web_application_firewall` +- `webplus_web_app_service` +- `xdragon_bare_metal_server` +- `xtrace` +- `yida` diff --git a/docs/shape-libraries/android.md b/docs/shape-libraries/android.md new file mode 100644 index 00000000..3a3fb9ce --- /dev/null +++ b/docs/shape-libraries/android.md @@ -0,0 +1,62 @@ +# android + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.android` + +## Usage + +```xml + + + +``` + +## Shapes (47) + +- `action_bar` +- `action_bar_landscape` +- `anchor` +- `checkbox` +- `contact_badge_focused` +- `contextual_action_bar` +- `contextual_action_bar_landscape` +- `contextual_split_action_bar` +- `contextual_split_action_bar_landscape` +- `contextual_split_action_bar_landscape_white` +- `indeterminateSpinner` +- `indeterminate_progress_bar` +- `keyboard` +- `navigation_bar_1` +- `navigation_bar_1_landscape` +- `navigation_bar_1_vertical` +- `navigation_bar_2` +- `navigation_bar_3` +- `navigation_bar_3_landscape` +- `navigation_bar_4` +- `navigation_bar_5` +- `navigation_bar_5_vertical` +- `navigation_bar_6` +- `phone2` +- `progressBar` +- `progressScrubberDisabled` +- `progressScrubberFocused` +- `progressScrubberPressed` +- `quick_contact` +- `quickscroll2` +- `quickscroll3` +- `rect` +- `rrect` +- `scrollbars2` +- `spinner2` +- `split_action_bar` +- `split_action_bar_landscape` +- `statusBar` +- `switch_off` +- `switch_on` +- `tab2` +- `textSelHandles` +- `text_insertion_point` +- `textfield` +- `time_picker` +- `time_picker_dark` +- `transparent` diff --git a/docs/shape-libraries/arrows2.md b/docs/shape-libraries/arrows2.md new file mode 100644 index 00000000..5cd8137c --- /dev/null +++ b/docs/shape-libraries/arrows2.md @@ -0,0 +1,33 @@ +# arrows2 + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.arrows2` + +## Usage + +```xml + + + +``` + +## Shapes (18) + +- `arrow` +- `bendArrow` +- `bendDoubleArrow` +- `calloutArrow` +- `calloutDouble90Arrow` +- `calloutDoubleArrow` +- `calloutQuadArrow` +- `jumpInArrow` +- `quadArrow` +- `sharpArrow` +- `sharpArrow2` +- `stripedArrow` +- `stylisedArrow` +- `tailedArrow` +- `tailedNotchedArrow` +- `triadArrow` +- `twoWayArrow` +- `uTurnArrow` diff --git a/docs/shape-libraries/atlassian.md b/docs/shape-libraries/atlassian.md new file mode 100644 index 00000000..d6612d8b --- /dev/null +++ b/docs/shape-libraries/atlassian.md @@ -0,0 +1,32 @@ +# atlassian + +**Type:** SVG images +**Path:** `img/lib/atlassian/` + +## Usage + +```xml + + + +``` + +## Shapes (17) + +- `Atlassian_Logo` +- `Bamboo_Logo` +- `Bitbucket_Logo` +- `Clover_Logo` +- `Confluence_Logo` +- `Crowd_Logo` +- `Crucible_Logo` +- `Fisheye_Logo` +- `Hipchat_Logo` +- `Jira_Core_Logo` +- `Jira_Logo` +- `Jira_Service_Desk_Logo` +- `Jira_Software_Logo` +- `Sourcetree_Logo` +- `Statuspage_Logo` +- `Stride_Logo` +- `Trello_Logo` diff --git a/docs/shape-libraries/aws4.md b/docs/shape-libraries/aws4.md new file mode 100644 index 00000000..4a1e2dd4 --- /dev/null +++ b/docs/shape-libraries/aws4.md @@ -0,0 +1,1049 @@ +# aws4 + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.aws4` + +## Usage + +```xml + + + +``` + +For simple shapes use: `shape=mxgraph.aws4.{shape};fillColor=#232F3D;` + +## Shapes (1032) + +- `a1_instance` +- `access_analyzer` +- `action` +- `activate` +- `actuator` +- `ad_connector` +- `addon` +- `agent` +- `agent2` +- `alarm` +- `alert` +- `alexa_enabled_device` +- `alexa_for_business` +- `alexa_skill` +- `alexa_smart_home_skill` +- `alexa_voice_service` +- `all_products` +- `ami` +- `amplify` +- `amplify_aws_amplify_studio` +- `analytics` +- `apache_mxnet_on_aws` +- `api_gateway` +- `app_config` +- `app_mesh` +- `app_runner` +- `app_studio` +- `app_wizard` +- `appfabric` +- `appflow` +- `application` +- `application_auto_scaling` +- `application_composer` +- `application_cost_profiler` +- `application_discovery_service` +- `application_discovery_service_aws_agentless_collector` +- `application_discovery_service_aws_discovery_agent` +- `application_discovery_service_migration_evaluator_collector` +- `application_integration` +- `application_load_balancer` +- `application_recovery_controller` +- `apps` +- `appstream_20` +- `appsync` +- `ar_vr` +- `archive` +- `artifact` +- `athena` +- `athena_data_source_connectors` +- `attribute` +- `attributes` +- `audit_manager` +- `augmented_ai` +- `aurora` +- `aurora_instance` +- `aurora_instance_alt` +- `authenticated_user` +- `auto_scaling` +- `auto_scaling2` +- `auto_scaling3` +- `automation` +- `autoscaling` +- `aws_backup_for_aws_cloudformation` +- `aws_backup_legal_hold` +- `aws_backup_support_for_amazon_fsx_for_netapp_ontap` +- `aws_backup_vault_lock` +- `aws_backup_virtual_machine_monitor` +- `aws_cloud` +- `aws_glue_data_quality` +- `aws_glue_for_ray` +- `aws_user_notifications` +- `b2b_data_interchange` +- `backint_agent` +- `backup` +- `backup_audit_manager` +- `backup_aws_backup_support_for_amazon_s3` +- `backup_aws_backup_support_for_vmware_workloads` +- `backup_backup_plan` +- `backup_backup_restore` +- `backup_compliance_reporting` +- `backup_compute` +- `backup_database` +- `backup_gateway` +- `backup_plan` +- `backup_recovery_point_objective` +- `backup_recovery_time_objective` +- `backup_restore` +- `backup_storage` +- `backup_vault` +- `backup_virtual_machine` +- `backup_virtual_machine_monitor` +- `bank` +- `batch` +- `bedrock` +- `blockchain` +- `blockchain_resource` +- `bottlerocket` +- `braket` +- `braket_chandelier` +- `braket_chip` +- `braket_embedded_simulator` +- `braket_managed_simulator` +- `braket_noise_simulator` +- `braket_qpu` +- `braket_simulator` +- `braket_simulator_1` +- `braket_simulator_2` +- `braket_simulator_3` +- `braket_simulator_4` +- `braket_state_vector` +- `braket_tensor_network` +- `bucket` +- `bucket_with_objects` +- `budgets` +- `budgets_2` +- `business_application` +- `bycicle` +- `c4_instance` +- `c5_instance` +- `c5a` +- `c5ad` +- `c5d` +- `c5n_instance` +- `c6g_instance` +- `c6gd` +- `cache_node` +- `cached_volume` +- `camera` +- `camera2` +- `car` +- `cart` +- `certificate_manager` +- `certificate_manager_2` +- `certificate_manager_3` +- `change_set` +- `chat` +- `chatbot` +- `checklist` +- `checklist_cost` +- `checklist_fault_tolerant` +- `checklist_performance` +- `checklist_security` +- `chime` +- `chime_sdk` +- `classic_load_balancer` +- `clean_rooms` +- `client` +- `client_vpn` +- `cloud9` +- `cloud_control_api` +- `cloud_development_kit` +- `cloud_digital_interface` +- `cloud_directory` +- `cloud_extension_ros` +- `cloud_map` +- `cloud_map_resource` +- `cloud_wan` +- `cloud_wan_segment_network` +- `cloud_wan_transit_gateway_route_table_attachment` +- `cloud_wan_virtual_pop` +- `cloudendure_disaster_recovery` +- `cloudendure_migration` +- `cloudformation` +- `cloudfront` +- `cloudfront_functions` +- `cloudhsm` +- `cloudsearch` +- `cloudsearch2` +- `cloudshell` +- `cloudtrail` +- `cloudtrail_cloudtrail_lake` +- `cloudwatch` +- `cloudwatch_2` +- `cloudwatch_cross_account_observability` +- `cloudwatch_data_protection` +- `cloudwatch_evidently` +- `cloudwatch_logs` +- `cloudwatch_metrics_insights` +- `cloudwatch_rum` +- `cloudwatch_synthetics` +- `cluster` +- `codeartifact` +- `codebuild` +- `codecatalyst` +- `codecommit` +- `codedeploy` +- `codeguru` +- `codeguru_2` +- `codepipeline` +- `codestar` +- `codewhisperer` +- `coffee_pot` +- `cognito` +- `cold_storage` +- `command_line_interface` +- `comprehend` +- `comprehend_medical` +- `compute` +- `compute_optimizer` +- `config` +- `connect` +- `connector` +- `contact_center` +- `container_1` +- `container_2` +- `container_3` +- `container_registry_image` +- `containers` +- `control_tower` +- `corporate_data_center` +- `corporate_data_center2` +- `corretto` +- `cost_and_usage_report` +- `cost_explorer` +- `cost_management` +- `credentials` +- `custom_billing_manager` +- `custom_event_bus_resource` +- `customer_enablement` +- `customer_engagement` +- `customer_gateway` +- `d2_instance` +- `d3_instance` +- `d3en_instance` +- `data_encryption_key` +- `data_exchange` +- `data_exchange_for_apis` +- `data_lake_resource_icon` +- `data_pipeline` +- `data_set` +- `data_stream` +- `data_table` +- `data_transfer_terminal` +- `database` +- `database_migration_service` +- `database_migration_workflow_job` +- `datasync` +- `datasync_discovery` +- `datazone` +- `datazone_business_data_catalog` +- `datazone_data_portal` +- `datazone_data_projects` +- `db_instance` +- `db_instance_read_replica` +- `db_instance_standby` +- `db_on_instance` +- `db_on_instance2` +- `deadline_cloud` +- `deep_learning_amis` +- `deep_learning_containers` +- `deepcomposer` +- `deeplens` +- `deepracer` +- `default_event_bus_resource` +- `dense_compute_node` +- `dense_storage_node` +- `deployment` +- `deployments` +- `desired_state` +- `desktop_and_app_streaming` +- `detective` +- `developer_tools` +- `development_environment` +- `device_farm` +- `devops_guru` +- `devops_guru_insights` +- `direct_connect` +- `directory_service` +- `disk` +- `distro_for_opentelemetry` +- `document` +- `documentdb_elastic_clusters` +- `documentdb_with_mongodb_compatibility` +- `documents` +- `documents2` +- `documents3` +- `door_lock` +- `download_distribution` +- `dynamodb` +- `dynamodb_dax` +- `dynamodb_standard_access_table_class` +- `dynamodb_standard_infrequent_access_table_class` +- `dynamodb_stream` +- `ec2` +- `ec2_aws_microservice_extractor_for_net` +- `ec2_c6a_instance` +- `ec2_c6gn_instance` +- `ec2_c6i_instance` +- `ec2_c6in_instance` +- `ec2_c7g_instance` +- `ec2_c7gn_instance` +- `ec2_dl1_instance` +- `ec2_g5_instance` +- `ec2_g5g_instance` +- `ec2_hpc6a_instance` +- `ec2_hpc6id_instance` +- `ec2_i4i_instance` +- `ec2_im4gn_instance` +- `ec2_image_builder` +- `ec2_inf2_instance` +- `ec2_instance_contents` +- `ec2_is4gen_instance` +- `ec2_m1_mac_instance` +- `ec2_m6a_instance` +- `ec2_m6i_instance` +- `ec2_m6idn_instance` +- `ec2_m6in_instance` +- `ec2_p4de_instance` +- `ec2_r6a_instance` +- `ec2_r6i_instance` +- `ec2_r6idn_instance` +- `ec2_r6in_instance` +- `ec2_r7iz_instance` +- `ec2_trn1_instance` +- `ec2_vt1_instance` +- `ec2_x2gd_instance` +- `ec2_x2idn_instance` +- `ec2_x2iedn_instance` +- `ec2_x2iezn_instance` +- `echo` +- `ecr` +- `ecs` +- `ecs_anywhere` +- `ecs_copilot_cli` +- `ecs_service` +- `ecs_service_connect` +- `ecs_task` +- `edge_location` +- `efs_infrequentaccess` +- `efs_standard` +- `eks` +- `eks_anywhere` +- `eks_cloud` +- `eks_distro` +- `eks_on_outposts` +- `elastic_beanstalk` +- `elastic_block_store` +- `elastic_block_store_amazon_data_lifecycle_manager` +- `elastic_block_store_volume_gp3` +- `elastic_fabric_adapter` +- `elastic_file_system` +- `elastic_file_system_elastic_throughput` +- `elastic_file_system_infrequent_access` +- `elastic_file_system_intelligent_tiering` +- `elastic_file_system_one_zone` +- `elastic_file_system_one_zone_infrequent_access` +- `elastic_file_system_one_zone_standard` +- `elastic_file_system_standard` +- `elastic_file_system_standard_infrequent_access` +- `elastic_inference` +- `elastic_inference_2` +- `elastic_ip_address` +- `elastic_load_balancing` +- `elastic_network_adapter` +- `elastic_network_interface` +- `elastic_transcoder` +- `elastic_vmware_service` +- `elasticache` +- `elasticache_for_memcached` +- `elasticache_for_redis` +- `elasticache_for_valkey` +- `elasticsearch_service` +- `elemental` +- `elemental_link` +- `elemental_mediaconnect` +- `elemental_mediaconvert` +- `elemental_medialive` +- `elemental_mediapackage` +- `elemental_mediastore` +- `elemental_mediatailor` +- `email` +- `email_2` +- `email_notification` +- `emr` +- `emr_engine` +- `emr_engine_mapr_m3` +- `emr_engine_mapr_m5` +- `emr_engine_mapr_m7` +- `encrypted_data` +- `end_user_messaging` +- `endpoint` +- `endpoints` +- `entity_resolution` +- `event` +- `event_event_based` +- `event_resource` +- `event_time_based` +- `eventbridge` +- `eventbridge_custom_event_bus_resource` +- `eventbridge_default_event_bus_resource` +- `eventbridge_pipes` +- `eventbridge_saas_partner_event_bus_resource` +- `eventbridge_scheduler` +- `eventbridge_schema` +- `eventbridge_schema_registry` +- `express_workflow` +- `external_sdk` +- `external_toolkit` +- `f1_instance` +- `factory` +- `fargate` +- `fault_injection_simulator` +- `file_cache` +- `file_cache_hybrid_nfs_linked_datasets` +- `file_cache_on_premises_nfs_linked_datasets` +- `file_cache_s3_linked_datasets` +- `file_gateway` +- `file_system` +- `filtering_rule` +- `finding` +- `finspace` +- `firetv` +- `firetv_stick` +- `firewall_manager` +- `fleet_management` +- `flow_logs` +- `folder` +- `folders` +- `forecast` +- `forums` +- `fraud_detector` +- `freertos` +- `fsx` +- `fsx_file_gateway` +- `fsx_for_lustre` +- `fsx_for_netapp_ontap` +- `fsx_for_openzfs` +- `fsx_for_windows_file_server` +- `g3_instance` +- `g4ad_instance` +- `g4dn` +- `game_tech` +- `game_tech2` +- `gamekit` +- `gamelift` +- `gamelift_2` +- `gamelift_streams` +- `games` +- `gamesparks` +- `gateway` +- `gateway_load_balancer` +- `gear` +- `general` +- `general_access_points` +- `generic` +- `generic_application` +- `generic_database` +- `generic_firewall` +- `genomics_cli` +- `git_repository` +- `glacier` +- `glacier_deep_archive` +- `global_accelerator` +- `global_secondary_index` +- `globe` +- `glue` +- `glue_crawlers` +- `glue_data_catalog` +- `glue_databrew` +- `glue_elastic_views` +- `greengrass` +- `ground_station` +- `group_account` +- `group_auto_scaling_group` +- `group_availability_zone` +- `group_aws_cloud` +- `group_aws_cloud_alt` +- `group_aws_step_functions_workflow` +- `group_corporate_data_center` +- `group_ec2_instance_contents` +- `group_elastic_beanstalk` +- `group_elastic_load_balancing` +- `group_iot_greengrass` +- `group_iot_greengrass_deployment` +- `group_on_premise` +- `group_region` +- `group_security_group` +- `group_spot_fleet` +- `group_subnet` +- `group_vpc` +- `group_vpc2` +- `guardduty` +- `h1_instance` +- `habana_gaudi` +- `hardware_board` +- `hdfs_cluster` +- `healthimaging` +- `healthlake` +- `healthscribe` +- `high_memory_instance` +- `honeycode` +- `hosted_zone` +- `house` +- `http2_protocol` +- `http_notification` +- `http_protocol` +- `i2` +- `i3_instance` +- `i3en` +- `identity_access_management_iam_roles_anywhere` +- `identity_and_access_management` +- `illustration_desktop` +- `illustration_devices` +- `illustration_notification` +- `illustration_office_building` +- `illustration_users` +- `import_export` +- `inf1` +- `inferentia` +- `infrequent_access_storage_class` +- `inspector` +- `instance` +- `instance2` +- `instance_with_cloudwatch` +- `instance_with_cloudwatch2` +- `instances` +- `instances_2` +- `intelligent_tiering` +- `interactive_video` +- `internet` +- `internet_alt1` +- `internet_alt2` +- `internet_alt22` +- `internet_gateway` +- `internet_of_things` +- `inventory` +- `iot_1click` +- `iot_analytics` +- `iot_analytics_channel` +- `iot_analytics_data_store` +- `iot_analytics_dataset` +- `iot_analytics_pipeline` +- `iot_button` +- `iot_core` +- `iot_core_device_advisor` +- `iot_core_device_location` +- `iot_device_defender` +- `iot_device_defender_iot_device_jobs` +- `iot_device_gateway` +- `iot_device_jobs_resource` +- `iot_device_management` +- `iot_device_management_fleet` +- `iot_device_tester` +- `iot_edukit` +- `iot_events` +- `iot_expresslink` +- `iot_fleetwise` +- `iot_greengrass_artifact` +- `iot_greengrass_component` +- `iot_greengrass_component_machine_learning` +- `iot_greengrass_component_nucleus` +- `iot_greengrass_component_private` +- `iot_greengrass_component_public` +- `iot_greengrass_interprocess_communication` +- `iot_greengrass_protocol` +- `iot_greengrass_recipe` +- `iot_greengrass_stream_manager` +- `iot_lorawan_protocol` +- `iot_over_the_air_update` +- `iot_roborunner` +- `iot_sailboat` +- `iot_sitewise` +- `iot_sitewise_asset` +- `iot_sitewise_asset_hierarchy` +- `iot_sitewise_asset_model` +- `iot_sitewise_asset_properties` +- `iot_sitewise_data_streams` +- `iot_thing_freertos_device` +- `iot_thing_humidity_sensor` +- `iot_thing_industrial_pc` +- `iot_thing_plc` +- `iot_thing_relay` +- `iot_thing_stacklight` +- `iot_thing_temperature_humidity_sensor` +- `iot_thing_temperature_sensor` +- `iot_thing_temperature_vibration_sensor` +- `iot_thing_vibration_sensor` +- `iot_things_graph` +- `iot_twinmaker` +- `iq` +- `item` +- `items` +- `json_script` +- `kendra` +- `key_management_service` +- `key_management_service_external_key_store` +- `keyspaces` +- `kinesis` +- `kinesis_data_analytics` +- `kinesis_data_firehose` +- `kinesis_data_streams` +- `kinesis_video_streams` +- `lake_formation` +- `lambda` +- `lambda_function` +- `layers` +- `lex` +- `license_manager` +- `license_manager_application_discovery` +- `license_manager_license_blending` +- `lightbulb` +- `lightsail` +- `lightsail_for_research` +- `local_zones` +- `location_service` +- `location_service_geofence` +- `location_service_map` +- `location_service_place` +- `location_service_routes` +- `location_service_track` +- `logs` +- `long_term_security_credential` +- `lookout_for_equipment` +- `lookout_for_metrics` +- `lookout_for_vision` +- `lumberyard` +- `m4_instance` +- `m5_instance` +- `m5a_instance` +- `m5d_instance` +- `m5dn_instance` +- `m5n` +- `m5n_instance` +- `m5zn_instance` +- `m6g_instance` +- `m6gd_instance` +- `mac_instance` +- `machine_learning` +- `macie` +- `magnifying_glass` +- `magnifying_glass_2` +- `mainframe_modernization` +- `mainframe_modernization_analyzer` +- `mainframe_modernization_compiler` +- `mainframe_modernization_converter` +- `mainframe_modernization_developer` +- `mainframe_modernization_runtime` +- `maintenance_windows` +- `managed_apache_cassandra_service` +- `managed_blockchain` +- `managed_ms_ad` +- `managed_service_for_apache_flink` +- `managed_service_for_grafana` +- `managed_service_for_prometheus` +- `managed_services` +- `managed_streaming_for_kafka` +- `managed_workflows_for_apache_airflow` +- `management_and_governance` +- `management_console` +- `management_console2` +- `marketplace` +- `media_services` +- `mediaconnect_gateway` +- `medical_emergency` +- `memorydb_for_redis` +- `mesh` +- `message` +- `metrics` +- `mfa_token` +- `migration_and_transfer` +- `migration_evaluator` +- `migration_hub` +- `migration_hub_refactor_spaces_applications` +- `migration_hub_refactor_spaces_environments` +- `migration_hub_refactor_spaces_services` +- `mobile` +- `mobile_application` +- `mobile_client` +- `mobile_hub` +- `monitoring` +- `monitron` +- `mq` +- `mq_broker` +- `mqtt_protocol` +- `ms_sql_instance` +- `ms_sql_instance_alternate` +- `msk_amazon_msk_connect` +- `multimedia` +- `multiple_volumes_resource` +- `mxgraph.aws4` +- `mysql_db_instance` +- `mysql_db_instance_alternate` +- `namespace` +- `nat_gateway` +- `neptune` +- `network_access_control_list` +- `network_firewall` +- `network_firewall_endpoints` +- `network_load_balancer` +- `networking_and_content_delivery` +- `neuron_ml_sdk` +- `nice_dcv` +- `nice_enginframe` +- `nimble_studio` +- `nitro_enclaves` +- `non_cached_volume` +- `notebook` +- `nova` +- `nova2` +- `object` +- `office_building` +- `omics` +- `one_zone_ia` +- `open_3d_engine` +- `open_3d_engine_2` +- `opensearch_dashboards` +- `opensearch_ingestion` +- `opensearch_observability` +- `opensearch_service_cluster_administrator_node` +- `opensearch_service_data_node` +- `opensearch_service_index` +- `opensearch_service_traces` +- `opensearch_service_ultrawarm_node` +- `opsworks` +- `opsworks_apps` +- `opsworks_permissions` +- `optimized_instance` +- `oracle_database_at_aws` +- `oracle_db_instance` +- `oracle_db_instance_alternate` +- `organizations` +- `organizations_account` +- `organizations_account2` +- `organizations_management_account` +- `organizations_management_account2` +- `organizations_organizational_unit` +- `organizations_organizational_unit2` +- `outposts` +- `outposts_1u_and_2u_servers` +- `outposts_family` +- `p2_instance` +- `p3_instance` +- `p3dn_instance` +- `p4_instance` +- `p4d_instance` +- `panorama` +- `parallel_cluster` +- `parallel_computing_service` +- `parameter_store` +- `patch_manager` +- `payment_cryptography` +- `peering` +- `permissions` +- `permissions_2` +- `personal_health_dashboard` +- `personalize` +- `pinpoint` +- `pinpoint_journey` +- `police_emergency` +- `policy` +- `polly` +- `postgresql_instance` +- `private_5g` +- `private_certificate_authority` +- `privatelink` +- `professional_services` +- `programming_language` +- `proton` +- `q` +- `quantum_ledger_database` +- `quantum_technologies` +- `question` +- `queue` +- `quicksight` +- `quicksight_paginated_reports` +- `r4_instance` +- `r5_instance` +- `r5a_instance` +- `r5ad_instance` +- `r5b_instance` +- `r5d_instance` +- `r5gd_instance` +- `r5n` +- `r5n_instance` +- `r6g_instance` +- `rdn_instance` +- `rds` +- `rds_blue_green_deployments` +- `rds_instance` +- `rds_instance_alt` +- `rds_mariadb_instance` +- `rds_mariadb_instance_alt` +- `rds_multi_az` +- `rds_multi_az_db_cluster` +- `rds_mysql_instance` +- `rds_mysql_instance_alt` +- `rds_on_vmware` +- `rds_optimized_writes` +- `rds_oracle_instance` +- `rds_oracle_instance_alt` +- `rds_piop` +- `rds_piops` +- `rds_postgresql_instance` +- `rds_postgresql_instance_alt` +- `rds_proxy` +- `rds_proxy_alt` +- `rds_sql_server_instance` +- `rds_sql_server_instance_alt` +- `rds_trusted_language_extensions_for_postgresql` +- `recover` +- `red_hat_openshift` +- `redshift` +- `redshift_auto_copy` +- `redshift_data_sharing_governance` +- `redshift_ml` +- `redshift_query_editor_v20_light` +- `redshift_ra3` +- `redshift_streaming_ingestion` +- `registry` +- `rekognition` +- `rekognition_2` +- `rekognition_image` +- `rekognition_video` +- `replication` +- `replication_time_control` +- `reported_state` +- `repost` +- `repost_private` +- `rescue` +- `reserved_instance_reporting` +- `resilience_hub` +- `resource` +- `resource_access_manager` +- `resource_explorer` +- `resources` +- `robomaker` +- `robotics` +- `role` +- `route_53` +- `route_53_application_recovery_controller` +- `route_53_readiness_checks` +- `route_53_resolver` +- `route_53_resolver_dns_firewall` +- `route_53_resolver_query_logging` +- `route_53_routing_controls` +- `route_table` +- `router` +- `rule` +- `rule_2` +- `rule_3` +- `run_command` +- `s3` +- `s3_batch_operations` +- `s3_express_one_zone` +- `s3_file_gateway` +- `s3_multi_region_access_points` +- `s3_object_lambda` +- `s3_object_lambda_access_points` +- `s3_object_lock` +- `s3_on_outposts` +- `s3_on_outposts_storage` +- `s3_replication_time_control` +- `s3_select` +- `s3_storage_lens` +- `s3_tables` +- `s3_vectors` +- `saas_event_bus_resource` +- `sagemaker` +- `sagemaker_2` +- `sagemaker_canvas` +- `sagemaker_geospatial_ml` +- `sagemaker_ground_truth` +- `sagemaker_model` +- `sagemaker_notebook` +- `sagemaker_shadow_testing` +- `sagemaker_studio_lab` +- `sagemaker_train` +- `saml_token` +- `satellite` +- `savings_plans` +- `search_documents` +- `secrets_manager` +- `security_group` +- `security_hub` +- `security_hub_finding` +- `security_identity_and_compliance` +- `security_incident_response` +- `security_lake` +- `sensor` +- `server_migration_service` +- `serverless` +- `serverless_application_repository` +- `servers` +- `service` +- `service_catalog` +- `service_management_connector` +- `servo` +- `shadow` +- `shield` +- `shield2` +- `shield_shield_advanced` +- `signer` +- `simple_ad` +- `simple_email_service` +- `simple_storage_service_directory_bucket` +- `simple_storage_service_s3_glacier_instant_retrieval` +- `simspace_weaver` +- `simulation` +- `simulator` +- `single_sign_on` +- `site_to_site_vpn` +- `snapshot` +- `snowball` +- `snowball_edge` +- `snowcone` +- `snowmobile` +- `sns` +- `source_code` +- `spot_instance` +- `sql_primary` +- `sql_replica` +- `sql_workbench` +- `sqs` +- `ssl_padlock` +- `stack` +- `stack2` +- `standard_ia` +- `state_manager` +- `step_functions` +- `storage` +- `storage_gateway` +- `streaming_distribution` +- `sts` +- `sts_alternate` +- `sumerian` +- `supply_chain` +- `support` +- `systems_manager` +- `systems_manager_application_manager` +- `systems_manager_change_calendar` +- `systems_manager_change_manager` +- `systems_manager_compliance` +- `systems_manager_distributor` +- `systems_manager_incident_manager` +- `systems_manager_opscenter` +- `systems_manager_session_manager` +- `t2_instance` +- `t3_instance` +- `t3a_instance` +- `t4g_instance` +- `table` +- `tape_gateway` +- `tape_storage` +- `telco_network_builder` +- `template` +- `temporary_security_credential` +- `tensorflow_on_aws` +- `textract` +- `textract_analyze_lending` +- `thermostat` +- `thinkbox_deadline` +- `thinkbox_draft` +- `thinkbox_frost` +- `thinkbox_krakatoa` +- `thinkbox_sequoia` +- `thinkbox_stoke` +- `thinkbox_xmesh` +- `timestream` +- `tools_and_sdks` +- `topic` +- `topic_2` +- `torchserve` +- `traditional_server` +- `training_certification` +- `trainium_instance` +- `transcribe` +- `transfer_family` +- `transfer_family_aws_as2` +- `transfer_for_ftp_resource` +- `transfer_for_ftps_resource` +- `transfer_for_sftp` +- `transfer_for_sftp_resource` +- `transform` +- `transit_gateway` +- `transit_gateway_attachment` +- `translate` +- `travel` +- `trusted_advisor` +- `user` +- `user_notifications` +- `users` +- `utility` +- `vault` +- `verified_access` +- `verified_permissions` +- `virtual_gateway` +- `virtual_node` +- `virtual_private_cloud` +- `virtual_router` +- `virtual_service` +- `virtual_tape_library` +- `vmware_cloud_on_aws` +- `volume` +- `volume_gateway` +- `vpc` +- `vpc_access_points` +- `vpc_carrier_gateway` +- `vpc_lattice` +- `vpc_network_access_analyzer` +- `vpc_privatelink` +- `vpc_reachability_analyzer` +- `vpc_traffic_mirroring` +- `vpc_virtual_private_cloud_vpc` +- `vpn_connection` +- `vpn_gateway` +- `waf` +- `waf_bad_bot` +- `waf_bot` +- `waf_bot_control` +- `waf_labels` +- `waf_managed_rule` +- `waf_rule` +- `wavelength` +- `well_architect_tool` +- `well_architected_tool` +- `wickr` +- `windfarm` +- `work_package` +- `workdocs` +- `worklink` +- `workmail` +- `workspaces` +- `workspaces_family` +- `workspaces_family_amazon_workspaces` +- `workspaces_family_amazon_workspaces_core` +- `workspaces_thin_client` +- `workspaces_workspaces_web` +- `x1_instance` +- `x1_instance2` +- `x1e_instance` +- `xray` +- `z1d_instance` diff --git a/docs/shape-libraries/azure2.md b/docs/shape-libraries/azure2.md new file mode 100644 index 00000000..8015c2a4 --- /dev/null +++ b/docs/shape-libraries/azure2.md @@ -0,0 +1,431 @@ +# azure2 + +**Type:** SVG images +**Path:** `img/lib/azure2/` + +## Usage + +```xml + + + +``` + +## Shapes (648) + +Shapes are organized by category: `azure2/{category}/{shape}.svg` + +### ai_machine_learning (30) + +- `AI_Studio` +- `Anomaly_Detector` +- `Azure_Applied_AI` +- `Azure_Experimentation_Studio` +- `Azure_Object_Understanding` +- `Azure_OpenAI` +- `Batch_AI` +- `Bonsai` +- `Bot_Services` +- `Cognitive_Services` +- `Cognitive_Services_Decisions` +- `Computer_Vision` +- `Content_Moderators` +- `Content_Safety` +- `Custom_Vision` +- `Face_APIs` +- `Form_Recognizers` +- `Genomics` +- `Immersive_Readers` +- `Language_Services` +- `Language_Understanding` +- `Machine_Learning` +- `Machine_Learning_Studio_Classic_Web_Services` +- `Machine_Learning_Studio_Web_Service_Plans` +- `Machine_Learning_Studio_Workspaces` +- `Personalizers` +- `QnA_Makers` +- `Serverless_Search` +- `Speech_Services` +- `Translator_Text` + +### analytics (14) + +- `Analysis_Services` +- `Azure_Databricks` +- `Azure_Synapse_Analytics` +- `Azure_Workbooks` +- `Data_Lake_Analytics` +- `Data_Lake_Store_Gen1` +- `Endpoint_Analytics` +- `Event_Hub_Clusters` +- `Event_Hubs` +- `HD_Insight_Clusters` +- `Log_Analytics_Workspaces` +- `Power_BI_Embedded` +- `Power_Platform` +- `Stream_Analytics_Jobs` + +### app_services (9) + +- `API_Management_Services` +- `App_Service_Certificates` +- `App_Service_Domains` +- `App_Service_Environments` +- `App_Service_Plans` +- `App_Services` +- `CDN_Profiles` +- `Notification_Hubs` +- `Search_Services` + +### compute (38) + +- `App_Services` +- `Application_Group` +- `Automanaged_VM` +- `Availability_Sets` +- `Azure_Compute_Galleries` +- `Azure_Spring_Cloud` +- `Batch_Accounts` +- `Cloud_Services_Classic` +- `Container_Instances` +- `Container_Services_Deprecated` +- `Disk_Encryption_Sets` +- `Disks` +- `Disks_Classic` +- `Disks_Snapshots` +- `Function_Apps` +- `Host_Groups` +- `Host_Pools` +- `Hosts` +- `Image_Definitions` +- `Image_Templates` +- `Image_Versions` +- `Images` +- `Kubernetes_Services` +- `Maintenance_Configuration` +- `Managed_Service_Fabric` +- `Mesh_Applications` +- `Metrics_Advisor` +- `OS_Images_Classic` +- `Restore_Points` +- `Restore_Points_Collections` +- `Service_Fabric_Clusters` +- `Shared_Image_Galleries` +- `VM_Images_Classic` +- `VM_Scale_Sets` +- `Virtual_Machine` +- `Virtual_Machines_Classic` +- `Workspaces` +- `Workspaces2` + +### containers (7) + +- `App_Services` +- `Azure_Red_Hat_OpenShift` +- `Batch_Accounts` +- `Container_Instances` +- `Container_Registries` +- `Kubernetes_Services` +- `Service_Fabric_Clusters` + +### databases (27) + +- `Azure_Cosmos_DB` +- `Azure_Data_Explorer_Clusters` +- `Azure_Database_MariaDB_Server` +- `Azure_Database_Migration_Services` +- `Azure_Database_MySQL_Server` +- `Azure_Database_PostgreSQL_Server` +- `Azure_Database_PostgreSQL_Server_Group` +- `Azure_Purview_Accounts` +- `Azure_SQL` +- `Azure_SQL_Edge` +- `Azure_SQL_Server_Stretch_Databases` +- `Azure_SQL_VM` +- `Azure_Synapse_Analytics` +- `Cache_Redis` +- `Data_Factory` +- `Elastic_Job_Agents` +- `Instance_Pools` +- `Managed_Database` +- `Oracle_Database` +- `SQL_Data_Warehouses` +- `SQL_Database` +- `SQL_Elastic_Pools` +- `SQL_Managed_Instance` +- `SQL_Server` +- `SQL_Server_Registries` +- `SSIS_Lift_And_Shift_IR` +- `Virtual_Clusters` + +### identity (35) + +- `AAD_Licenses` +- `Active_Directory_Connect_Health` +- `Active_Directory_Connect_Health2` +- `Administrative_Units` +- `App_Registrations` +- `Azure_AD_B2C` +- `Azure_AD_B2C2` +- `Azure_AD_Domain_Services` +- `Azure_AD_Identity_Protection` +- `Azure_AD_Privilege_Identity_Management` +- `Azure_Active_Directory` +- `Azure_Information_Protection` +- `Custom_Azure_AD_Roles` +- `Enterprise_Applications` +- `Entra_Connect` +- `Entra_Domain_Services` +- `Entra_Global_Secure_Access` +- `Entra_ID_Protection` +- `Entra_Internet_Access` +- `Entra_Managed_Identities` +- `Entra_Private_Access` +- `Entra_Privileged_Identity_Management` +- `Entra_Verified_ID` +- `External_Identities` +- `Groups` +- `Identity_Governance` +- `Managed_Identities` +- `Multi_Factor_Authentication` +- `PIM` +- `Security` +- `Tenant_Properties` +- `User_Settings` +- `Users` +- `Verifiable_Credentials` +- `Verification_As_A_Service` + +### networking (51) + +- `ATM_Multistack` +- `Application_Gateway_Containers` +- `Application_Gateways` +- `Azure_Communications_Gateway` +- `Azure_Firewall_Manager` +- `Azure_Firewall_Policy` +- `Bastions` +- `CDN_Profiles` +- `Connections` +- `DDoS_Protection_Plans` +- `DNS_Multistack` +- `DNS_Private_Resolver` +- `DNS_Security_Policy` +- `DNS_Zones` +- `ExpressRoute_Circuits` +- `Firewalls` +- `Front_Doors` +- `IP_Address_manager` +- `IP_Groups` +- `Load_Balancer_Hub` +- `Load_Balancers` +- `Local_Network_Gateways` +- `NAT` +- `Network_Interfaces` +- `Network_Security_Groups` +- `Network_Watcher` +- `On_Premises_Data_Gateways` +- `Private_Endpoint` +- `Private_Link` +- `Private_Link_Hub` +- `Private_Link_Service` +- `Proximity_Placement_Groups` +- `Public_IP_Addresses` +- `Public_IP_Addresses_Classic` +- `Public_IP_Prefixes` +- `Reserved_IP_Addresses_Classic` +- `Resource_Management_Private_Link` +- `Route_Filters` +- `Route_Tables` +- `Service_Endpoint_Policies` +- `Spot_VM` +- `Spot_VMSS` +- `Subnet` +- `Traffic_Manager_Profiles` +- `Virtual_Network_Gateways` +- `Virtual_Networks` +- `Virtual_Networks_Classic` +- `Virtual_Router` +- `Virtual_WAN_Hub` +- `Virtual_WANs` +- `Web_Application_Firewall_Policies_WAF` + +### security (14) + +- `Application_Security_Groups` +- `Azure_AD_Risky_Signins` +- `Azure_AD_Risky_Users` +- `Azure_Defender` +- `Azure_Sentinel` +- `Conditional_Access` +- `Detonation` +- `ExtendedSecurityUpdates` +- `Identity_Secure_Score` +- `Key_Vaults` +- `Keys` +- `MS_Defender_EASM` +- `Multifactor_Authentication` +- `Security_Center` + +### storage (17) + +- `Azure_Fileshare` +- `Azure_HCP_Cache` +- `Azure_NetApp_Files` +- `Azure_Stack_Edge` +- `Data_Box` +- `Data_Box_Edge` +- `Data_Lake_Storage_Gen1` +- `Data_Share_Invitations` +- `Data_Shares` +- `Import_Export_Jobs` +- `Recovery_Services_Vaults` +- `StorSimple_Data_Managers` +- `StorSimple_Device_Managers` +- `Storage_Accounts` +- `Storage_Accounts_Classic` +- `Storage_Explorer` +- `Storage_Sync_Services` + +### general (98) + +- `All_Resources` +- `Backlog` +- `Biz_Talk` +- `Blob_Block` +- `Blob_Page` +- `Branch` +- `Browser` +- `Bug` +- `Builds` +- `Cache` +- `Code` +- `Commit` +- `Controls` +- `Controls_Horizontal` +- `Cost_Alerts` +- `Cost_Analysis` +- `Cost_Budgets` +- `Cost_Management` +- `Cost_Management_and_Billing` +- `Counter` +- `Cubes` +- `Dashboard` +- `Dashboard2` +- `Dev_Console` +- `Download` +- `Error` +- `Extensions` +- `FTP` +- `File` +- `Files` +- `Folder_Blank` +- `Folder_Website` +- `Free_Services` +- `Gear` +- `Globe` +- `Globe_Error` +- `Globe_Success` +- `Globe_Warning` +- `Guide` +- `Heart` +- `Help_and_Support` +- `Image` +- `Information` +- `Input_Output` +- `Journey_Hub` +- `Launch_Portal` +- `Learn` +- `Load_Test` +- `Location` +- `Log_Streaming` +- `Management_Groups` +- `Management_Portal` +- `Marketplace` +- `Media` +- `Media_File` +- `Mobile` +- `Mobile_Engagement` +- `Module` +- `Power` +- `Power_Up` +- `Powershell` +- `Preview` +- `Preview_Features` +- `Process_Explorer` +- `Production_Ready_Database` +- `Quickstart_Center` +- `Recent` +- `Reservations` +- `Resource_Explorer` +- `Resource_Group_List` +- `Resource_Groups` +- `Resource_Linked` +- `SSD` +- `Scale` +- `Scheduler` +- `Search` +- `Search_Grid` +- `Server_Farm` +- `Service_Bus` +- `Service_Health` +- `Storage_Azure_Files` +- `Storage_Container` +- `Storage_Queue` +- `Subscriptions` +- `TFS_VC_Repository` +- `Table` +- `Tag` +- `Tags` +- `Templates` +- `Toolbox` +- `Troubleshoot` +- `Versions` +- `Web_Slots` +- `Web_Test` +- `Website_Power` +- `Website_Staging` +- `Workbooks` +- `Workflow` + +### other (149) + +(See draw.io for complete list of 149 shapes in the "other" category) + +Selected shapes: +- `Azure_Backup_Center` +- `Azure_Chaos_Studio` +- `Azure_Cloud_Shell` +- `Azure_Communication_Services` +- `Azure_Deployment_Environments` +- `Azure_Load_Testing` +- `Azure_Monitor_Dashboard` +- `Azure_Network_Manager` +- `Azure_Orbital` +- `Azure_Sphere` +- `Azure_Storage_Mover` +- `Grafana` +- `Kubernetes_Fleet_Manager` +- `SSH_Keys` + +### Additional Categories + +- **azure_ecosystem** (3): Applens, Azure_Hybrid_Center, Collaborative_Service +- **azure_stack** (8): Azure_Stack, Capacity, Infrastructure_Backup, Multi_Tenancy, Offers, Plans, Updates, User_Subscriptions +- **azure_vmware_solution** (1): AVS +- **blockchain** (6): ABS_Member, Azure_Blockchain_Service, Azure_Token_Service, Blockchain_Applications, Consortium, Outbound_Connection +- **cxp** (2): Elixir, Elixir_Purple +- **devops** (10): API_Connections, Application_Insights, Azure_DevOps, Change_Analysis, CloudTest, Code_Optimization, DevOps_Starter, DevTest_Labs, Lab_Accounts, Lab_Services +- **hybrid_multicloud** (5): Azure_Operator_5G_Core, Azure_Operator_Insights, Azure_Operator_Nexus, Azure_Operator_Service_Manager, Azure_Programmable_Connectivity +- **integration** (21): API_Management_Services, App_Configuration, Azure_API_for_FHIR, Azure_Data_Catalog, Event_Grid_Domains, Event_Grid_Subscriptions, Event_Grid_Topics, Integration_Accounts, Integration_Environments, Integration_Service_Environments, Logic_Apps, Logic_Apps_Custom_Connector, Partner_Namespace, Partner_Registration, Partner_Topic, Relays, SQL_Data_Warehouses, SendGrid_Accounts, Service_Bus, Software_as_a_Service, System_Topic +- **internet_of_things** (3): Digital_Twins, Logic_Apps, Time_Series_Insights_Access_Policies +- **intune** (17): Azure_AD_Roles_and_Administrators, Client_Apps, Device_Compliance, Device_Configuration, Device_Enrollment, Device_Security_Apple, Device_Security_Google, Device_Security_Windows, Devices, Exchange_Access, Intune, Intune_For_Education, Mindaro, Security_Baselines, Software_Updates, Tenant_Status, eBooks +- **iot** (19): Azure_IoT_Operations, Azure_Maps_Accounts, Azure_Stack_HCI_Sizer, Device_Provisioning_Services, Digital_Twins, Event_Hubs, Function_Apps, Industrial_IoT, IoT_Central_Applications, IoT_Edge, IoT_Hub, Logic_Apps, Notification_Hubs, Stack_HCI_Premium, Stream_Analytics_Jobs, Time_Series_Data_Sets, Time_Series_Insights_Environments, Time_Series_Insights_Event_Sources, Windows10_Core_Services +- **management_governance** (32): Activity_Log, Advisor, Alerts, Application_Insights, Arc_Machines, Automation_Accounts, Azure_Arc, Azure_Lighthouse, Blueprints, Compliance, Cost_Management_and_Billing, Customer_Lockbox_for_MS_Azure, Diagnostics_Settings, Education, Log_Analytics_Workspaces, MachinesAzureArc, Managed_Applications_Center, Managed_Desktop, Metrics, Monitor, My_Customers, Operation_Log_Classic, Policy, Recovery_Services_Vaults, Resource_Graph_Explorer, Resources_Provider, Scheduler_Job_Collections, Service_Catalog_MAD, Service_Providers, Solutions, Universal_Print, User_Privacy +- **menu** (1): Keys +- **migrate** (5): Azure_Migrate, Cost_Management_and_Billing, Data_Box, Data_Box_Edge, Recovery_Services_Vaults +- **mixed_reality** (2): Remote_Rendering, Spatial_Anchor_Accounts +- **monitor** (1): SAP_Azure_Monitor +- **power_platform** (9): AIBuilder, CopilotStudio, Dataverse, PowerApps, PowerAutomate, PowerBI, PowerFx, PowerPages, PowerPlatform +- **preview** (9): Azure_Cloud_Shell, Azure_Sphere, Azure_Workbooks, IoT_Edge, Private_Link_Hub, RTOS, Static_Apps, Time_Series_Data_Sets, Web_Environment +- **web** (5): API_Center, App_Space, Azure_Media_Service, Notification_Hub_Namespaces, SignalR diff --git a/docs/shape-libraries/basic.md b/docs/shape-libraries/basic.md new file mode 100644 index 00000000..9f73040a --- /dev/null +++ b/docs/shape-libraries/basic.md @@ -0,0 +1,48 @@ +# basic + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.basic` + +## Usage + +```xml + + + +``` + + + +## Shapes (31) + +- `4_point_star` +- `6_point_star` +- `8_point_star` +- `banner` +- `cloud_callout` +- `cloud_rect` +- `cone` +- `cross` +- `document` +- `flash` +- `half_circle` +- `heart` +- `loud_callout` +- `moon` +- `mxgraph.basic` +- `no_symbol` +- `octagon` +- `orthogonal_triangle` +- `oval_callout` +- `parallelepiped` +- `pentagon` +- `pointed_oval` +- `rectangular_callout` +- `rounded_rectangular_callout` +- `smiley` +- `star` +- `sun` +- `tick` +- `trapezoid` +- `wave` +- `x` diff --git a/docs/shape-libraries/bpmn.md b/docs/shape-libraries/bpmn.md new file mode 100644 index 00000000..6f9117eb --- /dev/null +++ b/docs/shape-libraries/bpmn.md @@ -0,0 +1,60 @@ +# bpmn + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.bpmn` + +## Usage + +```xml + + + +``` + +## Parameters + +- `outline` - Event type: `start`, `end`, `catching`, `throwing`, `none` +- `symbol` - Icon inside: `message`, `timer`, `error`, `cancel`, `compensation`, `link`, `terminate`, `general`, `multiple`, `rule` + +## Shapes (40) + +- `ad_hoc` +- `business_rule_task` +- `cancel_end` +- `cancel_intermediate` +- `compensation` +- `compensation_end` +- `compensation_intermediate` +- `error_end` +- `error_intermediate` +- `gateway` +- `gateway_and` +- `gateway_complex` +- `gateway_or` +- `gateway_xor_(data)` +- `gateway_xor_(event)` +- `general_end` +- `general_intermediate` +- `general_start` +- `link_end` +- `link_intermediate` +- `link_start` +- `loop` +- `loop_marker` +- `manual_task` +- `message_end` +- `message_intermediate` +- `message_start` +- `multiple_end` +- `multiple_instances` +- `multiple_intermediate` +- `multiple_start` +- `mxgraph.bpmn` +- `rule_intermediate` +- `rule_start` +- `script_task` +- `service_task` +- `terminate` +- `timer_intermediate` +- `timer_start` +- `user_task` diff --git a/docs/shape-libraries/cabinets.md b/docs/shape-libraries/cabinets.md new file mode 100644 index 00000000..553af7df --- /dev/null +++ b/docs/shape-libraries/cabinets.md @@ -0,0 +1,71 @@ +# cabinets + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.cabinets` + +## Usage + +```xml + + + +``` + + + +## Shapes (54) + +- `auxiliary_contact_contactor_1_32a` +- `auxiliary_contact_contactor_32_125a` +- `cb_1p` +- `cb_1p_x10` +- `cb_2p` +- `cb_2p_x10` +- `cb_3p` +- `cb_3p_x5` +- `cb_4p` +- `cb_4p_x5` +- `cb_auxiliary_contact` +- `contactor_125_400a` +- `contactor_1_32a` +- `contactor_32_125a` +- `din_rail` +- `distribution_block_4p_125a_11_connections` +- `distribution_block_4p_125a_11_connections_2` +- `mccb_25_63a_3p` +- `mccb_25_63a_4p` +- `mccb_63_250a_3p` +- `mccb_63_250a_4p` +- `motor_cb_125_400a` +- `motor_cb_1_32a` +- `motor_cb_32_125a` +- `motor_protection_cb` +- `motor_starter_125_400a` +- `motor_starter_1_32a` +- `motor_starter_32_125a` +- `motorized_switch_3p` +- `motorized_switch_4p` +- `mxgraph.cabinets` +- `overcurrent_relay_125_400a` +- `overcurrent_relay_1_32a` +- `overcurrent_relay_32_125a` +- `plugin_relay_1` +- `plugin_relay_2` +- `residual_current_device_2p` +- `residual_current_device_4p` +- `surge_protection_1p` +- `surge_protection_2p` +- `surge_protection_3p` +- `surge_protection_4p` +- `terminal_40mm2` +- `terminal_40mm2_x10` +- `terminal_4_6mm2` +- `terminal_4_6mm2_x10` +- `terminal_4mm2` +- `terminal_4mm2_x10` +- `terminal_50mm2` +- `terminal_50mm2_x10` +- `terminal_6_25mm2` +- `terminal_6_25mm2_x10` +- `terminal_75mm2` +- `terminal_75mm2_x10` diff --git a/docs/shape-libraries/cisco19.md b/docs/shape-libraries/cisco19.md new file mode 100644 index 00000000..ea1d70f2 --- /dev/null +++ b/docs/shape-libraries/cisco19.md @@ -0,0 +1,250 @@ +# cisco19 + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.cisco19` + +## Usage + +```xml + + + +``` + + + +## Shapes (233) + +- `3g_4g_indicator` +- `6500_vss` +- `6500_vss2` +- `access_control_and_trustsec` +- `aci` +- `aci2` +- `acibg` +- `acs` +- `ad_decoder` +- `ad_encoder` +- `analysis_correlation` +- `anomaly_detection` +- `anti_malware` +- `anti_malware2` +- `appnav` +- `asa_5500` +- `asr_1000` +- `asr_9000` +- `avc_application_visibility_control` +- `avc_application_visibility_control2` +- `bg1` +- `bg10` +- `bg2` +- `bg3` +- `bg4` +- `bg5` +- `bg6` +- `bg7` +- `bg8` +- `bg9` +- `blade_server` +- `branch` +- `branch2` +- `camera` +- `camera2` +- `cell_phone` +- `cell_phone2` +- `cisco_15800` +- `cisco_dna` +- `cisco_dna_center` +- `cisco_meetingplace_express` +- `cisco_security_manager` +- `cisco_unified_contact_center_enterprise_and_hosted` +- `cisco_unified_presence_service` +- `clock` +- `cloud` +- `cloud2` +- `cognitive` +- `collab1` +- `collab2` +- `collab3` +- `collab4` +- `communications_manager` +- `contact_center_express` +- `content_recording_streaming_server` +- `content_router` +- `csr_1000v` +- `da_decoder` +- `da_encoder` +- `data_center` +- `data_center2` +- `database_relational` +- `dns_server` +- `dns_server2` +- `dual_mode_access_point` +- `email_security` +- `fabric_interconnect` +- `fibre_channel_director_mds_9000` +- `fibre_channel_fabric_switch` +- `firewall` +- `flow_analytics` +- `flow_analytics2` +- `flow_collector` +- `h323` +- `handheld` +- `handheld2` +- `hdtv` +- `hdtv2` +- `home_office` +- `home_office2` +- `host_based_security` +- `hypervisor` +- `immersive_telepresence_endpoint` +- `ip_ip_gateway` +- `ip_phone` +- `ip_phone2` +- `ip_telephone_router` +- `ips_ids` +- `ironport` +- `ise` +- `joystick_keyboard` +- `joystick_keyboard2` +- `key` +- `key2` +- `l2_modular` +- `l2_modular2` +- `l2_switch` +- `l2_switch_with_dual_supervisor` +- `l3_modular` +- `l3_modular2` +- `l3_modular3` +- `l3_switch` +- `l3_switch_with_dual_supervisor` +- `laptop` +- `laptop2` +- `laptop_video_client` +- `laptop_video_client2` +- `layer3_nexus_5k_switch` +- `ldap` +- `ldap2` +- `load_balancer` +- `lock` +- `lock2` +- `media_server` +- `meeting_scheduling_and_management_server` +- `mesh_access_point` +- `monitor` +- `monitoring` +- `multipoint_meeting_server` +- `mxgraph.cisco19` +- `nac_appliance` +- `nam_virtual_service_blade` +- `net_mgmt_appliance` +- `netflow_router` +- `netflow_router2` +- `netflow_router3` +- `next_generation_intrusion_prevention_system` +- `nexus_1010` +- `nexus_1k` +- `nexus_1kv_vsm` +- `nexus_2000_10ge` +- `nexus_2k` +- `nexus_3k` +- `nexus_4k` +- `nexus_5k` +- `nexus_5k_with_integrated_vsm` +- `nexus_7k` +- `nexus_9300` +- `nexus_9500` +- `operations_manager` +- `phone_polycom` +- `phone_polycom2` +- `policy_configuration` +- `pos` +- `pos2` +- `posture_assessment` +- `primary_codec` +- `printer` +- `printer2` +- `router` +- `router_with_firewall` +- `router_with_firewall2` +- `router_with_voice` +- `rps` +- `secondary_codec` +- `secure_catalyst_switch_color` +- `secure_catalyst_switch_color2` +- `secure_catalyst_switch_color3` +- `secure_catalyst_switch_subdued` +- `secure_catalyst_switch_subdued2` +- `secure_endpoint_pc` +- `secure_endpoint_pc2` +- `secure_endpoints` +- `secure_endpoints2` +- `secure_router` +- `secure_server` +- `secure_server2` +- `secure_switch` +- `security_management` +- `server` +- `server2` +- `service_ready_engine` +- `set_top` +- `set_top2` +- `shield` +- `ssl_terminator` +- `stealthwatch_management_console_smc` +- `stealthwatch_management_console_smc2` +- `storage` +- `surveillance_camera` +- `surveillance_camera2` +- `tablet` +- `tablet2` +- `telepresence_endpoint` +- `telepresence_endpoint_twin_data_display` +- `telepresence_exchange` +- `threat_intelligence` +- `transcoder` +- `ucs_5108_blade_chassis` +- `ucs_c_series_server` +- `ucs_express` +- `unity` +- `upc_unified_personal_communicator` +- `upc_unified_personal_communicator2` +- `ups` +- `user` +- `user2` +- `vbond` +- `video_analytics` +- `video_call_server` +- `video_gateway` +- `virtual_desktop_service` +- `virtual_matrix_switch` +- `virtual_private_network` +- `virtual_private_network2` +- `virtual_private_network_connector` +- `vmanage` +- `vpn_concentrator` +- `vsmart` +- `vts` +- `vts2` +- `web_application_firewall` +- `web_reputation_filtering` +- `web_reputation_filtering_2` +- `web_security` +- `web_security_services` +- `web_security_services2` +- `webex` +- `wifi_indicator` +- `wireless_access_point` +- `wireless_access_point2` +- `wireless_bridge` +- `wireless_bridge2` +- `wireless_connector` +- `wireless_intrusion_prevention` +- `wireless_lan_controller` +- `wireless_location_appliance` +- `wireless_router` +- `workgroup_switch` +- `workstation` +- `workstation2` +- `x509_certificate` +- `x509_certificate2` diff --git a/docs/shape-libraries/citrix.md b/docs/shape-libraries/citrix.md new file mode 100644 index 00000000..cfa0668b --- /dev/null +++ b/docs/shape-libraries/citrix.md @@ -0,0 +1,115 @@ +# citrix + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.citrix` + +## Usage + +```xml + + + +``` + + + +## Shapes (98) + +- `1u_2u_server` +- `access_card` +- `branch_repeater` +- `browser` +- `cache_server` +- `calendar` +- `cell_phone` +- `chassis` +- `citrix_hdx` +- `citrix_logo` +- `cloud` +- `command_center` +- `database` +- `database_server` +- `datacenter` +- `desktop` +- `desktop_web` +- `dhcp_server` +- `directory_server` +- `dns_server` +- `document` +- `edgesight_server` +- `file_server` +- `firewall` +- `ftp_server` +- `geolocation_database` +- `globe` +- `goto_meeting` +- `government` +- `home_office` +- `hq_enterprise` +- `inspection` +- `ip_phone` +- `kiosk` +- `laptop_1` +- `laptop_2` +- `license_server` +- `merchandising_server` +- `middleware` +- `mxgraph.citrix` +- `netscaler_gateway` +- `netscaler_mpx` +- `netscaler_sdx` +- `netscaler_vpx` +- `pbx_server` +- `pda` +- `podio` +- `printer` +- `process` +- `provisioning_server` +- `proxy_server` +- `radius_server` +- `remote_office` +- `reporting` +- `role_appcontroller` +- `role_applications` +- `role_cloudbridge` +- `role_desktops` +- `role_load_testing_controller` +- `role_load_testing_launcher` +- `role_receiver` +- `role_repeater` +- `role_secure_access` +- `role_security` +- `role_services` +- `role_storefront` +- `role_storefront_services` +- `role_synchronizer` +- `role_xenmobile` +- `role_xenmobile_device_manager` +- `router` +- `security` +- `sharefile` +- `site` +- `smtp_server` +- `storefront_services` +- `switch` +- `tablet_1` +- `tablet_2` +- `thin_client` +- `tower_server` +- `user_control` +- `users` +- `web_server` +- `web_service` +- `worxenroll` +- `worxhome` +- `worxmail` +- `worxweb` +- `xenapp_server` +- `xenapp_services` +- `xenapp_web` +- `xencenter` +- `xenclient` +- `xenclient_synchronizer` +- `xendesktop_server` +- `xenmobile` +- `xenserver` diff --git a/docs/shape-libraries/electrical.md b/docs/shape-libraries/electrical.md new file mode 100644 index 00000000..ab41c006 --- /dev/null +++ b/docs/shape-libraries/electrical.md @@ -0,0 +1,50 @@ +# electrical + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.electrical` + +## Usage + +```xml + + + +``` + +Shapes are organized by category: `mxgraph.electrical.{category}.{shape}` + +## Categories + +### resistors +- `resistor_1` +- `resistor_2` + +### capacitors +- `capacitor_1` +- `capacitor_3` + +### inductors +- `inductor_3` +- `transformer_1` + +### diodes +- `diode` +- `zener_diode_1` + +### transistors +- `npn_transistor_1` +- `pnp_transistor_1` + +### mosfets1 +- `n-channel_mosfet_1` +- `p-channel_mosfet_1` + +### logic_gates +- `logic_gate` +- `dual_inline_ic` + +### electro-mechanical +- `singleSwitch` +- `pushbutton` + +(See draw.io Electrical shape library for complete list) diff --git a/docs/shape-libraries/floorplan.md b/docs/shape-libraries/floorplan.md new file mode 100644 index 00000000..ab67dd1e --- /dev/null +++ b/docs/shape-libraries/floorplan.md @@ -0,0 +1,62 @@ +# floorplan + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.floorplan` + +## Usage + +```xml + + + +``` + + + +## Shapes (45) + +- `bathtub` +- `bathtub2` +- `bed_double` +- `bed_single` +- `bookcase` +- `chair` +- `copier` +- `couch` +- `crt_tv` +- `desk_corner` +- `desk_corner_2` +- `dresser` +- `drying_machine` +- `elevator` +- `fireplace` +- `flat_tv` +- `floor_lamp` +- `laptop` +- `mxgraph.floorplan` +- `office_chair` +- `piano` +- `plant` +- `printer` +- `range_1` +- `range_2` +- `refrigerator` +- `shower` +- `shower2` +- `sink_1` +- `sink_2` +- `sink_22` +- `sink_double` +- `sink_double2` +- `sofa` +- `spiral_stairs` +- `table` +- `table_1` +- `table_2` +- `table_3` +- `table_4` +- `table_5` +- `toilet` +- `washing_machine` +- `water_cooler` +- `workstation` diff --git a/docs/shape-libraries/flowchart.md b/docs/shape-libraries/flowchart.md new file mode 100644 index 00000000..9c8e4a8c --- /dev/null +++ b/docs/shape-libraries/flowchart.md @@ -0,0 +1,52 @@ +# flowchart + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.flowchart` + +## Usage + +```xml + + + +``` + + + +## Shapes (35) + +- `annotation_1` +- `annotation_2` +- `card` +- `collate` +- `data` +- `database` +- `decision` +- `delay` +- `direct_data` +- `display` +- `document` +- `extract_or_measurement` +- `internal_storage` +- `loop_limit` +- `manual_input` +- `manual_operation` +- `merge_or_storage` +- `multi-document` +- `mxgraph.flowchart` +- `off-page_reference` +- `on-page_reference` +- `or` +- `paper_tape` +- `parallel_mode` +- `predefined_process` +- `preparation` +- `process` +- `sequential_data` +- `sort` +- `start_1` +- `start_2` +- `stored_data` +- `summing_function` +- `terminator` +- `transfer` diff --git a/docs/shape-libraries/fluidpower.md b/docs/shape-libraries/fluidpower.md new file mode 100644 index 00000000..58abfa27 --- /dev/null +++ b/docs/shape-libraries/fluidpower.md @@ -0,0 +1,264 @@ +# fluidpower + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.fluid_power` + +## Usage + +```xml + + + +``` + +Shapes are named like x10010, x10020, etc. + +## Shapes (247) + +- `mxgraph.fluid_power` +- `x10010` +- `x10020` +- `x10030` +- `x10040` +- `x10050` +- `x10060` +- `x10070` +- `x10080` +- `x10090` +- `x10100` +- `x10110` +- `x10120` +- `x10130` +- `x10140` +- `x10150` +- `x10160` +- `x10170` +- `x10180` +- `x10190` +- `x10200` +- `x10210` +- `x10220` +- `x10230` +- `x10240` +- `x10250` +- `x10260` +- `x10270` +- `x10280` +- `x10290` +- `x10300` +- `x10310` +- `x10320` +- `x10330` +- `x10340` +- `x10350` +- `x10360` +- `x10370` +- `x10380` +- `x10390` +- `x10400` +- `x10410` +- `x10420` +- `x10430` +- `x10440` +- `x10441` +- `x10442` +- `x10450` +- `x10460` +- `x10470` +- `x10480` +- `x10490` +- `x10500` +- `x10510` +- `x10520` +- `x10530` +- `x10540` +- `x10550` +- `x10560` +- `x10570` +- `x10580` +- `x10590` +- `x10600` +- `x10610` +- `x10620` +- `x10630` +- `x10640` +- `x10650` +- `x10660` +- `x10670` +- `x10680` +- `x10690` +- `x10700` +- `x10710` +- `x10720` +- `x10730` +- `x10740` +- `x10750` +- `x10760` +- `x10770` +- `x10780` +- `x10790` +- `x10800` +- `x10810` +- `x10820` +- `x10830` +- `x10840` +- `x10850` +- `x10860` +- `x10870` +- `x10880` +- `x10890` +- `x10900` +- `x10910` +- `x10920` +- `x10930` +- `x10940` +- `x10950` +- `x10960` +- `x10970` +- `x10980` +- `x10990` +- `x11000` +- `x11010` +- `x11020` +- `x11030` +- `x11040` +- `x11050` +- `x11060` +- `x11070` +- `x11080` +- `x11090` +- `x11100` +- `x11110` +- `x11120` +- `x11130` +- `x11140` +- `x11150` +- `x11160` +- `x11170` +- `x11180` +- `x11190` +- `x11200` +- `x11210` +- `x11220` +- `x11230` +- `x11240` +- `x11250` +- `x11260` +- `x11270` +- `x11280` +- `x11290` +- `x11300` +- `x11310` +- `x11320` +- `x11330` +- `x11340` +- `x11350` +- `x11360` +- `x11370` +- `x11380` +- `x11390` +- `x11400` +- `x11410` +- `x11420` +- `x11430` +- `x11440` +- `x11450` +- `x11460` +- `x11470` +- `x11480` +- `x11490` +- `x11500` +- `x11510` +- `x11520` +- `x11530` +- `x11540` +- `x11550` +- `x11560` +- `x11570` +- `x11580` +- `x11590` +- `x11600` +- `x11610` +- `x11620` +- `x11630` +- `x11640` +- `x11650` +- `x11660` +- `x11670` +- `x11680` +- `x11690` +- `x11700` +- `x11710` +- `x11720` +- `x11730` +- `x11740` +- `x11750` +- `x11760` +- `x11770` +- `x11780` +- `x11790` +- `x11800` +- `x11810` +- `x11820` +- `x11830` +- `x11840` +- `x11850` +- `x11860` +- `x11870` +- `x11880` +- `x11890` +- `x11900` +- `x11910` +- `x11920` +- `x11930` +- `x11940` +- `x11950` +- `x11960` +- `x11970` +- `x11980` +- `x11990` +- `x12000` +- `x12010` +- `x12020` +- `x12030` +- `x12040` +- `x12050` +- `x12060` +- `x12070` +- `x12080` +- `x12090` +- `x12100` +- `x12110` +- `x12120` +- `x12130` +- `x12140` +- `x12150` +- `x12160_detailed` +- `x12160_simplified` +- `x12170` +- `x12180` +- `x12190` +- `x12200` +- `x12210` +- `x12220` +- `x12230` +- `x12240` +- `x12250` +- `x12260` +- `x12270` +- `x12280` +- `x12290` +- `x12300` +- `x12310` +- `x12320` +- `x12330` +- `x12340` +- `x12350` +- `x12360` +- `x12370` +- `x12380` +- `x12390` +- `x12400` +- `x12410` +- `x12420` +- `x12430` diff --git a/docs/shape-libraries/gcp2.md b/docs/shape-libraries/gcp2.md new file mode 100644 index 00000000..729dcb6a --- /dev/null +++ b/docs/shape-libraries/gcp2.md @@ -0,0 +1,315 @@ +# gcp2 + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.gcp2` + +## Usage + +```xml + + + +``` + + + +## Shapes (298) + +- `a7_power` +- `admin_connected` +- `admob` +- `advanced_solutions_lab` +- `ai_hub` +- `anomaly_detection` +- `api_analytics` +- `api_monetization` +- `apigee_api_platform` +- `apigee_sense` +- `app_engine` +- `app_engine_icon` +- `application` +- `application_system` +- `arrow_cycle` +- `arrows_system` +- `aspect_ratio` +- `automl_natural_language` +- `automl_tables` +- `automl_translation` +- `automl_video_intelligence` +- `automl_vision` +- `avere` +- `beacon` +- `beyondcorp` +- `big_query` +- `bigquery` +- `biomedical_beaker` +- `biomedical_test_tube` +- `biomedical_trio` +- `blank` +- `blue_hexagon` +- `bucket` +- `bucket_scale` +- `calculator` +- `campaign_manager` +- `capabilities` +- `certified_industry_standard` +- `check` +- `check_2` +- `check_available` +- `check_scale` +- `circuit_board` +- `clock` +- `cloud` +- `cloud_apis` +- `cloud_armor` +- `cloud_automl` +- `cloud_bigtable` +- `cloud_cdn` +- `cloud_checkmark` +- `cloud_code` +- `cloud_composer` +- `cloud_computer` +- `cloud_connected_insight` +- `cloud_data_catalog` +- `cloud_data_fusion` +- `cloud_dataflow` +- `cloud_dataflow_icon` +- `cloud_datalab` +- `cloud_dataprep` +- `cloud_dataproc` +- `cloud_dataproc_icon` +- `cloud_datastore` +- `cloud_deployment_manager` +- `cloud_dns` +- `cloud_endpoints` +- `cloud_external_ip_addresses` +- `cloud_filestore` +- `cloud_firestore` +- `cloud_firewall_rules` +- `cloud_functions` +- `cloud_iam` +- `cloud_inference_api` +- `cloud_information` +- `cloud_iot_core` +- `cloud_iot_edge` +- `cloud_jobs_api` +- `cloud_load_balancing` +- `cloud_machine_learning` +- `cloud_memorystore` +- `cloud_messaging` +- `cloud_monitoring` +- `cloud_nat` +- `cloud_natural_language_api` +- `cloud_network` +- `cloud_pubsub` +- `cloud_router` +- `cloud_routes` +- `cloud_run` +- `cloud_scheduler` +- `cloud_security` +- `cloud_security_command_center` +- `cloud_security_scanner` +- `cloud_server` +- `cloud_service_mesh` +- `cloud_spanner` +- `cloud_speech_api` +- `cloud_sql` +- `cloud_storage` +- `cloud_sub_pub` +- `cloud_tasks` +- `cloud_test_lab` +- `cloud_text_to_speech` +- `cloud_tools_for_powershell` +- `cloud_tpu` +- `cloud_translation_api` +- `cloud_video_intelligence_api` +- `cloud_vision_api` +- `cloud_vpn` +- `cluster` +- `compute_engine` +- `compute_engine_2` +- `compute_engine_icon` +- `connected` +- `container_builder` +- `container_engine` +- `container_engine_icon` +- `container_optimized_os` +- `container_registry` +- `cost` +- `cost_arrows` +- `cost_savings` +- `data_access` +- `data_increase` +- `data_loss_prevention_api` +- `data_storage_cost` +- `data_studio` +- `database` +- `database_2` +- `database_3` +- `database_cycle` +- `database_speed` +- `database_uploading` +- `debugger` +- `dedicated_game_server` +- `dedicated_interconnect` +- `desktop` +- `desktop_and_mobile` +- `developer_portal` +- `dialogflow_enterprise_edition` +- `enhance_ui` +- `enhance_ui_2` +- `error_reporting` +- `external_data_center` +- `external_data_resource` +- `external_payment_form` +- `fastly` +- `files` +- `firebase` +- `folders` +- `forseti_lockup` +- `forseti_logo` +- `frontend_platform_services` +- `game` +- `gateway` +- `gateway_icon` +- `gear` +- `gear_arrow` +- `gear_chain` +- `gear_load` +- `genomics` +- `gke_on_prem` +- `globe_world` +- `google_ad_manager` +- `google_ads` +- `google_analytics` +- `google_analytics_360` +- `google_cloud_platform` +- `google_cloud_platform_lockup` +- `google_network` +- `google_network_edge_cache` +- `google_play_game_service` +- `gpu` +- `half_cloud` +- `https_load_balancer` +- `identity_aware_proxy` +- `image_services` +- `increase_cost_arrows` +- `internal_payment_authorization` +- `internet_connection` +- `istio_logo` +- `key` +- `key_management_service` +- `kubernetes_logo` +- `kubernetes_name` +- `laptop` +- `legacy_cloud` +- `legacy_cloud_2` +- `lifecycle` +- `lightbulb` +- `list` +- `live` +- `load_balancing` +- `loading` +- `loading_2` +- `loading_3` +- `lock` +- `logging` +- `logs_api` +- `management_security` +- `maps_api` +- `mem_instances` +- `memcache` +- `memory_card` +- `mobile_devices` +- `modifiers_autoscaling` +- `modifiers_custom_virtual_machine` +- `modifiers_high_cpu_machine` +- `modifiers_high_memory_machine` +- `modifiers_preemptable_vm` +- `modifiers_shared_core_machine_f1` +- `modifiers_shared_core_machine_g1` +- `modifiers_standard_machine` +- `modifiers_storage` +- `monitor` +- `monitor_2` +- `mxgraph.gcp2` +- `nat` +- `network` +- `network_load_balancer` +- `node` +- `outline_blank_1` +- `outline_blank_2` +- `outline_blank_3` +- `outline_highcomp` +- `outline_highmem` +- `partner_interconnect` +- `payment` +- `people_security_management` +- `persistent_disk` +- `persistent_disk_snapshot` +- `phone` +- `phone_android` +- `placeholder` +- `play_gear` +- `play_start` +- `prediction_api` +- `premium_network_tier` +- `primary` +- `process` +- `profiler` +- `push_notification_service` +- `recommendations_ai` +- `record` +- `replication_controller` +- `replication_controller_2` +- `replication_controller_3` +- `report` +- `repository` +- `repository_2` +- `repository_3` +- `repository_primary` +- `retail` +- `safety` +- `save` +- `scale` +- `scheduled_tasks` +- `search` +- `search_api` +- `security_key_enforcement` +- `segments` +- `segments_2` +- `segments_overlap` +- `servers_stacked` +- `service` +- `service_discovery` +- `social_media_time` +- `solution` +- `speaker` +- `speed` +- `squid_proxy` +- `stackdriver` +- `stacked_ownership` +- `standard_network_tier` +- `storage` +- `stream` +- `swap` +- `systems_check` +- `tape_record` +- `task_queues` +- `task_queues_2` +- `tensorflow_lockup` +- `tensorflow_logo` +- `thumbs_up` +- `time_clock` +- `trace` +- `traffic_director` +- `transfer_appliance` +- `users` +- `view_list` +- `virtual_file_system` +- `virtual_private_cloud` +- `visibility` +- `vpn` +- `vpn_gateway` +- `webcam` +- `website` diff --git a/docs/shape-libraries/infographic.md b/docs/shape-libraries/infographic.md new file mode 100644 index 00000000..2a98611c --- /dev/null +++ b/docs/shape-libraries/infographic.md @@ -0,0 +1,24 @@ +# infographic + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.infographic` + +## Usage + +```xml + + + +``` + +## Shapes + +- `shadedCube` (needs `isoAngle=15;`) +- `ribbonSimple` (needs `notch1=20;notch2=20;`) +- `ribbonRolled` +- `ribbonDoubleFolded` +- `shadedTriangle` +- `shadedPyramid` +- `cylinder` +- `banner` +- `flag` diff --git a/docs/shape-libraries/kubernetes.md b/docs/shape-libraries/kubernetes.md new file mode 100644 index 00000000..da023d3a --- /dev/null +++ b/docs/shape-libraries/kubernetes.md @@ -0,0 +1,58 @@ +# kubernetes + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.kubernetes` + +## Usage + +```xml + + + +``` + + + +## Shapes (41) + +- `api` +- `c_c_m` +- `c_m` +- `c_role` +- `cm` +- `crb` +- `crd` +- `cronjob` +- `deploy` +- `ds` +- `ep` +- `etcd` +- `frame` +- `group` +- `hpa` +- `ing` +- `job` +- `k_proxy` +- `kubelet` +- `limits` +- `master` +- `mxgraph.kubernetes` +- `netpol` +- `node` +- `ns` +- `pod` +- `psp` +- `pv` +- `pvc` +- `quota` +- `rb` +- `role` +- `rs` +- `sa` +- `sc` +- `sched` +- `secret` +- `sts` +- `svc` +- `user` +- `vol` diff --git a/docs/shape-libraries/lean_mapping.md b/docs/shape-libraries/lean_mapping.md new file mode 100644 index 00000000..811a18ad --- /dev/null +++ b/docs/shape-libraries/lean_mapping.md @@ -0,0 +1,31 @@ +# lean_mapping + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.lean_mapping` + +## Usage + +```xml + + + +``` + + + +## Shapes (14) + +- `airplane_7` +- `electronic_info_flow` +- `finished_goods_to_customer` +- `go_see_production_scheduling` +- `kaizen_lightening_burst` +- `kanban_post` +- `load_leveling` +- `manual_info_flow` +- `move_by_forklift` +- `mrp_erp` +- `mxgraph.lean_mapping` +- `operator` +- `quality_problem` +- `verbal` diff --git a/docs/shape-libraries/mscae.md b/docs/shape-libraries/mscae.md new file mode 100644 index 00000000..5d935ac4 --- /dev/null +++ b/docs/shape-libraries/mscae.md @@ -0,0 +1,22 @@ +# mscae + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.mscae` + +## Usage + +```xml + + + +``` + +## Categories + +Shapes are organized by category: `mscae.cloud`, `mscae.intune`, `mscae.oms`, `mscae.system_center` + +- `conditional_access_exchange` +- `conditional_access_sharepoint` +- `primary_site` + +(See draw.io for complete shape list within each category) diff --git a/docs/shape-libraries/network.md b/docs/shape-libraries/network.md new file mode 100644 index 00000000..47641573 --- /dev/null +++ b/docs/shape-libraries/network.md @@ -0,0 +1,72 @@ +# network + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.networks` + +## Usage + +```xml + + + +``` + +## Shapes (57) + +- `biometric_reader` +- `bus` +- `business_center` +- `cloud` +- `comm_link` +- `comm_link_edge` +- `community` +- `copier` +- `desktop_pc` +- `external_storage` +- `firewall` +- `gamepad` +- `hub` +- `laptop` +- `load_balancer` +- `mail_server` +- `mainframe` +- `mobile` +- `modem` +- `monitor` +- `nas_filer` +- `patch_panel` +- `phone_1` +- `phone_2` +- `printer` +- `proxy_server` +- `rack` +- `radio_tower` +- `router` +- `satellite` +- `satellite_dish` +- `scanner` +- `secured` +- `security_camera` +- `server` +- `server_storage` +- `storage` +- `supercomputer` +- `switch` +- `tablet` +- `tape_storage` +- `terminal` +- `unsecure` +- `ups_enterprise` +- `ups_small` +- `usb_stick` +- `user_female` +- `user_male` +- `users` +- `video_projector` +- `video_projector_screen` +- `virtual_pc` +- `virtual_server` +- `virus` +- `web_server` +- `wireless_hub` +- `wireless_modem` diff --git a/docs/shape-libraries/openstack.md b/docs/shape-libraries/openstack.md new file mode 100644 index 00000000..b92b8549 --- /dev/null +++ b/docs/shape-libraries/openstack.md @@ -0,0 +1,36 @@ +# openstack + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.openstack` + +## Usage + +```xml + + + +``` + + + +## Shapes (19) + +- `cinder_volume` +- `cinder_volumeattachment` +- `designate_recordset` +- `designate_zone` +- `heat_autoscalinggroup` +- `heat_resourcegroup` +- `heat_scalingpolicy` +- `mxgraph.openstack` +- `neutron_floatingip` +- `neutron_floatingipassociation` +- `neutron_net` +- `neutron_port` +- `neutron_router` +- `neutron_routerinterface` +- `neutron_securitygroup` +- `neutron_subnet` +- `nova_keypair` +- `nova_server` +- `swift_container` diff --git a/docs/shape-libraries/pid.md b/docs/shape-libraries/pid.md new file mode 100644 index 00000000..58b088ad --- /dev/null +++ b/docs/shape-libraries/pid.md @@ -0,0 +1,22 @@ +# pid + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.pid2valves`, `mxgraph.pid2inst`, `mxgraph.pid2misc` + +## Usage + +```xml + + + +``` + +## Valve Types + +For `mxgraph.pid2valves.valve`, use `valveType=` with: +- `gate`, `globe`, `needle`, `ball`, `butterfly`, `diaphragm`, `plug`, `check` + +## Other Prefixes + +- `mxgraph.pid2inst` - Instruments (discInst, sharedCont, compFunc) +- `mxgraph.pid2misc` - Miscellaneous diff --git a/docs/shape-libraries/rack.md b/docs/shape-libraries/rack.md new file mode 100644 index 00000000..18ecc3f8 --- /dev/null +++ b/docs/shape-libraries/rack.md @@ -0,0 +1,57 @@ +# rack + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.rack` + +## Usage + +```xml + + + +``` + +Shapes are organized by vendor: `mxgraph.rack.{vendor}.{model}` + +## Vendors + +### F5 + +- `arx_500` +- `big_ip_1600` +- `big_ip_2000` +- `big_ip_4000` + +### Dell + +- `dell_poweredge_1u` +- `poweredge_630` +- `poweredge_730` + +### HPE Aruba + +HPE Aruba shapes have subcategories: `mxgraph.rack.hpe_aruba.{category}.{model}` + +**gateways_controllers:** +- `aruba_7010_mobility_controller_front` +- `aruba_7010_mobility_controller_rear` +- `aruba_7024_mobility_controller_front` +- `aruba_7205_mobility_controller_front` + +**security:** +- `aruba_clearpass_c1000_front` +- `aruba_clearpass_c2000_front` +- `aruba_clearpass_c3000_front` + +**switches:** +- `j9772a_2530_48g_poeplus_switch` +- `j9773a_2530_24g_poeplus_switch` +- `jl253a_aruba_2930f_24g_4sfpplus_switch` + +### General (rackGeneral) + +Use `mxgraph.rackGeneral.{shape}` for generic rack items: +- `rackCabinet3` +- `plate` + +(See draw.io Rack shape library for complete list) diff --git a/docs/shape-libraries/salesforce.md b/docs/shape-libraries/salesforce.md new file mode 100644 index 00000000..43562439 --- /dev/null +++ b/docs/shape-libraries/salesforce.md @@ -0,0 +1,116 @@ +# salesforce + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.salesforce` + +## Usage + +```xml + + + +``` + +Replace `analytics` with any shape from the list below. + + + +## Shapes (97) + +- `analytics` +- `analytics2` +- `apps` +- `apps2` +- `automation` +- `automation2` +- `automotive` +- `automotive2` +- `bots` +- `bots2` +- `builders` +- `builders2` +- `channels` +- `channels2` +- `commerce` +- `commerce2` +- `communications` +- `communications2` +- `consumer_goods` +- `consumer_goods2` +- `customer_360` +- `customer_3602` +- `data` +- `data2` +- `education` +- `education2` +- `employees` +- `employees2` +- `energy` +- `energy2` +- `field_service` +- `field_service2` +- `financial_services` +- `financial_services2` +- `government` +- `government2` +- `health` +- `health2` +- `heroku` +- `heroku2` +- `inbox` +- `inbox2` +- `industries` +- `industries2` +- `integration` +- `integration2` +- `iot` +- `iot2` +- `learning` +- `learning2` +- `loyalty` +- `loyalty2` +- `manufacturing` +- `manufacturing2` +- `marketing` +- `marketing2` +- `media` +- `media2` +- `mxgraph.salesforce` +- `non_profit` +- `non_profit2` +- `partners` +- `partners2` +- `personalization` +- `personalization2` +- `philantrophy` +- `philantrophy2` +- `platform` +- `platform2` +- `privacy` +- `privacy2` +- `retail` +- `retail2` +- `sales` +- `sales2` +- `segments` +- `segments2` +- `service` +- `service2` +- `smb` +- `smb2` +- `social_studio` +- `social_studio2` +- `stream` +- `stream2` +- `success` +- `success2` +- `sustainability` +- `sustainability2` +- `transportation_and_technology` +- `transportation_and_technology2` +- `web` +- `web2` +- `work_com` +- `work_com2` +- `workflow` +- `workflow2` diff --git a/docs/shape-libraries/sap.md b/docs/shape-libraries/sap.md new file mode 100644 index 00000000..7c6853fd --- /dev/null +++ b/docs/shape-libraries/sap.md @@ -0,0 +1,179 @@ +# sap + +**Type:** SVG images +**Path:** `img/lib/sap/` + +## Usage + +```xml + + + +``` + +## Shapes (164) + +- `1` +- `2` +- `3` +- `4` +- `5` +- `6` +- `7` +- `8` +- `9` +- `10` +- `11` +- `12` +- `13` +- `Adapter` +- `Admin` +- `Alert` +- `API` +- `API_Business_Hub_Enterprise` +- `App` +- `Application_Autoscaler` +- `Application_Frontend_Service` +- `Application_Vulnerability_Report` +- `Building` +- `Business_Application_Studio` +- `Business_Entity_Recognition` +- `Business_Process_Model_Connector_for_SAP_Signavio_Solutions` +- `Cloud` +- `Cloud_Connector` +- `Cloud_Connector2` +- `Cloud_Integration_Automation` +- `Cloud_Integration_Automation2` +- `Cloud_Transport_Management` +- `Data_Attribute_Recommendation` +- `Deploy` +- `Desktop` +- `Devices` +- `Document` +- `Document_Information_Extraction` +- `Documents` +- `Edge_Integration_Cell` +- `Event` +- `Extensibility_Service` +- `Factory` +- `Feature` +- `HTML5_App_Repository` +- `Identity_Authentication` +- `Identity_Authentication2` +- `Identity_Directory` +- `Identity_Directory2` +- `Identity_Provisioning` +- `Identity_Provisioning2` +- `Info` +- `Intelligent_Situation_Automation` +- `Invoice_Object_Recommendation` +- `Invoice_Object_Recommendation2` +- `Key` +- `Landscape_Portal_for_SAP_S4HANA_Cloud_ABAP_Environment` +- `Link` +- `Locked` +- `Machine` +- `Message` +- `Mobile` +- `OAuth_20` +- `Object_Store_on_SAP_BTP` +- `On-Premise` +- `Personalized_Recommendation` +- `SAP_AI_Core` +- `SAP_AI_Launchpad` +- `SAP_Alert_Notification_service_for_SAP_BTP` +- `SAP_Analytics_Cloud` +- `SAP_Analytics_Cloud_Embedded_Edition` +- `SAP_Application_Logging_service_for_SAP_BTP` +- `SAP_Asset_Performance_Management` +- `SAP_Audit_Log_Service` +- `SAP_Authorization_Management_Service` +- `SAP_Authorization_and_Trust_Management_service` +- `SAP_Automation_Pilot` +- `SAP_BTP,_ABAP_environment` +- `SAP_BTP,_Cloud_Foundry_runtime` +- `SAP_BTP,_Kyma_runtime` +- `SAP_Build` +- `SAP_Build_Apps` +- `SAP_Build_Apps_-_Copy` +- `SAP_Build_Code` +- `SAP_Build_Process_Automation` +- `SAP_Build_Process_Automation_-_Copy` +- `SAP_Build_Work_Zone_-_Advanced_Edition` +- `SAP_Build_Work_Zone_-_Standard_Edition` +- `SAP_Business_Accelerator_Hub` +- `SAP_Business_Data_Cloud` +- `SAP_Cloud_ALM` +- `SAP_Cloud_Application_Programming_Model` +- `SAP_Cloud_Identity,_SAP_Malware_Scanning_Service` +- `SAP_Cloud_Identity_Service` +- `SAP_Cloud_Logging` +- `SAP_Cloud_Management_Service` +- `SAP_Cloud_Transport_Management` +- `SAP_Collaborative_Demand_and_Capacity_Management` +- `SAP_Connectivity_Service` +- `SAP_Content_Agent_Service` +- `SAP_Continuous_Integration_and_Delivery` +- `SAP_Credential_Store` +- `SAP_Custom_Domain_service` +- `SAP_Data_Privacy_Integration` +- `SAP_Data_Retention_Manager` +- `SAP_Datasphere` +- `SAP_Destination_service` +- `SAP_Digital_Assistant` +- `SAP_Digital_Assistant_Service` +- `SAP_Digital_Manufacturing` +- `SAP_Document_Grounding` +- `SAP_Document_Management_Service` +- `SAP_Event_Broker_for_SAP_Cloud_Applications` +- `SAP_Green_Token` +- `SAP_HANA_Cloud` +- `SAP_HANA_Spatial_Services` +- `SAP_Health_Data_Services_for_FHIR` +- `SAP_Integration_Suite` +- `SAP_Integration_Suite_-_API_Managment` +- `SAP_Integration_Suite_-_Advanced_Event_Mesh` +- `SAP_Integration_Suite_-_Cloud_Integration` +- `SAP_Integration_Suite_-_Data_Space_Integration` +- `SAP_Integration_Suite_-_Event_Mesh` +- `SAP_Integration_Suite_-_Integration_Advisor` +- `SAP_Integration_Suite_-_Integration_Assessment` +- `SAP_Integration_Suite_-_Migration_Assessment` +- `SAP_Integration_Suite_-_Open_Connectors` +- `SAP_Integration_Suite_-_SAP_Graph` +- `SAP_Integration_Suite_-_Trading_Partner_Management` +- `SAP_Job_Scheduling_service` +- `SAP_Keystore_Service` +- `SAP_Landscape_Management_Cloud` +- `SAP_Logo` +- `SAP_Master_Data_Governance` +- `SAP_Master_Data_Integration` +- `SAP_Mobile_Services` +- `SAP_Monitoring_service_for_SAP_BTP` +- `SAP_Omnichannel_Promotion_Pricing` +- `SAP_PKI_Certificate_Service` +- `SAP_Persistence_Service_ASE` +- `SAP_Personal_Data_Manager` +- `SAP_Private_Link_service` +- `SAP_Project_and_Resource_Management` +- `SAP_Responsibility_Management_Service` +- `SAP_S4HANA_Cloud_for_Intelligent_Intercompany_Reconciliation` +- `SAP_S4HANA_for_MS_Teams` +- `SAP_Secure_Login_Service_for_SAP_GUI` +- `SAP_Service_Manager` +- `SAP_Software_as_a_Service_Provisioning_Service` +- `SAP_Solution_Lifecycle_Management_Service` +- `SAP_Sustainability_Data_Exchange` +- `SAP_Task_Center` +- `SAP_Translation_Hub` +- `SAP_Variant_Configuration_and_Pricing` +- `SAP_Watch_List_Screening` +- `Service_Ticket_Intelligence` +- `Service_Ticket_Intelligence2` +- `Settings` +- `Success` +- `Third_Party` +- `UI5_flexibility_for_key_users` +- `UI_Theme_Designer` +- `User` +- `Web` diff --git a/docs/shape-libraries/sitemap.md b/docs/shape-libraries/sitemap.md new file mode 100644 index 00000000..bdd3cac1 --- /dev/null +++ b/docs/shape-libraries/sitemap.md @@ -0,0 +1,68 @@ +# sitemap + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.sitemap` + +## Usage + +```xml + + + +``` + + + +## Shapes (51) + +- `about_us` +- `audio` +- `biography` +- `blog` +- `calendar` +- `chart` +- `chat` +- `cloud` +- `contact` +- `contact_us` +- `document` +- `download` +- `error` +- `faq` +- `form` +- `gallery` +- `game` +- `home` +- `info` +- `jobs` +- `log` +- `login` +- `mail` +- `map` +- `mxgraph.sitemap` +- `news` +- `page` +- `payment` +- `photo` +- `portfolio` +- `post` +- `pricing` +- `print` +- `products` +- `profile` +- `references` +- `script` +- `search` +- `security` +- `services` +- `settings` +- `shopping` +- `sitemap` +- `slideshow` +- `sports` +- `success` +- `text` +- `upload` +- `user` +- `video` +- `warning` diff --git a/docs/shape-libraries/vvd.md b/docs/shape-libraries/vvd.md new file mode 100644 index 00000000..7d9540df --- /dev/null +++ b/docs/shape-libraries/vvd.md @@ -0,0 +1,112 @@ +# vvd + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.vvd` + +## Usage + +```xml + + + +``` + + + +## Shapes (95) + +- `administrator` +- `app` +- `app_volumes_manager` +- `appstack_volume` +- `array_manager` +- `blueprint` +- `business_continuity_data_protection` +- `cd` +- `cloud_computing` +- `collective_nsx_esg` +- `consumption_plane` +- `cpu` +- `datacenter` +- `datastore` +- `disk` +- `document` +- `edge_gateway` +- `endpoint` +- `ethernet_port` +- `external_networks` +- `flash_drive` +- `folder` +- `guest_agent_customization` +- `horizon` +- `infrastructure` +- `key` +- `keyboard` +- `laptop` +- `log_files` +- `logical_distribution` +- `logical_firewall` +- `machine` +- `memory` +- `monitor` +- `mouse` +- `mxgraph.vvd` +- `networking` +- `networks` +- `nfvo` +- `nsx` +- `nsx_controller` +- `nsx_dashboard` +- `nsx_edge_and_load_balancer` +- `nsx_esg` +- `nsx_manager` +- `nsx_public_cloud_gateway` +- `on_demand_self_service` +- `ovdc_networks` +- `pair_sites` +- `phone` +- `physical_network_adapter` +- `physical_storage` +- `physical_upstream_router` +- `platform_services_controller` +- `protection_group` +- `protection_group_config` +- `recovery_plan` +- `resource_pool` +- `scsi_controller` +- `security` +- `server` +- `service_provider_cloud_environment` +- `site` +- `site_container` +- `site_recovery` +- `site_recovery_functional_icon` +- `ssd` +- `storage` +- `switch` +- `telco_network` +- `template` +- `tenant_key` +- `user_group` +- `vapp_network` +- `vcenter_server` +- `vcloud_director` +- `virtual_appliance` +- `virtual_machine` +- `virtual_switch` +- `vm_group` +- `vnf_m` +- `volumes_agent` +- `vpn` +- `vrealize_automation` +- `vrealize_log_insight` +- `vrealize_operations` +- `vrealize_orchestrator` +- `vrops` +- `vsan` +- `vshield` +- `vxlan` +- `wavefront` +- `web_browser` +- `wi_fi` +- `writable_volume` diff --git a/docs/shape-libraries/webicons.md b/docs/shape-libraries/webicons.md new file mode 100644 index 00000000..c7f92717 --- /dev/null +++ b/docs/shape-libraries/webicons.md @@ -0,0 +1,194 @@ +# webicons + +**Type:** mxgraph shapes +**Prefix:** `mxgraph.webicons` + +## Usage + +```xml + + + +``` + + + +## Shapes (177) + +- `adfty` +- `adobe_pdf` +- `aim` +- `allvoices` +- `amazon` +- `amazon_2` +- `android` +- `apache` +- `apple` +- `apple_classic` +- `arduino` +- `ask` +- `atlassian` +- `audioboo` +- `aws` +- `aws_s3` +- `baidu` +- `bebo` +- `behance` +- `bing` +- `bitbucket` +- `blinklist` +- `blogger` +- `blogmarks` +- `bookmarks.fr` +- `box` +- `buddymarks` +- `buffer` +- `buzzfeed` +- `chrome` +- `citeulike` +- `confluence` +- `connotea` +- `dealsplus` +- `delicious` +- `designfloat` +- `deviantart` +- `digg` +- `diigo` +- `dopplr` +- `drawio1` +- `drawio2` +- `dribbble` +- `dropbox` +- `dropbox2` +- `drupal` +- `dzone` +- `ebay` +- `edmodo` +- `evernote` +- `facebook` +- `fancy` +- `fark` +- `fashiolista` +- `feed` +- `feedburner` +- `flickr` +- `folkd` +- `forrst` +- `fotolog` +- `freshbump` +- `fresqui` +- `friendfeed` +- `funp` +- `fwisp` +- `gabbr` +- `gamespot` +- `github` +- `gmail` +- `google` +- `google_drive` +- `google_hangout` +- `google_photos` +- `google_play` +- `google_play_light` +- `google_plus` +- `grooveshark` +- `hatena` +- `html5` +- `identi.ca` +- `instagram` +- `instapaper` +- `ios` +- `jamespot` +- `java` +- `joomla` +- `jquery` +- `json` +- `json_2` +- `last.fm` +- `linkagogo` +- `linkedin` +- `livejournal` +- `mail.ru` +- `meetup` +- `meneame` +- `messenger` +- `messenger_2` +- `messenger_3` +- `mind_body_green` +- `mongodb` +- `mxgraph.webicons` +- `myspace` +- `n4g` +- `netlog` +- `netvibes` +- `netvouz` +- `networkedblogs` +- `newsvine` +- `odnoklassniki` +- `oknotizie` +- `onedrive` +- `oracle` +- `paypal` +- `phone` +- `phonefavs` +- `pinterest` +- `plaxo` +- `playfire` +- `plurk` +- `pocket` +- `protopage` +- `readernaut` +- `reddit` +- `rss` +- `scoopit` +- `scribd` +- `segnalo` +- `sina` +- `sitejot` +- `skype` +- `skyrock` +- `slashdot` +- `sms` +- `socialvibe` +- `society6` +- `sonico` +- `soundcloud` +- `sourceforge` +- `sourceforge_2` +- `spring.me` +- `stackexchange` +- `stackoverflow` +- `startaid` +- `startlap` +- `steam` +- `stumbleupon` +- `stumpedia` +- `technorati` +- `translate` +- `tumblr` +- `tunein` +- `twitter` +- `two` +- `typepad` +- `viadeo` +- `viber` +- `viddler` +- `vimeo` +- `virb` +- `vkontakte` +- `wakoopa` +- `weheartit` +- `whatsapp` +- `wix` +- `wordpress` +- `wordpress_2` +- `xanga` +- `xerpi` +- `xing` +- `yahoo` +- `yahoo_2` +- `yammer` +- `yandex` +- `yelp` +- `yoolink` +- `youmob` diff --git a/lib/system-prompts.ts b/lib/system-prompts.ts index 2f1bc3bb..e2afa200 100644 --- a/lib/system-prompts.ts +++ b/lib/system-prompts.ts @@ -48,12 +48,19 @@ description: Continue generating diagram XML when display_diagram was truncated parameters: { xml: string // Continuation fragment (NO wrapper tags like or ) } +---Tool4--- +tool name: get_shape_library +description: Get shape/icon library documentation. Use this to discover available icon shapes (AWS, Azure, GCP, Kubernetes, etc.) before creating diagrams with cloud/tech icons. +parameters: { + library: string // Library name: aws4, azure2, gcp2, kubernetes, cisco19, flowchart, bpmn, etc. +} ---End of tools--- IMPORTANT: Choose the right tool: - Use display_diagram for: Creating new diagrams, major restructuring, or when the current diagram XML is empty - Use edit_diagram for: Small modifications, adding/removing elements, changing text/colors, repositioning items - Use append_diagram for: ONLY when display_diagram was truncated due to output length - continue generating from where you stopped +- Use get_shape_library for: Discovering available icons/shapes when creating cloud architecture or technical diagrams (call BEFORE display_diagram) Core capabilities: - Generate valid, well-formed XML strings for draw.io diagrams @@ -84,7 +91,7 @@ Note that: - When artistic drawings are requested, creatively compose them using standard diagram shapes and connectors while maintaining visual clarity. - Return XML only via tool calls, never in text responses. - If user asks you to replicate a diagram based on an image, remember to match the diagram style and layout as closely as possible. Especially, pay attention to the lines and shapes, for example, if the lines are straight or curved, and if the shapes are rounded or square. -- Note that when you need to generate diagram about aws architecture, use **AWS 2025 icons**. +- For cloud/tech diagrams (AWS, Azure, GCP, K8s), call get_shape_library first to discover available icon shapes and their syntax. - NEVER include XML comments () in your generated XML. Draw.io strips comments, which breaks edit_diagram patterns. When using edit_diagram tool: diff --git a/package-lock.json b/package-lock.json index 390fde31..43420f19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "next-ai-draw-io", - "version": "0.4.2", + "version": "0.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "next-ai-draw-io", - "version": "0.4.2", + "version": "0.4.3", "license": "Apache-2.0", "dependencies": { "@ai-sdk/amazon-bedrock": "^3.0.70", @@ -63,7 +63,7 @@ }, "devDependencies": { "@anthropic-ai/tokenizer": "^0.0.4", - "@biomejs/biome": "2.3.8", + "@biomejs/biome": "^2.3.8", "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.19", "@types/node": "^20", diff --git a/package.json b/package.json index 3cd31652..967a970d 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ }, "devDependencies": { "@anthropic-ai/tokenizer": "^0.0.4", - "@biomejs/biome": "2.3.8", + "@biomejs/biome": "^2.3.8", "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.19", "@types/node": "^20", From 378bef435ed2b8be8aec870dcdcf8f4edfd6754f Mon Sep 17 00:00:00 2001 From: Biki Kalita <86558912+Biki-dev@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:18:54 +0530 Subject: [PATCH 006/349] Add i18n support, language toggle UI, and translate Settings dialog (#334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * i18n support added * fix: align i18n implementation with Next.js 16 guide - Rename middleware.ts to proxy.ts (Next.js 16 convention) - Fix params type to Promise<{lang: string}> for layout/metadata - Add 'server-only' directive and dynamic imports to dictionaries.ts - Add hasLocale type guard and notFound() for invalid locales - Wrap LanguageToggle in Suspense for useSearchParams - Fix dictionary key mismatch (learnmore -> learnMore) - Improve Chinese translations per Gemini review: - loading ellipsis, new -> 新建, styledMode -> 精致 - goodResponse/badResponse -> 有帮助/无帮助 - closeProtection -> 关闭确认, fileExceeds phrasing - Improve Japanese translations per Gemini review: - closeProtection -> ページ離脱確認 - invalidAccessCode phrasing, appendDiagram -> に追加 - styledMode -> スタイル付き --------- Co-authored-by: dayuan.jiang --- app/{ => [lang]}/about/cn/page.tsx | 0 app/{ => [lang]}/about/ja/page.tsx | 0 app/{ => [lang]}/about/page.tsx | 0 app/[lang]/layout.tsx | 172 +++++++++++++++++++++++++++ app/{ => [lang]}/page.tsx | 8 +- app/layout.tsx | 125 -------------------- components/chat-example-panel.tsx | 48 ++++---- components/chat-input.tsx | 77 +++++++----- components/chat-panel.tsx | 35 +++--- components/file-preview-list.tsx | 15 +-- components/history-dialog.tsx | 26 ++-- components/language-toggle.tsx | 108 +++++++++++++++++ components/quota-limit-toast.tsx | 47 +++----- components/reset-warning-modal.tsx | 12 +- components/save-dialog.tsx | 48 +++++--- components/settings-dialog.tsx | 100 ++++++++++------ hooks/use-dictionary.ts | 29 +++++ lib/i18n/config.ts | 6 + lib/i18n/dictionaries.ts | 18 +++ lib/i18n/dictionaries/en.json | 184 +++++++++++++++++++++++++++++ lib/i18n/dictionaries/ja.json | 184 +++++++++++++++++++++++++++++ lib/i18n/dictionaries/zh.json | 184 +++++++++++++++++++++++++++++ lib/i18n/utils.ts | 14 +++ package-lock.json | 35 ++++++ package.json | 4 + proxy.ts | 63 ++++++++++ 26 files changed, 1235 insertions(+), 307 deletions(-) rename app/{ => [lang]}/about/cn/page.tsx (100%) rename app/{ => [lang]}/about/ja/page.tsx (100%) rename app/{ => [lang]}/about/page.tsx (100%) create mode 100644 app/[lang]/layout.tsx rename app/{ => [lang]}/page.tsx (95%) delete mode 100644 app/layout.tsx create mode 100644 components/language-toggle.tsx create mode 100644 hooks/use-dictionary.ts create mode 100644 lib/i18n/config.ts create mode 100644 lib/i18n/dictionaries.ts create mode 100644 lib/i18n/dictionaries/en.json create mode 100644 lib/i18n/dictionaries/ja.json create mode 100644 lib/i18n/dictionaries/zh.json create mode 100644 lib/i18n/utils.ts create mode 100644 proxy.ts diff --git a/app/about/cn/page.tsx b/app/[lang]/about/cn/page.tsx similarity index 100% rename from app/about/cn/page.tsx rename to app/[lang]/about/cn/page.tsx diff --git a/app/about/ja/page.tsx b/app/[lang]/about/ja/page.tsx similarity index 100% rename from app/about/ja/page.tsx rename to app/[lang]/about/ja/page.tsx diff --git a/app/about/page.tsx b/app/[lang]/about/page.tsx similarity index 100% rename from app/about/page.tsx rename to app/[lang]/about/page.tsx diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx new file mode 100644 index 00000000..5ab56c9c --- /dev/null +++ b/app/[lang]/layout.tsx @@ -0,0 +1,172 @@ +import { GoogleAnalytics } from "@next/third-parties/google" +import type { Metadata, Viewport } from "next" +import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google" +import { notFound } from "next/navigation" +import { DiagramProvider } from "@/contexts/diagram-context" +import { DictionaryProvider } from "@/hooks/use-dictionary" +import type { Locale } from "@/lib/i18n/config" +import { i18n } from "@/lib/i18n/config" +import { getDictionary, hasLocale } from "@/lib/i18n/dictionaries" + +import "../globals.css" + +const plusJakarta = Plus_Jakarta_Sans({ + variable: "--font-sans", + subsets: ["latin"], + weight: ["400", "500", "600", "700"], +}) + +const jetbrainsMono = JetBrains_Mono({ + variable: "--font-mono", + subsets: ["latin"], + weight: ["400", "500"], +}) + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, +} + +// Generate static params for all locales +export async function generateStaticParams() { + return i18n.locales.map((locale) => ({ lang: locale })) +} + +// Generate metadata per locale +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: string }> +}): Promise { + const { lang: rawLang } = await params + const lang = (rawLang in { en: 1, zh: 1, ja: 1 } ? rawLang : "en") as Locale + + // Default to English metadata + const titles: Record = { + en: "Next AI Draw.io - AI-Powered Diagram Generator", + zh: "Next AI Draw.io - AI powered diagram generator", + ja: "Next AI Draw.io - AI-powered diagram generator", + } + + const descriptions: Record = { + en: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.", + zh: "Use AI to create AWS architecture diagrams, flowcharts, and technical diagrams. Free online tool integrated with draw.io and AI assistance for professional diagram creation.", + ja: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Create professional diagrams with a free online tool that integrates draw.io with an AI assistant.", + } + + return { + title: titles[lang], + description: descriptions[lang], + keywords: [ + "AI diagram generator", + "AWS architecture", + "flowchart creator", + "draw.io", + "AI drawing tool", + "technical diagrams", + "diagram automation", + "free diagram generator", + "online diagram maker", + ], + authors: [{ name: "Next AI Draw.io" }], + creator: "Next AI Draw.io", + publisher: "Next AI Draw.io", + metadataBase: new URL("https://next-ai-drawio.jiang.jp"), + openGraph: { + title: titles[lang], + description: descriptions[lang], + type: "website", + url: "https://next-ai-drawio.jiang.jp", + siteName: "Next AI Draw.io", + locale: lang === "zh" ? "zh_CN" : lang === "ja" ? "ja_JP" : "en_US", + images: [ + { + url: "/architecture.png", + width: 1200, + height: 630, + alt: "Next AI Draw.io - AI-powered diagram creation tool", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: titles[lang], + description: descriptions[lang], + images: ["/architecture.png"], + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-video-preview": -1, + "max-image-preview": "large", + "max-snippet": -1, + }, + }, + icons: { + icon: "/favicon.ico", + }, + alternates: { + languages: { + en: "/en", + zh: "/zh", + ja: "/ja", + }, + }, + } +} + +export default async function RootLayout({ + children, + params, +}: Readonly<{ + children: React.ReactNode + params: Promise<{ lang: string }> +}>) { + const { lang } = await params + if (!hasLocale(lang)) notFound() + const validLang = lang as Locale + const dictionary = await getDictionary(validLang) + + const jsonLd = { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + name: "Next AI Draw.io", + applicationCategory: "DesignApplication", + operatingSystem: "Web Browser", + description: + "AI-powered diagram generator with targeted XML editing capabilities that integrates with draw.io for creating AWS architecture diagrams, flowcharts, and technical diagrams. Features diagram history, multi-provider AI support, and real-time collaboration.", + url: "https://next-ai-drawio.jiang.jp", + inLanguage: validLang, + offers: { + "@type": "Offer", + price: "0", + priceCurrency: "USD", + }, + } + + return ( + + + ` diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index d0e049a7..8f1dcafb 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -34,6 +34,12 @@ import { applyDiagramOperations, type DiagramOperation, } from "./diagram-operations.js" +import { + addHistoryEntry, + getHistory, + getHistoryCount, + getVersion as getHistoryVersion, +} from "./history.js" import { getServerPort, getState, @@ -197,6 +203,21 @@ server.registerTool( log.info(`Displaying diagram, ${xml.length} chars`) + // 1. Save current state to history BEFORE replacing (preserve user's work) + if (currentSession.xml) { + // Check last entry's source to use correct label + const lastEntry = getHistory(currentSession.id, 1)[0] + const actualSource = lastEntry?.source || "human" + addHistoryEntry(currentSession.id, { + xml: currentSession.xml, + svg: "", + source: actualSource, + tool: "display_diagram", + timestamp: new Date(), + description: "Before AI replaced", + }) + } + // Update session state currentSession.xml = xml currentSession.version++ @@ -204,6 +225,16 @@ server.registerTool( // Push to embedded server state setState(currentSession.id, xml) + // 2. Save new state to history AFTER generation (capture AI result) + addHistoryEntry(currentSession.id, { + xml: xml, + svg: "", + source: "ai", + tool: "display_diagram", + timestamp: new Date(), + description: "AI generated diagram", + }) + log.info(`Diagram displayed successfully`) return { @@ -295,6 +326,19 @@ server.registerTool( log.info(`Editing diagram with ${operations.length} operation(s)`) + // 1. Save current state to history BEFORE editing (preserve user's work) + // Check last entry's source to use correct label + const lastEntry = getHistory(currentSession.id, 1)[0] + const actualSource = lastEntry?.source || "human" + addHistoryEntry(currentSession.id, { + xml: currentSession.xml, + svg: "", + source: actualSource, + tool: "edit_diagram", + timestamp: new Date(), + description: "Before AI edit", + }) + // Validate and auto-fix new_xml for each operation const validatedOps = operations.map((op) => { if (op.new_xml) { @@ -336,6 +380,16 @@ server.registerTool( // Push to embedded server setState(currentSession.id, result) + // 2. Save new state to history AFTER editing (capture AI result) + addHistoryEntry(currentSession.id, { + xml: result, + svg: "", + source: "ai", + tool: "edit_diagram", + timestamp: new Date(), + description: `AI edit: ${operations.length} operation(s)`, + }) + log.info(`Diagram edited successfully`) const successMsg = `Diagram edited successfully!\n\nApplied ${operations.length} operation(s).` @@ -502,6 +556,216 @@ server.registerTool( }, ) +// Tool: list_history +server.registerTool( + "list_history", + { + description: + "List diagram version history for the current session. " + + "Shows version numbers, who made each change (AI vs human), and timestamps. " + + "Use this to find a version to restore.", + inputSchema: { + limit: z + .number() + .optional() + .describe("Maximum number of entries to return (default: 20)"), + }, + }, + async ({ limit = 20 }) => { + try { + if (!currentSession) { + return { + content: [ + { + type: "text", + text: "Error: No active session. Please call start_session first.", + }, + ], + isError: true, + } + } + + const history = getHistory(currentSession.id, limit) + + if (history.length === 0) { + return { + content: [ + { + type: "text", + text: "No history available yet. Make some changes to create history.", + }, + ], + } + } + + const historyText = history + .map((entry) => { + const time = entry.timestamp.toLocaleTimeString() + const source = entry.source === "ai" ? "AI" : "Human" + const desc = entry.description + ? ` - ${entry.description}` + : "" + return `v${entry.version} [${source}] ${time}${desc}` + }) + .join("\n") + + return { + content: [ + { + type: "text", + text: `Diagram History (${history.length} entries, newest first):\n\n${historyText}\n\nUse restore_version to restore a specific version.`, + }, + ], + } + } catch (error) { + const message = + error instanceof Error ? error.message : String(error) + log.error("list_history failed:", message) + return { + content: [{ type: "text", text: `Error: ${message}` }], + isError: true, + } + } + }, +) + +// Tool: restore_version +server.registerTool( + "restore_version", + { + description: + "Restore diagram to a previous version from history. " + + "Use list_history first to see available versions. " + + "This creates a NEW history entry (non-destructive).", + inputSchema: { + version: z.number().describe("Version number to restore"), + }, + }, + async ({ version }) => { + try { + if (!currentSession) { + return { + content: [ + { + type: "text", + text: "Error: No active session. Please call start_session first.", + }, + ], + isError: true, + } + } + + const entry = getHistoryVersion(currentSession.id, version) + if (!entry) { + return { + content: [ + { + type: "text", + text: `Error: Version ${version} not found in history. Use list_history to see available versions.`, + }, + ], + isError: true, + } + } + + // Restore by updating session and state + currentSession.xml = entry.xml + currentSession.version++ + setState(currentSession.id, entry.xml) + + // Add history entry for the restore + addHistoryEntry(currentSession.id, { + xml: entry.xml, + svg: entry.svg, + source: "ai", + tool: "restore_version", + timestamp: new Date(), + description: `Restored from v${version}`, + }) + + log.info(`Restored diagram to v${version}`) + + return { + content: [ + { + type: "text", + text: `Diagram restored to version ${version} successfully!\n\nThe browser will update automatically.`, + }, + ], + } + } catch (error) { + const message = + error instanceof Error ? error.message : String(error) + log.error("restore_version failed:", message) + return { + content: [{ type: "text", text: `Error: ${message}` }], + isError: true, + } + } + }, +) + +// Tool: get_version +server.registerTool( + "get_version", + { + description: + "Get the XML content of a specific version from history. " + + "Use this to inspect what a previous version looked like before restoring.", + inputSchema: { + version: z.number().describe("Version number to retrieve"), + }, + }, + async ({ version }) => { + try { + if (!currentSession) { + return { + content: [ + { + type: "text", + text: "Error: No active session. Please call start_session first.", + }, + ], + isError: true, + } + } + + const entry = getHistoryVersion(currentSession.id, version) + if (!entry) { + return { + content: [ + { + type: "text", + text: `Error: Version ${version} not found in history.`, + }, + ], + isError: true, + } + } + + const source = entry.source === "ai" ? "AI" : "Human" + const time = entry.timestamp.toISOString() + + return { + content: [ + { + type: "text", + text: `Version ${version} (${source} edit at ${time}):\n\n${entry.xml}`, + }, + ], + } + } catch (error) { + const message = + error instanceof Error ? error.message : String(error) + log.error("get_version failed:", message) + return { + content: [{ type: "text", text: `Error: ${message}` }], + isError: true, + } + } + }, +) + // Start the MCP server async function main() { log.info("Starting MCP server for Next AI Draw.io (embedded mode)...") From b9bc2a72c6faab046d8010ce0b69499e84fb63ba Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Sun, 21 Dec 2025 16:11:49 +0900 Subject: [PATCH 011/349] refactor(mcp): simplify history implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reduce history.ts from 169 to 51 lines - Remove AI tools (list_history, restore_version, get_version) - Remove /api/update-svg endpoint - Remove 10-second history polling - Simplify HistoryEntry to just {xml, svg} - Use array index instead of version numbers Total reduction: 1936 → 923 lines (-52%) --- packages/mcp-server/src/history.ts | 166 +----- packages/mcp-server/src/http-server.ts | 768 +++++-------------------- packages/mcp-server/src/index.ts | 493 +++------------- 3 files changed, 224 insertions(+), 1203 deletions(-) diff --git a/packages/mcp-server/src/history.ts b/packages/mcp-server/src/history.ts index 6070ad70..7f4d0ac9 100644 --- a/packages/mcp-server/src/history.ts +++ b/packages/mcp-server/src/history.ts @@ -1,169 +1,51 @@ /** - * Diagram Version History for MCP Server - * - * Stores diagram versions in-memory per session. - * Enables users and AI to restore previous diagram states. + * Simple diagram history - matches Next.js app pattern + * Stores {xml, svg} entries in a circular buffer */ import { log } from "./logger.js" -export interface HistoryEntry { - version: number - xml: string - svg: string // SVG data for thumbnail preview - source: "ai" | "human" - tool?: string // Which tool made the change (display_diagram, edit_diagram, browser_sync) - timestamp: Date - description?: string -} +const MAX_HISTORY = 20 +const historyStore = new Map>() -interface SessionHistory { - entries: HistoryEntry[] - nextVersion: number -} - -// In-memory history store keyed by session ID -const historyStore = new Map() - -// Configuration -const MAX_HISTORY_ENTRIES = 50 - -/** - * Add a new entry to session history - * Returns the assigned version number - */ -export function addHistoryEntry( - sessionId: string, - entry: Omit, -): number { +export function addHistory(sessionId: string, xml: string, svg = ""): number { let history = historyStore.get(sessionId) if (!history) { - history = { entries: [], nextVersion: 1 } + history = [] historyStore.set(sessionId, history) } - // Deduplicate: skip if XML is identical to last entry - const lastEntry = history.entries[history.entries.length - 1] - if (lastEntry && lastEntry.xml === entry.xml) { - log.debug(`Skipping duplicate history entry for session ${sessionId}`) - return lastEntry.version + // Dedupe: skip if same as last entry + const last = history[history.length - 1] + if (last?.xml === xml) { + return history.length - 1 } - const version = history.nextVersion++ - const newEntry: HistoryEntry = { - ...entry, - version, + history.push({ xml, svg }) + + // Circular buffer + if (history.length > MAX_HISTORY) { + history.shift() } - history.entries.push(newEntry) - - // Prune oldest entries if over limit (circular buffer) - if (history.entries.length > MAX_HISTORY_ENTRIES) { - const removed = history.entries.shift() - log.debug(`Pruned oldest history entry v${removed?.version}`) - } - - log.info( - `Added history v${version} for session ${sessionId} (source: ${entry.source}, entries: ${history.entries.length})`, - ) - - return version + log.debug(`History: session=${sessionId}, entries=${history.length}`) + return history.length - 1 } -/** - * Get history entries for a session - * Returns newest first, limited to specified count - */ export function getHistory( sessionId: string, - limit: number = 20, -): HistoryEntry[] { - const history = historyStore.get(sessionId) - if (!history) { - return [] - } - - // Return newest first - return [...history.entries].reverse().slice(0, limit) +): Array<{ xml: string; svg: string }> { + return historyStore.get(sessionId) || [] } -/** - * Get a specific version from history - */ -export function getVersion( +export function getHistoryEntry( sessionId: string, - version: number, -): HistoryEntry | undefined { + index: number, +): { xml: string; svg: string } | undefined { const history = historyStore.get(sessionId) - if (!history) { - return undefined - } - - return history.entries.find((e) => e.version === version) + return history?.[index] } -/** - * Get the latest version number for a session - */ -export function getLatestVersion(sessionId: string): number { - const history = historyStore.get(sessionId) - if (!history || history.entries.length === 0) { - return 0 - } - return history.entries[history.entries.length - 1].version -} - -/** - * Clear history for a session (used on session expiry) - */ export function clearHistory(sessionId: string): void { - if (historyStore.has(sessionId)) { - historyStore.delete(sessionId) - log.info(`Cleared history for session ${sessionId}`) - } -} - -/** - * Update the SVG of the latest entry (or specific version) that has empty SVG - * Used when browser generates SVG after loading AI diagram - */ -export function updateLatestEntrySvg( - sessionId: string, - svg: string, - targetVersion?: number, -): boolean { - const history = historyStore.get(sessionId) - if (!history || history.entries.length === 0) { - return false - } - - // Find entry to update - either specific version or latest without SVG - let entry: HistoryEntry | undefined - if (targetVersion !== undefined) { - entry = history.entries.find((e) => e.version === targetVersion) - } else { - // Find most recent entry without SVG - for (let i = history.entries.length - 1; i >= 0; i--) { - if (!history.entries[i].svg) { - entry = history.entries[i] - break - } - } - } - - if (entry && !entry.svg) { - entry.svg = svg - log.debug(`Updated SVG for history v${entry.version}`) - return true - } - - return false -} - -/** - * Get count of history entries for a session - */ -export function getHistoryCount(sessionId: string): number { - const history = historyStore.get(sessionId) - return history?.entries.length || 0 + historyStore.delete(sessionId) } diff --git a/packages/mcp-server/src/http-server.ts b/packages/mcp-server/src/http-server.ts index 05a338f1..21d9e31e 100644 --- a/packages/mcp-server/src/http-server.ts +++ b/packages/mcp-server/src/http-server.ts @@ -1,17 +1,14 @@ /** * Embedded HTTP Server for MCP - * - * Serves a static HTML page with draw.io embed and handles state sync. - * This eliminates the need for an external Next.js app. + * Serves draw.io embed with state sync and history UI */ import http from "node:http" import { - addHistoryEntry, + addHistory, clearHistory, getHistory, - getVersion, - updateLatestEntrySvg, + getHistoryEntry, } from "./history.js" import { log } from "./logger.js" @@ -21,42 +18,30 @@ interface SessionState { lastUpdated: Date } -// In-memory state store (shared with MCP server in same process) export const stateStore = new Map() let server: http.Server | null = null -let serverPort: number = 6002 -const MAX_PORT = 6020 // Don't retry beyond this port -const SESSION_TTL = 60 * 60 * 1000 // 1 hour +let serverPort = 6002 +const MAX_PORT = 6020 +const SESSION_TTL = 60 * 60 * 1000 -/** - * Get state for a session - */ export function getState(sessionId: string): SessionState | undefined { return stateStore.get(sessionId) } -/** - * Set state for a session - */ export function setState(sessionId: string, xml: string): number { const existing = stateStore.get(sessionId) const newVersion = (existing?.version || 0) + 1 - stateStore.set(sessionId, { xml, version: newVersion, lastUpdated: new Date(), }) - log.debug(`State updated: session=${sessionId}, version=${newVersion}`) return newVersion } -/** - * Start the embedded HTTP server - */ -export function startHttpServer(port: number = 6002): Promise { +export function startHttpServer(port = 6002): Promise { return new Promise((resolve, reject) => { if (server) { resolve(serverPort) @@ -88,15 +73,12 @@ export function startHttpServer(port: number = 6002): Promise { server.listen(port, () => { serverPort = port - log.info(`Embedded HTTP server running on http://localhost:${port}`) + log.info(`HTTP server running on http://localhost:${port}`) resolve(port) }) }) } -/** - * Stop the HTTP server - */ export function stopHttpServer(): void { if (server) { server.close() @@ -104,40 +86,29 @@ export function stopHttpServer(): void { } } -/** - * Clean up expired sessions - */ function cleanupExpiredSessions(): void { const now = Date.now() for (const [sessionId, state] of stateStore) { if (now - state.lastUpdated.getTime() > SESSION_TTL) { stateStore.delete(sessionId) - clearHistory(sessionId) // Also clean up history + clearHistory(sessionId) log.info(`Cleaned up expired session: ${sessionId}`) } } } -// Run cleanup every 5 minutes setInterval(cleanupExpiredSessions, 5 * 60 * 1000) -/** - * Get the current server port - */ export function getServerPort(): number { return serverPort } -/** - * Handle HTTP requests - */ function handleRequest( req: http.IncomingMessage, res: http.ServerResponse, ): void { const url = new URL(req.url || "/", `http://localhost:${serverPort}`) - // CORS headers for local development res.setHeader("Access-Control-Allow-Origin", "*") res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") res.setHeader("Access-Control-Allow-Headers", "Content-Type") @@ -148,58 +119,21 @@ function handleRequest( return } - // Route handling if (url.pathname === "/" || url.pathname === "/index.html") { - serveHtml(req, res, url) - } else if ( - url.pathname === "/api/state" || - url.pathname === "/api/mcp/state" - ) { + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(getHtmlPage(url.searchParams.get("mcp") || "")) + } else if (url.pathname === "/api/state") { handleStateApi(req, res, url) - } else if ( - url.pathname === "/api/history" || - url.pathname === "/api/mcp/history" - ) { + } else if (url.pathname === "/api/history") { handleHistoryApi(req, res, url) - } else if ( - url.pathname === "/api/restore" || - url.pathname === "/api/mcp/restore" - ) { - handleRestoreApi(req, res, url) - } else if ( - url.pathname === "/api/health" || - url.pathname === "/api/mcp/health" - ) { - res.writeHead(200, { "Content-Type": "application/json" }) - res.end(JSON.stringify({ status: "ok", mcp: true })) - } else if ( - url.pathname === "/api/update-svg" || - url.pathname === "/api/mcp/update-svg" - ) { - handleUpdateSvgApi(req, res) + } else if (url.pathname === "/api/restore") { + handleRestoreApi(req, res) } else { res.writeHead(404) res.end("Not Found") } } -/** - * Serve the HTML page with draw.io embed - */ -function serveHtml( - req: http.IncomingMessage, - res: http.ServerResponse, - url: URL, -): void { - const sessionId = url.searchParams.get("mcp") || "" - - res.writeHead(200, { "Content-Type": "text/html" }) - res.end(getHtmlPage(sessionId)) -} - -/** - * Handle state API requests - */ function handleStateApi( req: http.IncomingMessage, res: http.ServerResponse, @@ -212,14 +146,12 @@ function handleStateApi( res.end(JSON.stringify({ error: "sessionId required" })) return } - const state = stateStore.get(sessionId) res.writeHead(200, { "Content-Type": "application/json" }) res.end( JSON.stringify({ xml: state?.xml || null, version: state?.version || 0, - lastUpdated: state?.lastUpdated?.toISOString() || null, }), ) } else if (req.method === "POST") { @@ -229,28 +161,13 @@ function handleStateApi( }) req.on("end", () => { try { - const { sessionId, xml, svg } = JSON.parse(body) + const { sessionId, xml } = JSON.parse(body) if (!sessionId) { res.writeHead(400, { "Content-Type": "application/json" }) res.end(JSON.stringify({ error: "sessionId required" })) return } - - // Update state const version = setState(sessionId, xml) - - // Save to history when browser sends SVG (human edits) - if (svg) { - addHistoryEntry(sessionId, { - xml, - svg, - source: "human", - tool: "browser_sync", - timestamp: new Date(), - description: "Manual edit", - }) - } - res.writeHead(200, { "Content-Type": "application/json" }) res.end(JSON.stringify({ success: true, version })) } catch { @@ -264,9 +181,6 @@ function handleStateApi( } } -/** - * Handle history API requests - */ function handleHistoryApi( req: http.IncomingMessage, res: http.ServerResponse, @@ -285,71 +199,19 @@ function handleHistoryApi( return } - const limit = parseInt(url.searchParams.get("limit") || "20") - const history = getHistory(sessionId, limit) - + const history = getHistory(sessionId) res.writeHead(200, { "Content-Type": "application/json" }) res.end( JSON.stringify({ - entries: history.map((entry) => ({ - version: entry.version, - source: entry.source, - tool: entry.tool, - timestamp: entry.timestamp.toISOString(), - description: entry.description, - svg: entry.svg, - // Don't include full XML in list - use get_version for that - })), + entries: history.map((entry, i) => ({ index: i, svg: entry.svg })), count: history.length, }), ) } -/** - * Handle update-svg API requests (browser sends SVG after loading AI diagram) - */ -function handleUpdateSvgApi( - req: http.IncomingMessage, - res: http.ServerResponse, -): void { - if (req.method !== "POST") { - res.writeHead(405) - res.end("Method Not Allowed") - return - } - - let body = "" - req.on("data", (chunk) => { - body += chunk - }) - req.on("end", () => { - try { - const { sessionId, svg, version } = JSON.parse(body) - if (!sessionId || !svg) { - res.writeHead(400, { "Content-Type": "application/json" }) - res.end(JSON.stringify({ error: "sessionId and svg required" })) - return - } - - // Update the latest AI entry's SVG - const updated = updateLatestEntrySvg(sessionId, svg, version) - - res.writeHead(200, { "Content-Type": "application/json" }) - res.end(JSON.stringify({ success: true, updated })) - } catch { - res.writeHead(400, { "Content-Type": "application/json" }) - res.end(JSON.stringify({ error: "Invalid JSON" })) - } - }) -} - -/** - * Handle restore API requests - */ function handleRestoreApi( req: http.IncomingMessage, res: http.ServerResponse, - url: URL, ): void { if (req.method !== "POST") { res.writeHead(405) @@ -363,47 +225,29 @@ function handleRestoreApi( }) req.on("end", () => { try { - const { sessionId, version } = JSON.parse(body) - if (!sessionId || version === undefined) { + const { sessionId, index } = JSON.parse(body) + if (!sessionId || index === undefined) { res.writeHead(400, { "Content-Type": "application/json" }) res.end( - JSON.stringify({ error: "sessionId and version required" }), + JSON.stringify({ error: "sessionId and index required" }), ) return } - const entry = getVersion(sessionId, version) + const entry = getHistoryEntry(sessionId, index) if (!entry) { res.writeHead(404, { "Content-Type": "application/json" }) - res.end(JSON.stringify({ error: "Version not found" })) + res.end(JSON.stringify({ error: "Entry not found" })) return } - // Restore by setting state (this will trigger browser poll to load it) const newVersion = setState(sessionId, entry.xml) + addHistory(sessionId, entry.xml, entry.svg) - // Add history entry for the restore action - addHistoryEntry(sessionId, { - xml: entry.xml, - svg: entry.svg, - source: "human", - tool: "restore", - timestamp: new Date(), - description: `Restored from v${version}`, - }) - - log.info( - `Restored session ${sessionId} to v${version}, new version: ${newVersion}`, - ) + log.info(`Restored session ${sessionId} to index ${index}`) res.writeHead(200, { "Content-Type": "application/json" }) - res.end( - JSON.stringify({ - success: true, - restoredFrom: version, - newVersion, - }), - ) + res.end(JSON.stringify({ success: true, newVersion })) } catch { res.writeHead(400, { "Content-Type": "application/json" }) res.end(JSON.stringify({ error: "Invalid JSON" })) @@ -411,192 +255,72 @@ function handleRestoreApi( }) } -/** - * Generate the HTML page with draw.io embed - */ function getHtmlPage(sessionId: string): string { return ` - Draw.io MCP - ${sessionId || "No Session"} + Draw.io MCP @@ -604,381 +328,149 @@ function getHtmlPage(sessionId: string): string {
- - - - -
- ` diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 8f1dcafb..94b05c1f 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -4,23 +4,16 @@ * * Enables AI agents (Claude Desktop, Cursor, etc.) to generate and edit * draw.io diagrams with real-time browser preview. - * - * Uses an embedded HTTP server - no external dependencies required. */ // Setup DOM polyfill for Node.js (required for XML operations) import { DOMParser } from "linkedom" ;(globalThis as any).DOMParser = DOMParser -// Create XMLSerializer polyfill using outerHTML class XMLSerializerPolyfill { serializeToString(node: any): string { - if (node.outerHTML !== undefined) { - return node.outerHTML - } - if (node.documentElement) { - return node.documentElement.outerHTML - } + if (node.outerHTML !== undefined) return node.outerHTML + if (node.documentElement) return node.documentElement.outerHTML return "" } } @@ -34,40 +27,18 @@ import { applyDiagramOperations, type DiagramOperation, } from "./diagram-operations.js" -import { - addHistoryEntry, - getHistory, - getHistoryCount, - getVersion as getHistoryVersion, -} from "./history.js" -import { - getServerPort, - getState, - setState, - startHttpServer, -} from "./http-server.js" +import { addHistory } from "./history.js" +import { getState, setState, startHttpServer } from "./http-server.js" import { log } from "./logger.js" import { validateAndFixXml } from "./xml-validation.js" -// Server configuration -const config = { - port: parseInt(process.env.PORT || "6002"), -} +const config = { port: parseInt(process.env.PORT || "6002") } -// Session state (single session for simplicity) -let currentSession: { - id: string - xml: string - version: number -} | null = null +let currentSession: { id: string; xml: string } | null = null -// Create MCP server -const server = new McpServer({ - name: "next-ai-drawio", - version: "0.1.2", -}) +const server = new McpServer({ name: "next-ai-drawio", version: "0.1.2" }) -// Register prompt with workflow guidance +// Workflow guidance prompt server.prompt( "diagram-workflow", "Guidelines for creating and editing draw.io diagrams", @@ -77,26 +48,22 @@ server.prompt( role: "user", content: { type: "text", - text: `# Draw.io Diagram Workflow Guidelines + text: `# Draw.io Diagram Workflow ## Creating a New Diagram -1. Call start_session to open the browser preview -2. Use display_diagram with complete mxGraphModel XML to create a new diagram +1. Call start_session to open browser preview +2. Use display_diagram with mxGraphModel XML -## Adding Elements to Existing Diagram -1. Use edit_diagram with "add" operation -2. Provide a unique cell_id and complete mxCell XML -3. No need to call get_diagram first - the server fetches latest state automatically +## Adding Elements +Use edit_diagram with "add" operation - provide cell_id and new_xml -## Modifying or Deleting Existing Elements -1. FIRST call get_diagram to see current cell IDs and structure -2. THEN call edit_diagram with "update" or "delete" operations -3. For update, provide the cell_id and complete new mxCell XML +## Modifying/Deleting Elements +1. Call get_diagram to see current cell IDs +2. Use edit_diagram with "update" or "delete" operations -## Important Notes -- display_diagram REPLACES the entire diagram - only use for new diagrams -- edit_diagram PRESERVES user's manual changes (fetches browser state first) -- Always use unique cell_ids when adding elements (e.g., "shape-1", "arrow-2")`, +## Notes +- display_diagram REPLACES entire diagram +- edit_diagram PRESERVES user's manual changes`, }, }, ], @@ -108,25 +75,15 @@ server.registerTool( "start_session", { description: - "Start a new diagram session and open the browser for real-time preview. " + - "Starts an embedded server and opens a browser window with draw.io. " + - "The browser will show diagram updates as they happen.", + "Start a new diagram session and open browser for real-time preview.", inputSchema: {}, }, async () => { try { - // Start embedded HTTP server const port = await startHttpServer(config.port) - - // Create session const sessionId = `mcp-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}` - currentSession = { - id: sessionId, - xml: "", - version: 0, - } + currentSession = { id: sessionId, xml: "" } - // Open browser const browserUrl = `http://localhost:${port}?mcp=${sessionId}` await open(browserUrl) @@ -136,7 +93,7 @@ server.registerTool( content: [ { type: "text", - text: `Session started successfully!\n\nSession ID: ${sessionId}\nBrowser URL: ${browserUrl}\n\nThe browser will now show real-time diagram updates.`, + text: `Session started!\n\nSession ID: ${sessionId}\nBrowser: ${browserUrl}`, }, ], } @@ -158,9 +115,7 @@ server.registerTool( { description: "Display a NEW draw.io diagram from XML. REPLACES the entire diagram. " + - "Use this for creating new diagrams from scratch. " + - "To ADD elements to an existing diagram, use edit_diagram with 'add' operation instead. " + - "You should generate valid draw.io/mxGraph XML format.", + "Use edit_diagram to add elements to existing diagram.", inputSchema: { xml: z .string() @@ -174,14 +129,13 @@ server.registerTool( content: [ { type: "text", - text: "Error: No active session. Please call start_session first.", + text: "Error: No active session. Call start_session first.", }, ], isError: true, } } - // Validate and auto-fix XML let xml = inputXml const { valid, error, fixed, fixes } = validateAndFixXml(xml) if (fixed) { @@ -189,7 +143,6 @@ server.registerTool( log.info(`XML auto-fixed: ${fixes.join(", ")}`) } if (!valid && error) { - log.error(`XML validation failed: ${error}`) return { content: [ { @@ -201,47 +154,24 @@ server.registerTool( } } - log.info(`Displaying diagram, ${xml.length} chars`) - - // 1. Save current state to history BEFORE replacing (preserve user's work) + // Save current state before replacing if (currentSession.xml) { - // Check last entry's source to use correct label - const lastEntry = getHistory(currentSession.id, 1)[0] - const actualSource = lastEntry?.source || "human" - addHistoryEntry(currentSession.id, { - xml: currentSession.xml, - svg: "", - source: actualSource, - tool: "display_diagram", - timestamp: new Date(), - description: "Before AI replaced", - }) + addHistory(currentSession.id, currentSession.xml) } - // Update session state currentSession.xml = xml - currentSession.version++ - - // Push to embedded server state setState(currentSession.id, xml) - // 2. Save new state to history AFTER generation (capture AI result) - addHistoryEntry(currentSession.id, { - xml: xml, - svg: "", - source: "ai", - tool: "display_diagram", - timestamp: new Date(), - description: "AI generated diagram", - }) + // Save new state + addHistory(currentSession.id, xml) - log.info(`Diagram displayed successfully`) + log.info(`Displayed diagram, ${xml.length} chars`) return { content: [ { type: "text", - text: `Diagram displayed successfully!\n\nThe diagram is now visible in your browser.\n\nXML length: ${xml.length} characters`, + text: `Diagram displayed! (${xml.length} chars)`, }, ], } @@ -262,33 +192,22 @@ server.registerTool( "edit_diagram", { description: - "Edit the current diagram by ID-based operations (update/add/delete cells). " + - "ALWAYS fetches the latest state from browser first, so user's manual changes are preserved.\n\n" + - "IMPORTANT workflow:\n" + - "- For ADD operations: Can use directly - just provide new unique cell_id and new_xml.\n" + - "- For UPDATE/DELETE: Call get_diagram FIRST to see current cell IDs, then edit.\n\n" + + "Edit diagram by operations (update/add/delete cells). " + + "Fetches latest browser state first, preserving user's changes.\n\n" + "Operations:\n" + - "- add: Add a new cell. Provide cell_id (new unique id) and new_xml.\n" + - "- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.\n" + - "- delete: Remove a cell by its id. Only cell_id is needed.\n\n" + - "For add/update, new_xml must be a complete mxCell element including mxGeometry.", + "- add: Add new cell (cell_id + new_xml)\n" + + "- update: Replace cell (cell_id + new_xml)\n" + + "- delete: Remove cell (cell_id only)", inputSchema: { operations: z .array( z.object({ - type: z - .enum(["update", "add", "delete"]) - .describe("Operation type"), - cell_id: z.string().describe("The id of the mxCell"), - new_xml: z - .string() - .optional() - .describe( - "Complete mxCell XML element (required for update/add)", - ), + type: z.enum(["update", "add", "delete"]), + cell_id: z.string(), + new_xml: z.string().optional(), }), ) - .describe("Array of operations to apply"), + .describe("Operations to apply"), }, }, async ({ operations }) => { @@ -298,18 +217,17 @@ server.registerTool( content: [ { type: "text", - text: "Error: No active session. Please call start_session first.", + text: "Error: No active session. Call start_session first.", }, ], isError: true, } } - // Fetch latest state from browser + // Fetch latest from browser const browserState = getState(currentSession.id) if (browserState?.xml) { currentSession.xml = browserState.xml - log.info("Fetched latest diagram state from browser") } if (!currentSession.xml) { @@ -317,95 +235,50 @@ server.registerTool( content: [ { type: "text", - text: "Error: No diagram to edit. Please create a diagram first with display_diagram.", + text: "Error: No diagram to edit. Use display_diagram first.", }, ], isError: true, } } - log.info(`Editing diagram with ${operations.length} operation(s)`) + // Save before editing + addHistory(currentSession.id, currentSession.xml) - // 1. Save current state to history BEFORE editing (preserve user's work) - // Check last entry's source to use correct label - const lastEntry = getHistory(currentSession.id, 1)[0] - const actualSource = lastEntry?.source || "human" - addHistoryEntry(currentSession.id, { - xml: currentSession.xml, - svg: "", - source: actualSource, - tool: "edit_diagram", - timestamp: new Date(), - description: "Before AI edit", - }) - - // Validate and auto-fix new_xml for each operation + // Validate operations const validatedOps = operations.map((op) => { if (op.new_xml) { - const { valid, error, fixed, fixes } = validateAndFixXml( - op.new_xml, - ) + const { fixed, fixes } = validateAndFixXml(op.new_xml) if (fixed) { log.info( - `Operation ${op.type} ${op.cell_id}: XML auto-fixed: ${fixes.join(", ")}`, + `${op.type} ${op.cell_id}: auto-fixed: ${fixes.join(", ")}`, ) return { ...op, new_xml: fixed } } - if (!valid && error) { - log.warn( - `Operation ${op.type} ${op.cell_id}: XML validation failed: ${error}`, - ) - } } return op }) - // Apply operations const { result, errors } = applyDiagramOperations( currentSession.xml, validatedOps as DiagramOperation[], ) - if (errors.length > 0) { - const errorMessages = errors - .map((e) => `${e.type} ${e.cellId}: ${e.message}`) - .join("\n") - log.warn(`Edit had ${errors.length} error(s): ${errorMessages}`) - } - - // Update state currentSession.xml = result - currentSession.version++ - - // Push to embedded server setState(currentSession.id, result) - // 2. Save new state to history AFTER editing (capture AI result) - addHistoryEntry(currentSession.id, { - xml: result, - svg: "", - source: "ai", - tool: "edit_diagram", - timestamp: new Date(), - description: `AI edit: ${operations.length} operation(s)`, - }) + // Save after editing + addHistory(currentSession.id, result) - log.info(`Diagram edited successfully`) + log.info(`Edited diagram: ${operations.length} operation(s)`) - const successMsg = `Diagram edited successfully!\n\nApplied ${operations.length} operation(s).` - const errorMsg = + const msg = `Applied ${operations.length} operation(s).` + const warn = errors.length > 0 - ? `\n\nWarnings:\n${errors.map((e) => `- ${e.type} ${e.cellId}: ${e.message}`).join("\n")}` + ? `\nWarnings: ${errors.map((e) => `${e.type} ${e.cellId}: ${e.message}`).join(", ")}` : "" - return { - content: [ - { - type: "text", - text: successMsg + errorMsg, - }, - ], - } + return { content: [{ type: "text", text: msg + warn }] } } catch (error) { const message = error instanceof Error ? error.message : String(error) @@ -423,9 +296,8 @@ server.registerTool( "get_diagram", { description: - "Get the current diagram XML (fetches latest from browser, including user's manual edits). " + - "Call this BEFORE edit_diagram if you need to update or delete existing elements, " + - "so you can see the current cell IDs and structure.", + "Get current diagram XML (fetches latest from browser). " + + "Call before edit_diagram to see cell IDs.", }, async () => { try { @@ -434,14 +306,13 @@ server.registerTool( content: [ { type: "text", - text: "Error: No active session. Please call start_session first.", + text: "Error: No active session. Call start_session first.", }, ], isError: true, } } - // Fetch latest state from browser const browserState = getState(currentSession.id) if (browserState?.xml) { currentSession.xml = browserState.xml @@ -452,7 +323,7 @@ server.registerTool( content: [ { type: "text", - text: "No diagram exists yet. Use display_diagram to create one.", + text: "No diagram yet. Use display_diagram to create one.", }, ], } @@ -462,14 +333,13 @@ server.registerTool( content: [ { type: "text", - text: `Current diagram XML:\n\n${currentSession.xml}`, + text: `Current diagram:\n\n${currentSession.xml}`, }, ], } } catch (error) { const message = error instanceof Error ? error.message : String(error) - log.error("get_diagram failed:", message) return { content: [{ type: "text", text: `Error: ${message}` }], isError: true, @@ -482,13 +352,9 @@ server.registerTool( server.registerTool( "export_diagram", { - description: "Export the current diagram to a .drawio file.", + description: "Export diagram to .drawio file.", inputSchema: { - path: z - .string() - .describe( - "File path to save the diagram (e.g., ./diagram.drawio)", - ), + path: z.string().describe("File path (e.g., ./diagram.drawio)"), }, }, async ({ path }) => { @@ -498,14 +364,13 @@ server.registerTool( content: [ { type: "text", - text: "Error: No active session. Please call start_session first.", + text: "Error: No active session. Call start_session first.", }, ], isError: true, } } - // Fetch latest state const browserState = getState(currentSession.id) if (browserState?.xml) { currentSession.xml = browserState.xml @@ -514,10 +379,7 @@ server.registerTool( if (!currentSession.xml) { return { content: [ - { - type: "text", - text: "Error: No diagram to export. Please create a diagram first.", - }, + { type: "text", text: "Error: No diagram to export." }, ], isError: true, } @@ -527,27 +389,24 @@ server.registerTool( const nodePath = await import("node:path") let filePath = path - if (!filePath.endsWith(".drawio")) { - filePath = `${filePath}.drawio` - } + if (!filePath.endsWith(".drawio")) filePath += ".drawio" const absolutePath = nodePath.resolve(filePath) await fs.writeFile(absolutePath, currentSession.xml, "utf-8") - log.info(`Diagram exported to ${absolutePath}`) + log.info(`Exported to ${absolutePath}`) return { content: [ { type: "text", - text: `Diagram exported successfully!\n\nFile: ${absolutePath}\nSize: ${currentSession.xml.length} characters`, + text: `Exported to ${absolutePath}`, }, ], } } catch (error) { const message = error instanceof Error ? error.message : String(error) - log.error("export_diagram failed:", message) return { content: [{ type: "text", text: `Error: ${message}` }], isError: true, @@ -556,227 +415,15 @@ server.registerTool( }, ) -// Tool: list_history -server.registerTool( - "list_history", - { - description: - "List diagram version history for the current session. " + - "Shows version numbers, who made each change (AI vs human), and timestamps. " + - "Use this to find a version to restore.", - inputSchema: { - limit: z - .number() - .optional() - .describe("Maximum number of entries to return (default: 20)"), - }, - }, - async ({ limit = 20 }) => { - try { - if (!currentSession) { - return { - content: [ - { - type: "text", - text: "Error: No active session. Please call start_session first.", - }, - ], - isError: true, - } - } - - const history = getHistory(currentSession.id, limit) - - if (history.length === 0) { - return { - content: [ - { - type: "text", - text: "No history available yet. Make some changes to create history.", - }, - ], - } - } - - const historyText = history - .map((entry) => { - const time = entry.timestamp.toLocaleTimeString() - const source = entry.source === "ai" ? "AI" : "Human" - const desc = entry.description - ? ` - ${entry.description}` - : "" - return `v${entry.version} [${source}] ${time}${desc}` - }) - .join("\n") - - return { - content: [ - { - type: "text", - text: `Diagram History (${history.length} entries, newest first):\n\n${historyText}\n\nUse restore_version to restore a specific version.`, - }, - ], - } - } catch (error) { - const message = - error instanceof Error ? error.message : String(error) - log.error("list_history failed:", message) - return { - content: [{ type: "text", text: `Error: ${message}` }], - isError: true, - } - } - }, -) - -// Tool: restore_version -server.registerTool( - "restore_version", - { - description: - "Restore diagram to a previous version from history. " + - "Use list_history first to see available versions. " + - "This creates a NEW history entry (non-destructive).", - inputSchema: { - version: z.number().describe("Version number to restore"), - }, - }, - async ({ version }) => { - try { - if (!currentSession) { - return { - content: [ - { - type: "text", - text: "Error: No active session. Please call start_session first.", - }, - ], - isError: true, - } - } - - const entry = getHistoryVersion(currentSession.id, version) - if (!entry) { - return { - content: [ - { - type: "text", - text: `Error: Version ${version} not found in history. Use list_history to see available versions.`, - }, - ], - isError: true, - } - } - - // Restore by updating session and state - currentSession.xml = entry.xml - currentSession.version++ - setState(currentSession.id, entry.xml) - - // Add history entry for the restore - addHistoryEntry(currentSession.id, { - xml: entry.xml, - svg: entry.svg, - source: "ai", - tool: "restore_version", - timestamp: new Date(), - description: `Restored from v${version}`, - }) - - log.info(`Restored diagram to v${version}`) - - return { - content: [ - { - type: "text", - text: `Diagram restored to version ${version} successfully!\n\nThe browser will update automatically.`, - }, - ], - } - } catch (error) { - const message = - error instanceof Error ? error.message : String(error) - log.error("restore_version failed:", message) - return { - content: [{ type: "text", text: `Error: ${message}` }], - isError: true, - } - } - }, -) - -// Tool: get_version -server.registerTool( - "get_version", - { - description: - "Get the XML content of a specific version from history. " + - "Use this to inspect what a previous version looked like before restoring.", - inputSchema: { - version: z.number().describe("Version number to retrieve"), - }, - }, - async ({ version }) => { - try { - if (!currentSession) { - return { - content: [ - { - type: "text", - text: "Error: No active session. Please call start_session first.", - }, - ], - isError: true, - } - } - - const entry = getHistoryVersion(currentSession.id, version) - if (!entry) { - return { - content: [ - { - type: "text", - text: `Error: Version ${version} not found in history.`, - }, - ], - isError: true, - } - } - - const source = entry.source === "ai" ? "AI" : "Human" - const time = entry.timestamp.toISOString() - - return { - content: [ - { - type: "text", - text: `Version ${version} (${source} edit at ${time}):\n\n${entry.xml}`, - }, - ], - } - } catch (error) { - const message = - error instanceof Error ? error.message : String(error) - log.error("get_version failed:", message) - return { - content: [{ type: "text", text: `Error: ${message}` }], - isError: true, - } - } - }, -) - -// Start the MCP server +// Start server async function main() { - log.info("Starting MCP server for Next AI Draw.io (embedded mode)...") - + log.info("Starting MCP server...") const transport = new StdioServerTransport() await server.connect(transport) - - log.info("MCP server running on stdio") + log.info("MCP server running") } main().catch((error) => { - log.error("Fatal error:", error) + log.error("Fatal:", error) process.exit(1) }) From 864375b8e43052fba25223e2779707a5db0da3a8 Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Sun, 21 Dec 2025 16:58:41 +0900 Subject: [PATCH 012/349] fix(mcp): capture SVG for AI-generated diagrams - Sync browser state before saving history in display_diagram - Save AI result to history (in addition to state before) - Add SVG capture after browser loads AI diagrams - Add /api/history-svg endpoint to update last entry's SVG - Add updateLastHistorySvg() function to history module --- .mcp.json | 10 ---- packages/mcp-server/src/history.ts | 11 ++++ packages/mcp-server/src/http-server.ts | 81 +++++++++++++++++++++++--- packages/mcp-server/src/index.ts | 74 ++++++++++++++--------- 4 files changed, 132 insertions(+), 44 deletions(-) delete mode 100644 .mcp.json diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 46e0dd14..00000000 --- a/.mcp.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "mcpServers": { - "drawio": { - "command": "node", - "args": [ - "/Users/jiangdy/Documents/programming/next-ai-draw-io/packages/mcp-server/dist/index.js" - ] - } - } -} diff --git a/packages/mcp-server/src/history.ts b/packages/mcp-server/src/history.ts index 7f4d0ac9..492eda1b 100644 --- a/packages/mcp-server/src/history.ts +++ b/packages/mcp-server/src/history.ts @@ -49,3 +49,14 @@ export function getHistoryEntry( export function clearHistory(sessionId: string): void { historyStore.delete(sessionId) } + +export function updateLastHistorySvg(sessionId: string, svg: string): boolean { + const history = historyStore.get(sessionId) + if (!history || history.length === 0) return false + const last = history[history.length - 1] + if (!last.svg) { + last.svg = svg + return true + } + return false +} diff --git a/packages/mcp-server/src/http-server.ts b/packages/mcp-server/src/http-server.ts index 21d9e31e..27a2e7a8 100644 --- a/packages/mcp-server/src/http-server.ts +++ b/packages/mcp-server/src/http-server.ts @@ -9,6 +9,7 @@ import { clearHistory, getHistory, getHistoryEntry, + updateLastHistorySvg, } from "./history.js" import { log } from "./logger.js" @@ -16,6 +17,7 @@ interface SessionState { xml: string version: number lastUpdated: Date + svg?: string // Cached SVG from last browser save } export const stateStore = new Map() @@ -29,13 +31,14 @@ export function getState(sessionId: string): SessionState | undefined { return stateStore.get(sessionId) } -export function setState(sessionId: string, xml: string): number { +export function setState(sessionId: string, xml: string, svg?: string): number { const existing = stateStore.get(sessionId) const newVersion = (existing?.version || 0) + 1 stateStore.set(sessionId, { xml, version: newVersion, lastUpdated: new Date(), + svg: svg || existing?.svg, // Preserve cached SVG if not provided }) log.debug(`State updated: session=${sessionId}, version=${newVersion}`) return newVersion @@ -128,6 +131,8 @@ function handleRequest( handleHistoryApi(req, res, url) } else if (url.pathname === "/api/restore") { handleRestoreApi(req, res) + } else if (url.pathname === "/api/history-svg") { + handleHistorySvgApi(req, res) } else { res.writeHead(404) res.end("Not Found") @@ -161,13 +166,13 @@ function handleStateApi( }) req.on("end", () => { try { - const { sessionId, xml } = JSON.parse(body) + const { sessionId, xml, svg } = JSON.parse(body) if (!sessionId) { res.writeHead(400, { "Content-Type": "application/json" }) res.end(JSON.stringify({ error: "sessionId required" })) return } - const version = setState(sessionId, xml) + const version = setState(sessionId, xml, svg) res.writeHead(200, { "Content-Type": "application/json" }) res.end(JSON.stringify({ success: true, version })) } catch { @@ -255,6 +260,39 @@ function handleRestoreApi( }) } +function handleHistorySvgApi( + req: http.IncomingMessage, + res: http.ServerResponse, +): void { + if (req.method !== "POST") { + res.writeHead(405) + res.end("Method Not Allowed") + return + } + + let body = "" + req.on("data", (chunk) => { + body += chunk + }) + req.on("end", () => { + try { + const { sessionId, svg } = JSON.parse(body) + if (!sessionId || !svg) { + res.writeHead(400, { "Content-Type": "application/json" }) + res.end(JSON.stringify({ error: "sessionId and svg required" })) + return + } + + updateLastHistorySvg(sessionId, svg) + res.writeHead(200, { "Content-Type": "application/json" }) + res.end(JSON.stringify({ success: true })) + } catch { + res.writeHead(400, { "Content-Type": "application/json" }) + res.end(JSON.stringify({ error: "Invalid JSON" })) + } + }) +} + function getHtmlPage(sessionId: string): string { return ` @@ -358,6 +396,8 @@ function getHtmlPage(sessionId: string): string { const iframe = document.getElementById('drawio'); const statusEl = document.getElementById('status'); let currentVersion = 0, isReady = false, pendingXml = null, lastXml = null; + let pendingSvgExport = null; + let pendingAiSvg = false; window.addEventListener('message', (e) => { if (e.origin !== 'https://embed.diagrams.net') return; @@ -369,24 +409,49 @@ function getHtmlPage(sessionId: string): string { statusEl.className = 'status connected'; if (pendingXml) { loadDiagram(pendingXml); pendingXml = null; } } else if ((msg.event === 'save' || msg.event === 'autosave') && msg.xml && msg.xml !== lastXml) { - pushState(msg.xml); + // Request SVG export, then push state with SVG + pendingSvgExport = msg.xml; + iframe.contentWindow.postMessage(JSON.stringify({ action: 'export', format: 'svg' }), '*'); + // Fallback if export doesn't respond + setTimeout(() => { if (pendingSvgExport === msg.xml) { pushState(msg.xml, ''); pendingSvgExport = null; } }, 2000); + } else if (msg.event === 'export' && msg.data) { + let svg = msg.data; + if (!svg.startsWith('data:')) svg = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg))); + if (pendingSvgExport) { + const xml = pendingSvgExport; + pendingSvgExport = null; + pushState(xml, svg); + } else if (pendingAiSvg) { + pendingAiSvg = false; + fetch('/api/history-svg', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId, svg }) + }).catch(() => {}); + } } } catch {} }); - function loadDiagram(xml) { + function loadDiagram(xml, capturePreview = false) { if (!isReady) { pendingXml = xml; return; } lastXml = xml; iframe.contentWindow.postMessage(JSON.stringify({ action: 'load', xml, autosave: 1 }), '*'); + if (capturePreview) { + setTimeout(() => { + pendingAiSvg = true; + iframe.contentWindow.postMessage(JSON.stringify({ action: 'export', format: 'svg' }), '*'); + }, 500); + } } - async function pushState(xml) { + async function pushState(xml, svg = '') { if (!sessionId) return; try { const r = await fetch('/api/state', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ sessionId, xml }) + body: JSON.stringify({ sessionId, xml, svg }) }); if (r.ok) { const d = await r.json(); currentVersion = d.version; lastXml = xml; } } catch (e) { console.error('Push failed:', e); } @@ -400,7 +465,7 @@ function getHtmlPage(sessionId: string): string { const s = await r.json(); if (s.version > currentVersion && s.xml) { currentVersion = s.version; - loadDiagram(s.xml); + loadDiagram(s.xml, true); } } catch {} } diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 94b05c1f..23c22b7c 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -48,22 +48,26 @@ server.prompt( role: "user", content: { type: "text", - text: `# Draw.io Diagram Workflow + text: `# Draw.io Diagram Workflow Guidelines ## Creating a New Diagram -1. Call start_session to open browser preview -2. Use display_diagram with mxGraphModel XML +1. Call start_session to open the browser preview +2. Use display_diagram with complete mxGraphModel XML to create a new diagram -## Adding Elements -Use edit_diagram with "add" operation - provide cell_id and new_xml +## Adding Elements to Existing Diagram +1. Use edit_diagram with "add" operation +2. Provide a unique cell_id and complete mxCell XML +3. No need to call get_diagram first - the server fetches latest state automatically -## Modifying/Deleting Elements -1. Call get_diagram to see current cell IDs -2. Use edit_diagram with "update" or "delete" operations +## Modifying or Deleting Existing Elements +1. FIRST call get_diagram to see current cell IDs and structure +2. THEN call edit_diagram with "update" or "delete" operations +3. For update, provide the cell_id and complete new mxCell XML -## Notes -- display_diagram REPLACES entire diagram -- edit_diagram PRESERVES user's manual changes`, +## Important Notes +- display_diagram REPLACES the entire diagram - only use for new diagrams +- edit_diagram PRESERVES user's manual changes (fetches browser state first) +- Always use unique cell_ids when adding elements (e.g., "shape-1", "arrow-2")`, }, }, ], @@ -75,7 +79,9 @@ server.registerTool( "start_session", { description: - "Start a new diagram session and open browser for real-time preview.", + "Start a new diagram session and open the browser for real-time preview. " + + "Starts an embedded server and opens a browser window with draw.io. " + + "The browser will show diagram updates as they happen.", inputSchema: {}, }, async () => { @@ -93,7 +99,7 @@ server.registerTool( content: [ { type: "text", - text: `Session started!\n\nSession ID: ${sessionId}\nBrowser: ${browserUrl}`, + text: `Session started successfully!\n\nSession ID: ${sessionId}\nBrowser URL: ${browserUrl}\n\nThe browser will now show real-time diagram updates.`, }, ], } @@ -115,7 +121,9 @@ server.registerTool( { description: "Display a NEW draw.io diagram from XML. REPLACES the entire diagram. " + - "Use edit_diagram to add elements to existing diagram.", + "Use this for creating new diagrams from scratch. " + + "To ADD elements to an existing diagram, use edit_diagram with 'add' operation instead. " + + "You should generate valid draw.io/mxGraph XML format.", inputSchema: { xml: z .string() @@ -129,7 +137,7 @@ server.registerTool( content: [ { type: "text", - text: "Error: No active session. Call start_session first.", + text: "Error: No active session. Please call start_session first.", }, ], isError: true, @@ -154,16 +162,26 @@ server.registerTool( } } - // Save current state before replacing + // Sync from browser state first + const browserState = getState(currentSession.id) + if (browserState?.xml) { + currentSession.xml = browserState.xml + } + + // Save user's state before AI overwrites (with cached SVG) if (currentSession.xml) { - addHistory(currentSession.id, currentSession.xml) + addHistory( + currentSession.id, + currentSession.xml, + browserState?.svg || "", + ) } currentSession.xml = xml setState(currentSession.id, xml) - // Save new state - addHistory(currentSession.id, xml) + // Save AI result (no SVG yet) + addHistory(currentSession.id, xml, "") log.info(`Displayed diagram, ${xml.length} chars`) @@ -217,7 +235,7 @@ server.registerTool( content: [ { type: "text", - text: "Error: No active session. Call start_session first.", + text: "Error: No active session. Please call start_session first.", }, ], isError: true, @@ -242,8 +260,12 @@ server.registerTool( } } - // Save before editing - addHistory(currentSession.id, currentSession.xml) + // Save before editing (with cached SVG from browser) + addHistory( + currentSession.id, + currentSession.xml, + browserState?.svg || "", + ) // Validate operations const validatedOps = operations.map((op) => { @@ -267,8 +289,8 @@ server.registerTool( currentSession.xml = result setState(currentSession.id, result) - // Save after editing - addHistory(currentSession.id, result) + // Save AI result (no SVG yet) + addHistory(currentSession.id, result, "") log.info(`Edited diagram: ${operations.length} operation(s)`) @@ -306,7 +328,7 @@ server.registerTool( content: [ { type: "text", - text: "Error: No active session. Call start_session first.", + text: "Error: No active session. Please call start_session first.", }, ], isError: true, @@ -364,7 +386,7 @@ server.registerTool( content: [ { type: "text", - text: "Error: No active session. Call start_session first.", + text: "Error: No active session. Please call start_session first.", }, ], isError: true, From b97f3ccda9179de704a7a77f7293cf804a853dc2 Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Sun, 21 Dec 2025 17:41:27 +0900 Subject: [PATCH 013/349] fix(mcp): minimal history integration in index.ts Keep only essential history integration: - Import addHistory from history.js - Remove unused getServerPort import - Add browser state sync and history saving in display_diagram - Add history saving in edit_diagram No changes to prompts, descriptions, or code style. --- packages/mcp-server/src/index.ts | 181 +++++++++++++++++++++++-------- 1 file changed, 136 insertions(+), 45 deletions(-) diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 23c22b7c..ffb3912c 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -4,16 +4,23 @@ * * Enables AI agents (Claude Desktop, Cursor, etc.) to generate and edit * draw.io diagrams with real-time browser preview. + * + * Uses an embedded HTTP server - no external dependencies required. */ // Setup DOM polyfill for Node.js (required for XML operations) import { DOMParser } from "linkedom" ;(globalThis as any).DOMParser = DOMParser +// Create XMLSerializer polyfill using outerHTML class XMLSerializerPolyfill { serializeToString(node: any): string { - if (node.outerHTML !== undefined) return node.outerHTML - if (node.documentElement) return node.documentElement.outerHTML + if (node.outerHTML !== undefined) { + return node.outerHTML + } + if (node.documentElement) { + return node.documentElement.outerHTML + } return "" } } @@ -32,13 +39,25 @@ import { getState, setState, startHttpServer } from "./http-server.js" import { log } from "./logger.js" import { validateAndFixXml } from "./xml-validation.js" -const config = { port: parseInt(process.env.PORT || "6002") } +// Server configuration +const config = { + port: parseInt(process.env.PORT || "6002"), +} -let currentSession: { id: string; xml: string } | null = null +// Session state (single session for simplicity) +let currentSession: { + id: string + xml: string + version: number +} | null = null -const server = new McpServer({ name: "next-ai-drawio", version: "0.1.2" }) +// Create MCP server +const server = new McpServer({ + name: "next-ai-drawio", + version: "0.1.2", +}) -// Workflow guidance prompt +// Register prompt with workflow guidance server.prompt( "diagram-workflow", "Guidelines for creating and editing draw.io diagrams", @@ -86,10 +105,18 @@ server.registerTool( }, async () => { try { + // Start embedded HTTP server const port = await startHttpServer(config.port) - const sessionId = `mcp-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}` - currentSession = { id: sessionId, xml: "" } + // Create session + const sessionId = `mcp-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}` + currentSession = { + id: sessionId, + xml: "", + version: 0, + } + + // Open browser const browserUrl = `http://localhost:${port}?mcp=${sessionId}` await open(browserUrl) @@ -144,6 +171,7 @@ server.registerTool( } } + // Validate and auto-fix XML let xml = inputXml const { valid, error, fixed, fixes } = validateAndFixXml(xml) if (fixed) { @@ -151,6 +179,7 @@ server.registerTool( log.info(`XML auto-fixed: ${fixes.join(", ")}`) } if (!valid && error) { + log.error(`XML validation failed: ${error}`) return { content: [ { @@ -162,6 +191,8 @@ server.registerTool( } } + log.info(`Displaying diagram, ${xml.length} chars`) + // Sync from browser state first const browserState = getState(currentSession.id) if (browserState?.xml) { @@ -177,19 +208,23 @@ server.registerTool( ) } + // Update session state currentSession.xml = xml + currentSession.version++ + + // Push to embedded server state setState(currentSession.id, xml) - // Save AI result (no SVG yet) + // Save AI result (no SVG yet - will be captured by browser) addHistory(currentSession.id, xml, "") - log.info(`Displayed diagram, ${xml.length} chars`) + log.info(`Diagram displayed successfully`) return { content: [ { type: "text", - text: `Diagram displayed! (${xml.length} chars)`, + text: `Diagram displayed successfully!\n\nThe diagram is now visible in your browser.\n\nXML length: ${xml.length} characters`, }, ], } @@ -210,22 +245,33 @@ server.registerTool( "edit_diagram", { description: - "Edit diagram by operations (update/add/delete cells). " + - "Fetches latest browser state first, preserving user's changes.\n\n" + + "Edit the current diagram by ID-based operations (update/add/delete cells). " + + "ALWAYS fetches the latest state from browser first, so user's manual changes are preserved.\n\n" + + "IMPORTANT workflow:\n" + + "- For ADD operations: Can use directly - just provide new unique cell_id and new_xml.\n" + + "- For UPDATE/DELETE: Call get_diagram FIRST to see current cell IDs, then edit.\n\n" + "Operations:\n" + - "- add: Add new cell (cell_id + new_xml)\n" + - "- update: Replace cell (cell_id + new_xml)\n" + - "- delete: Remove cell (cell_id only)", + "- add: Add a new cell. Provide cell_id (new unique id) and new_xml.\n" + + "- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.\n" + + "- delete: Remove a cell by its id. Only cell_id is needed.\n\n" + + "For add/update, new_xml must be a complete mxCell element including mxGeometry.", inputSchema: { operations: z .array( z.object({ - type: z.enum(["update", "add", "delete"]), - cell_id: z.string(), - new_xml: z.string().optional(), + type: z + .enum(["update", "add", "delete"]) + .describe("Operation type"), + cell_id: z.string().describe("The id of the mxCell"), + new_xml: z + .string() + .optional() + .describe( + "Complete mxCell XML element (required for update/add)", + ), }), ) - .describe("Operations to apply"), + .describe("Array of operations to apply"), }, }, async ({ operations }) => { @@ -242,10 +288,11 @@ server.registerTool( } } - // Fetch latest from browser + // Fetch latest state from browser const browserState = getState(currentSession.id) if (browserState?.xml) { currentSession.xml = browserState.xml + log.info("Fetched latest diagram state from browser") } if (!currentSession.xml) { @@ -253,13 +300,15 @@ server.registerTool( content: [ { type: "text", - text: "Error: No diagram to edit. Use display_diagram first.", + text: "Error: No diagram to edit. Please create a diagram first with display_diagram.", }, ], isError: true, } } + log.info(`Editing diagram with ${operations.length} operation(s)`) + // Save before editing (with cached SVG from browser) addHistory( currentSession.id, @@ -267,40 +316,66 @@ server.registerTool( browserState?.svg || "", ) - // Validate operations + // Validate and auto-fix new_xml for each operation const validatedOps = operations.map((op) => { if (op.new_xml) { - const { fixed, fixes } = validateAndFixXml(op.new_xml) + const { valid, error, fixed, fixes } = validateAndFixXml( + op.new_xml, + ) if (fixed) { log.info( - `${op.type} ${op.cell_id}: auto-fixed: ${fixes.join(", ")}`, + `Operation ${op.type} ${op.cell_id}: XML auto-fixed: ${fixes.join(", ")}`, ) return { ...op, new_xml: fixed } } + if (!valid && error) { + log.warn( + `Operation ${op.type} ${op.cell_id}: XML validation failed: ${error}`, + ) + } } return op }) + // Apply operations const { result, errors } = applyDiagramOperations( currentSession.xml, validatedOps as DiagramOperation[], ) + if (errors.length > 0) { + const errorMessages = errors + .map((e) => `${e.type} ${e.cellId}: ${e.message}`) + .join("\n") + log.warn(`Edit had ${errors.length} error(s): ${errorMessages}`) + } + + // Update state currentSession.xml = result + currentSession.version++ + + // Push to embedded server setState(currentSession.id, result) - // Save AI result (no SVG yet) + // Save AI result (no SVG yet - will be captured by browser) addHistory(currentSession.id, result, "") - log.info(`Edited diagram: ${operations.length} operation(s)`) + log.info(`Diagram edited successfully`) - const msg = `Applied ${operations.length} operation(s).` - const warn = + const successMsg = `Diagram edited successfully!\n\nApplied ${operations.length} operation(s).` + const errorMsg = errors.length > 0 - ? `\nWarnings: ${errors.map((e) => `${e.type} ${e.cellId}: ${e.message}`).join(", ")}` + ? `\n\nWarnings:\n${errors.map((e) => `- ${e.type} ${e.cellId}: ${e.message}`).join("\n")}` : "" - return { content: [{ type: "text", text: msg + warn }] } + return { + content: [ + { + type: "text", + text: successMsg + errorMsg, + }, + ], + } } catch (error) { const message = error instanceof Error ? error.message : String(error) @@ -318,8 +393,9 @@ server.registerTool( "get_diagram", { description: - "Get current diagram XML (fetches latest from browser). " + - "Call before edit_diagram to see cell IDs.", + "Get the current diagram XML (fetches latest from browser, including user's manual edits). " + + "Call this BEFORE edit_diagram if you need to update or delete existing elements, " + + "so you can see the current cell IDs and structure.", }, async () => { try { @@ -335,6 +411,7 @@ server.registerTool( } } + // Fetch latest state from browser const browserState = getState(currentSession.id) if (browserState?.xml) { currentSession.xml = browserState.xml @@ -345,7 +422,7 @@ server.registerTool( content: [ { type: "text", - text: "No diagram yet. Use display_diagram to create one.", + text: "No diagram exists yet. Use display_diagram to create one.", }, ], } @@ -355,13 +432,14 @@ server.registerTool( content: [ { type: "text", - text: `Current diagram:\n\n${currentSession.xml}`, + text: `Current diagram XML:\n\n${currentSession.xml}`, }, ], } } catch (error) { const message = error instanceof Error ? error.message : String(error) + log.error("get_diagram failed:", message) return { content: [{ type: "text", text: `Error: ${message}` }], isError: true, @@ -374,9 +452,13 @@ server.registerTool( server.registerTool( "export_diagram", { - description: "Export diagram to .drawio file.", + description: "Export the current diagram to a .drawio file.", inputSchema: { - path: z.string().describe("File path (e.g., ./diagram.drawio)"), + path: z + .string() + .describe( + "File path to save the diagram (e.g., ./diagram.drawio)", + ), }, }, async ({ path }) => { @@ -393,6 +475,7 @@ server.registerTool( } } + // Fetch latest state const browserState = getState(currentSession.id) if (browserState?.xml) { currentSession.xml = browserState.xml @@ -401,7 +484,10 @@ server.registerTool( if (!currentSession.xml) { return { content: [ - { type: "text", text: "Error: No diagram to export." }, + { + type: "text", + text: "Error: No diagram to export. Please create a diagram first.", + }, ], isError: true, } @@ -411,24 +497,27 @@ server.registerTool( const nodePath = await import("node:path") let filePath = path - if (!filePath.endsWith(".drawio")) filePath += ".drawio" + if (!filePath.endsWith(".drawio")) { + filePath = `${filePath}.drawio` + } const absolutePath = nodePath.resolve(filePath) await fs.writeFile(absolutePath, currentSession.xml, "utf-8") - log.info(`Exported to ${absolutePath}`) + log.info(`Diagram exported to ${absolutePath}`) return { content: [ { type: "text", - text: `Exported to ${absolutePath}`, + text: `Diagram exported successfully!\n\nFile: ${absolutePath}\nSize: ${currentSession.xml.length} characters`, }, ], } } catch (error) { const message = error instanceof Error ? error.message : String(error) + log.error("export_diagram failed:", message) return { content: [{ type: "text", text: `Error: ${message}` }], isError: true, @@ -437,15 +526,17 @@ server.registerTool( }, ) -// Start server +// Start the MCP server async function main() { - log.info("Starting MCP server...") + log.info("Starting MCP server for Next AI Draw.io (embedded mode)...") + const transport = new StdioServerTransport() await server.connect(transport) - log.info("MCP server running") + + log.info("MCP server running on stdio") } main().catch((error) => { - log.error("Fatal:", error) + log.error("Fatal error:", error) process.exit(1) }) From 9d13bd74510cafdd67fdfe498334f7040bf47472 Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Sun, 21 Dec 2025 18:05:44 +0900 Subject: [PATCH 014/349] chore(mcp): bump version to 0.1.4 --- packages/mcp-server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 255d8837..039ce160 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@next-ai-drawio/mcp-server", - "version": "0.1.3", + "version": "0.1.4", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview", "type": "module", "main": "dist/index.js", From 287126536261d132e4184887e8593cef0b853ef4 Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Sun, 21 Dec 2025 18:07:29 +0900 Subject: [PATCH 015/349] docs(mcp): add version history feature to README --- packages/mcp-server/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 89bea94c..12701f90 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -86,6 +86,7 @@ Use the standard MCP configuration with: ## Features - **Real-time Preview**: Diagrams appear and update in your browser as the AI creates them +- **Version History**: Restore previous diagram versions with visual thumbnails - click the clock button (bottom-right) to browse and restore earlier states - **Natural Language**: Describe diagrams in plain text - flowcharts, architecture diagrams, etc. - **Edit Support**: Modify existing diagrams with natural language instructions - **Export**: Save diagrams as `.drawio` files From 14c1aa8e1c6c5117cfdaa5192c11b7c950feecc1 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Sun, 21 Dec 2025 19:38:35 +0900 Subject: [PATCH 016/349] fix(mcp): sync browser state before get_diagram to prevent data loss (#342) * fix(mcp): sync browser state before get_diagram to prevent data loss - Add syncRequested flag to SessionState for browser sync coordination - Add requestSync() and waitForSync() functions to http-server - Browser polls for syncRequested flag and immediately pushes current state - get_diagram now syncs fresh state from browser before returning - edit_diagram requires get_diagram to be called within 30s to prevent stale edits - Updated edit_diagram description to enforce workflow * fix(mcp): make lastGetDiagramTime session-scoped and handle missing session in requestSync - Move lastGetDiagramTime into currentSession object to prevent cross-session issues - requestSync now returns boolean indicating if request was made - Only wait for sync if session exists (avoids false-positive from undefined state) --- packages/mcp-server/src/http-server.ts | 43 ++++++++++++++++++++ packages/mcp-server/src/index.ts | 56 +++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/packages/mcp-server/src/http-server.ts b/packages/mcp-server/src/http-server.ts index 27a2e7a8..95ff6b2f 100644 --- a/packages/mcp-server/src/http-server.ts +++ b/packages/mcp-server/src/http-server.ts @@ -18,6 +18,7 @@ interface SessionState { version: number lastUpdated: Date svg?: string // Cached SVG from last browser save + syncRequested?: number // Timestamp when sync requested, cleared when browser responds } export const stateStore = new Map() @@ -39,11 +40,37 @@ export function setState(sessionId: string, xml: string, svg?: string): number { version: newVersion, lastUpdated: new Date(), svg: svg || existing?.svg, // Preserve cached SVG if not provided + syncRequested: undefined, // Clear sync request when browser pushes state }) log.debug(`State updated: session=${sessionId}, version=${newVersion}`) return newVersion } +export function requestSync(sessionId: string): boolean { + const state = stateStore.get(sessionId) + if (state) { + state.syncRequested = Date.now() + log.debug(`Sync requested for session=${sessionId}`) + return true + } + log.debug(`Sync requested for non-existent session=${sessionId}`) + return false +} + +export async function waitForSync( + sessionId: string, + timeoutMs = 3000, +): Promise { + const start = Date.now() + while (Date.now() - start < timeoutMs) { + const state = stateStore.get(sessionId) + if (!state?.syncRequested) return true // Sync completed + await new Promise((r) => setTimeout(r, 100)) + } + log.warn(`Sync timeout for session=${sessionId}`) + return false // Timeout +} + export function startHttpServer(port = 6002): Promise { return new Promise((resolve, reject) => { if (server) { @@ -157,6 +184,7 @@ function handleStateApi( JSON.stringify({ xml: state?.xml || null, version: state?.version || 0, + syncRequested: !!state?.syncRequested, }), ) } else if (req.method === "POST") { @@ -415,6 +443,13 @@ function getHtmlPage(sessionId: string): string { // Fallback if export doesn't respond setTimeout(() => { if (pendingSvgExport === msg.xml) { pushState(msg.xml, ''); pendingSvgExport = null; } }, 2000); } else if (msg.event === 'export' && msg.data) { + // Handle sync export (XML format) - server requested fresh state + if (pendingSyncExport && !msg.data.startsWith('data:') && !msg.data.startsWith(' currentVersion && s.xml) { currentVersion = s.version; loadDiagram(s.xml, true); diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index ffb3912c..e2af6738 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -35,7 +35,13 @@ import { type DiagramOperation, } from "./diagram-operations.js" import { addHistory } from "./history.js" -import { getState, setState, startHttpServer } from "./http-server.js" +import { + getState, + requestSync, + setState, + startHttpServer, + waitForSync, +} from "./http-server.js" import { log } from "./logger.js" import { validateAndFixXml } from "./xml-validation.js" @@ -49,6 +55,7 @@ let currentSession: { id: string xml: string version: number + lastGetDiagramTime: number // Track when get_diagram was last called (for enforcing workflow) } | null = null // Create MCP server @@ -114,6 +121,7 @@ server.registerTool( id: sessionId, xml: "", version: 0, + lastGetDiagramTime: 0, } // Open browser @@ -245,11 +253,14 @@ server.registerTool( "edit_diagram", { description: - "Edit the current diagram by ID-based operations (update/add/delete cells). " + - "ALWAYS fetches the latest state from browser first, so user's manual changes are preserved.\n\n" + - "IMPORTANT workflow:\n" + - "- For ADD operations: Can use directly - just provide new unique cell_id and new_xml.\n" + - "- For UPDATE/DELETE: Call get_diagram FIRST to see current cell IDs, then edit.\n\n" + + "Edit the current diagram by ID-based operations (update/add/delete cells).\n\n" + + "⚠️ REQUIRED: You MUST call get_diagram BEFORE this tool!\n" + + "This fetches the latest state from the browser including any manual user edits.\n" + + "Skipping get_diagram WILL cause user's changes to be LOST.\n\n" + + "Workflow:\n" + + "1. Call get_diagram to see current cell IDs and structure\n" + + "2. Use the returned XML to construct your edit operations\n" + + "3. Call edit_diagram with your operations\n\n" + "Operations:\n" + "- add: Add a new cell. Provide cell_id (new unique id) and new_xml.\n" + "- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.\n" + @@ -288,6 +299,27 @@ server.registerTool( } } + // Enforce workflow: require get_diagram to be called first + const timeSinceGet = Date.now() - currentSession.lastGetDiagramTime + if (timeSinceGet > 30000) { + // 30 seconds + log.warn( + "edit_diagram called without recent get_diagram - rejecting to prevent data loss", + ) + return { + content: [ + { + type: "text", + text: + "Error: You must call get_diagram first before edit_diagram.\n\n" + + "This ensures you have the latest diagram state including any manual edits the user made in the browser. " + + "Please call get_diagram, then use that XML to construct your edit operations.", + }, + ], + isError: true, + } + } + // Fetch latest state from browser const browserState = getState(currentSession.id) if (browserState?.xml) { @@ -411,6 +443,18 @@ server.registerTool( } } + // Request browser to push fresh state and wait for it + const syncRequested = requestSync(currentSession.id) + if (syncRequested) { + const synced = await waitForSync(currentSession.id) + if (!synced) { + log.warn("get_diagram: sync timeout - state may be stale") + } + } + + // Mark that get_diagram was called (for edit_diagram workflow check) + currentSession.lastGetDiagramTime = Date.now() + // Fetch latest state from browser const browserState = getState(currentSession.id) if (browserState?.xml) { From e03b65328dcdd035aed51d63547faa06ab15f85f Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Sun, 21 Dec 2025 19:44:01 +0900 Subject: [PATCH 017/349] chore(mcp): bump version to 0.1.5 (#343) --- packages/mcp-server/package-lock.json | 4 ++-- packages/mcp-server/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mcp-server/package-lock.json b/packages/mcp-server/package-lock.json index a8d57f33..55aa35d0 100644 --- a/packages/mcp-server/package-lock.json +++ b/packages/mcp-server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@next-ai-drawio/mcp-server", - "version": "0.1.3", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@next-ai-drawio/mcp-server", - "version": "0.1.3", + "version": "0.1.5", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4", diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 039ce160..9825de39 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@next-ai-drawio/mcp-server", - "version": "0.1.4", + "version": "0.1.5", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview", "type": "module", "main": "dist/index.js", From f58274bb8482f05d217dfc07599931e73147a766 Mon Sep 17 00:00:00 2001 From: chouheiwa <849131492@qq.com> Date: Mon, 22 Dec 2025 09:18:21 +0800 Subject: [PATCH 018/349] feat(electron): add desktop application support with electron (#344) * feat(electron): add desktop application support with electron - implement complete Electron main process architecture with window management, app menu, IPC handlers, and settings window - integrate Next.js server for production builds with embedded standalone server - add configuration management with persistent storage and env file support - create preload scripts with secure context bridge for renderer communication - set up electron-builder configuration for multi-platform packaging (macOS, Windows, Linux) - add GitHub Actions workflow for automated release builds - include development scripts for hot-reload during Electron development * feat(electron): enhance security and stability - encrypt API keys using Electron safeStorage API before persisting to disk - add error handling and rollback for preset switching failures - extract inline styles to external CSS file and remove unsafe-inline from CSP - implement dynamic port allocation with automatic fallback for production builds * fix(electron): add maintainer field for Linux .deb package - add maintainer email to linux configuration in electron-builder.yml - required for building .deb packages * fix(electron): use shx for cross-platform file copying - replace Unix-only cp -r with npx shx cp -r - add shx as devDependency for Windows compatibility * fix(electron): fix runtime icon path for all platforms - use icon.png directly instead of platform-specific formats - electron-builder handles icon conversion during packaging - macOS uses embedded icon from app bundle, no explicit path needed - add icon.png to extraResources for Windows/Linux runtime access * fix(electron): add security warning for plaintext API key storage - warn user when safeStorage is unavailable (Linux without keyring) - fail secure: throw error if encryption fails instead of storing plaintext - prevent duplicate warnings with hasWarnedAboutPlaintext flag * fix(electron): add remaining review fixes - Add Windows ARM64 architecture support - Add IPC input validation with config key whitelist - Add server.js existence check before starting Next.js server - Make afterPack throw error on missing directories - Add workflow permissions for release job --------- Co-authored-by: dayuan.jiang --- .github/workflows/electron-release.yml | 100 + .gitignore | 13 + electron-builder.yml | 96 + electron.d.ts | 74 + electron/main/app-menu.ts | 241 ++ electron/main/config-manager.ts | 460 +++ electron/main/env-loader.ts | 67 + electron/main/index.ts | 105 + electron/main/ipc-handlers.ts | 212 + electron/main/next-server.ts | 161 + electron/main/port-manager.ts | 129 + electron/main/settings-window.ts | 78 + electron/main/window-manager.ts | 84 + electron/preload/index.ts | 24 + electron/preload/settings.ts | 35 + electron/settings/index.html | 110 + electron/settings/settings.css | 311 ++ electron/settings/settings.js | 311 ++ electron/tsconfig.json | 18 + package-lock.json | 5193 +++++++++++++++++++++++- package.json | 22 +- resources/entitlements.mac.plist | 16 + resources/icon.png | Bin 0 -> 55260 bytes scripts/afterPack.cjs | 39 + scripts/electron-dev.mjs | 275 ++ scripts/prepare-electron-build.mjs | 41 + tsconfig.json | 2 +- 27 files changed, 8213 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/electron-release.yml create mode 100644 electron-builder.yml create mode 100644 electron.d.ts create mode 100644 electron/main/app-menu.ts create mode 100644 electron/main/config-manager.ts create mode 100644 electron/main/env-loader.ts create mode 100644 electron/main/index.ts create mode 100644 electron/main/ipc-handlers.ts create mode 100644 electron/main/next-server.ts create mode 100644 electron/main/port-manager.ts create mode 100644 electron/main/settings-window.ts create mode 100644 electron/main/window-manager.ts create mode 100644 electron/preload/index.ts create mode 100644 electron/preload/settings.ts create mode 100644 electron/settings/index.html create mode 100644 electron/settings/settings.css create mode 100644 electron/settings/settings.js create mode 100644 electron/tsconfig.json create mode 100644 resources/entitlements.mac.plist create mode 100644 resources/icon.png create mode 100644 scripts/afterPack.cjs create mode 100644 scripts/electron-dev.mjs create mode 100644 scripts/prepare-electron-build.mjs diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml new file mode 100644 index 00000000..dff69d2e --- /dev/null +++ b/.github/workflows/electron-release.yml @@ -0,0 +1,100 @@ +name: Electron Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + version: + description: "Version tag (e.g., v0.4.5)" + required: false + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + platform: mac + - os: windows-latest + platform: win + - os: ubuntu-latest + platform: linux + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build Electron app + run: npm run dist:${{ matrix.platform }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload macOS artifacts + if: matrix.platform == 'mac' + uses: actions/upload-artifact@v4 + with: + name: mac-build + path: | + release/*.dmg + release/*.zip + if-no-files-found: error + + - name: Upload Windows artifacts + if: matrix.platform == 'win' + uses: actions/upload-artifact@v4 + with: + name: win-build + path: | + release/*.exe + if-no-files-found: error + + - name: Upload Linux artifacts + if: matrix.platform == 'linux' + uses: actions/upload-artifact@v4 + with: + name: linux-build + path: | + release/*.AppImage + release/*.deb + if-no-files-found: error + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + if: startsWith(github.ref, 'refs/tags/') + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + artifacts/**/*.dmg + artifacts/**/*.zip + artifacts/**/*.exe + artifacts/**/*.AppImage + artifacts/**/*.deb + draft: true + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 28fd6acd..c4eab071 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,16 @@ push-via-ec2.sh .wrangler/ .env*.local +# Electron +/dist-electron/ +/release/ +/electron-standalone/ +*.dmg +*.exe +*.AppImage +*.deb +*.rpm +*.snap + +CLAUDE.md +.spec-workflow \ No newline at end of file diff --git a/electron-builder.yml b/electron-builder.yml new file mode 100644 index 00000000..616424ce --- /dev/null +++ b/electron-builder.yml @@ -0,0 +1,96 @@ +appId: com.nextaidrawio.app +productName: Next AI Draw.io +copyright: Copyright © 2024 Next AI Draw.io +electronVersion: 39.2.7 + +directories: + output: release + buildResources: resources + +afterPack: ./scripts/afterPack.cjs + +files: + - dist-electron/**/* + - "!node_modules" + +asarUnpack: + - "**/*.node" + +extraResources: + # Copy prepared standalone directory (includes node_modules) + - from: electron-standalone/ + to: standalone/ + # Copy icon for runtime use (Windows/Linux) + - from: resources/icon.png + to: icon.png + +# macOS configuration +mac: + category: public.app-category.productivity + icon: resources/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + hardenedRuntime: true + gatekeeperAssess: false + entitlements: resources/entitlements.mac.plist + entitlementsInherit: resources/entitlements.mac.plist + +dmg: + contents: + - x: 130 + y: 220 + - x: 410 + y: 220 + type: link + path: /Applications + window: + width: 540 + height: 380 + +# Windows configuration +win: + icon: resources/icon.png + target: + - target: nsis + arch: + - x64 + - arm64 + - target: portable + arch: + - x64 + - arm64 + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + deleteAppDataOnUninstall: false + createDesktopShortcut: true + createStartMenuShortcut: true + +# Linux configuration +linux: + icon: resources/icon.png + category: Office + maintainer: Next AI Draw.io + target: + - target: AppImage + arch: + - x64 + - arm64 + - target: deb + arch: + - x64 + - arm64 + +# Publish configuration (optional) +publish: + provider: github + releaseType: release diff --git a/electron.d.ts b/electron.d.ts new file mode 100644 index 00000000..a629e9b0 --- /dev/null +++ b/electron.d.ts @@ -0,0 +1,74 @@ +/** + * Type declarations for Electron API exposed via preload script + */ + +/** Configuration preset interface */ +interface ConfigPreset { + id: string + name: string + createdAt: number + updatedAt: number + config: { + AI_PROVIDER?: string + AI_MODEL?: string + AI_API_KEY?: string + AI_BASE_URL?: string + TEMPERATURE?: string + [key: string]: string | undefined + } +} + +/** Result of applying a preset */ +interface ApplyPresetResult { + success: boolean + error?: string + env?: Record +} + +declare global { + interface Window { + /** Main window Electron API */ + electronAPI?: { + /** Current platform (darwin, win32, linux) */ + platform: NodeJS.Platform + /** Whether running in Electron environment */ + isElectron: boolean + /** Get application version */ + getVersion: () => Promise + /** Minimize the window */ + minimize: () => void + /** Maximize/restore the window */ + maximize: () => void + /** Close the window */ + close: () => void + /** Open file dialog and return file path */ + openFile: () => Promise + /** Save data to file via save dialog */ + saveFile: (data: string) => Promise + } + + /** Settings window Electron API */ + settingsAPI?: { + /** Get all configuration presets */ + getPresets: () => Promise + /** Get current preset ID */ + getCurrentPresetId: () => Promise + /** Get current preset */ + getCurrentPreset: () => Promise + /** Save (create or update) a preset */ + savePreset: (preset: { + id?: string + name: string + config: Record + }) => Promise + /** Delete a preset */ + deletePreset: (id: string) => Promise + /** Apply a preset (sets environment variables and restarts server) */ + applyPreset: (id: string) => Promise + /** Close settings window */ + close: () => void + } + } +} + +export { ConfigPreset, ApplyPresetResult } diff --git a/electron/main/app-menu.ts b/electron/main/app-menu.ts new file mode 100644 index 00000000..4a99a85f --- /dev/null +++ b/electron/main/app-menu.ts @@ -0,0 +1,241 @@ +import { + app, + BrowserWindow, + dialog, + Menu, + type MenuItemConstructorOptions, + shell, +} from "electron" +import { + applyPresetToEnv, + getAllPresets, + getCurrentPresetId, + setCurrentPreset, +} from "./config-manager" +import { restartNextServer } from "./next-server" +import { showSettingsWindow } from "./settings-window" + +/** + * Build and set the application menu + */ +export function buildAppMenu(): void { + const template = getMenuTemplate() + const menu = Menu.buildFromTemplate(template) + Menu.setApplicationMenu(menu) +} + +/** + * Rebuild the menu (call this when presets change) + */ +export function rebuildAppMenu(): void { + buildAppMenu() +} + +/** + * Get the menu template + */ +function getMenuTemplate(): MenuItemConstructorOptions[] { + const isMac = process.platform === "darwin" + + const template: MenuItemConstructorOptions[] = [] + + // macOS app menu + if (isMac) { + template.push({ + label: app.name, + submenu: [ + { role: "about" }, + { type: "separator" }, + { + label: "Settings...", + accelerator: "CmdOrCtrl+,", + click: () => { + const win = BrowserWindow.getFocusedWindow() + showSettingsWindow(win || undefined) + }, + }, + { type: "separator" }, + { role: "services" }, + { type: "separator" }, + { role: "hide" }, + { role: "hideOthers" }, + { role: "unhide" }, + { type: "separator" }, + { role: "quit" }, + ], + }) + } + + // File menu + template.push({ + label: "File", + submenu: [ + ...(isMac + ? [] + : [ + { + label: "Settings", + accelerator: "CmdOrCtrl+,", + click: () => { + const win = BrowserWindow.getFocusedWindow() + showSettingsWindow(win || undefined) + }, + }, + { type: "separator" } as MenuItemConstructorOptions, + ]), + isMac ? { role: "close" } : { role: "quit" }, + ], + }) + + // Edit menu + template.push({ + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + ...(isMac + ? [ + { + role: "pasteAndMatchStyle", + } as MenuItemConstructorOptions, + { role: "delete" } as MenuItemConstructorOptions, + { role: "selectAll" } as MenuItemConstructorOptions, + ] + : [ + { role: "delete" } as MenuItemConstructorOptions, + { type: "separator" } as MenuItemConstructorOptions, + { role: "selectAll" } as MenuItemConstructorOptions, + ]), + ], + }) + + // View menu + template.push({ + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }) + + // Configuration menu with presets + template.push(buildConfigMenu()) + + // Window menu + template.push({ + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" } as MenuItemConstructorOptions, + { role: "front" } as MenuItemConstructorOptions, + ] + : [{ role: "close" } as MenuItemConstructorOptions]), + ], + }) + + // Help menu + template.push({ + label: "Help", + submenu: [ + { + label: "Documentation", + click: async () => { + await shell.openExternal( + "https://github.com/dayuanjiang/next-ai-draw-io", + ) + }, + }, + { + label: "Report Issue", + click: async () => { + await shell.openExternal( + "https://github.com/dayuanjiang/next-ai-draw-io/issues", + ) + }, + }, + ], + }) + + return template +} + +/** + * Build the Configuration menu with presets + */ +function buildConfigMenu(): MenuItemConstructorOptions { + const presets = getAllPresets() + const currentPresetId = getCurrentPresetId() + + const presetItems: MenuItemConstructorOptions[] = presets.map((preset) => ({ + label: preset.name, + type: "radio", + checked: preset.id === currentPresetId, + click: async () => { + const previousPresetId = getCurrentPresetId() + const env = applyPresetToEnv(preset.id) + + if (env) { + try { + await restartNextServer() + rebuildAppMenu() // Rebuild menu to update checkmarks + } catch (error) { + console.error("Failed to restart server:", error) + + // Revert to previous preset on failure + if (previousPresetId) { + applyPresetToEnv(previousPresetId) + } else { + setCurrentPreset(null) + } + + // Rebuild menu to restore previous checkmark state + rebuildAppMenu() + + // Show error dialog to notify user + dialog.showErrorBox( + "Configuration Error", + `Failed to apply preset "${preset.name}". The server could not be restarted.\n\nThe previous configuration has been restored.\n\nError: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } + }, + })) + + return { + label: "Configuration", + submenu: [ + ...(presetItems.length > 0 + ? [ + { label: "Switch Preset", enabled: false }, + { type: "separator" } as MenuItemConstructorOptions, + ...presetItems, + { type: "separator" } as MenuItemConstructorOptions, + ] + : []), + { + label: + presetItems.length > 0 + ? "Manage Presets..." + : "Add Configuration Preset...", + click: () => { + const win = BrowserWindow.getFocusedWindow() + showSettingsWindow(win || undefined) + }, + }, + ], + } +} diff --git a/electron/main/config-manager.ts b/electron/main/config-manager.ts new file mode 100644 index 00000000..2a66d358 --- /dev/null +++ b/electron/main/config-manager.ts @@ -0,0 +1,460 @@ +import { randomUUID } from "node:crypto" +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" +import path from "node:path" +import { app, safeStorage } from "electron" + +/** + * Fields that contain sensitive data and should be encrypted + */ +const SENSITIVE_FIELDS = ["AI_API_KEY"] as const + +/** + * Prefix to identify encrypted values + */ +const ENCRYPTED_PREFIX = "encrypted:" + +/** + * Check if safeStorage encryption is available + */ +function isEncryptionAvailable(): boolean { + return safeStorage.isEncryptionAvailable() +} + +/** + * Track if we've already warned about plaintext storage + */ +let hasWarnedAboutPlaintext = false + +/** + * Encrypt a sensitive value using safeStorage + * Warns if encryption is not available (API key stored in plaintext) + */ +function encryptValue(value: string): string { + if (!value) { + return value + } + + if (!isEncryptionAvailable()) { + if (!hasWarnedAboutPlaintext) { + console.warn( + "⚠️ SECURITY WARNING: safeStorage not available. " + + "API keys will be stored in PLAINTEXT. " + + "On Linux, install gnome-keyring or similar for secure storage.", + ) + hasWarnedAboutPlaintext = true + } + return value + } + + try { + const encrypted = safeStorage.encryptString(value) + return ENCRYPTED_PREFIX + encrypted.toString("base64") + } catch (error) { + console.error("Encryption failed:", error) + // Fail secure: don't store if encryption fails + throw new Error( + "Failed to encrypt API key. Cannot securely store credentials.", + ) + } +} + +/** + * Decrypt a sensitive value using safeStorage + * Returns the original value if it's not encrypted or decryption fails + */ +function decryptValue(value: string): string { + if (!value || !value.startsWith(ENCRYPTED_PREFIX)) { + return value + } + if (!isEncryptionAvailable()) { + console.warn( + "Cannot decrypt value: safeStorage encryption is not available", + ) + return value + } + try { + const base64Data = value.slice(ENCRYPTED_PREFIX.length) + const buffer = Buffer.from(base64Data, "base64") + return safeStorage.decryptString(buffer) + } catch (error) { + console.error("Failed to decrypt value:", error) + return value + } +} + +/** + * Encrypt sensitive fields in a config object + */ +function encryptConfig( + config: Record, +): Record { + const encrypted = { ...config } + for (const field of SENSITIVE_FIELDS) { + if (encrypted[field]) { + encrypted[field] = encryptValue(encrypted[field] as string) + } + } + return encrypted +} + +/** + * Decrypt sensitive fields in a config object + */ +function decryptConfig( + config: Record, +): Record { + const decrypted = { ...config } + for (const field of SENSITIVE_FIELDS) { + if (decrypted[field]) { + decrypted[field] = decryptValue(decrypted[field] as string) + } + } + return decrypted +} + +/** + * Configuration preset interface + */ +export interface ConfigPreset { + id: string + name: string + createdAt: number + updatedAt: number + config: { + AI_PROVIDER?: string + AI_MODEL?: string + AI_API_KEY?: string + AI_BASE_URL?: string + TEMPERATURE?: string + [key: string]: string | undefined + } +} + +/** + * Configuration file structure + */ +interface ConfigPresetsFile { + version: 1 + currentPresetId: string | null + presets: ConfigPreset[] +} + +const CONFIG_FILE_NAME = "config-presets.json" + +/** + * Get the path to the config file + */ +function getConfigFilePath(): string { + const userDataPath = app.getPath("userData") + return path.join(userDataPath, CONFIG_FILE_NAME) +} + +/** + * Load presets from the config file + * Decrypts sensitive fields automatically + */ +export function loadPresets(): ConfigPresetsFile { + const configPath = getConfigFilePath() + + if (!existsSync(configPath)) { + return { + version: 1, + currentPresetId: null, + presets: [], + } + } + + try { + const content = readFileSync(configPath, "utf-8") + const data = JSON.parse(content) as ConfigPresetsFile + + // Decrypt sensitive fields in each preset + data.presets = data.presets.map((preset) => ({ + ...preset, + config: decryptConfig(preset.config) as ConfigPreset["config"], + })) + + return data + } catch (error) { + console.error("Failed to load config presets:", error) + return { + version: 1, + currentPresetId: null, + presets: [], + } + } +} + +/** + * Save presets to the config file + * Encrypts sensitive fields automatically + */ +export function savePresets(data: ConfigPresetsFile): void { + const configPath = getConfigFilePath() + const userDataPath = app.getPath("userData") + + // Ensure the directory exists + if (!existsSync(userDataPath)) { + mkdirSync(userDataPath, { recursive: true }) + } + + // Encrypt sensitive fields before saving + const dataToSave: ConfigPresetsFile = { + ...data, + presets: data.presets.map((preset) => ({ + ...preset, + config: encryptConfig(preset.config) as ConfigPreset["config"], + })), + } + + try { + writeFileSync(configPath, JSON.stringify(dataToSave, null, 2), "utf-8") + } catch (error) { + console.error("Failed to save config presets:", error) + throw error + } +} + +/** + * Get all presets + */ +export function getAllPresets(): ConfigPreset[] { + const data = loadPresets() + return data.presets +} + +/** + * Get current preset ID + */ +export function getCurrentPresetId(): string | null { + const data = loadPresets() + return data.currentPresetId +} + +/** + * Get current preset + */ +export function getCurrentPreset(): ConfigPreset | null { + const data = loadPresets() + if (!data.currentPresetId) { + return null + } + return data.presets.find((p) => p.id === data.currentPresetId) || null +} + +/** + * Create a new preset + */ +export function createPreset( + preset: Omit, +): ConfigPreset { + const data = loadPresets() + const now = Date.now() + + const newPreset: ConfigPreset = { + id: randomUUID(), + name: preset.name, + config: preset.config, + createdAt: now, + updatedAt: now, + } + + data.presets.push(newPreset) + savePresets(data) + + return newPreset +} + +/** + * Update an existing preset + */ +export function updatePreset( + id: string, + updates: Partial>, +): ConfigPreset | null { + const data = loadPresets() + const index = data.presets.findIndex((p) => p.id === id) + + if (index === -1) { + return null + } + + const updatedPreset: ConfigPreset = { + ...data.presets[index], + ...updates, + updatedAt: Date.now(), + } + + data.presets[index] = updatedPreset + savePresets(data) + + return updatedPreset +} + +/** + * Delete a preset + */ +export function deletePreset(id: string): boolean { + const data = loadPresets() + const index = data.presets.findIndex((p) => p.id === id) + + if (index === -1) { + return false + } + + data.presets.splice(index, 1) + + // Clear current preset if it was deleted + if (data.currentPresetId === id) { + data.currentPresetId = null + } + + savePresets(data) + return true +} + +/** + * Set the current preset + */ +export function setCurrentPreset(id: string | null): boolean { + const data = loadPresets() + + if (id !== null) { + const preset = data.presets.find((p) => p.id === id) + if (!preset) { + return false + } + } + + data.currentPresetId = id + savePresets(data) + return true +} + +/** + * Map generic AI_API_KEY and AI_BASE_URL to provider-specific environment variables + */ +const PROVIDER_ENV_MAP: Record = { + openai: { apiKey: "OPENAI_API_KEY", baseUrl: "OPENAI_BASE_URL" }, + anthropic: { apiKey: "ANTHROPIC_API_KEY", baseUrl: "ANTHROPIC_BASE_URL" }, + google: { + apiKey: "GOOGLE_GENERATIVE_AI_API_KEY", + baseUrl: "GOOGLE_BASE_URL", + }, + azure: { apiKey: "AZURE_API_KEY", baseUrl: "AZURE_BASE_URL" }, + openrouter: { + apiKey: "OPENROUTER_API_KEY", + baseUrl: "OPENROUTER_BASE_URL", + }, + deepseek: { apiKey: "DEEPSEEK_API_KEY", baseUrl: "DEEPSEEK_BASE_URL" }, + siliconflow: { + apiKey: "SILICONFLOW_API_KEY", + baseUrl: "SILICONFLOW_BASE_URL", + }, + gateway: { apiKey: "AI_GATEWAY_API_KEY", baseUrl: "AI_GATEWAY_BASE_URL" }, + // bedrock and ollama don't use API keys in the same way + bedrock: { apiKey: "", baseUrl: "" }, + ollama: { apiKey: "", baseUrl: "OLLAMA_BASE_URL" }, +} + +/** + * Apply preset environment variables to the current process + * Returns the environment variables that were applied + */ +export function applyPresetToEnv(id: string): Record | null { + const data = loadPresets() + const preset = data.presets.find((p) => p.id === id) + + if (!preset) { + return null + } + + const appliedEnv: Record = {} + const provider = preset.config.AI_PROVIDER?.toLowerCase() + + for (const [key, value] of Object.entries(preset.config)) { + if (value !== undefined && value !== "") { + // Map generic AI_API_KEY to provider-specific key + if ( + key === "AI_API_KEY" && + provider && + PROVIDER_ENV_MAP[provider] + ) { + const providerApiKey = PROVIDER_ENV_MAP[provider].apiKey + if (providerApiKey) { + process.env[providerApiKey] = value + appliedEnv[providerApiKey] = value + } + } + // Map generic AI_BASE_URL to provider-specific key + else if ( + key === "AI_BASE_URL" && + provider && + PROVIDER_ENV_MAP[provider] + ) { + const providerBaseUrl = PROVIDER_ENV_MAP[provider].baseUrl + if (providerBaseUrl) { + process.env[providerBaseUrl] = value + appliedEnv[providerBaseUrl] = value + } + } + // Apply other env vars directly + else { + process.env[key] = value + appliedEnv[key] = value + } + } + } + + // Set as current preset + data.currentPresetId = id + savePresets(data) + + return appliedEnv +} + +/** + * Get environment variables from current preset + * Maps generic AI_API_KEY/AI_BASE_URL to provider-specific keys + */ +export function getCurrentPresetEnv(): Record { + const preset = getCurrentPreset() + if (!preset) { + return {} + } + + const env: Record = {} + const provider = preset.config.AI_PROVIDER?.toLowerCase() + + for (const [key, value] of Object.entries(preset.config)) { + if (value !== undefined && value !== "") { + // Map generic AI_API_KEY to provider-specific key + if ( + key === "AI_API_KEY" && + provider && + PROVIDER_ENV_MAP[provider] + ) { + const providerApiKey = PROVIDER_ENV_MAP[provider].apiKey + if (providerApiKey) { + env[providerApiKey] = value + } + } + // Map generic AI_BASE_URL to provider-specific key + else if ( + key === "AI_BASE_URL" && + provider && + PROVIDER_ENV_MAP[provider] + ) { + const providerBaseUrl = PROVIDER_ENV_MAP[provider].baseUrl + if (providerBaseUrl) { + env[providerBaseUrl] = value + } + } + // Apply other env vars directly + else { + env[key] = value + } + } + } + return env +} diff --git a/electron/main/env-loader.ts b/electron/main/env-loader.ts new file mode 100644 index 00000000..8182b11a --- /dev/null +++ b/electron/main/env-loader.ts @@ -0,0 +1,67 @@ +import fs from "node:fs" +import path from "node:path" +import { app } from "electron" + +/** + * Load environment variables from .env file + * Searches multiple locations in priority order + */ +export function loadEnvFile(): void { + const possiblePaths = [ + // Next to the executable (for portable installations) + path.join(path.dirname(app.getPath("exe")), ".env"), + // User data directory (persists across updates) + path.join(app.getPath("userData"), ".env"), + // Development: project root + path.join(app.getAppPath(), ".env.local"), + path.join(app.getAppPath(), ".env"), + ] + + for (const envPath of possiblePaths) { + if (fs.existsSync(envPath)) { + console.log(`Loading environment from: ${envPath}`) + loadEnvFromFile(envPath) + return + } + } + + console.log("No .env file found, using system environment variables") +} + +/** + * Parse and load environment variables from a file + */ +function loadEnvFromFile(filePath: string): void { + try { + const content = fs.readFileSync(filePath, "utf-8") + const lines = content.split("\n") + + for (const line of lines) { + const trimmed = line.trim() + + // Skip comments and empty lines + if (!trimmed || trimmed.startsWith("#")) continue + + const equalIndex = trimmed.indexOf("=") + if (equalIndex === -1) continue + + const key = trimmed.slice(0, equalIndex).trim() + let value = trimmed.slice(equalIndex + 1).trim() + + // Remove surrounding quotes + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1) + } + + // Don't override existing environment variables + if (!(key in process.env)) { + process.env[key] = value + } + } + } catch (error) { + console.error(`Failed to load env file ${filePath}:`, error) + } +} diff --git a/electron/main/index.ts b/electron/main/index.ts new file mode 100644 index 00000000..60254def --- /dev/null +++ b/electron/main/index.ts @@ -0,0 +1,105 @@ +import { app, BrowserWindow, dialog, shell } from "electron" +import { buildAppMenu } from "./app-menu" +import { getCurrentPresetEnv } from "./config-manager" +import { loadEnvFile } from "./env-loader" +import { registerIpcHandlers } from "./ipc-handlers" +import { startNextServer, stopNextServer } from "./next-server" +import { registerSettingsWindowHandlers } from "./settings-window" +import { createWindow, getMainWindow } from "./window-manager" + +// Single instance lock +const gotTheLock = app.requestSingleInstanceLock() + +if (!gotTheLock) { + app.quit() +} else { + app.on("second-instance", () => { + const mainWindow = getMainWindow() + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() + } + }) + + // Load environment variables from .env files + loadEnvFile() + + // Apply saved preset environment variables (overrides .env) + const presetEnv = getCurrentPresetEnv() + for (const [key, value] of Object.entries(presetEnv)) { + process.env[key] = value + } + + const isDev = process.env.NODE_ENV === "development" + let serverUrl: string | null = null + + app.whenReady().then(async () => { + // Register IPC handlers + registerIpcHandlers() + registerSettingsWindowHandlers() + + // Build application menu + buildAppMenu() + + try { + if (isDev) { + // Development: use the dev server URL + serverUrl = + process.env.ELECTRON_DEV_URL || "http://localhost:6002" + console.log(`Development mode: connecting to ${serverUrl}`) + } else { + // Production: start Next.js standalone server + serverUrl = await startNextServer() + } + + // Create main window + createWindow(serverUrl) + } catch (error) { + console.error("Failed to start application:", error) + dialog.showErrorBox( + "Startup Error", + `Failed to start the application: ${error instanceof Error ? error.message : "Unknown error"}`, + ) + app.quit() + } + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + if (serverUrl) { + createWindow(serverUrl) + } + } + }) + }) + + app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + stopNextServer() + app.quit() + } + }) + + app.on("before-quit", () => { + stopNextServer() + }) + + // Open external links in default browser + app.on("web-contents-created", (_, contents) => { + contents.setWindowOpenHandler(({ url }) => { + // Allow diagrams.net iframe + if ( + url.includes("diagrams.net") || + url.includes("draw.io") || + url.startsWith("http://localhost") + ) { + return { action: "allow" } + } + // Open other links in external browser + if (url.startsWith("http://") || url.startsWith("https://")) { + shell.openExternal(url) + return { action: "deny" } + } + return { action: "allow" } + }) + }) +} diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts new file mode 100644 index 00000000..7e801d1c --- /dev/null +++ b/electron/main/ipc-handlers.ts @@ -0,0 +1,212 @@ +import { app, BrowserWindow, dialog, ipcMain } from "electron" +import { + applyPresetToEnv, + type ConfigPreset, + createPreset, + deletePreset, + getAllPresets, + getCurrentPreset, + getCurrentPresetId, + setCurrentPreset, + updatePreset, +} from "./config-manager" +import { restartNextServer } from "./next-server" + +/** + * Allowed configuration keys for presets + * This whitelist prevents arbitrary environment variable injection + */ +const ALLOWED_CONFIG_KEYS = new Set([ + "AI_PROVIDER", + "AI_MODEL", + "AI_API_KEY", + "AI_BASE_URL", + "TEMPERATURE", +]) + +/** + * Sanitize preset config to only include allowed keys + */ +function sanitizePresetConfig( + config: Record, +): Record { + const sanitized: Record = {} + for (const key of ALLOWED_CONFIG_KEYS) { + if (key in config && typeof config[key] === "string") { + sanitized[key] = config[key] + } + } + return sanitized +} + +/** + * Register all IPC handlers + */ +export function registerIpcHandlers(): void { + // ==================== App Info ==================== + + ipcMain.handle("get-version", () => { + return app.getVersion() + }) + + // ==================== Window Controls ==================== + + ipcMain.on("window-minimize", (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + win?.minimize() + }) + + ipcMain.on("window-maximize", (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (win?.isMaximized()) { + win.unmaximize() + } else { + win?.maximize() + } + }) + + ipcMain.on("window-close", (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + win?.close() + }) + + // ==================== File Dialogs ==================== + + ipcMain.handle("dialog-open-file", async (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (!win) return null + + const result = await dialog.showOpenDialog(win, { + properties: ["openFile"], + filters: [ + { name: "Draw.io Files", extensions: ["drawio", "xml"] }, + { name: "All Files", extensions: ["*"] }, + ], + }) + + if (result.canceled || result.filePaths.length === 0) { + return null + } + + // Read the file content + const fs = await import("node:fs/promises") + try { + const content = await fs.readFile(result.filePaths[0], "utf-8") + return content + } catch (error) { + console.error("Failed to read file:", error) + return null + } + }) + + ipcMain.handle("dialog-save-file", async (event, data: string) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (!win) return false + + const result = await dialog.showSaveDialog(win, { + filters: [ + { name: "Draw.io Files", extensions: ["drawio"] }, + { name: "XML Files", extensions: ["xml"] }, + ], + }) + + if (result.canceled || !result.filePath) { + return false + } + + const fs = await import("node:fs/promises") + try { + await fs.writeFile(result.filePath, data, "utf-8") + return true + } catch (error) { + console.error("Failed to save file:", error) + return false + } + }) + + // ==================== Config Presets ==================== + + ipcMain.handle("config-presets:get-all", () => { + return getAllPresets() + }) + + ipcMain.handle("config-presets:get-current", () => { + return getCurrentPreset() + }) + + ipcMain.handle("config-presets:get-current-id", () => { + return getCurrentPresetId() + }) + + ipcMain.handle( + "config-presets:save", + ( + _event, + preset: Omit & { + id?: string + }, + ) => { + // Validate preset name + if (typeof preset.name !== "string" || !preset.name.trim()) { + throw new Error("Invalid preset name") + } + + // Sanitize config to only allow whitelisted keys + const sanitizedConfig = sanitizePresetConfig(preset.config ?? {}) + + if (preset.id) { + // Update existing preset + return updatePreset(preset.id, { + name: preset.name.trim(), + config: sanitizedConfig, + }) + } + // Create new preset + return createPreset({ + name: preset.name.trim(), + config: sanitizedConfig, + }) + }, + ) + + ipcMain.handle("config-presets:delete", (_event, id: string) => { + return deletePreset(id) + }) + + ipcMain.handle("config-presets:apply", async (_event, id: string) => { + const env = applyPresetToEnv(id) + if (!env) { + return { success: false, error: "Preset not found" } + } + + const isDev = process.env.NODE_ENV === "development" + + if (isDev) { + // In development mode, the config file change will trigger + // the file watcher in electron-dev.mjs to restart Next.js + // We just need to save the preset (already done in applyPresetToEnv) + return { success: true, env, devMode: true } + } + + // Production mode: restart the Next.js server to apply new environment variables + try { + await restartNextServer() + return { success: true, env } + } catch (error) { + return { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to restart server", + } + } + }) + + ipcMain.handle( + "config-presets:set-current", + (_event, id: string | null) => { + return setCurrentPreset(id) + }, + ) +} diff --git a/electron/main/next-server.ts b/electron/main/next-server.ts new file mode 100644 index 00000000..fce07ec2 --- /dev/null +++ b/electron/main/next-server.ts @@ -0,0 +1,161 @@ +import { existsSync } from "node:fs" +import path from "node:path" +import { app, type UtilityProcess, utilityProcess } from "electron" +import { + findAvailablePort, + getAllocatedPort, + getServerUrl, + isPortAvailable, +} from "./port-manager" + +let serverProcess: UtilityProcess | null = null + +/** + * Get the path to the standalone server resources + * In packaged app: resources/standalone + * In development: .next/standalone + */ +function getResourcePath(): string { + if (app.isPackaged) { + return path.join(process.resourcesPath, "standalone") + } + return path.join(app.getAppPath(), ".next", "standalone") +} + +/** + * Wait for the server to be ready by polling the health endpoint + */ +async function waitForServer(url: string, timeout = 30000): Promise { + const start = Date.now() + while (Date.now() - start < timeout) { + try { + const response = await fetch(url) + if (response.ok || response.status < 500) { + return + } + } catch { + // Server not ready yet + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } + throw new Error(`Server startup timeout after ${timeout}ms`) +} + +/** + * Start the Next.js standalone server using Electron's utilityProcess + * This API is designed for running Node.js code in the background + */ +export async function startNextServer(): Promise { + const resourcePath = getResourcePath() + const serverPath = path.join(resourcePath, "server.js") + + console.log(`Starting Next.js server from: ${resourcePath}`) + console.log(`Server script path: ${serverPath}`) + + // Verify server script exists before attempting to start + if (!existsSync(serverPath)) { + throw new Error( + `Server script not found at ${serverPath}. ` + + "Please ensure the app was built correctly with 'npm run build'.", + ) + } + + // Find an available port (random in production, fixed in development) + const port = await findAvailablePort() + console.log(`Using port: ${port}`) + + // Set up environment variables + const env: Record = { + NODE_ENV: "production", + PORT: String(port), + HOSTNAME: "localhost", + } + + // Set cache directory to a writable location (user's app data folder) + // This is necessary because the packaged app might be on a read-only volume + if (app.isPackaged) { + const cacheDir = path.join(app.getPath("userData"), "cache") + env.NEXT_CACHE_DIR = cacheDir + } + + // Copy existing environment variables + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined && !env[key]) { + env[key] = value + } + } + + // Use Electron's utilityProcess API for running Node.js in background + // This is the recommended way to run Node.js code in Electron + serverProcess = utilityProcess.fork(serverPath, [], { + cwd: resourcePath, + env, + stdio: "pipe", + }) + + serverProcess.stdout?.on("data", (data) => { + console.log(`[Next.js] ${data.toString().trim()}`) + }) + + serverProcess.stderr?.on("data", (data) => { + console.error(`[Next.js Error] ${data.toString().trim()}`) + }) + + serverProcess.on("exit", (code) => { + console.log(`Next.js server exited with code ${code}`) + serverProcess = null + }) + + const url = getServerUrl() + await waitForServer(url) + console.log(`Next.js server started at ${url}`) + + return url +} + +/** + * Stop the Next.js server process + */ +export function stopNextServer(): void { + if (serverProcess) { + console.log("Stopping Next.js server...") + serverProcess.kill() + serverProcess = null + } +} + +/** + * Wait for the server to fully stop + */ +async function waitForServerStop(timeout = 5000): Promise { + const port = getAllocatedPort() + if (port === null) { + return + } + + const start = Date.now() + while (Date.now() - start < timeout) { + const available = await isPortAvailable(port) + if (available) { + return + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } + console.warn("Server stop timeout, port may still be in use") +} + +/** + * Restart the Next.js server with new environment variables + */ +export async function restartNextServer(): Promise { + console.log("Restarting Next.js server...") + + // Stop the current server + stopNextServer() + + // Wait for the port to be released + await waitForServerStop() + + // Start the server again + return startNextServer() +} diff --git a/electron/main/port-manager.ts b/electron/main/port-manager.ts new file mode 100644 index 00000000..eb453792 --- /dev/null +++ b/electron/main/port-manager.ts @@ -0,0 +1,129 @@ +import net from "node:net" +import { app } from "electron" + +/** + * Port configuration + */ +const PORT_CONFIG = { + // Development mode uses fixed port for hot reload compatibility + development: 6002, + // Production mode port range (will find first available) + production: { + min: 10000, + max: 65535, + }, + // Maximum attempts to find an available port + maxAttempts: 100, +} + +/** + * Currently allocated port (cached after first allocation) + */ +let allocatedPort: number | null = null + +/** + * Check if a specific port is available + */ +export function isPortAvailable(port: number): Promise { + return new Promise((resolve) => { + const server = net.createServer() + server.once("error", () => resolve(false)) + server.once("listening", () => { + server.close() + resolve(true) + }) + server.listen(port, "127.0.0.1") + }) +} + +/** + * Generate a random port within the production range + */ +function getRandomPort(): number { + const { min, max } = PORT_CONFIG.production + return Math.floor(Math.random() * (max - min + 1)) + min +} + +/** + * Find an available port + * - In development: uses fixed port (6002) + * - In production: finds a random available port + * - If a port was previously allocated, verifies it's still available + * + * @param reuseExisting If true, try to reuse the previously allocated port + * @returns Promise The available port + * @throws Error if no available port found after max attempts + */ +export async function findAvailablePort(reuseExisting = true): Promise { + const isDev = !app.isPackaged + + // Try to reuse cached port if requested and available + if (reuseExisting && allocatedPort !== null) { + const available = await isPortAvailable(allocatedPort) + if (available) { + return allocatedPort + } + console.warn( + `Previously allocated port ${allocatedPort} is no longer available`, + ) + allocatedPort = null + } + + if (isDev) { + // Development mode: use fixed port + const port = PORT_CONFIG.development + const available = await isPortAvailable(port) + if (available) { + allocatedPort = port + return port + } + console.warn( + `Development port ${port} is in use, finding alternative...`, + ) + } + + // Production mode or dev port unavailable: find random available port + for (let attempt = 0; attempt < PORT_CONFIG.maxAttempts; attempt++) { + const port = isDev + ? PORT_CONFIG.development + attempt + 1 + : getRandomPort() + + const available = await isPortAvailable(port) + if (available) { + allocatedPort = port + console.log(`Allocated port: ${port}`) + return port + } + } + + throw new Error( + `Failed to find available port after ${PORT_CONFIG.maxAttempts} attempts`, + ) +} + +/** + * Get the currently allocated port + * Returns null if no port has been allocated yet + */ +export function getAllocatedPort(): number | null { + return allocatedPort +} + +/** + * Reset the allocated port (useful for testing or restart scenarios) + */ +export function resetAllocatedPort(): void { + allocatedPort = null +} + +/** + * Get the server URL with the allocated port + */ +export function getServerUrl(): string { + if (allocatedPort === null) { + throw new Error( + "No port allocated yet. Call findAvailablePort() first.", + ) + } + return `http://localhost:${allocatedPort}` +} diff --git a/electron/main/settings-window.ts b/electron/main/settings-window.ts new file mode 100644 index 00000000..b5ee490b --- /dev/null +++ b/electron/main/settings-window.ts @@ -0,0 +1,78 @@ +import path from "node:path" +import { app, BrowserWindow, ipcMain } from "electron" + +let settingsWindow: BrowserWindow | null = null + +/** + * Create and show the settings window + */ +export function showSettingsWindow(parentWindow?: BrowserWindow): void { + // If settings window already exists, focus it + if (settingsWindow && !settingsWindow.isDestroyed()) { + settingsWindow.focus() + return + } + + // Determine path to settings preload script + // In compiled output: dist-electron/preload/settings.js + const preloadPath = path.join(__dirname, "..", "preload", "settings.js") + + // Determine path to settings HTML + // In packaged app: app.asar/dist-electron/settings/index.html + // In development: electron/settings/index.html + const settingsHtmlPath = app.isPackaged + ? path.join(__dirname, "..", "settings", "index.html") + : path.join(__dirname, "..", "..", "electron", "settings", "index.html") + + settingsWindow = new BrowserWindow({ + width: 600, + height: 700, + minWidth: 500, + minHeight: 500, + parent: parentWindow, + modal: false, + show: false, + title: "Settings - Next AI Draw.io", + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + }, + }) + settingsWindow.loadFile(settingsHtmlPath) + + settingsWindow.once("ready-to-show", () => { + settingsWindow?.show() + }) + + settingsWindow.on("closed", () => { + settingsWindow = null + }) +} + +/** + * Close the settings window if it exists + */ +export function closeSettingsWindow(): void { + if (settingsWindow && !settingsWindow.isDestroyed()) { + settingsWindow.close() + settingsWindow = null + } +} + +/** + * Check if settings window is open + */ +export function isSettingsWindowOpen(): boolean { + return settingsWindow !== null && !settingsWindow.isDestroyed() +} + +/** + * Register settings window IPC handlers + */ +export function registerSettingsWindowHandlers(): void { + ipcMain.on("settings:close", () => { + closeSettingsWindow() + }) +} diff --git a/electron/main/window-manager.ts b/electron/main/window-manager.ts new file mode 100644 index 00000000..d4eb6aca --- /dev/null +++ b/electron/main/window-manager.ts @@ -0,0 +1,84 @@ +import path from "node:path" +import { app, BrowserWindow, screen } from "electron" + +let mainWindow: BrowserWindow | null = null + +/** + * Get the icon path based on platform + * Note: electron-builder converts icon.png during packaging, + * but at runtime we use PNG directly - Electron handles it + */ +function getIconPath(): string | undefined { + // macOS doesn't need explicit icon - it's embedded in the app bundle + if (process.platform === "darwin" && app.isPackaged) { + return undefined + } + + const iconName = "icon.png" + + if (app.isPackaged) { + return path.join(process.resourcesPath, iconName) + } + + // Development: use icon.png from resources + return path.join(__dirname, "../../resources/icon.png") +} + +/** + * Create the main application window + */ +export function createWindow(serverUrl: string): BrowserWindow { + const { width, height } = screen.getPrimaryDisplay().workAreaSize + + mainWindow = new BrowserWindow({ + width: Math.min(1400, Math.floor(width * 0.9)), + height: Math.min(900, Math.floor(height * 0.9)), + minWidth: 800, + minHeight: 600, + title: "Next AI Draw.io", + icon: getIconPath(), + show: false, // Don't show until ready + webPreferences: { + preload: path.join(__dirname, "../preload/index.js"), + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + webSecurity: true, + }, + }) + + // Load the Next.js application + mainWindow.loadURL(serverUrl) + + // Show window when ready to prevent flashing + mainWindow.once("ready-to-show", () => { + mainWindow?.show() + }) + + // Open DevTools in development + if (process.env.NODE_ENV === "development") { + mainWindow.webContents.openDevTools() + } + + mainWindow.on("closed", () => { + mainWindow = null + }) + + // Handle page title updates + mainWindow.webContents.on("page-title-updated", (event, title) => { + if (title && !title.includes("localhost")) { + mainWindow?.setTitle(title) + } else { + event.preventDefault() + } + }) + + return mainWindow +} + +/** + * Get the main window instance + */ +export function getMainWindow(): BrowserWindow | null { + return mainWindow +} diff --git a/electron/preload/index.ts b/electron/preload/index.ts new file mode 100644 index 00000000..2b460c75 --- /dev/null +++ b/electron/preload/index.ts @@ -0,0 +1,24 @@ +import { contextBridge, ipcRenderer } from "electron" + +/** + * Expose safe APIs to the renderer process + */ +contextBridge.exposeInMainWorld("electronAPI", { + // Platform information + platform: process.platform, + + // Check if running in Electron + isElectron: true, + + // Application version + getVersion: () => ipcRenderer.invoke("get-version"), + + // Window controls (optional, for custom title bar) + minimize: () => ipcRenderer.send("window-minimize"), + maximize: () => ipcRenderer.send("window-maximize"), + close: () => ipcRenderer.send("window-close"), + + // File operations + openFile: () => ipcRenderer.invoke("dialog-open-file"), + saveFile: (data: string) => ipcRenderer.invoke("dialog-save-file", data), +}) diff --git a/electron/preload/settings.ts b/electron/preload/settings.ts new file mode 100644 index 00000000..d3fe0114 --- /dev/null +++ b/electron/preload/settings.ts @@ -0,0 +1,35 @@ +/** + * Preload script for settings window + * Exposes APIs for managing configuration presets + */ +import { contextBridge, ipcRenderer } from "electron" + +// Expose settings API to the renderer process +contextBridge.exposeInMainWorld("settingsAPI", { + // Get all presets + getPresets: () => ipcRenderer.invoke("config-presets:get-all"), + + // Get current preset ID + getCurrentPresetId: () => + ipcRenderer.invoke("config-presets:get-current-id"), + + // Get current preset + getCurrentPreset: () => ipcRenderer.invoke("config-presets:get-current"), + + // Save (create or update) a preset + savePreset: (preset: { + id?: string + name: string + config: Record + }) => ipcRenderer.invoke("config-presets:save", preset), + + // Delete a preset + deletePreset: (id: string) => + ipcRenderer.invoke("config-presets:delete", id), + + // Apply a preset (sets environment variables and restarts server) + applyPreset: (id: string) => ipcRenderer.invoke("config-presets:apply", id), + + // Close settings window + close: () => ipcRenderer.send("settings:close"), +}) diff --git a/electron/settings/index.html b/electron/settings/index.html new file mode 100644 index 00000000..9c91eb5b --- /dev/null +++ b/electron/settings/index.html @@ -0,0 +1,110 @@ + + + + + + + Settings - Next AI Draw.io + + + +
+

Configuration Presets

+ +
+

Presets

+
+ +
+ +
+
+ + + + + + + + +
+ + + + diff --git a/electron/settings/settings.css b/electron/settings/settings.css new file mode 100644 index 00000000..8d4ee4a6 --- /dev/null +++ b/electron/settings/settings.css @@ -0,0 +1,311 @@ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f5f5f5; + --bg-hover: #e8e8e8; + --text-primary: #1a1a1a; + --text-secondary: #666666; + --border-color: #e0e0e0; + --accent-color: #0066cc; + --accent-hover: #0052a3; + --danger-color: #dc3545; + --success-color: #28a745; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: #1a1a1a; + --bg-secondary: #2d2d2d; + --bg-hover: #3d3d3d; + --text-primary: #ffffff; + --text-secondary: #a0a0a0; + --border-color: #404040; + --accent-color: #4da6ff; + --accent-hover: #66b3ff; + } +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, + sans-serif; + background-color: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; +} + +.container { + max-width: 560px; + margin: 0 auto; + padding: 24px; +} + +h1 { + font-size: 24px; + font-weight: 600; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border-color); +} + +h2 { + font-size: 16px; + font-weight: 600; + margin-bottom: 16px; + color: var(--text-secondary); +} + +.section { + margin-bottom: 32px; +} + +.preset-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 16px; +} + +.preset-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; + cursor: pointer; + transition: all 0.2s ease; +} + +.preset-card:hover { + background: var(--bg-hover); +} + +.preset-card.active { + border-color: var(--accent-color); + box-shadow: 0 0 0 1px var(--accent-color); +} + +.preset-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.preset-name { + font-weight: 600; + font-size: 15px; +} + +.preset-badge { + background: var(--accent-color); + color: white; + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; +} + +.preset-info { + font-size: 13px; + color: var(--text-secondary); +} + +.preset-actions { + display: flex; + gap: 8px; + margin-top: 12px; +} + +.btn { + padding: 8px 16px; + border: none; + border-radius: 6px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; + font-weight: 500; +} + +.btn-primary { + background: var(--accent-color); + color: white; +} + +.btn-primary:hover { + background: var(--accent-hover); +} + +.btn-secondary { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover { + background: var(--bg-hover); +} + +.btn-danger { + background: var(--danger-color); + color: white; +} + +.btn-danger:hover { + opacity: 0.9; +} + +.btn-sm { + padding: 6px 12px; + font-size: 13px; +} + +.empty-state { + text-align: center; + padding: 40px 20px; + color: var(--text-secondary); +} + +.empty-state p { + margin-bottom: 16px; +} + +/* Modal */ +.modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 100; + align-items: center; + justify-content: center; +} + +.modal-overlay.show { + display: flex; +} + +.modal { + background: var(--bg-primary); + border-radius: 12px; + width: 90%; + max-width: 480px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); +} + +.modal-header { + padding: 20px 24px; + border-bottom: 1px solid var(--border-color); +} + +.modal-header h3 { + font-size: 18px; + font-weight: 600; +} + +.modal-body { + padding: 24px; +} + +.modal-footer { + padding: 16px 24px; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-size: 14px; + font-weight: 500; + margin-bottom: 6px; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 14px; + background: var(--bg-primary); + color: var(--text-primary); +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2); +} + +.form-group .hint { + font-size: 12px; + color: var(--text-secondary); + margin-top: 4px; +} + +.loading { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid var(--border-color); + border-top-color: var(--accent-color); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: var(--text-primary); + color: var(--bg-primary); + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + z-index: 200; + opacity: 0; + transition: opacity 0.3s ease; +} + +.toast.show { + opacity: 1; +} + +.toast.success { + background: var(--success-color); + color: white; +} + +.toast.error { + background: var(--danger-color); + color: white; +} + +/* Inline style replacements */ +.delete-warning { + color: var(--text-secondary); + margin-top: 8px; + font-size: 14px; +} diff --git a/electron/settings/settings.js b/electron/settings/settings.js new file mode 100644 index 00000000..3e550c91 --- /dev/null +++ b/electron/settings/settings.js @@ -0,0 +1,311 @@ +// Settings page JavaScript +// This file handles the UI interactions for the settings window + +let presets = [] +let currentPresetId = null +let editingPresetId = null +let deletingPresetId = null + +// DOM Elements +const presetList = document.getElementById("preset-list") +const addPresetBtn = document.getElementById("add-preset-btn") +const presetModal = document.getElementById("preset-modal") +const deleteModal = document.getElementById("delete-modal") +const presetForm = document.getElementById("preset-form") +const modalTitle = document.getElementById("modal-title") +const toast = document.getElementById("toast") + +// Form fields +const presetIdField = document.getElementById("preset-id") +const presetNameField = document.getElementById("preset-name") +const aiProviderField = document.getElementById("ai-provider") +const aiModelField = document.getElementById("ai-model") +const aiApiKeyField = document.getElementById("ai-api-key") +const aiBaseUrlField = document.getElementById("ai-base-url") +const temperatureField = document.getElementById("temperature") + +// Buttons +const cancelBtn = document.getElementById("cancel-btn") +const saveBtn = document.getElementById("save-btn") +const deleteCancelBtn = document.getElementById("delete-cancel-btn") +const deleteConfirmBtn = document.getElementById("delete-confirm-btn") + +// Initialize +document.addEventListener("DOMContentLoaded", async () => { + await loadPresets() + setupEventListeners() +}) + +// Load presets from main process +async function loadPresets() { + try { + presets = await window.settingsAPI.getPresets() + currentPresetId = await window.settingsAPI.getCurrentPresetId() + renderPresets() + } catch (error) { + console.error("Failed to load presets:", error) + showToast("Failed to load presets", "error") + } +} + +// Render presets list +function renderPresets() { + if (presets.length === 0) { + presetList.innerHTML = ` +
+

No presets configured yet.

+

Add a preset to quickly switch between different AI configurations.

+
+ ` + return + } + + presetList.innerHTML = presets + .map((preset) => { + const isActive = preset.id === currentPresetId + const providerLabel = getProviderLabel(preset.config.AI_PROVIDER) + + return ` +
+
+ ${escapeHtml(preset.name)} + ${isActive ? 'Active' : ""} +
+
+ ${providerLabel ? `Provider: ${providerLabel}` : "No provider configured"} + ${preset.config.AI_MODEL ? ` • Model: ${escapeHtml(preset.config.AI_MODEL)}` : ""} +
+
+ ${!isActive ? `` : ""} + + +
+
+ ` + }) + .join("") + + // Add event listeners to buttons + presetList.querySelectorAll(".apply-btn").forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation() + applyPreset(btn.dataset.id) + }) + }) + + presetList.querySelectorAll(".edit-btn").forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation() + openEditModal(btn.dataset.id) + }) + }) + + presetList.querySelectorAll(".delete-btn").forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation() + openDeleteModal(btn.dataset.id) + }) + }) +} + +// Setup event listeners +function setupEventListeners() { + addPresetBtn.addEventListener("click", () => openAddModal()) + cancelBtn.addEventListener("click", () => closeModal()) + saveBtn.addEventListener("click", () => savePreset()) + deleteCancelBtn.addEventListener("click", () => closeDeleteModal()) + deleteConfirmBtn.addEventListener("click", () => confirmDelete()) + + // Close modal on overlay click + presetModal.addEventListener("click", (e) => { + if (e.target === presetModal) closeModal() + }) + deleteModal.addEventListener("click", (e) => { + if (e.target === deleteModal) closeDeleteModal() + }) + + // Handle Enter key in form + presetForm.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault() + savePreset() + } + }) +} + +// Open add modal +function openAddModal() { + editingPresetId = null + modalTitle.textContent = "Add Preset" + presetForm.reset() + presetIdField.value = "" + presetModal.classList.add("show") + presetNameField.focus() +} + +// Open edit modal +function openEditModal(id) { + const preset = presets.find((p) => p.id === id) + if (!preset) return + + editingPresetId = id + modalTitle.textContent = "Edit Preset" + + presetIdField.value = preset.id + presetNameField.value = preset.name + aiProviderField.value = preset.config.AI_PROVIDER || "" + aiModelField.value = preset.config.AI_MODEL || "" + aiApiKeyField.value = preset.config.AI_API_KEY || "" + aiBaseUrlField.value = preset.config.AI_BASE_URL || "" + temperatureField.value = preset.config.TEMPERATURE || "" + + presetModal.classList.add("show") + presetNameField.focus() +} + +// Close modal +function closeModal() { + presetModal.classList.remove("show") + editingPresetId = null +} + +// Open delete modal +function openDeleteModal(id) { + const preset = presets.find((p) => p.id === id) + if (!preset) return + + deletingPresetId = id + document.getElementById("delete-preset-name").textContent = preset.name + deleteModal.classList.add("show") +} + +// Close delete modal +function closeDeleteModal() { + deleteModal.classList.remove("show") + deletingPresetId = null +} + +// Save preset +async function savePreset() { + const name = presetNameField.value.trim() + if (!name) { + showToast("Please enter a preset name", "error") + presetNameField.focus() + return + } + + const preset = { + id: editingPresetId || undefined, + name: name, + config: { + AI_PROVIDER: aiProviderField.value || undefined, + AI_MODEL: aiModelField.value.trim() || undefined, + AI_API_KEY: aiApiKeyField.value.trim() || undefined, + AI_BASE_URL: aiBaseUrlField.value.trim() || undefined, + TEMPERATURE: temperatureField.value.trim() || undefined, + }, + } + + // Remove undefined values + Object.keys(preset.config).forEach((key) => { + if (preset.config[key] === undefined) { + delete preset.config[key] + } + }) + + try { + saveBtn.disabled = true + saveBtn.innerHTML = '' + + await window.settingsAPI.savePreset(preset) + await loadPresets() + closeModal() + showToast( + editingPresetId ? "Preset updated" : "Preset created", + "success", + ) + } catch (error) { + console.error("Failed to save preset:", error) + showToast("Failed to save preset", "error") + } finally { + saveBtn.disabled = false + saveBtn.textContent = "Save" + } +} + +// Confirm delete +async function confirmDelete() { + if (!deletingPresetId) return + + try { + deleteConfirmBtn.disabled = true + deleteConfirmBtn.innerHTML = '' + + await window.settingsAPI.deletePreset(deletingPresetId) + await loadPresets() + closeDeleteModal() + showToast("Preset deleted", "success") + } catch (error) { + console.error("Failed to delete preset:", error) + showToast("Failed to delete preset", "error") + } finally { + deleteConfirmBtn.disabled = false + deleteConfirmBtn.textContent = "Delete" + } +} + +// Apply preset +async function applyPreset(id) { + try { + const btn = presetList.querySelector(`.apply-btn[data-id="${id}"]`) + if (btn) { + btn.disabled = true + btn.innerHTML = '' + } + + const result = await window.settingsAPI.applyPreset(id) + if (result.success) { + currentPresetId = id + renderPresets() + showToast("Preset applied, server restarting...", "success") + } else { + showToast(result.error || "Failed to apply preset", "error") + } + } catch (error) { + console.error("Failed to apply preset:", error) + showToast("Failed to apply preset", "error") + } +} + +// Get provider display label +function getProviderLabel(provider) { + const labels = { + openai: "OpenAI", + anthropic: "Anthropic", + google: "Google AI", + azure: "Azure OpenAI", + bedrock: "AWS Bedrock", + openrouter: "OpenRouter", + deepseek: "DeepSeek", + siliconflow: "SiliconFlow", + ollama: "Ollama", + } + return labels[provider] || provider +} + +// Show toast notification +function showToast(message, type = "") { + toast.textContent = message + toast.className = "toast show" + (type ? ` ${type}` : "") + + setTimeout(() => { + toast.classList.remove("show") + }, 3000) +} + +// Escape HTML to prevent XSS +function escapeHtml(text) { + const div = document.createElement("div") + div.textContent = text + return div.innerHTML +} diff --git a/electron/tsconfig.json b/electron/tsconfig.json new file mode 100644 index 00000000..6b163d3d --- /dev/null +++ b/electron/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "lib": ["ES2022"], + "outDir": "../dist-electron", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/package-lock.json b/package-lock.json index ed470c12..c3f6f495 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,12 +74,19 @@ "@types/pako": "^2.0.3", "@types/react": "^19", "@types/react-dom": "^19", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "electron": "^39.2.7", + "electron-builder": "^26.0.12", + "esbuild": "^0.27.2", "eslint": "9.39.1", "eslint-config-next": "16.0.5", "husky": "^9.1.7", "lint-staged": "^16.2.7", + "shx": "^0.4.0", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5", + "wait-on": "^9.0.3" } }, "node_modules/@ai-sdk/amazon-bedrock": { @@ -1692,6 +1699,550 @@ "node": ">=18" } }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmmirror.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.2.18", + "resolved": "https://registry.npmmirror.com/@electron/asar/-/asar-3.2.18.tgz", + "integrity": "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/node-gyp": { + "version": "10.2.0-electron.1", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "integrity": "sha512-CrYo6TntjpoMO1SHjl5Pa/JoUsECNqNdB7Kx49WLQpWzPw53eEITJ2Hs9fh/ryUYDn4pxZz11StaBYBrLFJdqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^8.1.0", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.2.1", + "nopt": "^6.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/node-gyp/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/@electron/osx-sign/-/osx-sign-1.3.1.tgz", + "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.7.0", + "resolved": "https://registry.npmmirror.com/@electron/rebuild/-/rebuild-3.7.0.tgz", + "integrity": "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/rebuild/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@emnapi/core": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", @@ -1725,6 +2276,455 @@ "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -1916,6 +2916,67 @@ "tslib": "^2.8.0" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@hapi/tlds/-/tlds-1.1.4.tgz", + "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2434,6 +3495,96 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2540,6 +3691,84 @@ "@opentelemetry/api": "^1.9.0" } }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2758,6 +3987,35 @@ "node": ">=12.4.0" } }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/@openrouter/ai-sdk-provider": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-1.2.3.tgz", @@ -2995,6 +4253,17 @@ "node": ">=14" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -4581,6 +5850,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@smithy/abort-controller": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", @@ -5191,6 +6473,19 @@ "tslib": "^2.8.0" } }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.2.tgz", @@ -5441,6 +6736,16 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -5452,6 +6757,19 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5476,6 +6794,16 @@ "@types/estree": "*" } }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -5485,6 +6813,13 @@ "@types/unist": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -5499,6 +6834,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmmirror.com/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -5537,6 +6882,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, "node_modules/@types/prismjs": { "version": "1.26.5", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", @@ -5562,12 +6919,41 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.48.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", @@ -6118,6 +7504,20 @@ "node": ">=14.6" } }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -6150,6 +7550,33 @@ "node": ">= 14" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ai": { "version": "5.0.107", "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.107.tgz", @@ -6202,6 +7629,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/ansi-escapes": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", @@ -6247,6 +7684,116 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmmirror.com/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.0.12", + "resolved": "https://registry.npmmirror.com/app-builder-lib/-/app-builder-lib-26.0.12.tgz", + "integrity": "sha512-+/CEPH1fVKf6HowBUs6LcAIoRcjeqgvAeoSE+cl7Y7LndyQ9ViGPYibNk7wmhMHzNgHIuIbw4nWADPO+4mjgWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.2.18", + "@electron/fuses": "^1.8.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.1", + "@electron/rebuild": "3.7.0", + "@electron/universal": "2.0.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chromium-pickle-js": "^0.2.0", + "config-file-ts": "0.2.8-rc1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.0.11", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.0", + "plist": "3.1.0", + "resedit": "^1.7.0", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.0.12", + "electron-builder-squirrel-windows": "26.0.12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6436,6 +7983,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -6443,6 +8001,34 @@ "dev": true, "license": "MIT" }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -6459,6 +8045,16 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -6491,6 +8087,18 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -6554,6 +8162,27 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/bowser": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", @@ -6618,6 +8247,239 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.0.11", + "resolved": "https://registry.npmmirror.com/builder-util/-/builder-util-26.0.11.tgz", + "integrity": "sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.3.1", + "resolved": "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz", + "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmmirror.com/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -6764,6 +8626,39 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -6776,6 +8671,16 @@ "url": "https://polar.sh/cva" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -6792,6 +8697,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-truncate": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", @@ -6815,6 +8733,117 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -6883,6 +8912,16 @@ "node": ">=20" } }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6890,6 +8929,115 @@ "dev": true, "license": "MIT" }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmmirror.com/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/config-file-ts": { + "version": "0.2.8-rc1", + "resolved": "https://registry.npmmirror.com/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", + "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.12", + "typescript": "^5.4.3" + } + }, + "node_modules/config-file-ts/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmmirror.com/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6897,6 +9045,52 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmmirror.com/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7054,6 +9248,35 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -7061,6 +9284,29 @@ "dev": true, "license": "MIT" }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -7125,6 +9371,14 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -7144,6 +9398,100 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dmg-builder": { + "version": "26.0.12", + "resolved": "https://registry.npmmirror.com/dmg-builder/-/dmg-builder-26.0.12.tgz", + "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -7157,6 +9505,35 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmmirror.com/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -7171,6 +9548,180 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "39.2.7", + "resolved": "https://registry.npmmirror.com/electron/-/electron-39.2.7.tgz", + "integrity": "sha512-KU0uFS6LSTh4aOIC3miolcbizOFP7N1M46VTYVfqIgFiuA2ilfNaOHLDS9tCMvwwHRowAsvqBrh9NgMXcTOHCQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.0.12", + "resolved": "https://registry.npmmirror.com/electron-builder/-/electron-builder-26.0.12.tgz", + "integrity": "sha512-cD1kz5g2sgPTMFHjLxfMjUK5JABq3//J4jPswi93tOPFz6btzXYtK5NrDt717NRbukCUDOrrvmYVOWERlqoiXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "dmg-builder": "26.0.12", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.0.12", + "resolved": "https://registry.npmmirror.com/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.0.12.tgz", + "integrity": "sha512-kpwXM7c/ayRUbYVErQbsZ0nQZX4aLHQrPEG9C4h9vuJCXylwFH8a7Jgi2VpKIObzCXO7LKHiCw4KdioFLFOgqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "26.0.11", + "resolved": "https://registry.npmmirror.com/electron-publish/-/electron-publish-26.0.11.tgz", + "integrity": "sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.262", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", @@ -7178,6 +9729,61 @@ "dev": true, "license": "ISC" }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmmirror.com/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -7185,6 +9791,27 @@ "dev": true, "license": "MIT" }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -7211,6 +9838,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -7224,6 +9861,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -7397,6 +10041,56 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -7902,12 +10596,163 @@ "node": ">=18.0.0" } }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7987,6 +10832,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -8000,6 +10855,39 @@ "node": ">=16.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8051,6 +10939,27 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -8067,6 +10976,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -8110,6 +11036,41 @@ } } }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8170,6 +11131,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", @@ -8229,6 +11200,22 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -8260,6 +11247,28 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8273,6 +11282,25 @@ "node": ">=10.13.0" } }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -8315,6 +11343,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmmirror.com/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -8477,6 +11531,39 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -8499,6 +11586,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -8512,6 +11606,20 @@ "node": ">= 14" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -8525,6 +11633,16 @@ "node": ">= 14" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -8541,6 +11659,118 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-corefoundation/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-corefoundation/node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/iconv-corefoundation/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/iconv-corefoundation/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-corefoundation/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-corefoundation/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-corefoundation/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -8553,6 +11783,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8590,6 +11841,42 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -8611,6 +11898,26 @@ "node": ">= 0.4" } }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -8729,6 +12036,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -8875,6 +12195,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -8994,6 +12331,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -9045,6 +12392,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -9098,6 +12458,19 @@ "dev": true, "license": "MIT" }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9123,6 +12496,40 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmmirror.com/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -9133,6 +12540,25 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joi": { + "version": "18.0.2", + "resolved": "https://registry.npmmirror.com/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-tiktoken": { "version": "1.0.21", "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", @@ -9242,6 +12668,14 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -9255,6 +12689,16 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonrepair": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.1.tgz", @@ -9310,6 +12754,13 @@ "node": ">=0.10" } }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -9622,6 +13073,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9629,6 +13087,23 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -9678,6 +13153,16 @@ "loose-envify": "cli.js" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -9693,6 +13178,96 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmmirror.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -9703,6 +13278,34 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10569,6 +14172,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -10590,6 +14206,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -10603,6 +14229,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -10626,6 +14262,130 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/motion": { "version": "12.23.25", "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.25.tgz", @@ -10825,6 +14585,44 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -10832,6 +14630,58 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/nwsapi": { "version": "2.2.20", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", @@ -10977,6 +14827,16 @@ "zod": "^4.0.16" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -11011,6 +14871,103 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -11029,6 +14986,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -11061,6 +15038,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", @@ -11127,6 +15127,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -11144,6 +15154,55 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -11176,6 +15235,31 @@ "node": ">=0.10" } }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/plist/node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -11229,6 +15313,36 @@ "node": ">=4" } }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmmirror.com/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11252,6 +15366,47 @@ "react": ">=16.0.0" } }, + "node_modules/proc-log": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -11298,6 +15453,24 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -11328,6 +15501,19 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react": { "version": "19.1.2", "resolved": "https://registry.npmjs.org/react/-/react-19.1.2.tgz", @@ -11480,6 +15666,46 @@ } } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -11590,6 +15816,34 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -11611,6 +15865,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -11631,6 +15892,19 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -11648,6 +15922,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -11666,6 +15950,42 @@ "dev": true, "license": "MIT" }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmmirror.com/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -11696,6 +16016,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -11716,6 +16046,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -11757,6 +16108,23 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmmirror.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.3.tgz", + "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -11788,6 +16156,31 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/server-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", @@ -11911,6 +16304,85 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shelljs": { + "version": "0.9.2", + "resolved": "https://registry.npmmirror.com/shelljs/-/shelljs-0.9.2.tgz", + "integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "execa": "^1.0.0", + "fast-glob": "^3.3.2", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/shelljs/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/shelljs/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/shx": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/shx/-/shx-0.4.0.tgz", + "integrity": "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.8", + "shelljs": "^0.9.2" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -12000,6 +16472,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/slice-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", @@ -12030,6 +16515,60 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -12040,6 +16579,16 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -12049,6 +16598,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -12059,6 +16619,27 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -12066,6 +16647,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -12080,6 +16671,16 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -12107,6 +16708,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -12250,6 +16907,30 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -12260,6 +16941,16 @@ "node": ">=4" } }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -12326,6 +17017,19 @@ } } }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -12406,6 +17110,134 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/third-party-capital": { "version": "1.0.20", "resolved": "https://registry.npmjs.org/third-party-capital/-/third-party-capital-1.0.20.tgz", @@ -12431,6 +17263,26 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -12497,6 +17349,26 @@ "integrity": "sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==", "license": "MIT" }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -12534,6 +17406,16 @@ "node": ">=18" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -12554,6 +17436,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -12612,6 +17504,20 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -12772,6 +17678,32 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -12840,6 +17772,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unpdf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/unpdf/-/unpdf-1.4.0.tgz", @@ -12982,6 +17924,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12989,6 +17938,22 @@ "dev": true, "license": "MIT" }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -13029,6 +17994,36 @@ "node": ">=18" } }, + "node_modules/wait-on": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/wait-on/-/wait-on-9.0.3.tgz", + "integrity": "sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.13.2", + "joi": "^18.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -13205,6 +18200,80 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", @@ -13243,6 +18312,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", @@ -13273,12 +18349,32 @@ "node": ">=18" } }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -13302,6 +18398,101 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 095eadf6..faee5111 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.4.4", "license": "Apache-2.0", "private": true, + "main": "dist-electron/main/index.js", "scripts": { "dev": "next dev --turbopack --port 6002", "build": "next build", @@ -10,7 +11,17 @@ "lint": "biome lint .", "format": "biome check --write .", "check": "biome ci", - "prepare": "husky" + "prepare": "husky", + "electron:dev": "node scripts/electron-dev.mjs", + "electron:build": "npm run build && npm run electron:compile", + "electron:compile": "npx esbuild electron/main/index.ts electron/preload/index.ts electron/preload/settings.ts --bundle --platform=node --outdir=dist-electron --external:electron --sourcemap --packages=external && npx shx cp -r electron/settings dist-electron/", + "electron:start": "npx cross-env NODE_ENV=development npx electron .", + "electron:prepare": "node scripts/prepare-electron-build.mjs", + "dist": "npm run electron:build && npm run electron:prepare && npx electron-builder", + "dist:mac": "npm run electron:build && npm run electron:prepare && npx electron-builder --mac", + "dist:win": "npm run electron:build && npm run electron:prepare && npx electron-builder --win", + "dist:linux": "npm run electron:build && npm run electron:prepare && npx electron-builder --linux", + "dist:all": "npm run electron:build && npm run electron:prepare && npx electron-builder --mac --win --linux" }, "dependencies": { "@ai-sdk/amazon-bedrock": "^3.0.70", @@ -84,11 +95,18 @@ "@types/pako": "^2.0.3", "@types/react": "^19", "@types/react-dom": "^19", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "electron": "^39.2.7", + "electron-builder": "^26.0.12", + "esbuild": "^0.27.2", "eslint": "9.39.1", "eslint-config-next": "16.0.5", "husky": "^9.1.7", "lint-staged": "^16.2.7", + "shx": "^0.4.0", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5", + "wait-on": "^9.0.3" } } diff --git a/resources/entitlements.mac.plist b/resources/entitlements.mac.plist new file mode 100644 index 00000000..809024b2 --- /dev/null +++ b/resources/entitlements.mac.plist @@ -0,0 +1,16 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..38abad4528a03a8d05be863887e62a92b78a4df1 GIT binary patch literal 55260 zcmYIwcRXAF_x}}5jnt|(_No@GUDOt(EoyZav1$~J6{BX1s-iaU)~H!jwf3e)j3%gx zRn&@Adn9HM{G#vo=llD^BR4nqb?@t*b6)41*YkOfhp#S{}0hr0H^~xw{AZ2rdmypd^J{GY@aB*WDV<4JUL16 zf{j+{`&L~R=fcGKdOOBq-aP^L2Ni+*1MULZ2J0GU(Ff@om(TtCwOrkJis$r{mxoc_ z3*)T+u9UD1z6MCxb|yUhzkyyhlv9&^&)$8p4W^7by|VTn@E4%=pDVfs|2J?lo^r}t zE9~(X^GVPS!ak{H=}Cm`VbS|*P334$v3p>&=c>sAd_4FX%fxYM*L+QlPmvL{1Ns@n z56?X?bO^qyU8B5PzO&|NCi)Ki{&YZs=Zn<>v3n6q2UxQkO-<2=&~o$>i2r1lk-u&5 z2cxOzlk1*WK7UAa6EA-r%n)e!)Z&QrjCyO-SMK;9=x2Gr&}Y@Feo5Zo*z3i+2P+)n z>M!Nn9IQu^Il%h^9Ub|EPtdwXl5Y@X>B>P6@jKEcqdSz z2Bo9tOEG4TSX}X8#-kI_;P>kVcaOc?^)~LrQ#A4OzH05q$7fVXRKvRzbvv98{n0NKBcUaIW!sse15jVp0F1aO0D=jRl>2 z-euPSUXm-HE#jTjBwnXD{r4#8a17~S3iS@87{Js~y7B2TE#whP=^pz^XJEg_?Qh1{_BU=A>l;<6A3@PyBC2J?P<2;hoLY>luu0fz zL~W+~q5Q2Hcjx|@)|r{qZ^z~b7k9#yCctX`4^Lh=oNeK9@^Mq+S{M&KD$Z_?K32m_ zj}Q2kCBxgaIQqgsybI4|#G`E|mo0`U&o@P9G~Sx{sF^5{ark*IxmIfPF7>X&zTfOd zw3<+uogw|+OZ%|Zmf-;nyPOQqPbf{ZwqN>#jgR#^&}A(m&JQe>l)cjA4Km3Zzl-%@ zn9nK_7A-4ASpf_L(92)JHRug|Ghc!-x$X~g@P|s-hBtzXpxP=Y6pqZ<@RCn!KOU@Y z!mA4VNADQQ>OsUVQrD;rwD}6tdnx%vjj|x+Hshq&N<47bq9~aGGVj`KzTv~o*oXi$ z?6=`;O28tHoOw1tGO>Z#m&wcE^tqEjLw~W5apLtE-`Y-;=&hKBu5Q$#Ig7nsTo;<>soRY7z`g9u7G$WM7~jRv2JSL8MM}eMb0tU`jq^NLy)R%mr`k zF%9#c-Fb}JzE-IVQ0AtEJbEWc2O=G#jo9&UX z_rThKyGBxW-)<2)75Q`>TP98_sI1~RtR2oZ6Qx#r&}vjQ&2jE2U2E7m8IeI`;(VQl zH*VyKdzWYXMqGW!FNQQ}+tj;`v~;{=S22dPcAhbTI;{gK$xRz_NpKSTnREz#d7sV0 z+i!M2Xn7$|oZ?Er;%})X<<9TbxZoLen!>P`Ea_qqbqx0As^i2xdaMm{eBR92I?^`` zXt1V%&lN|ERjo75$2}N&q=4dLu9GZlxoKXJ6~1zdtbw=_4<|&5>yG~}pF+#<=F2@3WR6AHkZ$nJD7PvqLFETeGV@`dTC$3JUabv!@Cjic^0Et;Dl5_`?M z^Uqs9bB&f+>r#1az3j>ORnu8#d4@oPh!r{0P74dFqH@G^P{5@FcnD&Ic zg=5?95S@J0`;n0));C&@jZ~(}@3b(UI~4l;yPRaa`o{)n@xj@4D9$|fdQFdC*)I7^ z2bs+pq2Xm{PkWAQ#Q*Fw15K+W52yW!P-W_GHPXVnKerwTWN`|OvL?R;VYH{L56S!C zOqIBPL2hp~CgfsC@C^)%jWvNxg9?9KymAlH6(UB2TJ)K_1YZMQj`rp*%O0}48gupK zL}f+bbMYaYbhD-o>Wk+0Y3%DH@w~N)Q;q?2b#|UN$`S*q59e)y3T1}yGPVfb2|@Rt zXC~zI&mcfE-cOBe?vff(^cP$)82Qe}c_9+#CFs0FPUuw|a2EDz?P>nebe?JpY$Wu# z=v``^^vju3nFX*IR{U8dUmLSKKJ5iJKjFdQO3_NUG+N_pFo~ZYd4-+&`e%i5Ph0?j z&XhaN@6bBOMR%-fq{2I!Za-A3FY7taO(TEo&7tJWTI0TZS$63uv!8*L{qhjM`Q3?R zL&+-@+3VVG5w|m*g2yxNLD_Fq?w@<<91uI6{xQb^O5k~*r7RZSrFnTs53$pDGCQId zGFJz(nCgAbi&0Bl)U03*+GSAE;Ym(tmbN#Q?2tK&xfJASKne?3d8LJd&)!zXpT??E zGmrib#`NbOEkdM!pAfNp!kw@^{ap^sOpx@@+ygB1AE+n(tqZ*h&9lQ=rh)fyU&F@* zJOo$nROhANuxPW;aCYfqn|^5kQaz?V`rh8wA!MUx2bZsocd;7lP_gIOoDoW|kv0oB z`nk8>kj=`#`QdW>=tyEM-OzE8q=f-J5E%^RNp%rK@kjWI$^_7$HG-s(3!il>_xp|J zMEx@Jxg z1skIIE~AHtWKKil$bEk#LGEosn)08oJZHySx?U*%wzxKu=a@9!(P>t2&aufQWQx@Z zG|~P-<4yo1?0IW!+{T%9wOTM$uaN5AkvSC|pD8;slFDQDfgRC4R^i6=;n)dEbh2!* zQGh#_YH`vj2)%Sotf-C44951jS$x>_4OJ%S&x#FUar27Ux}d$ zG0{qY@7er)aow*voBV8VE16EFHM~OdK$aDycfewo)}#{v>As$$;<|gzaF8w)wU%+% z4_U8KzX{hTjWZJgS1?G11f|BqZ&% zk~reahhFy0@t%TX#o~z;B?2{hw`7v?KW52osEo##IoY5(cN5BUSNy+(jXHb1E$(#9C+`UfT-TLbpP;OeSG$srp@FVoO6A2-H)oDig${g3Mby}GB{eS{(5P|qV6aa;T)_I zfB0HLqLMw!<+||nhwUU+(yT(+xrxEpnYLJ}-hodOOO$9IW)0qhQ=D(iPmjB-r7@g` zQgG(9U7~#D9x^NMKwirooamoElxtuvO7nH4}^~K0f%dv zj+X-u3CWAYSu2Gtg(YK(ud@`TxDT$6pD};-OEkRwf?dQFuQWNL5C&h>VZ~|6Jzk}k z)9Cf<_%|u34C==KSJUJAQPdw(#)wVQq`A5#XcQ1%rbBx8EP&9Q>w(e7hZhNBW zqSw;EigSgss{@0`P|e~+={PON+@vdvwOS6v(EevCSbJdT%W)WU@yy!d*l9UC09LdG~f3v zkg=^#KH0|ws(0`$cfRB;ZkO4|PyV1EU{oVmna~br#E+lfCAXqn76hxZb)+5%u9okfBFx zdTWVYaGGRKr{eIyI^7kEWni;qGUck5&|$_dBI3GGfVVZDX(~8~&O$qimFE0Fc&2I3 zZAQbRjwy=G)6PZKw|%gFhF~I8F5Z7Ktp!nWq6*3pGgDkT%w>vFh%wmu6-Zvi%TsB1 zTP4ucuy=Uu{4mHoEK(KQD3r^w{~%uap0A7aLEnc5dOKc}s znvs3s_7YDrk~Cv#)9=3{nad8?Mb%ITj03VlRyBE-nehE2#g~DDiiXC(Ev*?#cbHOT zXU6bmS$yeI;-Ef3dS4SKF63kDQ{mlKdgI`8Vy){>793q5;yOabNS%5?TTHbSdSEU# zHT_2uC3#?BZaBlBBa7j2mEcFdC9*e1z3X-A32S~wxfS$`y2M)mbNv>t>|7hf(TuQz zF(Kpb`qhH~EWxA|X_Us5KU4-cTqVi`LV)wIT5ymHofxOyaaT-d^Hbnfdd_5jFgE@6 zi|MCj>KupHo~v3cUSYh&v6k!Bl+cOM8U8k_ zOB$}C4nx(pM}wY^RF0&iF1vaD0O4$dBgbhW8WJh@=GiM(f}RXnyk&t8N9f3O$wY{T zsWo6EW{ffr+@WD*MgZvy{u;OTuD>L|8h2eW`Mx}{wuuZrgnD;@)a@K@+RO3`-_qZ# zrj;r<%*7wj23-nzo(<&2shc}bsu$`!q=_lYv(AlGfGuBxsN{j)*fhqKNIEAZS3XTZ zy00lTcb00TEyTikhZgpS*vqcFZxIqyW>|G@+?aVPxzD9VS$UHhb3s}@A}==WGKlwh zVVvO}@-z%h_0`5x1@u9SBQr`Y^XKJRRz{#ymywChbi5z&_B?C(AX?Bbk&qpU@xH?wLaV$C%$@Gitb|wTXg?K7May!kz4bG zCdcsez9wyEJJ0&2JNr~4G-xb_q;O6Ccq2`2sNF?ZYZ;<0cm2Lu(O zM_Ue>JVhqsN(Dy4!K*V-zkb!A%`O7mrVYi&I3I?gkoRzpDKZam{u>>PjS5Or$DBng zwInz;bPZtt@zijm`{|LW(bS-LKG@B}nP__R-TAr+&_Mh)PA;@ zoXDlMb0r{Sl;fQ@h2Gq(VQD58@@cbJwz4VJfZxrU5ctC|Uer-3o6upHxKtnn*_GP*IBwJj(*|DXz_uEyVq2LW}35Xb4|!SYa9?`2q8bzPPY z8%?#uzBXB9Cy{(rC@Q_@zT_bqDjvh8W;`W94=^H@>c2lLf1^*+7J;lC;sP?s`;mca zJBRm!Txj)KunmWo;;=CQ3OuNxxHS5i{M6^!7JbBmp!VV7fl!eyHShFKeh4U z%|bC!!|me^vDWMNeR(TN{11p#FvF8$=*1Ow-RZEWi%KjK(YKrI z+VKqlf#7WGpB9X{QY>>jJkc0vj|IrrvzpIqyCn(nfvNR5I;!f{KcfpZa|=GqYAj8P z^h4fL5I!x4cP97Mq_#Lz=b=kYaeI-#Z*lWQ1t~`nQRsI^o`p)JX5k zl{9s`H~jN(rBB9U=LZE+#FnEg#t&BqNQ}-bQ*H00helqosLw1|u**(fmnb1}gsaSb z5NuzYm17AMlk`1uUjGOfjbiecn1s9a^Do??8CVJu@<|NXSimJHOg)+5zF#B-}I8NqwbTF`B#gdl92IHP*QzO_2N7pkbA9Wqdm!v$*7sZltu z(?i#C^ywtR!(J9qw+XlnTn)bLpWVr4S78hgp!`CC7LS*byKcT;snbeN2x#ZQgVMdk zm7V&sDNfnNiE1MRzA|0!w+>q{y)}HN)uprXF-xzB^1w)Q*~_RR&BCMLkuG?UfI}j= zOqgzbWO;0exP}OC>#q6!nyH6VeXVgJs=Oz5viTbb2(g$&yPoCI(CY5ZEwcWe3sah! z`Yhzj!>Z+}7qCD*TfeH$gWjr=eebiDDTA#8{{Yogqx06+{D_Bseb!6d%6#l%`aZvs zXv-oN3Q>z81B}G@*J=&x!ux@_3@UB0EXUm|ft=rO+FyW#?QgZEx{06BJMR5ujjS+K z@lFDx^KjH!gnr{omDj*`7C`a^TEbDC_*ZD;e#+PJ+V{Rdj(NPfxAD|{@hn`kkd)}` zeJ2m5L*KOz)IA~@(x_BvB;|dh$g&rxE<1XQKSKMI9w;@~^31M~rGc_{vFC!M4L*=k z7L-^fLsT~&hlNQ3uCIqKrr*zxqNH?*^*WOvby}21565y^#{f$XqiXsk!)Xepjn;(> zHg5F`4S!s{3Q|Z)CiGn$SR4qCw2r!*f-@wq2P2D1n#tFNPNXTA0pLz$$r2{tx3%XJK?ih>7=;R;}lp;eBfj` z80g)RaEZnykV?Vo8_6H7!#|K^WQmX?T6KFCbi-mCix-3C%5opP;kGD5L<@>*~0(V~dBDa}AZ^)}pXCY41p zENV!EoFZQOQnD}_Rw&(u9Zl(K@xbTUlfMq!wCf6__pP`aFDV~SxWyH!vtHA~` zI!qi;h%#qk<`GniuI;%EQ-XipNP`Xvu>Zv?TPaDGeek4eUk)rMdPF9EPBzta_RU`4vHq0!X#y|vE{zGr zu7fC}W#m7W$f@q+}nPe?*$&bG4S13fzwkjb|OZnWAAJMz{b* zcgBRp#88=;vh#=zn4tD~5G@S>e_ek$`ihukdDAU{tU}$zDv=PH?7C>8o2Bzbko5cc z8X$%vFtnd-xTGZn%zG>t)MmgH-UDh6c7rLstM%Va7o1S*6`kp)KaE?b%+wUSQ%@pk zbpOLso|&2C)iAo*RCfXOn^OJ31@UC9QSw>zh-f#ttl79k*w&vCf|Vj_880T*Rd%x; z&aliFZZ}?%D6;$?GZoYKJhR<>VGV9OB^0k5F`}>+{lS{g#NK*Sp2*~JC@rzzz2^V^ zOYPT|F~Slh=j2Ykm3nWXE5;TUt5wgKl#RG3_LsC7E=LQrIGb8Zp4=(ZuWmCD3aD+@ z?1!~Kv)}q)%-S4C1Mtq4)@}tQF1Ee3ZVqUuL=Rj9Qu&p1+x6#rJ)|6V|KKQzPQ7`{ zU((DK0vY!keb_8*V8wj?h}IzUI66ltaEhu%iam>H9FSf8$>Ql4$PfnE=peR&bfw<*^jKt>KK{q&N-FS1F`_m-3|BPHEwSo*E0}VqQ=E^6bJCXmg zeUlxajkP)>6qBpS*=65!aK;y)_mR-%l|`eBffY@h`<%GZUvkgcf$G8R17B0lZ+RI- zRR$hg;_8ve;5%Z)-VK&nK8sf&L7o-diY76GZiP^WtEvac(AHCRm7~|hToWu^YF5|Z z8ElUeD=nnGz7>20w5#9tup)m~_s>cGd;iHU(B-~lZNcLwO5#P|zTi!1D`%wK{I>Jt zGJRIBo}9N-apk$QXtb`0zx(jce^O$C8tE6hF%>wKSinz!yd{6EGN1*ueHT>L)U^be=Q zDd%VTF5}_O;$*h?U{43c3I6rpmO33Q8S;9kW z6hy_0s#pH|5^c{@M(-Di%kTbOE|hD2k2i1oUfHmxVB=@uHeo-Ga_Z9sDcf&fEYLK*ap1WsU`6kMfBL zqxS!d<*BcsFF8p`rvyex6sBaGgMnO|+dgh}4-gKc7{h-`}bHzAP zv22I*cJeQhC$XUL?EL+S|44H0P;gWmLP}B~@YB;~CyJ&(gKh0G;$9nQ3>evw*8^YI(o4dX2*};p-XIQULe`B77ypWT%AV@vtYuW*44hx3 zI%km1J>b*Rz;g6JBULyfOZpu$_lTR&pGwTzCx2C+@e3q*KENA@M6#yon!L$WLn(1Y zHq5r~hq+xUB6NZbp90%hf74jwGmx8R1=r1KIvv1*ET64*mv#3rbOy zBQDd)(*4#JDjI~pfwLSf0}skSZc99g!ZuY$lzEC~BnGDCkH7l6FoUkTCivgR{(dE@ zWvrS!ToCZD9YP@u;guNk3^I-2+#9p%c6^Zv&T7GZZ~q@KTG62T2A1g03_Y7-e)Lyn zgpSjif!U_3XCJJ5*IiUuTX+u%b~MDwG#(^^Gpd1b#;iog(C&Bgv%A5RjOf%!*j1t< z=lG(g*a^ABCsP-rugs83ZL)bzLAqYIf3(L!5b%ZeCQ7n-TBUhc~D#h)q&(%oGci=fC zP5w0vCMbL22{Ud;LLuC*QhM@$1u#4jbpgWbFw?Zr134=6Z?o&(xaybK@dnm|)bqoc z^CNVWm6B^2?KjyToZ*k$zmeWI(^}=$>NP2&Y)KK?J*amxl>d!Wo!I%Qg{70(w8Dsz z%Yn@_>MB(n)-tj}kmwr4{hXvSugDl&#})-y3@fxwChPPlTwu#wSTR+wXs?jzmnTAW z$Vz!Dwa#y%7FzkY9yR|}^LQdS9PU@r#ucQv&nk%B0M1FvVt(a;&_-giJO zpUITuWATD`ICTn|qGk^R;$m)(ATfk`cHks+E6eaA72?SkBE=A=c*WEkMt##=8MmSu zg-+VMfcaR!$~0O5ndB{YhR?Q}xo$hhpIB6{CWwD{5#TM2$sS1d*PjzQ=B=bHN% z3f{5vP#y9T_q_UFZV?F%xBXV)6NBs}*>CdAI(kFmSeubUbCR-ZT6Ht66ZLApUsJ5m z^_&EMjJ8Hhob>s+o-0uX$w(UonngTyDXsb^wv{I=S#p1q&{+Fm>8_r#8|%1sN0zu* z(aua#Y9v64`qo3mX`N$Aq|IRr3z(n*`;KSv~7w}bFOxN5q|Z2a@`8$$D>7yZJY-fD>izFBNjs@vHXJLm&Z zfbPsw&5{lN%HrES8NIgIdrrLq(kOjId9S&?=NHu#;$;zHnDX% zaR>MRhKI>s5GJ=!BRAwjTnjuJIU_=pVWVn)n9vvsD1d`dZ^83Y_~f+Vhs~eaD#w#b zrfg1K@=W$hZf`sCn#B1V3K0{-M`gZp`-y`(GkqK>E><-_5bjb-8SS+DWcXn0U$^C& z4-MD3I8I`F*8V4g>HcL;JSdLf1f|~9Kkm80%c|>89Gnt0J>&JF{@cTqs}LY9^vt=z zXRbo5u-Z--8_;l&5AYYj0TCMG4msl1C?D_FCkHW*l-bc9nxV&kUt|3zNO~xn3&knz zShUal2M%Ns^mo-QjuzyB?w#2Y@lM!^kITyjHdID2fR?t}jIE7ew|`eKU>y7)K4DO2 zzS95C9FruNdb5^YizW+| z26af7c|W!vFf%H2!7Z8y1#1P-+f=>6Ufj6lcdrd(`q~g7&!6%0b>fGg6gzF^<8Je* zI(B^hDn@`p2>~K}{*V>?M(V-sq2A=%4YGZ3z{dXEgb5HwKKhKF2{tM;9lj&H!@6s4 zd}6l&v!!BYG7F_Xzf|VfSve~_{3nDrSViMR0RL+lPA7r zb&-oB-%v&XOCvmE=h#FgRX~mUwXtw=IWV<><&|jc&fx9O@7yhXTauvOUnq2p(cNmK z3L6!L2mu!Dd&L0n%^F||5~3s~{|4cHZtBLI$jTp-PdypPisCEK5x>-dcr z4RyF%@+!J%GB%jmZsF2C_UZ8j;BsKvyZLC#RvhzhC3jun9SMreD#2ZN38y}6T+JL? z%x&69-D8Y;V&IBkP%_zvTD{xD{7qCD6!Ar~+BY_1Kcn&FA21LW6C!qzN+Fpsx-EVV zX_;7aU3*d}5E4w=;?xm5j6}X=K=QBnmYM$xv-kxO&c=?nQaOq{VpD8v$`sv$j;~{pNR%6{_$I z%n94ygLi`$*|`vyChx&3vxGrUGl*#`Mw}yKD67=-Bxxa`bvX>#I8LAjP&Nt^nNr{UOI_V+N*nE z(+Ntdex_@l8;q@p@#c;6mHEV{pdkIKKYQzQEp-8kw1C zwfSdBc{>`R4goT!tXpxiFi0un zM2{wRf4QO_Z9LL-v8OFU|9G6;qB(#klcaOdh+$9{2-5q*Nm$eBNHzAd-ydyFTzC}t z!D(;gYNlohAC8?n6Li;3pD-C}dZeVWpN*uFk@)Ec(2DQoDa!w-vLG~xNo5@P@7fQo z#L{^yc=30_gl!Jny(MYU9 zeJ{?=fbZmEu-|P{&aE7aySRHk>ct});3+*XXWf^d9-pjsPFVd3VbTF9q&8L` zf2FXn*ZPi;(P8N$G-1!nV=Zw=b?vCdUp_(+d+K^ z856^q7}#)A40zpiF^Gm!K1o~{fIaWnHU}-P5qTFCr_JE`@Dgmm`=Kc78I1+EVq$ko zPDy7NPLWL{mXZK>XX9TW^` z>A71dP!cD@9D5HB>aQh8OL`Lysz^;>jDs~#`Z4HLaMf& z|i zPE=C+M=fhiH;t4Sp&?wTSK_k#RQE+n$C6eqh*^13o z28K1z4Z;3NtCJ5sJl3;M#XEXkk1wNUMv~*~MAj;fsLr?PZKJPn8xut3XOMc$se{S~ z%}qi!4aLJ0I1CbGU+-Pv#@Z)L>lTsd$ie!G^};lDYKQ0Z^MQ^+CyOLX;$|5v=`WZ36#>61K9i~xvV{$jKx)|T({76|zw!QJr!umLv$KZ-C~PvgS_r_VUJQ@fhSrTVF%GiGeUfn4E|fGHQT5UqDoNwZl3q zPio2me>syl66*pCKIU<=IH2fw$wRzuT)&ZR%M ze57K1W-hrjg=XSI+Z;~cW-|;n8$Zt=;h}073B}^+n?ud_YR@V;Ii$&V;^*0J6Iscj z5JeKzy%TD;o3XM>Q94?T_}!tCOLIEWI6pr_FSTi^>Br8mbx8`-Isv!FEZl#PTHfoT z7~^y<;{gt?#ADa$G)Vv328>4 z=%CT_M0L^UDv$L?m+5}()d1tcxf022h(CeA!-B-!Hw%`X_qp089Oxf6d=%CWqw&eu z_NFEbOvOThW1c*Mg1!hHcCC?^;xW0P6R2YdU;lP z+F+IA>Y(6sxfIby2CIS(C(!WWQEx<#U;e~E#xJmTq}>ZxzxKMKs~hiTDw(c)YX21z zEK)y1n=3@B#Vjx_a}_hS#uYR@E0S@nLq1_YrWloco~G-A|ez(!E@n2>5Z-Jr0}mvgr5vMXXq&v`51xOth2`X znr$g{=s&^FU1{=D$aVM6&9l7XS`F4!u%<+CgWZiUReb-|`_l zLtO5OZA!-<&!!T3DFs>Se9!!MUu^MuH|4w-W&xa=cqlKjMx z*TjAkJ=> z!GfusQ_%iZG*a7{Tf((?oG+q{iF@pWn1(C1!GU)Bj0rzgBRHj|sz4KRDW`bF*fJJ1 z{io3VhX+0I>T}EawH+2b^gck^4d0iY2U2L7)mGNms2vWh&yA4&kn~+9fpcpF7BpcS z_Yl^35_r52h`GapR!F+O#ffyZDdcl;row68HbL3v1Wt*Dz?LG#KbPDId@y&%d|U~CbcsY<|fg0_}XpA9T?xaeRV#OsY2K4~)v==>q+8;z%;bpc4Umj2XI zY{1E3^T}Z_rQ~NDHGKYyX3Jq|(87f(;?E$vYVfi<;+}Eg!l<~(U_RmRgf1S=WStrI zUTWxNABgz=Cwi+rxbCV}{Sj@FE2hB|8_;rCrf#rXWCd_WWJFfjFv>P_-`!lO`UVET zc>&ZL$s=v7@a@Mo%U{vd8-)3w{92+Q!0$r!CO;4cB8`z*fYXTu$pe>9PUpo12@;W+ zMd}mkCx<|3xc-aS?|#gRQNJm0u1}M-x&&56jd3XU>#3v|=6~IUcm9%YzvUk?5}$X) z-C^DQ_=>UIOE|;`Xm7{2>g+;60h5L^3s0%_DO;|l`lrqTpaDL39R3X9S8-_2CYc@G z3`VEIwh)1B@StYmZFM#JsY9S`446FUT81(CR#b(kl7lR=A^iQYdmR&P-=4-AZD@pZZCiEtx;!qv7+fii`_fla z9c1^CL3Oul@<$*;dZyX)i-6Y*)#N!pNU-w#=D|fH{)~LPb1TpW!A*abW*JHO4 zH-}cL5Jkpr_#w~&X++U|@?}zNoiD|i;T~h7gHie6w_pBg^C*&oUi{n=X)-mw<&oj4 zUjVg;tQHDGPM#@jVqiayGK;kfc1Jyrz2aPk;YDSn)X)bhs~!30Ji6(M4Xe6e<^4&u z;mX~~bJOpJQujDVU$783r{(q}#9YPkOHS)UD?8gF2d9mDJ=?F{x%aE+8mi_Iwi$It zsV@Z}?J^f}t`1jyWDJus*!4GsrN~2(+pkJgo7yk%@8_zKd<)FQ{gU1FZ#OcYRmh&6T4yO@ijC1ws-wSNz-8K$|RfA3JoX=mZrl&e#0FH0+BeQ;QoC6i>A*X41jC+#z3&(Gi1Z8og3w`yqzNyGhwO%Neo06*9 ziV;%PU01eTRrz&fg&q_1WXep=%pn&9CFP4&QnzJz4`b{Ux6Q(Ke%(}A?l5+ATK01A zHu;h%j9RPV>PbAPH{LHa7hZc=;kRe^!?5>Sf5rRz?{~v1yDg5^{@*lJMu49zL9>@W;)4tp*4J352G&`K*NvAehD=i@lWJ3 z4dOPHVx+^$pyOW*iAC_KX-Lqj<%*EvpYRxZ1NEp%cK+{zQ8y1CZQA-ynkK~$CAod| zJm75LR@BNLK4{YwH@K?1nPVX~Ei{zuk;7*TyA`sS_@XjbH;t7jg7qAHj1to$GXh(R zk>U)Kx)w>@`$=Ck&A(x*D*TGb?(>pMgY|4O(R0Ublr}oU1N3{9d7FCKKhhEbJ!&s{ z{+3wTXel}San1YlN2@0z?awZp+Zw5UT;pzbC{$ZbUQ;$ZdDX{joHFd*v#q)!qu7tIIMnujlSevqR@i`#HSz*0%CC+d)5IXh=YUx3WSMk~E3*aP^2V;{U z-#ax%2#4s;T54nF_5L%5%#n)6D70eT#=~Z{p#z5Vk*~!xQRRC3 z^ci-+l(M8A8SIi+x$C7x%jHWk-Qls!h>gk{8hbkUP^Ie`&mr>X=g5PJA6!Aw&CS<4n;$#%D+@1X z;LhXZu)BqP8=c`fd??gi)E}yT?PJmhkNq^h!%z#%X_-YOw{pK#DJxd-yI=2X$$w&x zuapew_a`=o)d=U@Zhk*LWP+$Hs&AJuPptFw|EiaGl`PK9X-(>)G%9Rp9-j7bOb=TG z9udY|no*>Kos}?r;K>nng#1DB=0Cw{BO|)2S^@qS4u8|Oci4#!kvISRARQhD1bi)u z-=j3r@PMkf;Bh-49qnPJf$GpJ8LG86-&NQyM2u{i7rT3OE_^stAr24(iRXCQ zXlg1O&h8@m*1s3;eZsau0fn1CA_Vmtz;wlaoHri@5YL&}#j>t?&Nq_bo^|iXi&lPk zIjHt1vU(c760hsk>t(sa%}bt#QLxPskuxH|BDGG(1)%6skdM%A?USKIY3v`n;%adH z*%gjT9=n*PquE$kMt2AnQ$N%5v)=Sz1K@tJNaa&zmFW887XFcCaEI|kTUD->8vaS< zoU=3stEX(sKmYq*0D+O8aMIY1{IeskFSY!T%gN}UyGm*OpeTMsD2y+?rAU{$+7-mz znU>E_E$+Q8^Sqyi^MaswznQLTPpDs-FVjZdfz-zNm0O3V8k2R05x!}nIdV*Oq$jUg zyw1Tc{XU%Iqof!Fn;6MuIAI`}kE`5TQ}~d;q7vL4%|;u;Ua_8Oi;OS8mx#)1&GZFQg1>ZW`lv5IVMQhM=`XWx{ z*9l=d8v-yUw+%m=3D$(aIIEEP+J7hYnFJ_#7$L6u(I5$3va~w<&(Ub(#;>(hyS_x8 zqv&?7!>RtD%f1$ecAuG+?Sule-O723c6?t7)>XuGW^Ub-Kp4@^9-eWu5fPs4j+8z4 ziIA3QPAf}3fYMlWE#H6nWstQZG6Uy0JSTd+Wa-cTSB-T|;l44w?OmCO8fpq)3r}i%7@S9u`qvMYnvTlcYp6Xw9p@sKiQP& zm+mWhqULlk4j-O)5m~f(z*!XQ_{-Mp!3XP~8{F8FZ8^(!cmPe{*Q=5UYM$fw`su8h zp0N zwN3|;yP2#zDfgLIe*`&g=Jiuz{u4B^J?3qsyGh6hN!3jEz<1sUmSQjys}n$XLg4{n z5=f3Xebsel46;9+Lq(YJyctH^q#Bhwc zem%-jtYXv*It$+n4{I0?V_$}eGOyq1v1J)BR2eaI?x#bEWSw1=t=-BDJT3V%DOzqO zJnVilJXs*}OV^%t+GQnSC2AHH6na(eIRBu#MRt^FfgX`6pX;NneJ%<;prcWu%C_Yn zwy^Bsz2ZNp4+YxJXZyXp`1CD0_38 zHtw^JG=Ap=#2(DL7$4KOMs`!gqE7E`GrsCsiMbS|!Ug^wh{$I3paR%PJ<|8L@{O)< zY^GnD>!mEMg>Dd+Kfk7*H((xed#{fcc~!5#R`0uL;-<|f#!pysK_}x3scgOiIamu; z(iXFYm!C^}V#Kf!)Y;hW&BQHwh0HaL{Mv(f6klj;%h5m;$vciZ`~_YV>V3o4i#MzY zc#}f~#|wIW;|g+vN+)}b5I5;ZF?xvu7C=T<`Gy?iwvy@t2}NDh%Qj@fGy^XppKkGf z+XGJ-AzyXX??ILvErrLPX6L-8GnF3Icm>~27`gqR``ul_om@xSG_RQN+Z(y{C8tco z)@EYp%awN@YlG+*jIHFUy}I+dVjKRI_=>AVdr}|eR`MG{qkd<xW1Q(TzlSeyzYDwoj zpQG90(RmbpvMtyc@NsAq)F35LFVBf@Md?3YFy($tZki6>I~eurZ)opGR)hb|#04h~ zlg!{M^2rF#%b)7>=`^d+oQpTfRVx%eX=_H4FG4{P9%8T;#vs%}_nUFq9)N#zDzk!a zan%)b9bLsW;4IWgDJSJM{%)sJyPg_#B7DlC51iOxyAN|7`GabcbbDUbriZ@TjP~tb zt)W~A8}6}abAXoD_txtQ?DMnlFGniMw1B@);g7LH9nbDj%Nh-=uf*b8+;1Ks*MXD< zY28Y^Xa4FO&VLU9*0+AKMtP~yVn$kW+qzlnDA$(jLM!O+rm9MPZ#9-vT*ZNvK|{^D z-JID|0hNO2GU$F&oi%h26pXZZ`>Fbq?yN@8X76dt8jtRGqDGT~*tiivCE4ST0^7~E zmB3BEq>rs7Hz9m(R!L|UHx&eA-P(2Nb-@iP^sgHB*YftlBLOaMB1y@(`0|nX(ksq6 zK3GGc<2;MYn)Zf)l{v#p(4W+r3#XOAo9?!d9WJbRUi<^8OI zn7_CsRUTI+Ug=oiql=XEndcD#YB)On*=oL z!hDnKu!zYPb64%)-IL%T_rf}?=V)!;d_U5B)VUUWRwjd~yLs&H5nbEJ>?*}l?f1l>JJ zR{is5*2wi@k=ZS6Ib%-tOzBfVZ>5Z=r$g`^a?F>)_5I3UK}EqXUv1i6ytg>5X$%Q^ zH1Z;i!6>#D?qF<5w`wEL4A|;^aVS&Eb~udB+v7W$Qvw_}4v;}5N8Z-1|DnX3DQuLG zYJt(6xM=>SRHFAt&#!{J3_sRrP&>SlX9`nr&!(l8C7^y&QmFm+liOi-GLTU5k`Ja45jdSc)nK#oGuSTc6MpS?2YjcZscG>&gLr<$xQuB7>#2g|!oH1~w+@ho+Y4*0fM1R)RWl*s36(LrS6{>#nmMs-Z-<9dPr2ol?tlIouGduT6-U^=Z2Be!#xI&Aa9r3#>u^MWG+RnyxN-NzF}U9O4Jk zIDr(F4A1|B>@JwgzpxyG2R2;MKPs9rTHTf2PY(MQ>l|3%QC9Gj?$5tJ7c#f=O-auc zRuM$z))107y&W0X3|}4YX>DS=gM6n|n7=<5|N5j$b6UP!U<=RYi|mBJYmS(=%~B1f zqt;_D;^rBg)K8PXPCp@4wUtKy_os74=K48>efB=YLp*2Yc5axw#InZ0 z@{z+gx5!et1x3gQaopqeJ^UOqK{|SPfdQ?2?NTy7F}1zIia((j%+v9%RUsd=Y7jlH zqrP_|LB-i*J)F~rvQ?4*tg>D|dYzLZ?6kB1U2z-5fBbWUVb~gaXIU0`^y9$wkpG8b z@9KUF=A^ylh4sPQs>OM)JV8F0e06{z%T8YI^V#rIZ;u{fmrWX zitUDD*IFiJgGQo`5cgLC55b!{s-c@yNxZb$BwW&w7WY1Pw|PITy0c(UuV?$-OWjlN z2$y??;B1rT@BdonN&_?0%v{$QD-pCe)qMTeW-u`O5w?KLD!-fu#$_o-r~4sm7<$ygSMtb- zdkk008F1uVk_k^_-J6QYKbYw?XKp7B^@~Rb_nLg6Tr@=_RN+bSHuzDq<(apVC6)Di z<+uHBf%h=4`g8-Ly@|+e?My!P)pmmd=+xv$sW^H;p^+LDCIsJ>kdJXKTUo2iifR?V z#TW?-lY=u8fT>nOx4dpbT4{O~Z30XBN`2CP8!p~_f zpAK_X`V3^)Q>#+SJ8~bKrX$+o+nFdBbU`tYhh#~4eCizGJs8zN#~pVJ-b%NXu3gzwp z8X`C}h8pXSP={}3$r(GAK^hxOqgLwohf-RZv317FCrvAbGJ@zXZ@fU!u-{?xRuq%2 zBrD3|O<%?>1hE(-rAzn%*~AtK=vE3O2>XY7=aie+A1`54vq?Ddw;i*Geca;T0oZMT`s{FEV|L61O> zJ>9Ga3D&qJvCky0mRlD`o_piy9H0mOhWxK3DpalA>&K#|KuPF*&Y@ei{! z;^=6Pj<4$8t*z0@KJ#(7O))%EOeJpbXTS|SAE^lG^w3zvzBVfK?k7DS>XmT8canPW z`$D&%@LGdI*9-n|n7D+k7Lu;4_@$XcfKBA0oW*Q+!?C$tQehLL+)%?)+AZ-XDYB19 z&7D7+l#SKbgoG6_MtiDwTxcKHGLNJgH~baW>Xym4;1FKFNY=j_cO z!RCFITR?F9#8&%?g{i{vbO&y}yv+;{F!&L;<;-iZ5#-yRWUMv*tyqacFE(5oW85~C z`-e7^dY5jtK7<(-QLI3EH^q1;72u)yI=fdSby1vbN!rqtP!}Z!bYrNQhFX^a|s_ObRK$}N^% zn8Uxz4YpM)P37C$Z5!%lIkr2mifzTmhpSZs-L$0#wNhJpt+`zma#QOSxoB@NTG}m2 z$W|Y4E|sbWJMM3IZs)P&vVy#k7RIL-TyuCNd*QJOy^mp868)lh-=}&yfQd~NKnRcg zPHv#U7VU)()6m@&uB#ez#tRrGj zGd3^AFUT?b{7q7X(v7mro=J`Ie~Mz3OL^+}{dZ6m+~@ZASGI#~CvtR}0h7G&YL?d4 zM@j&MrAxX`)7Mn|sv)91u45zeeJHJ2&Z! zws>xN^;F279-%R>;%t-xW66kKYB5ngtV4zI@kPULxxPD9U%4?O%k>CsBf)#@&Pi1u zHPl6?yt@LqMf!)WkA&ejQfVYLH~~9Tkz44OQzzDhnd6)?2zGlgy_Pf9Dh^#Sn;G~X$ixP@ou%U47x_QwoUHK_k7&jvM| zS+$MaAyVy(5&pSVAC1juhmVS(0hKVlrDc6ULclM?GeF7TBF9MQypwn9ITr_Cslf4o zKkt)ARQ#DctTAZ664lPCk50hb`%B-bB&fqVc{Q00uWCfYPwNmDf9cqOsjj!xCWDs1>v;F$B6;FC^|c-z<6gLu37CI~aGto_KqAWFW;E}UiVz=_ zXgauPz+s++n_Q#biDR&`g7-233H^T1)@k!nwLE&zGpZ4}r0QFtd&Z>Zyf9MT0fpq1UrHk=Yk8c z#m28kTDW^X+EUYwJO0W2vJTt7R{=ujDYIAyno@)bK6U{D2} zO+z>Bf%vq@r??RJDGlSS^FRA_a-RD`V+^tp>w;zaJ4wk+TJqz1hgrXjNi!>#Uq-zj zrub3bF{oQ7Nu5W83dP@SRj|Y8O42b7Dgo3ll5s9%91`43&j!O(hG2aR)OQGOIFZmh z^B%Eoq4rFeXRv>HWX8wIh+Z;o!*sJF^594b@ZB{mR(b@v4klTJ-&t}`5KB)A`~D#w z#$=ttW^0aQYNTfo(o9~;GylI8(>C(Tsux+vXpIelf zF6QeFAebV?>?O^>?!FPOKnu;%{g4a%xftnY{xMs=~F}*A6rv0_97C;|0iu;c%4fvQBWNWPEr!M@uM@u`DOm ze#C&3)j$W(GM~lBD%JjAsJ9>Gvd4(wgb^FD*8u8xQ(7X5e1XDU(fU%n$2k!64B;8! zigUqocyW-vG>MTrqYgVHdg1r4!R7JBkSKh|IJl*&oN<+*1Ljg-UkbSwV7=)aaIWYg zLgIA)@>{3~(S98Nj4E*`#CJyX&o>~?H39{7`00>XbWtFekrFc>IPr9J?6k{IV)gZg8HSy<$C2M({;Tq3a_TTm1BCkrGK-bn5Bu{0$y)- zLzC)qEWqYG6L-NfMctaIP`+5;-109Bw=_Q-U?6vNCPbwudXa`xIY=Wy#+1Au`#H@c z0%NJhHncaz&mANmg>rlVu14YtT}!p5rErRz(n-w59N-p}&<4+-pWv|31Nm*J0CPt# zLlG?RaMBYMpzXK{U-BW)3!S4d`}0ryB%;gXrONS51?Qx=h2uGwK=iY|YG#|G?r7YH z)H~i>wVL91n>W#}xHbg`>o*7ybeKe9$5Ieu2#@eIx(UOSb0F4)6P>!4!%k)VNA-Jp z)m8A1V#}*$2(^#YHg=0|;&zJMJum%W_?nojMU1R)i(+{fezU3EAx$!NvI2ilk1J}p9qf*??uYOz#r(5%lO0ZsXoITDCf4++1a65lI z(Iaus_Vb@E^E&f2OL7P2oN$N5w)O5_P3zND8{W*R*{f^?I=UqLD_-9ORQ?!~byM1{ zT__mP6w3@|phkrcX%P)|*=f+?Ntcn9Dt%&4u^gl*hQ>=(zr~pcTDZN^8Zk$_KC_P~ zOstbv{JWesVN+G$fwAEL?tCRa&m|sgL_ecC9251|mbuM5eQkbq*&<+Ss`#ilx3NYb zC@gWi0X1HQoGv(9-^)uS|E-v+3DmA#_&pJXfxmyy@~JLWsKD6FYRLoR$vpE=f~=Tb!#=pN;oxkk+KrJJ(L$a=w(h#(Ce$ zbTE~;GP;zJOP4w3WM?7=zCgNcKy_Uyz)yA8;O|1)AaRi$QRU$=$GYRa6z8u3RS)+I zft6cb&o1?9^tfH8l5%uhvQSDPSmspC%>ehsov z&}_Xc)=F|V9>nvhiWyN>VL+QiqXm@2`1u2pw>*5hS!ziz_m>UCx#&9s@&wXf=}M;Y|YJ);x->iylNdDBr(d&)p_Qd?{N zU)#VVFR-9hoDKUCD@qA|&C2{ei9=d6vE*CWZuZJfo&F6$U(dSt+o5hLF!&TdQ0UCP z@qnLom2d@y7H+09V{UZSQQvWX3dq2%TzA%pHJxsA?&N`iXf;lG%6GBq1seGRT@hEJ zz+>v;j6AT=Lw@JghW>sqRmomzqF<3BG472G-LUvN=|9fU%XyRvW4YS0-6E^Vb%U=v z^HR?s=lR&GA-AUqOEji}9a zno=CE+w23L(h38JP4K1z&q2chX-zPwI%Fy*7zEkAzuDDrD))flwQK6}dL6ri;SJ@+ z;;5jV$fghPltjabLVYx;-9dzCU%x~n1Z8Rdo&2JE8@-8t*ofwOrT$_&M?BGc7mNa9@XA&gu?)IMaS2H>`M;$MFIemQ>MwOd?%S7F{5X2@C%P zA&q#{R($#O!H(=iL6MElQk7m{40Sl)tN!cwn=Ov}&jr^{&71Bt%QWLx2X7AY`TW^JGBR|Zh??Dok}Fz2+=?EE7O&paj`clX`Yn-0BXKdF2??UH zwG&I5Gnb!|pXGbM{flcPd?0K+I+!LEp_GFMHkWJUy9dc{RZj)#$D$=A?mIsC5pN-n zG?&KIc}-`(BY#n!V7H-lwr)9$pIMKfw{A(6nA&%(6Y1d|th!qJ`iOs}3V1YLMBeU4 z=HMxI_J<`DLvnRDJyvC3Y*ly1@AvOLKh6N@VoFq1!1%gca?Y#GKRR#)i+H}vjYUOYNOmc|JpLgM?7ePqgOtmacjM}=uhbzf zS5!Tj8uzcp9k0ZOdQDyHJ~8CfOa>Zo#t4RPEU^;`wrn###xK-l4y z=W08@{d#OK)Vx5TKLP1}!%Ll<`sZ$+#lLVXfam8rc@Ix_-&ps?bfVp0|IYUs2eLwB${!Ag3{N0MEhTxTIQ16W7aa=f#TEPW?GNIMGncobI@6}zn;j%sNpGTH`lv<4v+x8F|q*Pk;?+M}{9*{oj>Hn2_3 zdhDiv0LbgT=j6gVb)*_)|<*w_r2HWbEE%w>=kI2-x>Dy`!;+%?=(^LAUJ$S z@&P(-yvQ5mI&wT!WPUi@25ezN6UxbF6j#MA1zb4%hkSMXD@Q9XPUk&=Si?=;)&iSd zv7PIFADwKhvis9-!XQtLr|brS48U^=?=^$HKAe0`PhK2*+0jjm)2F!$eHMVLm`XiZ z%5x6S(GS2DeL_NLrWq)aHQyepCrGViQQ-Y!c*9CCIeor75=>Mj*kZMD?qyNLHH(PU zqIcr?P`81VtfB_fQI+NoLPkavm@`z-%ZroUg91U^VzqzB2u=;(5QaTkTlY6(?lwb- zpQmAUKe9go1bn*BW}tpQ+uB*=QwGlabv9wz#v6NQ=DwVXnlS_2O_V}@LG{*Hlirso zHd<`~lfc%VUoBhj!w@7)KlhjHKeVj16*EF<~Og>+ZO8N*U20tQ^c|U{cT3^z#P^I73M0^1P$8=x9K6>0g zi5v08NAw)6HjQY3$92_p(Q@e2HU^%eP%5?$_u0YM(ymeGaZlFqBaxQOkL(8@#fF}= z_etYF2^zAbedh208gCQTFZGv$n(naVYEoyj7FB9hX@*NJ86;O_KmT1; zYE+V4zO~$=?ZFaoD30XQYI%~Xg8z3E!CL&I-+m;jtMX`}pj_5a>U+d?1xpZ36)~wS z#3Ofszh5!H_@V~llyy9+)M;_C1F7m!6G)y$DCw`UUCS%ig-prsW!?*Zf>LzS9(DT6r1Vjt_I=hLPrDcj}%}= zIZA9|)?8jdpOQzu_DnS;8i<5Nq<9B3UujNSH7`@TJBGi32ojPK(+l7q4oXO%2!+5f zUXLO8$LUHi8%6rFU_xkFTx#+j2xm1^*FIZ~-vHI8w>Hc~DPNpjEm^eqxQ9IUj+0Zp zg=y239pcd+hrJJZF9*61>j?-U5Wd0yn_`uy?xONy=}EDZRL05c8qE{wGDGh^l8!zD z1(cyf({j&Rg5RNw+8B=FwS^myBzht;0)4(bTow-^Bn43)Pv$%3)a@fRT%ft=@pmIw zo?+!v;UTkir-dHh^=^8by;yA}Qqor79G6kAfBq4#An-7lx=g7z+rfg?i%r*}a~&pe z_$1_NPVr?;de-#4GwdXoz~b$7s1X9W3lL4e#u<6Wf?d-!z)1&>!$yVA6E3E>8^Jp# zmnq)*j# zz3~#$01lmVhNpbwqcVdhs;t?Am-U%6L<1rl;1@buON$(de9VgxmpCc>iP1@<-dGjTc!wa#TrJ{3)S8kisJ;5Ss08ocbmJ+v8A$Qhh2H#z}c=HVN z4lMW`&?>51q$)wL9MbJ;^pTG+uDi)?c5yqVgsQ@3#+Q>m$a|>a7*KbOZSIL zCJ#mSw7qV=&fuoddb1q+Vbus(sM47`q)1y6M5MgwWnec%9rcycZe62<>NZ7;H2pyB`z(mJH5Yr3K5XG{C~$B}fDFD^Owlj6Juyj}N5 z-0uNU^5k8a$FlX<9!l8PSo81Qb$$)~kV|Ui_-kQwz_7&3FrMv_jkznSq7HHRzRPgJ z2&@bJO{weX8b>7eeuC0pIj97*Mqmo5zCPN9 zhc&g;Q~8Q+{EFTEnd(cK%M(}VSj|QZh^-!(9OY{ZfYA!d={}d2?$WbS1sUP!Jznvn z=ND;V_0GPI5>pPL?`k4uF9L+A7Nth?EkWC!tdsg0`TNVJwwX)RTe>5^jj=a~QJc=V zE7Nct(rg{>D;RyeV?$)8mX6S9r#druzxCO#1|IsQxl3la^};)4-_-i7Cuj=>8&MAS zzChEmsf%5-p~HY7ZwyEspcdBJ^Vrv5%Pkqg#66DLt%=5bn__UtAu`N$U*fe!vsI>d z#@ngX_qb%xhh_=E0`RuIW@yZDFRM+A{B)cu_V5QR;5{iqhy%L7p$Gpq6x8QT=HK?3 z5PlD`6$WW-Uhe-MY=E#AI9(3)#|B_xbhT|c{A$nP zE$|Wrq|pilRj>_U)=9vrJovoy5J$D@o85(B#Z-1x+2iy; zkGKl(RasRF-!U#E0~Su?AyfDT9{BmOsQtq9GT)A7UY{$J`O<~tZi$h8ususla8v$L zH=B^58goSi$WEQ&!tX?IE2Wfb`Fj2RVH_*y$8pehufK?-xBq3mO@+5*T}k5t@_2kE zF@d4^`}(jmVWw?!p8o8+l_*f1M1KuUv7R7{$Wk^S_Lg%;tc^pSn!2XlR6QX#`Ox~p z>Dgh-zsEOH`OsIB{h7J1xu^6EqeA;DiL9#&&*WW{1w!b>1Y#5{J(<5SWT*WyT@6vq z#o-)V(k0!Q*Co|f>_^i%IY_v`y)2@Ia`3?U0}lhzOvgI3av0Ih1|qgQzjN=@fdj4!i_d58*R!nc!-kkUMVjvpILtxG9jO zn6vRabxVK2SU$8bz~PCD7SBbDj6HW4hsF0@sU0s+C+gFyJ>NN4FMs+fvf}lXWY8Jt zEqBNGvavdMQm)>R?w<$20nkqBXJA^=VaJiM>FJQ=-(RR!2DS$;LiE%Ye%U>; z(Y3C>{QEaJT$3$Tn_+G*II=6VGk0%L=Q8?xB_Mp7?`hQiGS`OMjL`!tjE-)s^06J% z0Cq!9aBM?UZTUFMThMp@*4_+otCYHR9e4vi`SX{mq0$WY+vYeg<~CrVN_r@XJ}x*C zSz+M3$C$xTB zWEabl^8I~B-t@5hYwr$HQg^!*;^pM@NK@t5xPa;n~Dz0=qo92dIcDhdrja>9vVD5%A7?E4I_ zndM%3;88x%-P;uv-MGMu*rg8Fj()Khi_W~W^m0`-+}H{9wJT;Wmo<(KpmcMnNN(j( zkG+-T$;)=#?jiO~-Kf|L@3T0$)K&%n@b%h^W&sed!=K}OLuu}DW&p=SfwafVHN7#)X-pxe*DAz0X-{%9`MU#vle<8sB!`XD6HV9D&*~>!ww*Jnp^c zZ=Iz;WBS7@t2XpUvqd^6K~W)na17*74p~{Y^TXqR@%Iq#zgJ&%Ch5bgx}D?1BpBl< zc`qX~-#pC%bkdhgAcw#3ZZA>aK@}8T&N9vuk%$-yT~AMoDgc%Xhni1wyJq>oI&ZC+ zxGD_Ir67+)?9G|mq3$-ls+4R159aW#kX0T#bsA$z2nAa_-|Zz68Lvnr-@v5>zNhJ` zqPstb3)!zUj9zK@4>aW8V{QcU>;@yci1KMKI%9B~D}lbCQaYN?^D zp>lp|>h0bwBMgZ>!v>jpt4ODm*Su=9WUOXFgoy}Jq#W|u#&meo@1W)adth^9?@yvZ zFoC7lVXICHoJ411X0VF#pK^HFM$)x;16{P8$s5%gnD!4Zc>YZ0dvXLfETfsb7-BbN!~IH^lPIbS$bLI`r#~U**L=}?kJLE21sIpi3i${e57<~=R*Xdav`#9&XzcO5PP*eIE zRXY(m&${XKev{r_a?=1YR@65Rdhz?eWcL&qFTJ;CeRWZ-@^8bg)n|^R`rFMVTb4Y+ z;BzH2_0;qHIwn~^vRzjv5?;8ap;LDeir0J;`F-}Jje;>xt(KByx#%)N`VMD1qHxoZ zbHpBo@F;m%aG~BLa%#K$Jk|J35&2d^lWbj>ik!`-8=NJ zT|B-dI{VRCB1_l$G%#AsleC`xg(zK=VwrL5y4segilI?I)XgB?Yg&%7moyRhDcuZ< zuqpG3P8LUmWJ2CxfCCFrCm1BdPnATDc!!x{nBH_mm^EpplAXj8*r`4&85IWLw$Unf6`Ib^@=0GIZj%c4gN_!rp1h1VjIW&3WaWC>~biP|`z75A?zB^|oS+L(5LgzK^ zMenoROzMd%#=f8PjLWf|QUMk8pR-R)(lRflJSQxAJqHygxLAq^7T(fkdkk? z3k0zD`lbkze^Tw>)HP0gf;MKxo7eH$C#IepM+f+uei#M@5tZX$NKu+|Sc9 zD`xVgATc0)LYmJx(C~!)Ugxbl>J?5_>H3?|UqX_ioFwnOVCb9uqCzKHyF%$TfVPEQ zqfq$}XQB!Cf%A;bsqSNgi``1{bt$PZ<9nNZ8jMbzBRi@ufg#;5va_bcyjEoHNR~y1 zK^~hWM3o;;kY2b0F;pgznrgqOw4Cx2Z+S>6c8f%vY%Ra-aFM#h^rg4Jm?y-+?2X#F zjygfKWn8=h_u3j+d9Az{vHlQBqMdP3*cq@atNo2@Q;#&)quE&>dvcF?0$U zBnFQAQ!#~#JJNegqZ40s5`5xkGn?9JKg7@I-=~38k@<14x~hJjLCWO^CjIv+z`Inwv?rB zVH>`yqf;#JSk*oW9+*MVr1$BQmHa%6YEkx`g9_n174m1DUru(^l;?d4B|Go)j) zX`7Gu+-9=5Fy6CZRrMR8ukn*oMbT%|&Fc-?3Aak|55OwUj`vPG>&EalU2D14Y_6;R zn1Fbo#Ma|fiQ zME^`sMEf4MW~naP;a$VWjkG=Wy#78q%EZ5nQodQxs3YZpEs2}v`eDysB!Pu!Utwo^ z<5uuX;{?kb;|9w|%}fnHK9X=O8*ak9+Kw=6q3|-6p!8S}p!43HR5DQ<@H=~_*BM7! zSZy3s452B{=A~KhskB*g*)+!M?;U<6fmg%=yBfdi&z6%a9J7%HN|QVk-<=QLLeeNY zWc~WvdSX0t<6O6({YX{4pj*$pztm)Tbpc`|(9l-;zhn5t@3(j~bWYbJNhn#@Kt^3Ut4% zRMc}r#(-rRqQ16N%D+jK*vec<#rNbYRRhz?rcY#G@%hhFnec_36?coiJoo#r!FSaE zgBKDctdi&14YO+~V~?)4C%pc)@w^COOzhH)XiAf+^od~A7@dTB{LY}1&;yt$mylK8@! zwEm1Fn0j>0_jbZKQuUz37tM|yVR8S37Z!mHke`5(_?_X69A(U+R z%`Y-zV(z0lXIA|;1OeB5TaD7yznF96EH^aFNb}db-D7*h?xM);evbKFCOhYs^TYxw z_?3UAD(w2hzV|94;s+BQO?gW z`C_Xe=Jc1w`g3Al<@b`Zc;*HEUgWJ*GL(D<9fkqGBUuGA{!5T+@X^`3XhL1L_1Kv! z0oBlzHk#Ev5jLe4(o6T_x=NRNGWVg^mGFqJ=QxuGko`X zJt%x0K>J#)VsX{~-LhSk-B<4?DLRRRJc=8fzNB(0PC6v-TIcx}{c+(@4TB+*(cwr+ zawLP}=C!rr&1G`g}^*9}kIRF7Yj=LCR*c#DWWeU=m=aeT+7|G5zjI z?%U+`O^oKIiBmy5n=_?1^hQBzxCSEdpVGOC9@>!zXWgg*^l1`vdk^kGD3{i_iM62RiHJO7R{>9Tv=LK z|CtTa`MJy+hdmh-S?dl_0lU6xk`1`zjyF5Z2ZxcU0J)K@ms+2-hz<s*gpXk!xP#$ z*CfM^P09*KR}Qn^NLOp99nf&gNPX%(ftEl`1UhEy%H{NRKGdf!laR0OO zMmzI3q>G*7@A?EiB!2&6I>j2^rdMhBy0IKLjd16LjbnVh6KpVrSo9 zb2GJMem1*MvBtjX?Jg)(#u0;9e;=lAWx8p)`6XzingLN;`$Y_~hB=UphjqI-^z@S^ z>N?Y*KVm8%62Sy4nSJ?p4i<-^nO1+Cm3)>vt%&~seZCKb z3w6mc#B5%HJk1Jr*V`#QLA=XSNb_tEbSSe4$z4XL%d7svP%W2#;nrRlOTZ^X`Xenc z<>6}?Fz_h=_jb{kD*LADd9QKgZV?^(vq*v1a73dR_!);g-bNd+jkC!1b&M@`F{ zeM=he?h?$L7MtkxbM;>%c~kv1MDhKZe*Vw?M%z<@Cg?4!t{_10GO9IN7Z3_rPKga% zViXxU@e)n+`2Ltl@p?9kvf_fJ=%wnEB!lcy%AFdfAOE{kt4KwcKNT|u%d|D{liUt5 z$qK`PF0-A0?n2gpyR|1&*&3Hlt9bp3qud2W6_7shWopx(K-c+Z2Jt?I!Q~3*xkw~A zZtXRje-e8H2j|{eF$T^|ibjeAD!lrxAqcR&JGH8Bu@uI35+l+;d%F4TFA(1-GS7RE zkv7~ks(Y-xbAW3mex3GnBBdp7Zap{J;H?cYYJRb7#@z5^`n`eW)!U8ww-y;QSG1`+EWQ?`7rac**;w1l zZ5p}1`I!7>%n?XzApIowoYp{`F+1mvoNOpX09Be}0&jR7h|LF1bijJPK@Cn62XL3T4ke ztExX{ZS^t9@;_CwNROO$9i|IxYO6uvcd+!03Tq*+@j3V{Wfm|Rl5Bgt>2(d zq|sB2wS>-{|8Be9`2K&Sz4u>}ORzW`ihxG}5fu<5APOi50@4G)gMx~Pq5?{9BGQR; zNI+CXnlwWXRhm@kB?0L*^e&->-Vy>yNcqNd?|uJ)_mki9%=7H*^xd7A73;nHErR<5 zJ>{S|jbKCv^y}CcyO3iUZ$@FrsJCXpk;CE{dL4;r;m5qw&XhzQ9wsrc2pT6@URvgD z^Ss^o>TGW6;DDXrrVd0pVN*0cH9}GDxZX2-P*o7U-Gxc1E49AnvpfW+MrOrqDg(k5 zL2?Bqjp0@KoRUg}`za93e(3zzji&7WJ+4}qHD|Xni`MO4N8&{-VwEJ`nl6L&qa8l8x zZEmc_rO>JL{a*D1>FC|B=mvV?>gC1OYsV$a+rKdMe|v&b>q6_5hVtDlTD8-8zXmZf z8)4u7qyhnGcXuukrXM)R5Y8HBdG6QBQS2Y?erjmV?G}PFb;*SMkeBc(PTRs|Kk)w6Nh$!Ec~F zsd|Pkb>i6n2`7Z}&=<+*=LkwKduyC}zHZP8qZF#eLINA^)d8N}Ur#ky;U}(%Ev)o~ z3g8`%0;VOfoKTNlU(yKyrpcl1 zin?nr7Zz>T-NTLVO?y0d-1!>yDxyA4H7*7~^k-Ua5ON!Ie&#+C;L?Ba{8RPnyiZB@ zO4b7RQK9FoA6EUInz5$Jq zdL69UP^5-arN4-l#x>7$>;-w}G0-Cc{L|b?B6+icBy!HDT(tx88%zHYCK~er_G+m$ zMK?*Rcnow66^Zch8Z`AVi%9m|QFp^esLC@eurSlEfb)wJ;+`39d0HOo`l#*A7FQUBU4xZ}|kTv*Udu6dYB#&3?auPp93>VO>8M`Ng8VWRZb4R0xf#=bp0X-EV;K!g;0_8%jgv^rzqE41OT z+ivF!{syGofI=_^XmXi^o^%mnA>(_H9UpW_19Z|!czVzziO*0N(5X$~g}6(6o>@uD z85z_KhBUJ@Z!+y~pMu!oPX2{CJP^UEo$3iCU+y(IZ&$1t5T%>W9r@V*2J(Zb;6Iz# zV4r1vZu=Uk5;#h3T9A7!b}9cNu$efDX}CnloBA;MW~6x%|LBO9&yNB(xLNa1K^(_e ztyCRLrj;juQo=u4REP=G@*IXtCs`W4Cj_jxrN`9kRvvCV_j4Dz=y3{y*HMcbHkQNw z#0&il%2Bnv?>)sHd^dCu##-lGHHu+&H<4wpY5mxcoa^Wig|^BXU6o=E4^P$NB)7c2 zxhufytMnSG_B_VJ$Yu9_a@bPfh7X~(hEQwha@{N1uhL$V^aI5MP{4)95no#L$M*38 zeQz{Ty(oJEWy|I9*Jp8qd6tG0TpV!t&=ga>nl@Qd(P5(SI>$Ctp*-RsUgKiYFhaewa35{B*ezmdO1O@6@wTIj^`uVeWJM`c4TPgWGIt6;UU6X8Br2=larQ z#?f%Vs^=7Y%h{WPQd<}LNu%Jyez6u2Udfv~xl_ttMvS>u=8xn1Qh7%xAc?Ui~dyFm~xiD{9!e{(NzgS&qD z(;hG-3VF&sQfcG}^orzBiYkD;P^fq)9N8mR@wuTk-k6PmLwB(>RACB*j+t(|DAfpt zP;eWtfBvN!Fi*b~nBxJ@hI2Z9cyRh1ZtucMz{FK?XO6EfE+d!rR{lIb?9qFUk$bwC zaeeI@V=E{$&Y)%#=9Lu*e|HA6nio*IcF`p?f)9#KsO0$^-q>^yax-51XutMCSIO{Z zK16#|8OZc-2Q3$~Px|hKB=avXtI6j_^2oniGGDfPJa@7VnHD?dv5L$cB=sJ;a|2lX z8z`!8^Zorb z3ZA@lv>Km*b-@ZUFRZkhsc-OF%1K=689f?$o^fXxfO05=Y=n_=%E+mnUU}n}pj)W- z=<22-m-3!a)rt?bMrkg3;ZY#lm8_ZIHx=o42l}NwlN$~DgQ-n^+QQ@S5X-ry5Fd;m ztGrBMRGM$ZFQQ&-pI$R1Z;F3BZS5kd4FtI9i?6wOctZ=1VLm5GZ0#^uIGY>t{e0cZ zmf0U#ba1NphEEA5=Egx{L#)n=D{ozkVyPN-gN49kbyHeVJ|?uAi1gHwngW)t{zN;vbQvD=1B^25gzlP0LGpvO)W*L8|Mw0ul}JW8mv6)_)_D! z{JgNDJ1m6{Mhmzuknhtt4X$*W>OyLr1)0hP)Ae6w$Q60?`gWfXP%!}8 zYmAoXivEnv4M0-P)ow!+lgcR%wF1tuhe|HUUUiFJGu)7Lb$Sq@J5*OVZYe&M^VLK< zf;i^g>p7=z;AIgQI;o4l;ds=eZc(&X z0aY=2RkI~jRSd?)!l9bd<6dSn5M=J5&nsO(VC3FqqwWq3wCM6Z1ZXS*BR8ZtyOnXJ zam|aC`ni&6ii?fq_15L>=n7O#qH?ovNP8u28aapr=dkQY<}OW_~BD63Nh zlu%GEHTR9yL(aUqYTkjFG4;og-_3ti40_nmsUUswyItnDwpUiaJRRtuVVM%Ra7o4cV5ZQwl^eF_&f)b+UQ*q5^f*h_lh0rpdeB!XO2?k!S#N+V}s?q>>2tzw3FW+ zPoy2ZVCd^(XH$v64y-62D*i5eOz`vLIMXUL$?NiHe~ifOUNIrk)foKZC5cI4CNUD+ zF9L{-Q>!5ulU|=(!hnO$CY7dN39nTt>A3gjQB>kGCWJPn4EWHgEEJROm z+_gq_anuMuDf*45W+?Cic{B|br^Zz%tq@?kZI~-)J<-eVscGhZ&r5$h%e-;U*~#rgGa^9 zc4YEPyWF&Nl5!*avbJ0Fo^_%uJD>(_up=ug+YaO+MLV&2>f{iV)hu+vtHE-q{;d8h z+?sUEdtLeMpQ~Y>j+=~5_IKy)VXQjDeCcHo%MF^vF1T$`81P|x1ft##3Og7p6n-By zsblE%{5xYQI_5&-V|Gdstu4dZIG}d&hg9s=%x=bSIuS~0`pP3xsT@(LfR-&5N`F4m zHje3dHj5R31RikQ^`&V94$DZ-TY6;rDo-_@Q9m0c5thA~Gv;@UD+DrKr6gK>^%-v@ z+0fOf-hB`U(=EL+Qq_Ib+4W9yjV8IPPzjj&Wer>`M_RQ^|axnc;_Ccz;6Fd zOKZd9G`f`d>m~)}oVrLm^~Qq>cw-HKH?_UeY}4SCmjP~qW6rA_r6^zatFD@!CS~mY zWbN77%eCj(G(XDBDoPx7{bQ*=puD_Mc|4Dgn&)5k(fg!9ojJ&4kL#e6_ow-$6E0q% z4@Gc4CcT%RdMvh2<6tc50C8b%mB?)jcr>H@H=SXH1fSX#TPPjy z7|2<;{0DyeuDU-rv(M_h2Ji7yDo@=J4tWoz89{zdACTJ!v1Qdk9O_mctTXOD^as>| zlrY%4l+S#}*Tdq7bB{1LhFv?XT;|>&OE}+=Ohxu1n~yJP5w3dcwkVKaLU341rWba8 zti-4mII^Slo^|$?R+ibYg;tia-9H}ze$B?N$I48~pK{sFgIgvfC?&`D&(wkd$`e3qlF;>TML6Go4o{167CJE*}+$}9Iz`GZ%nd!}3119aA+7{iOZ*tq5 zq*n)h8L#GzYNxp0})*Ll8QdC??_tM<9{1R=NVAIxOOw-Hi^K{~wn`jPsCgXp_YeW{Ot4M4 zEQR@jvLFMZ+%n8@`Q&_v0@wG2)CjkeCT})jqq?`qTm=Dj~PjJAjkxHEf zP1eCZ;>k8@b{z_6GQDSMuVDZ?C&ZqFq{Q$>9IXVM^Tn>xxe&DQKn9B?=rI*>ZRXqX zAl`lHeKHPqX+#4pA-dn+M^AU(f&D{F&-Od4rYtPCl#%lSw5q-8-3U92db7@~noJ?h zvb13dYV7vQie2`WsMy&pI$M(WUrYFrv}+vzJm;f6#rBsG?^vTN%QSNEb4Y(Vuu3NE z)PCr~WTDgfaqn?h#8EWnA0g~F-w1}a{0sc&+`1c}G;)0=@8=xfV&WV60#S{NRH9T| zV(`w*oEAX4ybynTw_1rAtz+e632uW||1u%XsM5}2T$6K9Om(NK*<)1Uch<CB-ZMTz+J*A^j1CEjKOJYR7;jZ zVY(CR8`jc4lVGM)3@fF5uC{-gJJ|H;mfRXo64dM9=IXf8PVvGFREas}UM5OE;93gG zdFKeAa@^xR29DE!_P88$@i6Z1$po^3Gemd(+6fR-$RA+<5xVsWx>YB!F#g=^LrbeH zvE+JMBLz)xo8Cn-FstJZpvt&<{yUQ$o z&x#Jk1b2OUf6NXjJvv3J3$D&%y!@6`^9qwN6ZKpj_)G>4&@#YSgQf&lRXM%aT{b;x z;}9Z_(zwM&M;Wr+!pU){y}&kP(>*E-=%42E6`_}IpE1Lc#%Y)7rHdP0ip+aov>1YJ z`T{`)m_RCkY);)x6_f?;Pf-3Qk6psZ{P_?@;0Bz?2Loi9w`Ip0p*o#2S`F8w0Q5+_o-4P#>gH#>uyY?5d$$G0J#z+^S~|d4|hoIKMt}#cvLPk zQ#tBDSHU)3aHalI@HxlOqUYMB3Hxrrw zxRKDJ?g>-hq;wBf*RqEdl$pU{Ei?Ia#rlj9*c079#+g*k7O8uML4oS5fwz%=e*52z zYKgeI(6M_RoGsf>^|H3iA7F+nTs@!u(v550qbDmUe6STx;@RCv%%5zhU;Y!r@kq9k zhbYS0M3GIWy{78zXqxU7vA6t6-y;1dYJa_!junuV;b^%_U8?_&Ln?QesQ{JtzBBdz ziTeK^7-PD&R+ziROe9zp#FKFn?k?M@ZTZaXMAOA8Hh)JZgIr`>Yf6b0tIiA6F_mQN8N`nsqizgr zpZ{#af2N`3B?pLc?U!EjW*^NkcmAH+atKjeYTN!l2=@o9M<;9x{jEr^VK>5+^Bvxs zQp^rE^q2Lf|Esy+kd`bY(|QD5!q6&TwN{wPG)TRE^v(a$9eLHgfW5_dvVpM45lJK4 zUJD?2)zJTYwdpEDRD3%Mn!lmVh;VZ<-0@1##^leX%YSmp8mUgK7Nt{#UyI*#=BQD$~ybe9D~=f%G@ zf2-A6h)3~E(V2!YtW6!_(+)gzA2OjO-|Q=*axe&&SMt~Biv0yB_nk!$fdVy?RfJtQ zW`evvy9pa$^n{LTS-ZN=g%R35-UCSc$&ta#P}knWUbJY74>MGDziW|hfJ^(d`=9+? zP-2~5G(^2wZG^Hsm8Fw!X}MS~`)(m+nBma#N22=;Qq)ya-d50m6lf_H-FM`JMxkA~ zgZ)X9;krA}bCFN|ET^yYEjNOg=as)UGvbxzbHrTTRtCuNqi~A>B!@Hjlm&^Wyty*y znadV~itd8UAZE=)QHBIsZH>ZUy{KEF7{3Jgij?Kyo)E&qRBZn=N8s;I3_K24TC^K3 zuX~G?e{I>vP%P28IrL$Sgz&lI3TjBS?lOvScXbXYGz)w?C`wSHBKZ3vfPjEc2fTMN zXZmz!3Evv~WMYCBwHIo~A^2_T?IgstMO%wIJ@lixdTr5j6?!Q7vN+xR8pzxwbe(CJ z>+}wCS#$&v4;jtQqxhkh3 zwC+blt;IrZbYFccjFYvrKGeeYzO5`;6Ck5;t_y2QF5&>KmaE5`HJZ^JWWpdAqo#Kt zOvhdT(4uxl(biSqSSAUDSJUC6J~%4nch&?9rlA{cD=&Tt;QsI@eeZOsmX(@?UgeG_ z$#U^oF_MjWGL5CyY^gTVk8@?FPiKJ}M#L)$zU45X1L>d3n=0M6hGsX_QCQDDZZ`LW zJx`Kb5Dv2`NSw+>++WyLaB2&}rAjNJ(GJ_6D|Y==S!*=kPjr~&n05*0H8awc??3VM zo%ahyvV7ySCNMt|4s5I=KhJmUK~G2B|1NVKO+IdV>)l!#H6L_|X|XgQ({_wQh$G|# z2$1<9RQWZPfuE2+T7yX2Q^ng`h=c#>(}986bb^~>wn-$q<`m(P1z$*)2B5`i8SsZU z+h1k!C<9Wv!y@MXP_uOXJ=YbL(s(C$=URQtjIHg&d=w9H#Vr$f0^nah(N|+`>a9!< zbfv5$=hmR~=RSxs!-eDEo17Abzi~k)+Aa!{y&am3730R#?V}h`cUSNvEzXdSq(83# zI+jUckCRTPefVQdeaofy98wM<#h9-#`I9l!$`BpO7m zE&~Auk%f0nbDT;GP!sfrI8#loy08H@H}W5 z{^_DCR(mtPG^&FBY|QU)S;&4qb})zHsE*>ft%SOcsgL~Be53JW8Ai)-Epfe%AttAB zQm2+Af`W)*B3z8(#h)J0f>hMylUpZqp;yTY@I&LScbEsIKR1@oJ(#P0Z5tYxn@_Vi z{I2RI@C#1gbM`s2R;{^WN=QHOFJy0#4DQX{6Z)w!>3tw3A~Sfn*2mTxy#lYWlz}N> zvc(2OeGbMa-@*>8Q*yBeHSwrEgmB;`CVauoXq(KR(a$VgK-obNE>yu`UE-M1pqSO1 zj@(q7^)wCtt0|*%5eQ(D$$BaNXH#$^7m7#m)De0112`I&XIK3;vA@l4dJw%gioX1} zgPepAR6>zLWAv!2gG zV@lqkF*4eZaKso04hVc0tg9wAeI4KYYp|N3#ozIOwBT&mgf-+UdsF5eAKAV+JZS&f zAv;2+Lnt=-{p%wT>lf|LtE4qQxr{>ckMEv^xF08Cw|lvlpWc|4jGxrrd60-w3rc1W zZZS?_n%aDBaAh_zW2=n3H>Ih;Y$Ve$pa{{*XI8qD3@^D*E!&{#Y5+axxv_6u+P!$- zv*>V_?kmYWX_ea0v;57)u~&F=|5WD)`*^HNk8TjSo{b81crB-_V|Y+|%e-x6oPKWK zUKrrSB%;MuvN>hE`UlAq;&}tw24^uCU6wGrZ`n?6>YJY!dP||~i;sq%B)d40zxlsJPQd*K=>J-Me3@u3IA0T(XJV9(L%pFA z@QFYsSt_7=f&W`1uU}#po;#|6*zNc5ze1H+gZt*_k-x zrr1KBFn~?(Cb7E2%)ivK`A4modxIJLjDoB$QI#!J^GzV*XPE?f0cSCV<)V*bNt#*L zkfvMu%{++8Tj~MFdBDVcm+_dfXGV@!o}V-Z2{7B-vQKE@FB?v4mAerS>(Fh{0|9h0 zrQ*+jRGbG~*Z;%7k%hUhW4But4h|XyFI&uVfzAz39?-O8ke{TSrlMDF#FxEznlOtD zbDh3S$dmR9dN6&t%ysmH=RstJj%hMOtkRUrjyO~fSz1H{?iAuKS5-qpu`078iuF`| z6u7kAIhqM@EA^eLECum_wqXDxvnZZa#}Wr)!Y+W~5R_JfEk4Uj^df|lrn=G}!4ip+ zRG^s1_lLv%N7YRVa!*Wqh3^t{XKNVg^4Nq6S56G+JkX$Q&fEPqrp`ORZVm8vJ4BA@ z6_xm8al-(Bi4yxeO0CKdy5*nZrpro%Vk~P2&opNBQDWctqkQe~_UP-ED@da#N*|DR zN?`1@%xmE3sU#PJ_j|YwOb22b9sZYnsr8AJ?X8vdp1lso)zU1+d6Btp@Y!?5g?@2z zB1Sa^{?E^@W|^*NSbN4E>zt(9qpHrq1(d$CGSH0!PBmq z138(Ryv9gH|jyW8r!eqj3*A&nMIVkY=6sajHy&N6Ae@q3HFa~BD~|u zevSIL6xvprQ@e$V0>{VEPEyO%%Wz(m_T0bpW z2h;rkbs6#K3TWUHdOQZAQT8sN)8RK-zRUhyVB19Cd8b*as_04Rh@Ne{e_}0X$v7xB z#lUbiI_DMwsss~eaxcE0oMd#9_Ap96uQ1+Y56#! z1cP2f^a1hPQElKRa*yMMkc)&O_&n^mz9RO=lNq8We(Mpf|2H)I_Fg?IX{rL!~QGYB6h8pMXjCwLjp_t6humW18mlLEmDz24<08{0Vamj|3G*tEdAUGiOA zpo8|7@-?HCQ1yuk2R4A~u!;4HdSuCQRVc4_LS!EwG(0}0HIEbg}{M#BiE`R-_JLEzq7 zwjuAtJgE5H(pp*SJ=;`vgRgbs;4Z68IQB;}>eLs`mnf!vp4+CD*rX>Dex*GNvTDR+ zjs4V^;Hof0JaVf$Vl}mFv6lSMBcg`yt|7T0P#4f7i8t_Df01~-Q*)$0KGGPQe1fRx zQOEcg?84uwQxlJwU*(WXku|%=n+npA`;Vk|%H%x^8ZL>g78W=A+-39+VV*j3wpDoe z>^~s2kyy=2nMu*7a(zb}%a`LR?O@B&zWkk6F)RqBvDbg_OI)hYgzkIlL;W91E4^v9 zM{br0YC4Xql8Vr0Is!W!k}G>SLQX4xO-gq+w>x~}>U)4z{k6j5QFM?MR9W5M3Yz?W z7*S#BLe_yw_61%hQ8-{_<`{`}iPA)6rH4_xn2B5=WHgMA!Z_Fj6u`e<2F$YQeg*k$ ztH;>p!Uw56{x%Y97u9DI&$Gw|DNj&DOt{?0_+Z3`dzsLe5}{neOw-Lh@QOFHe3pDL zP*N7js{}WVxv$)%fN}}Sa(QSmo&S9Jp&Fjo)y6Ly+w-piR}bQ_jZGz=Vlh{dr|&ou zW`O8F$(^?Bn%}}mMDqP=Fj_UeQ(>UF{G2p4T2oP`*<^E~3j~-3pZ3lZ__i{xEiP+* z#^lJ2gxd%bOEVPb9!N^_MLj~;w6Ur8C>Zf}3zb>?V|m+B24{`o>)>a^B;zBK)0q8w zSK3R&QEmc3t2xfGzb5X$3~!=BFzQUv8E3F~SqOMsjix zj_BgSS(8_o^K(O%Rm)B1oKU`6E#KqS*1f}9!wK3pG5r*^o~y@^U1A3Yrip0M0>N=G z2#C776E;zi4&36nw)-ycvGgy4gK`BE*7nIe zRTCvz^4x^Kg&b@8|5V@Rvmf9iXnqnuGNoA3aBx+&#ECk7h2?m;airYTnPo;rCHU|R zm~r(U4BA&b>~Esbuo7E;{U!FbS{YKaWevR9rljl`yDGN!;~yIK3blR6_ur@k zVZTrMHk(5IT@+%LjbY$2uJ6kZT$2Qjd~18ORx7PC6d z0Fh*X*+2L3(+I+YQSxeI{kVPJHyND4A|^QX>k{7+eX?aCg~$RZ2b2hZW9IaN2nb0s z*=gI0C?xGG7WB8aM(WGL3N;^C-uM!8bn$EflQ`-3-v#PBhLjQRRbdvBRU_U_MZyQ0mrul6KFPm1ygxnxwS^6tB^9+l<%K z`769zW>WcPuRR}7CM3D#iYny7&ePQJ|cR+?BCr69cbVq*NP_|D$j6l3$Y(#0CL#pNUu)V zvS=n}fp_|rQJMh4L$I2WFRN@^=UlhY!vp-99|4P_QW<^2D#33A&xrS%uJw7PZ5jMA zUVTL`$Tg!xsq)8x1-NHP01=%G^ZdXFkC{;UurbmgNpTaLy?vV-5c*mokSVfrk)u}IB-fDRtD_urKopad?$QeziA>+FI@~5 z#&FPFS}E)M0<0|etTfy?=hwAv6Qh@d2Y-X6d^E`xir?x>^+=;$cNWe1B7+vtt8`h9 zr}gzJ?Um6>PnB~IZr61B$!=F{Wu12Og5H2;In3u`_7x}1r0T!uzrE!A>UjoexpLj1 z*CMuT%ZBrpb1;ge1wG(6=Yu=~n^Tt1tEl~@GNgB;1U`!5do5$IH7!}0*ooXvTiRAN zr;SR`VUG!W&*^E;<7J$zK;BqXjEIl8t{=zahwnX;DM<=%;!t{}X_snSBbSn zcW0X@;riH^<;;AQ$NZCKkHBfLww*SgE4xep8)kL4&-T0y!JL;;Z-%bl{Wcc-uP%46 zpLRvhcfa_--wI$0FW!vObo(94@AX!i^na|D@QfJSh>~|S`~9l&rTxx$6NfZNXwbpN zpw5gv@+a}WP)VuijjhZb|F|FYhVWas0lzHx%2zUcU2Sr>z=@x9ANZ28mX@@%m``Wy z>X_m1t0*fjOIccENQHb?)n1^OrE8+~DDf?Vlr$?;2nUl%0HUBJoxbCS5(bQ3HrTTC zgw%kelEukbtn{muewgF%njQi6sI1hnIGxDLf^V7Yj+nP(TG0k*bS0?|Rh@}(u^;le z5f%vGC~ax1_toc><}!*)ewWEGGN!x+%V zB++Sw5bQU3FdlmKmkTJXzT&7;R=K=MbDh^?%z*p-Y-wNhloI;3{e4(NP9`bd(|WLNANe0qiJUwI{C-N*(k(2(0w#bBx!t3~g2LI`AkSgU*aSJKJI$ znMGJq_Q@4sad`*3S4#;@M7C)H&buq$5sYL>)bbnp{rt2IXRhE#Xh=nLsK1J>C2gW* zQC7(^tKameb5+BR37TTbBn_`JAzv)<)&665LvNn{ZZ(Ty>C@$AW$2(nnOh1xNe)ai z={`k$jEP4Rtov-B*30u=*2gbBdkKgf2@1UFd;pjUaXSO|NciU>FT@ugt+{+E&*s>I zB5QFbz`jQ`s8iXqg;=pmV!CZkfi0H}*QfnZBsy`ynw97p?RHx*RlV+kp3V)P+tq8l zZz}J1I-aBC<&}0oDU)UIlvjIl9A-@fPx>z|X_=oKy16E6&$M`#jMw_S1s*Y3 z1uN7!ODbPH{h097efi}hEl$@f5iWKy|J?D8uBniDID(m+%+)9fxhals?Od!8au<4G zlM&|LyU=QuBIgk*U|axsD0@H>zJj&3fN&rxa~VxtOJkJS&xEPnH^sg+=TqwsaDmRr zf3AmSFKoTeLZ?(9@YX#gWR4^`1WwZSTa%nze(sEq=0=L!+DatmYbst2Mf00@D=4_q zi}}Icf}*16$6>H^VJ28IF07Y-a!@lBM(A9&zDz+a9L;viLj5?>@O{>0k6vM3S8zYN z&b9R?h_&#mjoma$cFxLs-+legF-&WtgwfAu*e6U$br zx_tc@>-WoTUG)QRQhp9xgwAk4UrK#mIy3Jq^L8~cue8bI@jq@&z}lp}?`dK;An|9g zygA+M)RM94>-LdKfdISGVRYKM)(usR^b~P%iZfB$r~Xaa$J!56GdNY1PJ`9&8`Q~? z6k&!IT!ePvjJftFdEK778d|;8x53aD85o&JMxgG|VWX?)`Q&46r?L7^VN7=)&)2l(#c(E`66}@_d0aFWihBPm=?;8z((lN1 z3yAMG=1G+l_o5Ds=0lY_J)eflSz0!P<6de2-#th8Keg4OoOkkDJFDz%YYt8z-RSJq zoFS&DCTxZH>^NZnw=?E&qTgzgN0mVLfzWCs{T@AS@c0=zcC7kmb!)1oB++Dz@+D1R zb%^4q`o6TMcoTiAz{>U`7-z;S=b*9q21D6El+oeuxY9kHF0Wa%4mt;92cEfoeRNw1 z?04>QItr#(F5RsvYYsdry81_@Sz|k5FH1J#%9sqZ^%xKWOUsM+F!VBxhZTkntxIGyrp@sHhmdPv+bPWREmjk zV{3s&*`JLcq^)<|L%iH6=)>Oea_h`=7Y=4F#np>+I5f>DfY;a zc6PpA7c%US2WuCDAX3B(_w_TVCzf=AC5(usQ_!j?P}R6xe!tY3X^t4eE!G-V1&?*G zSmvwvaXA2MOZ^$LDbSG0j^DiE? zND3oaheJPR;=NM&0)hbju<*vifGE;aA2g7x!Tf7Ytz3 z@yuN{EP)PU|^y_f=lx-Op|~1zzyFw!>aFcaC4B>||t`T3qsTVWiVon;H=)zOIK_Muglb|Fav|(~vdc-)9L=eqM#r|&tz9hV>)K@lFbNPn*3T#FU*IYEGH7`* z^nAczI#euR(zBZ#U?un^m-S0j;-{mT)x9?diqXQU1G+3t57`Em#N#HSJ`26NQJvX|0$}%6zSvTyI@RSuQ5`{(O!yA`g!Xr;B{J8NP282Q&{ zf_ldTu8kjE_JU5p56qWa@5r9`FFMS+C45Vc!1A8VH4P9p*Hm8}q0Ymm*a0HUnu#q* zb%rOWh3@tbbc8IfBG_g916Mow--*9uo6%p%Q*+_Vvw2K6QVV?MnF-Bj;va3K=g=B@UAjm622wazcDp?>a#bEp5)HEc>y z5;pkts4>wYAuW}_JDQ9A%FgP<96>H`H|~5^@lX_NwC!0Q@O0K2I&%rqo%S$beW=E5 zY+|r^%r9L#KulG>h@ao-E$%QvR5dQl8_VnI+1rQFTw%0Qn`@hyWT%#E;7q5R6I3NT zLkX?V5BjoYOTltUvZU4Lny5($KK>Ycq)Rr6#U4FL5;H{S^4#w-vyna+l7Zw|iU}Qhj2(ZiWJF+R zKPK39XQwSZJAF;?YbM#TuiZh2EFRzU(k0unxXiNvvKb~gq;x^R|ENd%SlZFjqPJ-u zVy^TZ4o({TCxfDW-KuAgy4L2e7Ca4dE_#%>+KI~?95^*E0X}rd3b2W<>1bD(4s&Z4 zeuSvEarFoZAf<{$c35C}Y)jVtND7zC?F`A%kshvON7Kr55VZ>NEY0EGLF#LKK}|CS z+w>Y&h>hC;H4R90KHrj13Y;klSdND{$cQMlGxy4AY=_Eb~03E>*ypws`8N9GxGv@N(JE_4oDnfUSH>yhxaGBXyFL-C=gda@xSmlIi zvU?iY5f|>dFlQPJC^c2)4n{ebWdjY@evWx*cYIh0L(oOzJ|Qa7xNay-ZXDD4j2-Ih zJ=;WqSH@<1Tm#1pNVx&lFLpMydk=lQ z`2)k5i%$UnzN-)K-F*@8G-xXO@L5z@oxrhb?e``dn7>uL)ClZ*0jwxNWrcWS5|NUAYf}L!~F4%nw3$K5p?lqKW zDeyi)+sSjSAwdnDqDg%<+*bo~%6lU-;QQEb1zKf}ZwGyd(|fgs?}xZ#VH4^Ry+YEmXK(AaEb&lUYf((5-%NnOWK9CTe(Zh;1VfiX-JEt|3na%zZotceD^f0{- zu6e(^F^bZYRFjbIWnlkq#iJ94WhsQE;X3cmoyAvi&#Fj};vd{G6V z=T#bwo0m7*E+rfo9a3UwQY8jmoh$1uxn}LY_b3ZUsXKA-+)KK z_RJWOF5r+Am|-{2Uafx))%*iqAQ!E#b@i^J6a2u>3bGjRHs3-#7$QIT!If;E3iVr` z?@+Cg3|d$y1Ax-9T-hX3+uLeg<+_qbk}J99QI2OZ>`#GVAp`R3_TKFCA~n4 z0|6^9vD`I3u}#$nmr-wcsVFmp-08H^+F0_$}_a0a4OhUhu~t{=a-}qbe<(B zC%uUplRJg>9V2b?0cB;D22^gI((uac9+rdSL?3Dn!1@)f7k!=Ia)0H#>0Q!IV5h!j zRI}`g&Gb7A>(1v6>yn?D7Sy83+>nb;DlDqr}V$^s{>en-4#L{QL zF#M|9lUrP09c>EEUUW9>k)uKG4odE4m2j6TdX2JCs|W^2!<{+3ibkXL*`n~!liT># zFl!5%Q_LeEQ>3!2l%iYmaeS{e5RjaZ6FlXXeeS8!rMuoNw+F9@we;#iR=|I-8|Q8yNhEru)U!v{EKJqt0^yarGQ@SzDW)>Q89KAlz^NLly_J+ z4y*7xJv_17{mj`WLMTqW?Cc}q*Pc)8s!Qg@3TTa&mURN!#qb~CaojjVJ112;VLzR_ z11}+xg;0=9bD7<8Id`z)xrZrp(&9W!??YF!imx2bu?lw|icWrBekiRpY(x;vycOgr z_Apt@Zze{X_{8JN!{Ri~g;N@8xjR0#9#+3vaSPtI6{m+spZqa%WqfyF@H?kDG0 zo!z#eYzTDCWLa`vxbA2}#YtJVHQbGPNYUr!fTpwEeD_w4%bNck{j)W``*Q4q!~NGG zXI>55!1V@xxXf~JC*Mqg7S)>)nA@2D7~DQg2si^*Asp<{vRO!peybXLKUKS4! zQ4x2*v39rcn6M0p-^IwSduXkRrZ3OExOO&t`Lmkt^jp;!%9o`!v45Zr9#_P^W_V7l ze>Pd(@lm)Nk8Yb8D7Ynehpjz_oC4i4O|(CmBaStH?8;D$4ZNa z!T)2b$k&>92BU{)iYpsyn-Th*5tw%v^;MJ6gfYEsd@2iG-P_ukrmCy)bKnO4=Krhh zOvBkq`#;W@R+mxJj%lfuPVKE$MbQw6QbHB2sHGx0C3Hk+#o7j~cB=LzB9>Ov+FGJQ zn4nZ^jhzybT1E(>DI!v0`^WRl)BlU-^>bd{FTU6Lo$K7!Ip;pV&*#R(h*}%jDOK#$ zZ7cflx+XAOnN2yz{?}_9YYh(LWJOJNZFfpOB7oO*rn*mJ-(d0m4F&m&Kn38?=4dWI zv^q(8UG_B^JnDsLf}JE%Jq}%Nx!E|p*`r&pv;&ZSbO0=_6fJd1T;=U*lIIJ941W8y zAEf0{T09Jcmzg4m1=mz#G>vF@e(iAtL53c*zdNv2+gv6?em_SQX8y3M>FuKGFD_ff zC~81h;O9K%+VAxPY9aL7z7^nuTM6AG1~w;T-4QbqISAn>qEmz2(P`4Sy5ArL88NdQ zZ0}Tw`aI>Vw$RDRgzdEz0j_@F4EMYjN1%6GO()D7lw4fsncE8f*!GaV9{A}rIomB# z$gJ$WE~_Lzyg6aZ;+d8M`}p$wqHcQJ5(z-xV~v2P9!Aka5>j;7iK(DG1HImYOlVKE z^w0!qc&E6w(Y8v0^yra?Ndlw&n!37p&`)8bi$`G38YJ=iO?e?r#5|WMiu^UsW@1ih zQ_4cB;tgr`Id5Xt-7BL33d6VeKJx|q;d?Qx$!*PwR`7r@Vo>xE&W@`Uz5wD4*e8T? zZmmFb&8B_z=flL3FpN~TSu$X2DAC95L*l(2N?o>YrU>}2C8i8`ogaF_2g_ZWu)i=UhbY2fv^d@coUcU62-7m4H9I0WV%^$|#s?m~^ z#uB68^+7kcW#9aKY6lcmdxc$Fa-LU8wV``QC@8zs%XeDaYh0rCzh$wn-qM!~XSJ!! z*uAszi4S;742`s*O}-&L4s=fah4-6`$P&~zSR_u@#cyf9~$x#PsgQDaacij%(2aT=we@rtD*tV5?>iEYDTd@INM?N zIrFOCOc5|mhC~pWqlW(o5!=_Uk`;3*7y4EzU))RSuQ#dvf=7V+pRd!wV(I$&{97B0 z=2~UB{*E8Vr^cLo_i4yq18d)xxJgrmJp*H6{0fAa4u+QVqDqkO(Eac!Nn50mb5e!s znb182>ZTCwAI~20IsX^sU?UA!->r6+x8|@CE3&-NEWR2HDN>wmNkgkQfeWjV`ibP1 zfNvR8TxcrD{Y5WUM1+GY(~`S%!Ld^x_hGgMBmA3Bi=6tRZza(CIE>{_MkVbpAN1oO zC}}ZlC7=;oAk@o6U2+UX*9oj%1DhYr;0jNSSJ$K4JQSk!tA_g@OgIY+4@%cxjl2?> z5F@VqM!l+_eIH2vsp+R?Q_nj|>OQCSWIl0J8Mz|`Q&~~`qY%TOS!sGv1{?xH!EyKJ z;?6kP1Ie|~M|474f#qweL&xTBjdEP%G376SbpHcPer19t?Kve>WxNW7$bb!WoeS|t z#b-jhkiIH-s2}yjlcOS_l0|aFbQr7@|voz{>^fUfUA;1T~SX+h&gT zuxB?DD7BP^VD7_cNwLYSD|zu|x6kc8W+ZJJDeZR$nKJzwJXHT?K_CuDknjBH?A-`P zSh;T8JrSlFG%!8Xv0A4wte(}=Fx}Dv_Awx=DF0dH`G))Dp(-)_XQfzh<@tjE*8YM> z@r9ZJ_B}S>iCy=bY5dLNL;kZ#nWojRM17G7jg&G?daNcB@5Oa+_i^FWc1ufpWAdx< zZYIlZG%W8rGf=83(Qaw*4hSx>EfQ&6@-Ou8=~nUo8x9^?d9FSDr4yT69-0rIz?`I= zDKi+g8LxlrJ0r7&0wTYT*w#7tBzi;}hRR$xbi2 z{vy)%^dFr3IO>>%3;$LK{Rm)2wLB|Ua2&#rx#`LL__A8wTiN~mRzE@a0aqAtbteUG z7xm(GXfk)YL*xax%cJymzW+>ieG{(i&`$PXI`ucU{Ngx;K3}%XwtDx)Cn!PtNw{lo88E86G9i{b2Dj)&iCe)pRr7Zvfex>ZV9_P9FY5ans%SoLF@Bu*wNxbdC9AvSux{g=PDHw7R-U(;&~C+} zq@2?L+dzizdArA;N$HhFo-spO<+2??D)KX!F*ou|t>d-N>Clv3>zTsjJ8t0l;p5f9 zrGlf#KK@NaCaNh|-*>(yk~Gp=8VQY+8NHe(PHrh!_Z6%zgnb1FW#fTT3D<{Nm9o#g-8J-vq4 zPIlNCq%*#0aBD)u;v&uNS>Cf!HQ^~s6>x_2-20F^m&ZJ=>mRwO51Uz?IYPstR5suT z!Sk%lxOk61ol%>ZvKvFb1@mfQyY-Ae4i1anKQd$z!Is$yjMF})H=Qt4PevmwD{i9c zdIrvX`O)XSYb?iVTv4wCa6OeuIzhlr-@ombIK%mF&o^@s=dz;eQ9>}h zM*?yN#Y_HL(%~eMDH&A8#~naWMhO1MsfEkdnv)f1h0C}aX*n~NRw1wpUQMGI_gxvc(wtzB{}^cK$aV^H6YyySsgb^Gu)uQ>j=> zP(~SSoYv9ePrw>PVaujag890I-%n~(q9N{o<37Ohg(8QFTLxJb6BQW8pv_lCIv<@8 z&DRzNcIq@wNk3xg?T5!=cU4c7Wqp-T$!Y!-F{3 zjI-C&4?$_ExU*F)Y4wlPRF~LCK2dX=D_od86!Q#>y}3V97`k7#2p$`-5ck2iYzQi> z+Z3UTq84ggPS&RCzOW_bqXmMJHdTMvWlLi{oV=#K4UbVu4kaIqnEXf;Eg1kvG$&M9 zpy}wM6GI8HVflCEg|A}=Pyk1`%hZ!D?Bk-uYN!13Bbl~yAxD1rGUp&hB(AEQ zOC&Q8u@CWsK__R`F_#c$6naFi+TgrWEfx+F{ z-0S`m`T$6Gosn(m7(O?6_1pC?>G?|zdsTWLl(x^j%Byy#zZOLGysP*b6juha2ffFI zd$3{E^RwhAPjHhAf;O4#27|I-4q@c@p1PyMY1+R?uSi_I z>8|V>*{?sOz^WgKsYp&`l%#K8O zaM$a_A&=6Qk%C21GB{L7=UEyB^mo()5_5QSE){2`y9J+}!bb??!^G|3@Qob2qr(hI za{VZgRkOzLpxOyIAXIUM|4SO$stFHfOz%q#Vjt#q5ou4d1ZPjK0>7z*xaGzsJm6Vo zwCOEGHa<15pcZRfpAW&IIaWxS1p(5G(R$L|I@H!uE7W!Sm0 z@Wj>CkC`l`pR$wVNIU&sQrDO&&z9!;I zNuturl_{=~>~36zGa*OP5Vk<3>Vdcg7dr1642w=4egptZYb3 zEuJX457L{@j(N2NV$tqm8M@34LmTgj=Y>CQ1Fk+Xh}Z7fQxaQ1IV^&%H;9^Opjevx0+7$jIBIT6RZWy_au z132;>%~~7P9)hy|8=J!cHPfgdJMa|j*_Mx*+eS4u^jkyRmJ#UE%vv*A^uWNEFBfl7 zTxVtDCsL5a`c!I4ImU1G=9n!y;W{F!`H_Pw#?5GJ67}n^a};<0%gUmmSjuc5Kg3HPaTJ3I6;8=ueRscAs5Di+<4jA zVO3lxlX>#9Vs_55@NcpDzxj$gVy6lr6%~T)l^0u>Ex+^yTUR?)om4QZGQKzYfnW5{ zIrrh=O$!$k23x$zirAC%ID$`i_``7BOO=1uy7vWyz7@T>Z^!6fNYJ%KXL@DWn8YHn zI#ha_z83CR`gL_>e7)RRvuNL5bdUt*%e(#+-x$>etA{{fSmsm=fZ literal 0 HcmV?d00001 diff --git a/scripts/afterPack.cjs b/scripts/afterPack.cjs new file mode 100644 index 00000000..e11d96ff --- /dev/null +++ b/scripts/afterPack.cjs @@ -0,0 +1,39 @@ +/** + * electron-builder afterPack hook + * Copies node_modules to the standalone directory in the packaged app + */ + +const { cpSync, existsSync } = require("fs") +const path = require("path") + +module.exports = async function (context) { + const appOutDir = context.appOutDir + const resourcesDir = path.join( + appOutDir, + context.packager.platform.name === "mac" + ? `${context.packager.appInfo.productFilename}.app/Contents/Resources` + : "resources" + ) + const standaloneDir = path.join(resourcesDir, "standalone") + const sourceNodeModules = path.join( + context.packager.projectDir, + "electron-standalone", + "node_modules" + ) + const targetNodeModules = path.join(standaloneDir, "node_modules") + + console.log(`[afterPack] Copying node_modules to ${targetNodeModules}`) + + if (existsSync(sourceNodeModules) && existsSync(standaloneDir)) { + cpSync(sourceNodeModules, targetNodeModules, { recursive: true }) + console.log("[afterPack] node_modules copied successfully") + } else { + console.error("[afterPack] Source or target directory not found!") + console.error(` Source: ${sourceNodeModules} exists: ${existsSync(sourceNodeModules)}`) + console.error(` Target dir: ${standaloneDir} exists: ${existsSync(standaloneDir)}`) + throw new Error( + "[afterPack] Failed: Required directories not found. " + + "Ensure 'npm run electron:prepare' was run before building." + ) + } +} diff --git a/scripts/electron-dev.mjs b/scripts/electron-dev.mjs new file mode 100644 index 00000000..5c1e6e9e --- /dev/null +++ b/scripts/electron-dev.mjs @@ -0,0 +1,275 @@ +#!/usr/bin/env node + +/** + * Development script for running Electron with Next.js + * 1. Reads preset configuration (if exists) + * 2. Starts Next.js dev server with preset env vars + * 3. Waits for it to be ready + * 4. Compiles Electron TypeScript + * 5. Launches Electron + * 6. Watches for preset changes and restarts Next.js + */ + +import { spawn } from "node:child_process" +import { fileURLToPath } from "node:url" +import { existsSync, readFileSync, watch } from "node:fs" +import path from "node:path" +import os from "node:os" + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootDir = path.join(__dirname, "..") + +const NEXT_PORT = 6002 +const NEXT_URL = `http://localhost:${NEXT_PORT}` + +/** + * Get the user data path (same as Electron's app.getPath("userData")) + */ +function getUserDataPath() { + const appName = "next-ai-draw-io" + switch (process.platform) { + case "darwin": + return path.join(os.homedir(), "Library", "Application Support", appName) + case "win32": + return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), appName) + default: + return path.join(os.homedir(), ".config", appName) + } +} + +/** + * Load preset configuration from config file + */ +function loadPresetConfig() { + const configPath = path.join(getUserDataPath(), "config-presets.json") + + if (!existsSync(configPath)) { + console.log("📋 No preset configuration found, using .env.local") + return null + } + + try { + const content = readFileSync(configPath, "utf-8") + const data = JSON.parse(content) + + if (!data.currentPresetId) { + console.log("📋 No active preset, using .env.local") + return null + } + + const preset = data.presets.find(p => p.id === data.currentPresetId) + if (!preset) { + console.log("📋 Active preset not found, using .env.local") + return null + } + + console.log(`📋 Using preset: "${preset.name}"`) + return preset.config + } catch (error) { + console.error("Failed to load preset config:", error.message) + return null + } +} + +/** + * Wait for the Next.js server to be ready + */ +async function waitForServer(url, timeout = 120000) { + const start = Date.now() + console.log(`Waiting for server at ${url}...`) + + while (Date.now() - start < timeout) { + try { + const response = await fetch(url) + if (response.ok || response.status < 500) { + console.log("Server is ready!") + return true + } + } catch { + // Server not ready yet + } + await new Promise((r) => setTimeout(r, 500)) + process.stdout.write(".") + } + + throw new Error(`Timeout waiting for server at ${url}`) +} + +/** + * Run a command and wait for it to complete + */ +function runCommand(command, args, options = {}) { + return new Promise((resolve, reject) => { + const proc = spawn(command, args, { + cwd: rootDir, + stdio: "inherit", + shell: true, + ...options, + }) + + proc.on("close", (code) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`Command failed with code ${code}`)) + } + }) + + proc.on("error", reject) + }) +} + +/** + * Start Next.js dev server with preset environment + */ +function startNextServer(presetEnv) { + const env = { ...process.env } + + // Apply preset environment variables + if (presetEnv) { + for (const [key, value] of Object.entries(presetEnv)) { + if (value !== undefined && value !== "") { + env[key] = value + } + } + } + + const nextProcess = spawn("npm", ["run", "dev"], { + cwd: rootDir, + stdio: "inherit", + shell: true, + env, + }) + + nextProcess.on("error", (err) => { + console.error("Failed to start Next.js:", err) + }) + + return nextProcess +} + +/** + * Main entry point + */ +async function main() { + console.log("🚀 Starting Electron development environment...\n") + + // Load preset configuration + const presetEnv = loadPresetConfig() + + // Start Next.js dev server with preset env + console.log("1. Starting Next.js development server...") + let nextProcess = startNextServer(presetEnv) + + // Wait for Next.js to be ready + try { + await waitForServer(NEXT_URL) + console.log("") + } catch (err) { + console.error("\n❌ Next.js server failed to start:", err.message) + nextProcess.kill() + process.exit(1) + } + + // Compile Electron TypeScript + console.log("\n2. Compiling Electron code...") + try { + await runCommand("npm", ["run", "electron:compile"]) + } catch (err) { + console.error("❌ Electron compilation failed:", err.message) + nextProcess.kill() + process.exit(1) + } + + // Start Electron + console.log("\n3. Starting Electron...") + const electronProcess = spawn("npm", ["run", "electron:start"], { + cwd: rootDir, + stdio: "inherit", + shell: true, + env: { + ...process.env, + NODE_ENV: "development", + ELECTRON_DEV_URL: NEXT_URL, + }, + }) + + // Watch for preset config changes + const configPath = path.join(getUserDataPath(), "config-presets.json") + let configWatcher = null + let restartPending = false + + function setupConfigWatcher() { + if (!existsSync(path.dirname(configPath))) { + // Directory doesn't exist yet, check again later + setTimeout(setupConfigWatcher, 5000) + return + } + + try { + configWatcher = watch(configPath, { persistent: false }, async (eventType) => { + if (eventType === "change" && !restartPending) { + restartPending = true + console.log("\n🔄 Preset configuration changed, restarting Next.js server...") + + // Kill current Next.js process + nextProcess.kill() + + // Wait a bit for process to die + await new Promise(r => setTimeout(r, 1000)) + + // Reload preset and restart + const newPresetEnv = loadPresetConfig() + nextProcess = startNextServer(newPresetEnv) + + try { + await waitForServer(NEXT_URL) + console.log("✅ Next.js server restarted with new configuration\n") + } catch (err) { + console.error("❌ Failed to restart Next.js:", err.message) + } + + restartPending = false + } + }) + console.log("👀 Watching for preset configuration changes...") + } catch (err) { + // File might not exist yet, that's ok + setTimeout(setupConfigWatcher, 5000) + } + } + + // Start watching after a delay (config file might not exist yet) + setTimeout(setupConfigWatcher, 2000) + + electronProcess.on("close", (code) => { + console.log(`\nElectron exited with code ${code}`) + if (configWatcher) configWatcher.close() + nextProcess.kill() + process.exit(code || 0) + }) + + electronProcess.on("error", (err) => { + console.error("Electron error:", err) + if (configWatcher) configWatcher.close() + nextProcess.kill() + process.exit(1) + }) + + // Handle termination signals + const cleanup = () => { + console.log("\n🛑 Shutting down...") + if (configWatcher) configWatcher.close() + electronProcess.kill() + nextProcess.kill() + process.exit(0) + } + + process.on("SIGINT", cleanup) + process.on("SIGTERM", cleanup) +} + +main().catch((err) => { + console.error("Fatal error:", err) + process.exit(1) +}) diff --git a/scripts/prepare-electron-build.mjs b/scripts/prepare-electron-build.mjs new file mode 100644 index 00000000..d2e8d0ba --- /dev/null +++ b/scripts/prepare-electron-build.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node + +/** + * Prepare standalone directory for Electron packaging + * Copies the Next.js standalone output to a temp directory + * that electron-builder can properly include + */ + +import { cpSync, rmSync, existsSync, mkdirSync } from "node:fs" +import { join } from "node:path" +import { fileURLToPath } from "node:url" + +const __dirname = fileURLToPath(new URL(".", import.meta.url)) +const rootDir = join(__dirname, "..") + +const standaloneDir = join(rootDir, ".next", "standalone") +const staticDir = join(rootDir, ".next", "static") +const targetDir = join(rootDir, "electron-standalone") + +console.log("Preparing Electron build...") + +// Clean target directory +if (existsSync(targetDir)) { + console.log("Cleaning previous build...") + rmSync(targetDir, { recursive: true }) +} + +// Create target directory +mkdirSync(targetDir, { recursive: true }) + +// Copy standalone (includes node_modules) +console.log("Copying standalone directory...") +cpSync(standaloneDir, targetDir, { recursive: true }) + +// Copy static files +console.log("Copying static files...") +const targetStaticDir = join(targetDir, ".next", "static") +mkdirSync(targetStaticDir, { recursive: true }) +cpSync(staticDir, targetStaticDir, { recursive: true }) + +console.log("Done! Files prepared in electron-standalone/") diff --git a/tsconfig.json b/tsconfig.json index fa238a08..6b47dbef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,5 +29,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": ["node_modules", "packages"] + "exclude": ["node_modules", "packages", "electron", "dist-electron"] } From fc5eca877a4c41f5182f9866bd7c461f29334ade Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:39:28 +0900 Subject: [PATCH 019/349] chore: bump version to 0.4.5 (#346) * chore: bump version to 0.4.5 and add desktop app to README * style: auto-format with Biome --------- Co-authored-by: github-actions[bot] --- README.md | 23 ++++++++++ package-lock.json | 4 +- package.json | 2 +- scripts/afterPack.cjs | 16 ++++--- scripts/electron-dev.mjs | 70 +++++++++++++++++++----------- scripts/prepare-electron-build.mjs | 2 +- 6 files changed, 82 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 42903b60..ec172133 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1 - [MCP Server (Preview)](#mcp-server-preview) - [Getting Started](#getting-started) - [Try it Online](#try-it-online) + - [Desktop Application](#desktop-application) - [Run with Docker (Recommended)](#run-with-docker-recommended) - [Installation](#installation) - [Deployment](#deployment) @@ -135,6 +136,28 @@ No installation needed! Try the app directly on our demo site: > **Bring Your Own API Key**: You can use your own API key to bypass usage limits on the demo site. Click the Settings icon in the chat panel to configure your provider and API key. Your key is stored locally in your browser and is never stored on the server. +### Desktop Application + +Download the native desktop app for your platform from the [Releases page](https://github.com/DayuanJiang/next-ai-draw-io/releases): + +| Platform | Download | +|----------|----------| +| macOS | `.dmg` (Intel & Apple Silicon) | +| Windows | `.exe` installer (x64 & ARM64) | +| Linux | `.AppImage` or `.deb` (x64 & ARM64) | + +**Features:** +- **Secure API key storage**: Credentials encrypted using OS keychain +- **Configuration presets**: Save and switch between AI providers via menu +- **Native file dialogs**: Open/save `.drawio` files directly +- **Offline capable**: Works without internet after first launch + +**Quick Setup:** +1. Download and install for your platform +2. Open the app → **Menu → Configuration → Manage Presets** +3. Add your AI provider credentials +4. Start creating diagrams! + ### Run with Docker (Recommended) If you just want to run it locally, the best way is to use Docker. diff --git a/package-lock.json b/package-lock.json index c3f6f495..1731ee9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "next-ai-draw-io", - "version": "0.4.4", + "version": "0.4.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "next-ai-draw-io", - "version": "0.4.4", + "version": "0.4.5", "license": "Apache-2.0", "dependencies": { "@ai-sdk/amazon-bedrock": "^3.0.70", diff --git a/package.json b/package.json index faee5111..4c39e269 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next-ai-draw-io", - "version": "0.4.4", + "version": "0.4.5", "license": "Apache-2.0", "private": true, "main": "dist-electron/main/index.js", diff --git a/scripts/afterPack.cjs b/scripts/afterPack.cjs index e11d96ff..839c46b7 100644 --- a/scripts/afterPack.cjs +++ b/scripts/afterPack.cjs @@ -6,19 +6,19 @@ const { cpSync, existsSync } = require("fs") const path = require("path") -module.exports = async function (context) { +module.exports = async (context) => { const appOutDir = context.appOutDir const resourcesDir = path.join( appOutDir, context.packager.platform.name === "mac" ? `${context.packager.appInfo.productFilename}.app/Contents/Resources` - : "resources" + : "resources", ) const standaloneDir = path.join(resourcesDir, "standalone") const sourceNodeModules = path.join( context.packager.projectDir, "electron-standalone", - "node_modules" + "node_modules", ) const targetNodeModules = path.join(standaloneDir, "node_modules") @@ -29,11 +29,15 @@ module.exports = async function (context) { console.log("[afterPack] node_modules copied successfully") } else { console.error("[afterPack] Source or target directory not found!") - console.error(` Source: ${sourceNodeModules} exists: ${existsSync(sourceNodeModules)}`) - console.error(` Target dir: ${standaloneDir} exists: ${existsSync(standaloneDir)}`) + console.error( + ` Source: ${sourceNodeModules} exists: ${existsSync(sourceNodeModules)}`, + ) + console.error( + ` Target dir: ${standaloneDir} exists: ${existsSync(standaloneDir)}`, + ) throw new Error( "[afterPack] Failed: Required directories not found. " + - "Ensure 'npm run electron:prepare' was run before building." + "Ensure 'npm run electron:prepare' was run before building.", ) } } diff --git a/scripts/electron-dev.mjs b/scripts/electron-dev.mjs index 5c1e6e9e..c8eb946d 100644 --- a/scripts/electron-dev.mjs +++ b/scripts/electron-dev.mjs @@ -11,10 +11,10 @@ */ import { spawn } from "node:child_process" -import { fileURLToPath } from "node:url" import { existsSync, readFileSync, watch } from "node:fs" -import path from "node:path" import os from "node:os" +import path from "node:path" +import { fileURLToPath } from "node:url" const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootDir = path.join(__dirname, "..") @@ -29,9 +29,18 @@ function getUserDataPath() { const appName = "next-ai-draw-io" switch (process.platform) { case "darwin": - return path.join(os.homedir(), "Library", "Application Support", appName) + return path.join( + os.homedir(), + "Library", + "Application Support", + appName, + ) case "win32": - return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), appName) + return path.join( + process.env.APPDATA || + path.join(os.homedir(), "AppData", "Roaming"), + appName, + ) default: return path.join(os.homedir(), ".config", appName) } @@ -57,7 +66,7 @@ function loadPresetConfig() { return null } - const preset = data.presets.find(p => p.id === data.currentPresetId) + const preset = data.presets.find((p) => p.id === data.currentPresetId) if (!preset) { console.log("📋 Active preset not found, using .env.local") return null @@ -207,31 +216,42 @@ async function main() { } try { - configWatcher = watch(configPath, { persistent: false }, async (eventType) => { - if (eventType === "change" && !restartPending) { - restartPending = true - console.log("\n🔄 Preset configuration changed, restarting Next.js server...") + configWatcher = watch( + configPath, + { persistent: false }, + async (eventType) => { + if (eventType === "change" && !restartPending) { + restartPending = true + console.log( + "\n🔄 Preset configuration changed, restarting Next.js server...", + ) - // Kill current Next.js process - nextProcess.kill() + // Kill current Next.js process + nextProcess.kill() - // Wait a bit for process to die - await new Promise(r => setTimeout(r, 1000)) + // Wait a bit for process to die + await new Promise((r) => setTimeout(r, 1000)) - // Reload preset and restart - const newPresetEnv = loadPresetConfig() - nextProcess = startNextServer(newPresetEnv) + // Reload preset and restart + const newPresetEnv = loadPresetConfig() + nextProcess = startNextServer(newPresetEnv) - try { - await waitForServer(NEXT_URL) - console.log("✅ Next.js server restarted with new configuration\n") - } catch (err) { - console.error("❌ Failed to restart Next.js:", err.message) + try { + await waitForServer(NEXT_URL) + console.log( + "✅ Next.js server restarted with new configuration\n", + ) + } catch (err) { + console.error( + "❌ Failed to restart Next.js:", + err.message, + ) + } + + restartPending = false } - - restartPending = false - } - }) + }, + ) console.log("👀 Watching for preset configuration changes...") } catch (err) { // File might not exist yet, that's ok diff --git a/scripts/prepare-electron-build.mjs b/scripts/prepare-electron-build.mjs index d2e8d0ba..6cb5eafe 100644 --- a/scripts/prepare-electron-build.mjs +++ b/scripts/prepare-electron-build.mjs @@ -6,7 +6,7 @@ * that electron-builder can properly include */ -import { cpSync, rmSync, existsSync, mkdirSync } from "node:fs" +import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs" import { join } from "node:path" import { fileURLToPath } from "node:url" From 4f07a5fafcd6978815fb132158e55bf018035b1c Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Mon, 22 Dec 2025 11:08:03 +0900 Subject: [PATCH 020/349] fix: add write permissions to electron build jobs (#347) --- .github/workflows/electron-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml index dff69d2e..5c1717d2 100644 --- a/.github/workflows/electron-release.yml +++ b/.github/workflows/electron-release.yml @@ -12,6 +12,8 @@ on: jobs: build: + permissions: + contents: write strategy: fail-fast: false matrix: From b25b9446000857b2682a3ac780f9aec9459a30a5 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Mon, 22 Dec 2025 11:36:38 +0900 Subject: [PATCH 021/349] fix(ci): simplify electron release - let electron-builder publish directly (#349) --- .github/workflows/electron-release.yml | 58 +------------------------- 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml index 5c1717d2..f652b5a9 100644 --- a/.github/workflows/electron-release.yml +++ b/.github/workflows/electron-release.yml @@ -40,63 +40,7 @@ jobs: - name: Install dependencies run: npm ci - - name: Build Electron app + - name: Build and publish Electron app run: npm run dist:${{ matrix.platform }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload macOS artifacts - if: matrix.platform == 'mac' - uses: actions/upload-artifact@v4 - with: - name: mac-build - path: | - release/*.dmg - release/*.zip - if-no-files-found: error - - - name: Upload Windows artifacts - if: matrix.platform == 'win' - uses: actions/upload-artifact@v4 - with: - name: win-build - path: | - release/*.exe - if-no-files-found: error - - - name: Upload Linux artifacts - if: matrix.platform == 'linux' - uses: actions/upload-artifact@v4 - with: - name: linux-build - path: | - release/*.AppImage - release/*.deb - if-no-files-found: error - - release: - needs: build - runs-on: ubuntu-latest - permissions: - contents: write - if: startsWith(github.ref, 'refs/tags/') - - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Create Release - uses: softprops/action-gh-release@v1 - with: - files: | - artifacts/**/*.dmg - artifacts/**/*.zip - artifacts/**/*.exe - artifacts/**/*.AppImage - artifacts/**/*.deb - draft: true - generate_release_notes: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From b088a0653e5879322e3c8b76ddfe3272a3c1a20f Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:24:08 +0900 Subject: [PATCH 022/349] chore: update app icons with new diagram hierarchy design (#350) --- public/favicon-192x192.png | Bin 13778 -> 14697 bytes public/favicon-512x512.png | Bin 55260 -> 113915 bytes public/favicon.ico | Bin 15406 -> 20666 bytes resources/icon.png | Bin 55260 -> 113915 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/favicon-192x192.png b/public/favicon-192x192.png index 98eb3046f9197ab05a04fa58126d7b288ff9a568..ab5a016143795fe0685041c50aa5eaa8c908ae34 100644 GIT binary patch literal 14697 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 diff --git a/public/favicon-512x512.png b/public/favicon-512x512.png index 38abad4528a03a8d05be863887e62a92b78a4df1..20c6148c848c978342e90593a4ed540d138da839 100644 GIT binary patch literal 113915 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

xd)lkCjP); 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:feN{*SG2yNUqJvW98h-3-DE#C{!VzyJR@>_RVny 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 diff --git a/public/favicon.ico b/public/favicon.ico index 869186adee238dfd3a52a8c5818a3ff825913e98..4fe45ffa0c80a641bdcd36d474b236848873b815 100644 GIT binary patch literal 20666 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 Gf&T#l)P@!S diff --git a/resources/icon.png b/resources/icon.png index 38abad4528a03a8d05be863887e62a92b78a4df1..20c6148c848c978342e90593a4ed540d138da839 100644 GIT binary patch literal 113915 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

xd)lkCjP); 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:feN{*SG2yNUqJvW98h-3-DE#C{!VzyJR@>_RVny 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 From 85cb441e26eb0c14a55bc2e90a915788f0f5f1f2 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:36:36 +0900 Subject: [PATCH 023/349] feat: multi-provider model configuration with UI/UX improvements (#355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add multi-provider model configuration - Add model config dialog for managing multiple AI providers - Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway - Add model selector dropdown in chat panel header - Add API key validation endpoint - Add custom model ID input with keyboard navigation - Fix hover highlight in Command component - Add suggested models for each provider including latest Claude 4.5 series - Store configuration locally in browser * feat: improve model config UI and move selector to chat input - Move model selector from header to chat input (left of send button) - Add per-model validation status (queued, running, valid, invalid) - Filter model selector to only show verified models - Add editable model IDs in config dialog - Add custom model input field alongside suggested models dropdown - Fix hover states on provider buttons and select triggers - Update OpenAI suggested models with GPT-5 series - Add alert-dialog component for delete confirmation * refactor: revert shadcn component changes, apply hover fix at usage site * feat: add AWS credentials support for Bedrock provider - Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock - Show different credential fields based on provider type - Update validation API to handle Bedrock with AWS credentials - Add region selector with common AWS regions * fix: reset Test button after validation completes * fix: reset validation button to Test after success * fix: complete bedrock support and UI/UX improvements - Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials - Pass AWS credentials through full chain (headers → API → provider) - Replace non-existent GPT-5 models with real ones (o1, o3-mini) - Add accessibility: aria-labels, focus-visible rings, inline errors - Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo) - Fix setTimeout cleanup with useRef on component unmount - Fix TypeScript type consistency in getSelectedAIConfig fallback * chore: remove unused code - Remove unused setAccessCodeRequired state in chat-panel.tsx - Remove unused getSelectedModel export in model-config.ts * fix: UI/UX improvements for model configuration dialog - Add gradient header styling with icon badge - Change Configuration section icon from Key to Settings2 - Add duplicate model detection with warning banner and inline removal - Filter out already-added models from suggestions dropdown - Add type-to-confirm for deleting providers with 3+ models - Enhance delete confirmation dialog with warning icon - Improve model selector discoverability (show model name + chevron) - Add truncation for long model names with title tooltip - Remove AI provider settings from Settings dialog (now in Model Config) - Extract ValidationButton into reusable component * fix: prevent duplicate model IDs within same provider - Block adding model if ID already exists in provider - Block editing model ID to match existing model in provider * fix: improve duplicate model ID notifications - Add toast notification when trying to add duplicate model - Allow free typing when editing model ID, validate on blur - Show warning toast instead of blocking input * fix: improve duplicate model validation UX in config dialog - Add inline error display for duplicate model IDs - Show red border on input when error exists - Validate on blur with shake animation for edit errors - Prevent saving empty model names - Clear errors when user starts typing - Simplify error styling (small red text, no heavy chips) --- app/api/chat/route.ts | 5 + app/api/validate-model/route.ts | 213 +++ components/ai-elements/model-selector.tsx | 156 +++ components/chat-input.tsx | 19 + components/chat-panel.tsx | 40 +- components/model-config-dialog.tsx | 1492 +++++++++++++++++++++ components/model-selector.tsx | 216 +++ components/settings-dialog.tsx | 205 --- components/ui/alert-dialog.tsx | 157 +++ components/ui/command.tsx | 191 +++ components/ui/popover.tsx | 48 + hooks/use-model-config.ts | 373 ++++++ lib/ai-providers.ts | 31 +- lib/i18n/dictionaries/en.json | 19 + lib/i18n/dictionaries/ja.json | 19 + lib/i18n/dictionaries/zh.json | 19 + lib/storage.ts | 4 + lib/types/model-config.ts | 277 ++++ package-lock.json | 913 +++++++++++-- package.json | 7 +- 20 files changed, 4090 insertions(+), 314 deletions(-) create mode 100644 app/api/validate-model/route.ts create mode 100644 components/ai-elements/model-selector.tsx create mode 100644 components/model-config-dialog.tsx create mode 100644 components/model-selector.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/popover.tsx create mode 100644 hooks/use-model-config.ts create mode 100644 lib/types/model-config.ts diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 5c446832..b9024e4e 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -214,6 +214,11 @@ async function handleChatRequest(req: Request): Promise { baseUrl: req.headers.get("x-ai-base-url"), apiKey: req.headers.get("x-ai-api-key"), modelId: req.headers.get("x-ai-model"), + // AWS Bedrock credentials + awsAccessKeyId: req.headers.get("x-aws-access-key-id"), + awsSecretAccessKey: req.headers.get("x-aws-secret-access-key"), + awsRegion: req.headers.get("x-aws-region"), + awsSessionToken: req.headers.get("x-aws-session-token"), } // Read minimal style preference from header diff --git a/app/api/validate-model/route.ts b/app/api/validate-model/route.ts new file mode 100644 index 00000000..7beeaa47 --- /dev/null +++ b/app/api/validate-model/route.ts @@ -0,0 +1,213 @@ +import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock" +import { createAnthropic } from "@ai-sdk/anthropic" +import { createDeepSeek, deepseek } from "@ai-sdk/deepseek" +import { createGateway } from "@ai-sdk/gateway" +import { createGoogleGenerativeAI } from "@ai-sdk/google" +import { createOpenAI } from "@ai-sdk/openai" +import { createOpenRouter } from "@openrouter/ai-sdk-provider" +import { generateText } from "ai" +import { NextResponse } from "next/server" +import { createOllama } from "ollama-ai-provider-v2" + +export const runtime = "nodejs" + +interface ValidateRequest { + provider: string + apiKey: string + baseUrl?: string + modelId: string + // AWS Bedrock specific + awsAccessKeyId?: string + awsSecretAccessKey?: string + awsRegion?: string +} + +export async function POST(req: Request) { + try { + const body: ValidateRequest = await req.json() + const { + provider, + apiKey, + baseUrl, + modelId, + awsAccessKeyId, + awsSecretAccessKey, + awsRegion, + } = body + + if (!provider || !modelId) { + return NextResponse.json( + { valid: false, error: "Provider and model ID are required" }, + { status: 400 }, + ) + } + + // Validate credentials based on provider + if (provider === "bedrock") { + if (!awsAccessKeyId || !awsSecretAccessKey || !awsRegion) { + return NextResponse.json( + { + valid: false, + error: "AWS credentials (Access Key ID, Secret Access Key, Region) are required", + }, + { status: 400 }, + ) + } + } else if (provider !== "ollama" && !apiKey) { + return NextResponse.json( + { valid: false, error: "API key is required" }, + { status: 400 }, + ) + } + + let model: any + + switch (provider) { + case "openai": { + const openai = createOpenAI({ + apiKey, + ...(baseUrl && { baseURL: baseUrl }), + }) + model = openai.chat(modelId) + break + } + + case "anthropic": { + const anthropic = createAnthropic({ + apiKey, + baseURL: baseUrl || "https://api.anthropic.com/v1", + }) + model = anthropic(modelId) + break + } + + case "google": { + const google = createGoogleGenerativeAI({ + apiKey, + ...(baseUrl && { baseURL: baseUrl }), + }) + model = google(modelId) + break + } + + case "azure": { + const azure = createOpenAI({ + apiKey, + baseURL: baseUrl, + }) + model = azure.chat(modelId) + break + } + + case "bedrock": { + const bedrock = createAmazonBedrock({ + accessKeyId: awsAccessKeyId, + secretAccessKey: awsSecretAccessKey, + region: awsRegion, + }) + model = bedrock(modelId) + break + } + + case "openrouter": { + const openrouter = createOpenRouter({ + apiKey, + ...(baseUrl && { baseURL: baseUrl }), + }) + model = openrouter(modelId) + break + } + + case "deepseek": { + if (baseUrl || apiKey) { + const ds = createDeepSeek({ + apiKey, + ...(baseUrl && { baseURL: baseUrl }), + }) + model = ds(modelId) + } else { + model = deepseek(modelId) + } + break + } + + case "siliconflow": { + const sf = createOpenAI({ + apiKey, + baseURL: baseUrl || "https://api.siliconflow.com/v1", + }) + model = sf.chat(modelId) + break + } + + case "ollama": { + const ollama = createOllama({ + baseURL: baseUrl || "http://localhost:11434", + }) + model = ollama(modelId) + break + } + + case "gateway": { + const gw = createGateway({ + apiKey, + ...(baseUrl && { baseURL: baseUrl }), + }) + model = gw(modelId) + break + } + + default: + return NextResponse.json( + { valid: false, error: `Unknown provider: ${provider}` }, + { status: 400 }, + ) + } + + // Make a minimal test request + const startTime = Date.now() + await generateText({ + model, + prompt: "Say 'OK'", + maxOutputTokens: 20, + }) + const responseTime = Date.now() - startTime + + return NextResponse.json({ + valid: true, + responseTime, + }) + } catch (error) { + console.error("[validate-model] Error:", error) + + let errorMessage = "Validation failed" + if (error instanceof Error) { + // Extract meaningful error message + if ( + error.message.includes("401") || + error.message.includes("Unauthorized") + ) { + errorMessage = "Invalid API key" + } else if ( + error.message.includes("404") || + error.message.includes("not found") + ) { + errorMessage = "Model not found" + } else if ( + error.message.includes("429") || + error.message.includes("rate limit") + ) { + errorMessage = "Rate limited - try again later" + } else if (error.message.includes("ECONNREFUSED")) { + errorMessage = "Cannot connect to server" + } else { + errorMessage = error.message.slice(0, 100) + } + } + + return NextResponse.json( + { valid: false, error: errorMessage }, + { status: 200 }, // Return 200 so client can read error message + ) + } +} diff --git a/components/ai-elements/model-selector.tsx b/components/ai-elements/model-selector.tsx new file mode 100644 index 00000000..d5bcae2c --- /dev/null +++ b/components/ai-elements/model-selector.tsx @@ -0,0 +1,156 @@ +import { Cloud } from "lucide-react" +import type { ComponentProps, ReactNode } from "react" +import { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +} from "@/components/ui/command" +import { + Dialog, + DialogContent, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { cn } from "@/lib/utils" + +export type ModelSelectorProps = ComponentProps + +export const ModelSelector = (props: ModelSelectorProps) => ( +

+) + +export type ModelSelectorTriggerProps = ComponentProps + +export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => ( + +) + +export type ModelSelectorContentProps = ComponentProps & { + title?: ReactNode +} + +export const ModelSelectorContent = ({ + className, + children, + title = "Model Selector", + ...props +}: ModelSelectorContentProps) => ( + + {title} + + {children} + + +) + +export type ModelSelectorDialogProps = ComponentProps + +export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => ( + +) + +export type ModelSelectorInputProps = ComponentProps + +export const ModelSelectorInput = ({ + className, + ...props +}: ModelSelectorInputProps) => ( + +) + +export type ModelSelectorListProps = ComponentProps + +export const ModelSelectorList = (props: ModelSelectorListProps) => ( + +) + +export type ModelSelectorEmptyProps = ComponentProps + +export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => ( + +) + +export type ModelSelectorGroupProps = ComponentProps + +export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => ( + +) + +export type ModelSelectorItemProps = ComponentProps + +export const ModelSelectorItem = (props: ModelSelectorItemProps) => ( + +) + +export type ModelSelectorShortcutProps = ComponentProps + +export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => ( + +) + +export type ModelSelectorSeparatorProps = ComponentProps< + typeof CommandSeparator +> + +export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => ( + +) + +export type ModelSelectorLogoProps = Omit< + ComponentProps<"img">, + "src" | "alt" +> & { + provider: string +} + +export const ModelSelectorLogo = ({ + provider, + className, + ...props +}: ModelSelectorLogoProps) => { + // Use Lucide icon for bedrock since models.dev doesn't have a good AWS icon + if (provider === "amazon-bedrock") { + return + } + + return ( + {`${provider} + ) +} + +export type ModelSelectorLogoGroupProps = ComponentProps<"div"> + +export const ModelSelectorLogoGroup = ({ + className, + ...props +}: ModelSelectorLogoGroupProps) => ( +
img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground", + className, + )} + {...props} + /> +) + +export type ModelSelectorNameProps = ComponentProps<"span"> + +export const ModelSelectorName = ({ + className, + ...props +}: ModelSelectorNameProps) => ( + +) diff --git a/components/chat-input.tsx b/components/chat-input.tsx index 684d94d1..fd67c845 100644 --- a/components/chat-input.tsx +++ b/components/chat-input.tsx @@ -14,6 +14,7 @@ import { toast } from "sonner" import { ButtonWithTooltip } from "@/components/button-with-tooltip" import { ErrorToast } from "@/components/error-toast" import { HistoryDialog } from "@/components/history-dialog" +import { ModelSelector } from "@/components/model-selector" import { ResetWarningModal } from "@/components/reset-warning-modal" import { SaveDialog } from "@/components/save-dialog" import { Button } from "@/components/ui/button" @@ -28,6 +29,7 @@ import { useDiagram } from "@/contexts/diagram-context" import { useDictionary } from "@/hooks/use-dictionary" import { formatMessage } from "@/lib/i18n/utils" import { isPdfFile, isTextFile } from "@/lib/pdf-utils" +import type { FlattenedModel } from "@/lib/types/model-config" import { FilePreviewList } from "./file-preview-list" const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB @@ -156,6 +158,11 @@ interface ChatInputProps { error?: Error | null minimalStyle?: boolean onMinimalStyleChange?: (value: boolean) => void + // Model selector props + models?: FlattenedModel[] + selectedModelId?: string + onModelSelect?: (modelId: string | undefined) => void + onConfigureModels?: () => void } export function ChatInput({ @@ -173,6 +180,10 @@ export function ChatInput({ error = null, minimalStyle = false, onMinimalStyleChange = () => {}, + models = [], + selectedModelId, + onModelSelect = () => {}, + onConfigureModels = () => {}, }: ChatInputProps) { const dict = useDictionary() const { @@ -465,6 +476,14 @@ export function ChatInput({ disabled={isDisabled} /> + +
+ ) +} + +export function ModelConfigDialog({ + open, + onOpenChange, + modelConfig, +}: ModelConfigDialogProps) { + const dict = useDictionary() + const [selectedProviderId, setSelectedProviderId] = useState( + null, + ) + const [showApiKey, setShowApiKey] = useState(false) + const [validationStatus, setValidationStatus] = + useState("idle") + const [validationError, setValidationError] = useState("") + const [scrollState, setScrollState] = useState({ top: false, bottom: true }) + const [customModelInput, setCustomModelInput] = useState("") + const scrollRef = useRef(null) + const validationResetTimeoutRef = useRef | null>(null) + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) + const [deleteConfirmText, setDeleteConfirmText] = useState("") + const [validatingModelIndex, setValidatingModelIndex] = useState< + number | null + >(null) + const [duplicateError, setDuplicateError] = useState("") + const [editError, setEditError] = useState<{ + modelId: string + message: string + } | null>(null) + + const { + config, + addProvider, + updateProvider, + deleteProvider, + addModel, + updateModel, + deleteModel, + } = modelConfig + + // Get selected provider + const selectedProvider = config.providers.find( + (p) => p.id === selectedProviderId, + ) + + // Track scroll position for gradient shadows + useEffect(() => { + const scrollEl = scrollRef.current?.querySelector( + "[data-radix-scroll-area-viewport]", + ) as HTMLElement | null + if (!scrollEl) return + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = scrollEl + setScrollState({ + top: scrollTop > 10, + bottom: scrollTop < scrollHeight - clientHeight - 10, + }) + } + + handleScroll() // Initial check + scrollEl.addEventListener("scroll", handleScroll) + return () => scrollEl.removeEventListener("scroll", handleScroll) + }, [selectedProvider]) + + // Cleanup validation reset timeout on unmount + useEffect(() => { + return () => { + if (validationResetTimeoutRef.current) { + clearTimeout(validationResetTimeoutRef.current) + } + } + }, []) + + // Get suggested models for current provider + const suggestedModels = selectedProvider + ? SUGGESTED_MODELS[selectedProvider.provider] || [] + : [] + + // Filter out already-added models from suggestions + const existingModelIds = + selectedProvider?.models.map((m) => m.modelId) || [] + const availableSuggestions = suggestedModels.filter( + (modelId) => !existingModelIds.includes(modelId), + ) + + // Handle adding a new provider + const handleAddProvider = (providerType: ProviderName) => { + const newProvider = addProvider(providerType) + setSelectedProviderId(newProvider.id) + setValidationStatus("idle") + } + + // Handle provider field updates + const handleProviderUpdate = ( + field: keyof ProviderConfig, + value: string | boolean, + ) => { + if (!selectedProviderId) return + updateProvider(selectedProviderId, { [field]: value }) + // Reset validation when credentials change + const credentialFields = [ + "apiKey", + "baseUrl", + "awsAccessKeyId", + "awsSecretAccessKey", + "awsRegion", + ] + if (credentialFields.includes(field)) { + setValidationStatus("idle") + updateProvider(selectedProviderId, { validated: false }) + } + } + + // Handle adding a model to current provider + // Returns true if model was added successfully, false otherwise + const handleAddModel = (modelId: string): boolean => { + if (!selectedProviderId || !selectedProvider) return false + // Prevent duplicate model IDs + if (existingModelIds.includes(modelId)) { + setDuplicateError(`Model "${modelId}" already exists`) + return false + } + setDuplicateError("") + addModel(selectedProviderId, modelId) + return true + } + + // Handle deleting a model + const handleDeleteModel = (modelConfigId: string) => { + if (!selectedProviderId) return + deleteModel(selectedProviderId, modelConfigId) + } + + // Handle deleting the provider + const handleDeleteProvider = () => { + if (!selectedProviderId) return + deleteProvider(selectedProviderId) + setSelectedProviderId(null) + setValidationStatus("idle") + setDeleteConfirmOpen(false) + } + + // Validate all models + const handleValidate = useCallback(async () => { + if (!selectedProvider) return + + // Check credentials based on provider type + const isBedrock = selectedProvider.provider === "bedrock" + if (isBedrock) { + if ( + !selectedProvider.awsAccessKeyId || + !selectedProvider.awsSecretAccessKey || + !selectedProvider.awsRegion + ) { + return + } + } else if (!selectedProvider.apiKey) { + return + } + + // Need at least one model to validate + if (selectedProvider.models.length === 0) { + setValidationError("Add at least one model to validate") + setValidationStatus("error") + return + } + + setValidationStatus("validating") + setValidationError("") + + let allValid = true + let errorCount = 0 + + // Validate each model + for (let i = 0; i < selectedProvider.models.length; i++) { + const model = selectedProvider.models[i] + setValidatingModelIndex(i) + + try { + const response = await fetch("/api/validate-model", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + provider: selectedProvider.provider, + apiKey: selectedProvider.apiKey, + baseUrl: selectedProvider.baseUrl, + modelId: model.modelId, + // AWS Bedrock credentials + awsAccessKeyId: selectedProvider.awsAccessKeyId, + awsSecretAccessKey: selectedProvider.awsSecretAccessKey, + awsRegion: selectedProvider.awsRegion, + }), + }) + + const data = await response.json() + + if (data.valid) { + updateModel(selectedProviderId!, model.id, { + validated: true, + validationError: undefined, + }) + } else { + allValid = false + errorCount++ + updateModel(selectedProviderId!, model.id, { + validated: false, + validationError: data.error || "Validation failed", + }) + } + } catch { + allValid = false + errorCount++ + updateModel(selectedProviderId!, model.id, { + validated: false, + validationError: "Network error", + }) + } + } + + setValidatingModelIndex(null) + + if (allValid) { + setValidationStatus("success") + updateProvider(selectedProviderId!, { validated: true }) + // Reset to idle after showing success briefly (with cleanup) + if (validationResetTimeoutRef.current) { + clearTimeout(validationResetTimeoutRef.current) + } + validationResetTimeoutRef.current = setTimeout(() => { + setValidationStatus("idle") + validationResetTimeoutRef.current = null + }, 1500) + } else { + setValidationStatus("error") + setValidationError(`${errorCount} model(s) failed validation`) + } + }, [selectedProvider, selectedProviderId, updateProvider, updateModel]) + + // Get all available provider types + const availableProviders = Object.keys(PROVIDER_INFO) as ProviderName[] + + // Get display name for provider + const getProviderDisplayName = (provider: ProviderConfig) => { + return provider.name || PROVIDER_INFO[provider.provider].label + } + + return ( + + + + +
+ +
+ {dict.modelConfig?.title || "AI Model Configuration"} +
+ + {dict.modelConfig?.description || + "Configure multiple AI providers and models for your workspace"} + +
+ +
+ {/* Provider List (Left Sidebar) */} +
+
+ + Providers + +
+ + +
+ {config.providers.length === 0 ? ( +
+
+ +
+

+ Add a provider to get started +

+
+ ) : ( +
+ {config.providers.map((provider) => ( + + ))} +
+ )} +
+
+ + {/* Add Provider */} +
+ +
+
+ + {/* Provider Details (Right Panel) */} +
+ {selectedProvider ? ( + <> + {/* Top gradient shadow */} +
+ {/* Bottom gradient shadow */} +
+ +
+ {/* Provider Header */} +
+
+ +
+
+

+ { + PROVIDER_INFO[ + selectedProvider + .provider + ].label + } +

+

+ {selectedProvider.models + .length === 0 + ? "No models configured" + : `${selectedProvider.models.length} model${selectedProvider.models.length > 1 ? "s" : ""} configured`} +

+
+ {selectedProvider.validated && ( +
+ + + Verified + +
+ )} +
+ + {/* Configuration Section */} +
+
+ + Configuration +
+ +
+ {/* Display Name */} +
+ + + handleProviderUpdate( + "name", + e.target.value, + ) + } + placeholder={ + PROVIDER_INFO[ + selectedProvider + .provider + ].label + } + className="h-9" + /> +
+ + {/* Credentials - different for Bedrock vs other providers */} + {selectedProvider.provider === + "bedrock" ? ( + <> + {/* AWS Access Key ID */} +
+ + + handleProviderUpdate( + "awsAccessKeyId", + e.target + .value, + ) + } + placeholder="AKIA..." + className="h-9 font-mono text-xs" + /> +
+ + {/* AWS Secret Access Key */} +
+ +
+ + handleProviderUpdate( + "awsSecretAccessKey", + e + .target + .value, + ) + } + placeholder="Enter your secret access key" + className="h-9 pr-10 font-mono text-xs" + /> + +
+
+ + {/* AWS Region */} +
+ + +
+ + {/* Test Button for Bedrock */} +
+ + {validationStatus === + "error" && + validationError && ( +

+ + { + validationError + } +

+ )} +
+ + ) : ( + <> + {/* API Key */} +
+ +
+
+ + handleProviderUpdate( + "apiKey", + e + .target + .value, + ) + } + placeholder="Enter your API key" + className="h-9 pr-10 font-mono text-xs" + /> + +
+ +
+ {validationStatus === + "error" && + validationError && ( +

+ + { + validationError + } +

+ )} +
+ + {/* Base URL */} +
+ + + handleProviderUpdate( + "baseUrl", + e.target + .value, + ) + } + placeholder={ + PROVIDER_INFO[ + selectedProvider + .provider + ] + .defaultBaseUrl || + "Custom endpoint URL" + } + className="h-9 font-mono text-xs" + /> +
+ + )} +
+
+ + {/* Models Section */} +
+
+
+ + Models +
+
+
+ { + setCustomModelInput( + e.target + .value, + ) + // Clear duplicate error when typing + if ( + duplicateError + ) { + setDuplicateError( + "", + ) + } + }} + onKeyDown={(e) => { + if ( + e.key === + "Enter" && + customModelInput.trim() + ) { + const success = + handleAddModel( + customModelInput.trim(), + ) + if ( + success + ) { + setCustomModelInput( + "", + ) + } + } + }} + className={cn( + "h-8 w-48 font-mono text-xs", + duplicateError && + "border-destructive focus-visible:ring-destructive", + )} + /> + {/* Show duplicate error for custom model input */} + {duplicateError && ( +

+ {duplicateError} +

+ )} +
+ + +
+
+ + {/* Model List */} +
+ {selectedProvider.models + .length === 0 ? ( +
+
+ +
+

+ No models configured +

+
+ ) : ( +
+ {selectedProvider.models.map( + (model, index) => ( +
+
+ {/* Status icon */} +
+ {validatingModelIndex !== + null && + index === + validatingModelIndex ? ( + // Currently validating +
+ +
+ ) : validatingModelIndex !== + null && + index > + validatingModelIndex && + model.validated === + undefined ? ( + // Queued +
+ +
+ ) : model.validated === + true ? ( + // Valid +
+ +
+ ) : model.validated === + false ? ( + // Invalid +
+ +
+ ) : ( + // Not validated yet +
+ +
+ )} +
+ { + // Allow free typing - validation happens on blur + // Clear edit error when typing + if ( + editError?.modelId === + model.id + ) { + setEditError( + null, + ) + } + updateModel( + selectedProviderId!, + model.id, + { + modelId: + e + .target + .value, + validated: + undefined, + validationError: + undefined, + }, + ) + }} + onKeyDown={( + e, + ) => { + if ( + e.key === + "Enter" + ) { + e.currentTarget.blur() + } + }} + onBlur={( + e, + ) => { + const newModelId = + e.target.value.trim() + + // Helper to show error with shake + const showError = + ( + message: string, + ) => { + setEditError( + { + modelId: + model.id, + message, + }, + ) + e.target.animate( + [ + { + transform: + "translateX(0)", + }, + { + transform: + "translateX(-4px)", + }, + { + transform: + "translateX(4px)", + }, + { + transform: + "translateX(-4px)", + }, + { + transform: + "translateX(4px)", + }, + { + transform: + "translateX(0)", + }, + ], + { + duration: 400, + easing: "ease-in-out", + }, + ) + e.target.focus() + } + + // Check for empty model name + if ( + !newModelId + ) { + showError( + "Model ID cannot be empty", + ) + return + } + + // Check for duplicate + const otherModelIds = + selectedProvider?.models + .filter( + ( + m, + ) => + m.id !== + model.id, + ) + .map( + ( + m, + ) => + m.modelId, + ) || + [] + if ( + otherModelIds.includes( + newModelId, + ) + ) { + showError( + "This model ID already exists", + ) + return + } + + // Clear error on valid blur + setEditError( + null, + ) + }} + className="flex-1 min-w-0 font-mono text-sm h-8 border-0 bg-transparent focus-visible:bg-background focus-visible:ring-1" + /> + +
+ {/* Show validation error inline */} + {model.validated === + false && + model.validationError && ( +

+ { + model.validationError + } +

+ )} + {/* Show edit error inline */} + {editError?.modelId === + model.id && ( +

+ { + editError.message + } +

+ )} +
+ ), + )} +
+ )} +
+
+ + {/* Danger Zone */} +
+ +
+
+
+ + ) : ( +
+
+ +
+

+ Configure AI Providers +

+

+ Select a provider from the list or add a new + one to configure API keys and models +

+
+ )} +
+
+ + {/* Footer */} +
+

+ + API keys are stored locally in your browser +

+
+ + + {/* Delete Confirmation Dialog */} + { + setDeleteConfirmOpen(open) + if (!open) setDeleteConfirmText("") + }} + > + + +
+ +
+ + Delete Provider + + + Are you sure you want to delete{" "} + + {selectedProvider + ? selectedProvider.name || + PROVIDER_INFO[selectedProvider.provider] + .label + : "this provider"} + + ? This will remove all configured models and cannot + be undone. + +
+ {selectedProvider && + selectedProvider.models.length >= 3 && ( +
+ + + setDeleteConfirmText(e.target.value) + } + placeholder="Type provider name..." + className="h-9" + /> +
+ )} + + Cancel + = 3 && + deleteConfirmText !== + (selectedProvider.name || + PROVIDER_INFO[selectedProvider.provider] + .label) + } + className="bg-destructive text-destructive-foreground hover:bg-destructive/90 disabled:opacity-50" + > + Delete + + +
+
+
+ ) +} diff --git a/components/model-selector.tsx b/components/model-selector.tsx new file mode 100644 index 00000000..37baa4cf --- /dev/null +++ b/components/model-selector.tsx @@ -0,0 +1,216 @@ +"use client" + +import { Bot, Check, ChevronDown, Server, Settings2 } from "lucide-react" +import { useMemo, useState } from "react" +import { + ModelSelectorContent, + ModelSelectorEmpty, + ModelSelectorGroup, + ModelSelectorInput, + ModelSelectorItem, + ModelSelectorList, + ModelSelectorLogo, + ModelSelectorName, + ModelSelector as ModelSelectorRoot, + ModelSelectorSeparator, + ModelSelectorTrigger, +} from "@/components/ai-elements/model-selector" +import { ButtonWithTooltip } from "@/components/button-with-tooltip" +import type { FlattenedModel } from "@/lib/types/model-config" +import { cn } from "@/lib/utils" + +interface ModelSelectorProps { + models: FlattenedModel[] + selectedModelId: string | undefined + onSelect: (modelId: string | undefined) => void + onConfigure: () => void + disabled?: boolean +} + +// Map our provider names to models.dev logo names +const PROVIDER_LOGO_MAP: Record = { + openai: "openai", + anthropic: "anthropic", + google: "google", + azure: "azure", + bedrock: "amazon-bedrock", + openrouter: "openrouter", + deepseek: "deepseek", + siliconflow: "siliconflow", + gateway: "vercel", +} + +// Group models by providerLabel (handles duplicate providers) +function groupModelsByProvider( + models: FlattenedModel[], +): Map { + const groups = new Map< + string, + { provider: string; models: FlattenedModel[] } + >() + for (const model of models) { + const key = model.providerLabel + const existing = groups.get(key) + if (existing) { + existing.models.push(model) + } else { + groups.set(key, { provider: model.provider, models: [model] }) + } + } + return groups +} + +export function ModelSelector({ + models, + selectedModelId, + onSelect, + onConfigure, + disabled = false, +}: ModelSelectorProps) { + const [open, setOpen] = useState(false) + // Only show validated models in the selector + const validatedModels = useMemo( + () => models.filter((m) => m.validated === true), + [models], + ) + const groupedModels = useMemo( + () => groupModelsByProvider(validatedModels), + [validatedModels], + ) + + // Find selected model for display + const selectedModel = useMemo( + () => models.find((m) => m.id === selectedModelId), + [models, selectedModelId], + ) + + const handleSelect = (value: string) => { + if (value === "__configure__") { + onConfigure() + } else if (value === "__server_default__") { + onSelect(undefined) + } else { + onSelect(value) + } + setOpen(false) + } + + const tooltipContent = selectedModel + ? `${selectedModel.modelId} (click to change)` + : "Using server default model (click to change)" + + return ( + + + + + + {selectedModel ? selectedModel.modelId : "Default"} + + + + + + + + + {validatedModels.length === 0 && models.length > 0 + ? "No verified models. Test your models first." + : "No models found."} + + + {/* Server Default Option */} + + + + + + Server Default + + + + + {/* Configured Models by Provider */} + {Array.from(groupedModels.entries()).map( + ([ + providerLabel, + { provider, models: providerModels }, + ]) => ( + + {providerModels.map((model) => ( + handleSelect(model.id)} + className="cursor-pointer" + > + + + + {model.modelId} + + + ))} + + ), + )} + + {/* Configure Option */} + + + + + + Configure Models... + + + + {/* Info text */} +
+ Only verified models are shown +
+
+
+
+ ) +} diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx index b2ba8b7c..029fc8d9 100644 --- a/components/settings-dialog.tsx +++ b/components/settings-dialog.tsx @@ -12,13 +12,6 @@ import { } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { useDictionary } from "@/hooks/use-dictionary" @@ -35,10 +28,6 @@ interface SettingsDialogProps { export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code" export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection" const STORAGE_ACCESS_CODE_REQUIRED_KEY = "next-ai-draw-io-access-code-required" -export const STORAGE_AI_PROVIDER_KEY = "next-ai-draw-io-ai-provider" -export const STORAGE_AI_BASE_URL_KEY = "next-ai-draw-io-ai-base-url" -export const STORAGE_AI_API_KEY_KEY = "next-ai-draw-io-ai-api-key" -export const STORAGE_AI_MODEL_KEY = "next-ai-draw-io-ai-model" function getStoredAccessCodeRequired(): boolean | null { if (typeof window === "undefined") return null @@ -64,10 +53,6 @@ export function SettingsDialog({ const [accessCodeRequired, setAccessCodeRequired] = useState( () => getStoredAccessCodeRequired() ?? false, ) - const [provider, setProvider] = useState("") - const [baseUrl, setBaseUrl] = useState("") - const [apiKey, setApiKey] = useState("") - const [modelId, setModelId] = useState("") useEffect(() => { // Only fetch if not cached in localStorage @@ -104,12 +89,6 @@ export function SettingsDialog({ // Default to true if not set setCloseProtection(storedCloseProtection !== "false") - // Load AI provider settings - setProvider(localStorage.getItem(STORAGE_AI_PROVIDER_KEY) || "") - setBaseUrl(localStorage.getItem(STORAGE_AI_BASE_URL_KEY) || "") - setApiKey(localStorage.getItem(STORAGE_AI_API_KEY_KEY) || "") - setModelId(localStorage.getItem(STORAGE_AI_MODEL_KEY) || "") - setError("") } }, [open]) @@ -197,190 +176,6 @@ export function SettingsDialog({ )}
)} -
- -

- {dict.settings.aiProviderDescription} -

-
-
- - -
- {provider && provider !== "default" && ( - <> -
- - { - setModelId(e.target.value) - localStorage.setItem( - STORAGE_AI_MODEL_KEY, - e.target.value, - ) - }} - placeholder={ - provider === "openai" - ? "e.g., gpt-4o" - : provider === "anthropic" - ? "e.g., claude-sonnet-4-5" - : provider === "google" - ? "e.g., gemini-2.0-flash-exp" - : provider === - "deepseek" - ? "e.g., deepseek-chat" - : dict.settings - .modelId - } - /> -
-
- - { - setApiKey(e.target.value) - localStorage.setItem( - STORAGE_AI_API_KEY_KEY, - e.target.value, - ) - }} - placeholder={ - dict.settings.apiKeyPlaceholder - } - autoComplete="off" - /> -

- {dict.settings.overrides}{" "} - {provider === "openai" - ? "OPENAI_API_KEY" - : provider === "anthropic" - ? "ANTHROPIC_API_KEY" - : provider === "google" - ? "GOOGLE_GENERATIVE_AI_API_KEY" - : provider === "azure" - ? "AZURE_API_KEY" - : provider === - "openrouter" - ? "OPENROUTER_API_KEY" - : provider === - "deepseek" - ? "DEEPSEEK_API_KEY" - : provider === - "siliconflow" - ? "SILICONFLOW_API_KEY" - : "server API key"} -

-
-
- - { - setBaseUrl(e.target.value) - localStorage.setItem( - STORAGE_AI_BASE_URL_KEY, - e.target.value, - ) - }} - placeholder={ - provider === "anthropic" - ? "https://api.anthropic.com/v1" - : provider === "siliconflow" - ? "https://api.siliconflow.com/v1" - : dict.settings - .customEndpoint - } - /> -
- - - )} -
-
-
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/components/ui/command.tsx b/components/ui/command.tsx new file mode 100644 index 00000000..82e269a1 --- /dev/null +++ b/components/ui/command.tsx @@ -0,0 +1,191 @@ +"use client" + +import * as React from "react" +import { Command as CommandPrimitive } from "cmdk" +import { SearchIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" + +function Command({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + className, + ...props +}: React.ComponentProps & { + title?: string + description?: string + className?: string +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + { + // Ensure hover updates selection for visual feedback + const item = e.currentTarget + item.setAttribute("data-selected", "true") + // Deselect siblings + const siblings = item.parentElement?.querySelectorAll("[cmdk-item]") + siblings?.forEach((sibling) => { + if (sibling !== item) { + sibling.setAttribute("data-selected", "false") + } + }) + }} + {...props} + /> + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx new file mode 100644 index 00000000..01e468b6 --- /dev/null +++ b/components/ui/popover.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/hooks/use-model-config.ts b/hooks/use-model-config.ts new file mode 100644 index 00000000..08c89c4f --- /dev/null +++ b/hooks/use-model-config.ts @@ -0,0 +1,373 @@ +"use client" + +import { useCallback, useEffect, useState } from "react" +import { STORAGE_KEYS } from "@/lib/storage" +import { + createEmptyConfig, + createModelConfig, + createProviderConfig, + type FlattenedModel, + findModelById, + flattenModels, + type ModelConfig, + type MultiModelConfig, + PROVIDER_INFO, + type ProviderConfig, + type ProviderName, +} from "@/lib/types/model-config" + +// Old storage keys for migration +const OLD_KEYS = { + aiProvider: "next-ai-draw-io-ai-provider", + aiBaseUrl: "next-ai-draw-io-ai-base-url", + aiApiKey: "next-ai-draw-io-ai-api-key", + aiModel: "next-ai-draw-io-ai-model", +} + +/** + * Migrate from old single-provider format to new multi-model format + */ +function migrateOldConfig(): MultiModelConfig | null { + if (typeof window === "undefined") return null + + const oldProvider = localStorage.getItem(OLD_KEYS.aiProvider) + const oldApiKey = localStorage.getItem(OLD_KEYS.aiApiKey) + const oldModel = localStorage.getItem(OLD_KEYS.aiModel) + + // No old config to migrate + if (!oldProvider || !oldApiKey || !oldModel) return null + + const oldBaseUrl = localStorage.getItem(OLD_KEYS.aiBaseUrl) + + // Create new config from old format + const provider = createProviderConfig(oldProvider as ProviderName) + provider.apiKey = oldApiKey + if (oldBaseUrl) provider.baseUrl = oldBaseUrl + + const model = createModelConfig(oldModel) + provider.models.push(model) + + const config: MultiModelConfig = { + version: 1, + providers: [provider], + selectedModelId: model.id, + } + + // Clear old keys after migration + localStorage.removeItem(OLD_KEYS.aiProvider) + localStorage.removeItem(OLD_KEYS.aiBaseUrl) + localStorage.removeItem(OLD_KEYS.aiApiKey) + localStorage.removeItem(OLD_KEYS.aiModel) + + return config +} + +/** + * Load config from localStorage + */ +function loadConfig(): MultiModelConfig { + if (typeof window === "undefined") return createEmptyConfig() + + // First, check if new format exists + const stored = localStorage.getItem(STORAGE_KEYS.modelConfigs) + if (stored) { + try { + return JSON.parse(stored) as MultiModelConfig + } catch { + console.error("Failed to parse model config") + } + } + + // Try migration from old format + const migrated = migrateOldConfig() + if (migrated) { + // Save migrated config + localStorage.setItem( + STORAGE_KEYS.modelConfigs, + JSON.stringify(migrated), + ) + return migrated + } + + return createEmptyConfig() +} + +/** + * Save config to localStorage + */ +function saveConfig(config: MultiModelConfig): void { + if (typeof window === "undefined") return + localStorage.setItem(STORAGE_KEYS.modelConfigs, JSON.stringify(config)) +} + +export interface UseModelConfigReturn { + // State + config: MultiModelConfig + isLoaded: boolean + + // Getters + models: FlattenedModel[] + selectedModel: FlattenedModel | undefined + selectedModelId: string | undefined + + // Actions + setSelectedModelId: (modelId: string | undefined) => void + addProvider: (provider: ProviderName) => ProviderConfig + updateProvider: ( + providerId: string, + updates: Partial, + ) => void + deleteProvider: (providerId: string) => void + addModel: (providerId: string, modelId: string) => ModelConfig + updateModel: ( + providerId: string, + modelConfigId: string, + updates: Partial, + ) => void + deleteModel: (providerId: string, modelConfigId: string) => void + resetConfig: () => void +} + +export function useModelConfig(): UseModelConfigReturn { + const [config, setConfig] = useState(createEmptyConfig) + const [isLoaded, setIsLoaded] = useState(false) + + // Load config on mount + useEffect(() => { + const loaded = loadConfig() + setConfig(loaded) + setIsLoaded(true) + }, []) + + // Save config whenever it changes (after initial load) + useEffect(() => { + if (isLoaded) { + saveConfig(config) + } + }, [config, isLoaded]) + + // Derived state + const models = flattenModels(config) + const selectedModel = config.selectedModelId + ? findModelById(config, config.selectedModelId) + : undefined + + // Actions + const setSelectedModelId = useCallback((modelId: string | undefined) => { + setConfig((prev) => ({ + ...prev, + selectedModelId: modelId, + })) + }, []) + + const addProvider = useCallback( + (provider: ProviderName): ProviderConfig => { + const newProvider = createProviderConfig(provider) + setConfig((prev) => ({ + ...prev, + providers: [...prev.providers, newProvider], + })) + return newProvider + }, + [], + ) + + const updateProvider = useCallback( + (providerId: string, updates: Partial) => { + setConfig((prev) => ({ + ...prev, + providers: prev.providers.map((p) => + p.id === providerId ? { ...p, ...updates } : p, + ), + })) + }, + [], + ) + + const deleteProvider = useCallback((providerId: string) => { + setConfig((prev) => { + const provider = prev.providers.find((p) => p.id === providerId) + const modelIds = provider?.models.map((m) => m.id) || [] + + // Clear selected model if it belongs to deleted provider + const newSelectedId = + prev.selectedModelId && modelIds.includes(prev.selectedModelId) + ? undefined + : prev.selectedModelId + + return { + ...prev, + providers: prev.providers.filter((p) => p.id !== providerId), + selectedModelId: newSelectedId, + } + }) + }, []) + + const addModel = useCallback( + (providerId: string, modelId: string): ModelConfig => { + const newModel = createModelConfig(modelId) + setConfig((prev) => ({ + ...prev, + providers: prev.providers.map((p) => + p.id === providerId + ? { ...p, models: [...p.models, newModel] } + : p, + ), + })) + return newModel + }, + [], + ) + + const updateModel = useCallback( + ( + providerId: string, + modelConfigId: string, + updates: Partial, + ) => { + setConfig((prev) => ({ + ...prev, + providers: prev.providers.map((p) => + p.id === providerId + ? { + ...p, + models: p.models.map((m) => + m.id === modelConfigId + ? { ...m, ...updates } + : m, + ), + } + : p, + ), + })) + }, + [], + ) + + const deleteModel = useCallback( + (providerId: string, modelConfigId: string) => { + setConfig((prev) => ({ + ...prev, + providers: prev.providers.map((p) => + p.id === providerId + ? { + ...p, + models: p.models.filter( + (m) => m.id !== modelConfigId, + ), + } + : p, + ), + // Clear selected model if it was deleted + selectedModelId: + prev.selectedModelId === modelConfigId + ? undefined + : prev.selectedModelId, + })) + }, + [], + ) + + const resetConfig = useCallback(() => { + setConfig(createEmptyConfig()) + }, []) + + return { + config, + isLoaded, + models, + selectedModel, + selectedModelId: config.selectedModelId, + setSelectedModelId, + addProvider, + updateProvider, + deleteProvider, + addModel, + updateModel, + deleteModel, + resetConfig, + } +} + +/** + * Get the AI config for the currently selected model. + * Returns format compatible with existing getAIConfig() usage. + */ +export function getSelectedAIConfig(): { + accessCode: string + aiProvider: string + aiBaseUrl: string + aiApiKey: string + aiModel: string + // AWS Bedrock credentials + awsAccessKeyId: string + awsSecretAccessKey: string + awsRegion: string + awsSessionToken: string +} { + const empty = { + accessCode: "", + aiProvider: "", + aiBaseUrl: "", + aiApiKey: "", + aiModel: "", + awsAccessKeyId: "", + awsSecretAccessKey: "", + awsRegion: "", + awsSessionToken: "", + } + + if (typeof window === "undefined") return empty + + // Get access code (separate from model config) + const accessCode = localStorage.getItem(STORAGE_KEYS.accessCode) || "" + + // Load multi-model config + const stored = localStorage.getItem(STORAGE_KEYS.modelConfigs) + if (!stored) { + // Fallback to old format for backward compatibility + return { + accessCode, + aiProvider: localStorage.getItem(OLD_KEYS.aiProvider) || "", + aiBaseUrl: localStorage.getItem(OLD_KEYS.aiBaseUrl) || "", + aiApiKey: localStorage.getItem(OLD_KEYS.aiApiKey) || "", + aiModel: localStorage.getItem(OLD_KEYS.aiModel) || "", + // Old format didn't support AWS + awsAccessKeyId: "", + awsSecretAccessKey: "", + awsRegion: "", + awsSessionToken: "", + } + } + + let config: MultiModelConfig + try { + config = JSON.parse(stored) + } catch { + return { ...empty, accessCode } + } + + // No selected model = use server default + if (!config.selectedModelId) { + return { ...empty, accessCode } + } + + // Find selected model + const model = findModelById(config, config.selectedModelId) + if (!model) { + return { ...empty, accessCode } + } + + return { + accessCode, + aiProvider: model.provider, + aiBaseUrl: model.baseUrl || "", + aiApiKey: model.apiKey, + aiModel: model.modelId, + // AWS Bedrock credentials + awsAccessKeyId: model.awsAccessKeyId || "", + awsSecretAccessKey: model.awsSecretAccessKey || "", + awsRegion: model.awsRegion || "", + awsSessionToken: model.awsSessionToken || "", + } +} diff --git a/lib/ai-providers.ts b/lib/ai-providers.ts index 101ea706..02ecb1ec 100644 --- a/lib/ai-providers.ts +++ b/lib/ai-providers.ts @@ -33,6 +33,11 @@ export interface ClientOverrides { baseUrl?: string | null apiKey?: string | null modelId?: string | null + // AWS Bedrock credentials + awsAccessKeyId?: string | null + awsSecretAccessKey?: string | null + awsRegion?: string | null + awsSessionToken?: string | null } // Providers that can be used with client-provided API keys @@ -41,6 +46,7 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [ "anthropic", "google", "azure", + "bedrock", "openrouter", "deepseek", "siliconflow", @@ -537,12 +543,25 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { switch (provider) { case "bedrock": { - // Use credential provider chain for IAM role support (Lambda, EC2, etc.) - // Falls back to env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) for local dev - const bedrockProvider = createAmazonBedrock({ - region: process.env.AWS_REGION || "us-west-2", - credentialProvider: fromNodeProviderChain(), - }) + // Use client-provided credentials if available, otherwise fall back to IAM/env vars + const hasClientCredentials = + overrides?.awsAccessKeyId && overrides?.awsSecretAccessKey + const bedrockRegion = + overrides?.awsRegion || process.env.AWS_REGION || "us-west-2" + + const bedrockProvider = hasClientCredentials + ? createAmazonBedrock({ + region: bedrockRegion, + accessKeyId: overrides.awsAccessKeyId!, + secretAccessKey: overrides.awsSecretAccessKey!, + ...(overrides?.awsSessionToken && { + sessionToken: overrides.awsSessionToken, + }), + }) + : createAmazonBedrock({ + region: bedrockRegion, + credentialProvider: fromNodeProviderChain(), + }) model = bedrockProvider(modelId) // Add Anthropic beta options if using Claude models via Bedrock if (modelId.includes("anthropic.claude")) { diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index 636abf57..44912cf3 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -180,5 +180,24 @@ "seekingSponsorship": "Call for Sponsorship", "contactMe": "Contact Me", "usageNotice": "Due to high usage, I have changed the model from Claude to minimax-m2 and added some usage limits. See About page for details." + }, + "modelConfig": { + "title": "AI Model Configuration", + "description": "Configure multiple AI providers and models", + "configure": "Configure", + "addProvider": "Add Provider", + "addModel": "Add Model", + "modelId": "Model ID", + "modelLabel": "Display Label", + "streaming": "Enable Streaming", + "deleteProvider": "Delete Provider", + "deleteModel": "Delete Model", + "noModels": "No models configured. Add a model to get started.", + "selectProvider": "Select a provider or add a new one", + "configureMultiple": "Configure multiple AI providers and switch between them easily", + "apiKeyStored": "API keys are stored locally in your browser", + "test": "Test", + "validationError": "Validation failed", + "addModelFirst": "Add at least one model to validate" } } diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json index 0e9f8067..68001dd7 100644 --- a/lib/i18n/dictionaries/ja.json +++ b/lib/i18n/dictionaries/ja.json @@ -180,5 +180,24 @@ "seekingSponsorship": "スポンサー募集", "contactMe": "お問い合わせ", "usageNotice": "利用量の増加に伴い、コスト削減のためモデルを Claude から minimax-m2 に変更し、いくつかの利用制限を設けました。詳細は概要ページをご覧ください。" + }, + "modelConfig": { + "title": "AIモデル設定", + "description": "複数のAIプロバイダーとモデルを設定", + "configure": "設定", + "addProvider": "プロバイダーを追加", + "addModel": "モデルを追加", + "modelId": "モデルID", + "modelLabel": "表示名", + "streaming": "ストリーミングを有効", + "deleteProvider": "プロバイダーを削除", + "deleteModel": "モデルを削除", + "noModels": "モデルが設定されていません。モデルを追加してください。", + "selectProvider": "プロバイダーを選択または追加してください", + "configureMultiple": "複数のAIプロバイダーを設定して簡単に切り替え", + "apiKeyStored": "APIキーはブラウザにローカル保存されます", + "test": "テスト", + "validationError": "検証に失敗しました", + "addModelFirst": "検証するには少なくとも1つのモデルを追加してください" } } diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json index 3b86c2cd..43a8e7e6 100644 --- a/lib/i18n/dictionaries/zh.json +++ b/lib/i18n/dictionaries/zh.json @@ -180,5 +180,24 @@ "seekingSponsorship": "寻求赞助(求大佬捞一把)", "contactMe": "联系我", "usageNotice": "由于使用量过高,我已将模型从 Claude 更换为 minimax-m2,并设置了一些用量限制。详情请查看关于页面。" + }, + "modelConfig": { + "title": "AI 模型配置", + "description": "配置多个 AI 提供商和模型", + "configure": "配置", + "addProvider": "添加提供商", + "addModel": "添加模型", + "modelId": "模型 ID", + "modelLabel": "显示名称", + "streaming": "启用流式输出", + "deleteProvider": "删除提供商", + "deleteModel": "删除模型", + "noModels": "尚未配置模型。添加模型以开始使用。", + "selectProvider": "选择一个提供商或添加新的", + "configureMultiple": "配置多个 AI 提供商并轻松切换", + "apiKeyStored": "API 密钥存储在您的浏览器本地", + "test": "测试", + "validationError": "验证失败", + "addModelFirst": "请先添加至少一个模型以进行验证" } } diff --git a/lib/storage.ts b/lib/storage.ts index a782cafe..12e7ba83 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -24,4 +24,8 @@ export const STORAGE_KEYS = { aiBaseUrl: "next-ai-draw-io-ai-base-url", aiApiKey: "next-ai-draw-io-ai-api-key", aiModel: "next-ai-draw-io-ai-model", + + // Multi-model configuration + modelConfigs: "next-ai-draw-io-model-configs", + selectedModelId: "next-ai-draw-io-selected-model-id", } as const diff --git a/lib/types/model-config.ts b/lib/types/model-config.ts new file mode 100644 index 00000000..efe6a79f --- /dev/null +++ b/lib/types/model-config.ts @@ -0,0 +1,277 @@ +// Types for multi-provider model configuration + +export type ProviderName = + | "openai" + | "anthropic" + | "google" + | "azure" + | "bedrock" + | "openrouter" + | "deepseek" + | "siliconflow" + | "gateway" + +// Individual model configuration +export interface ModelConfig { + id: string // UUID for this model + modelId: string // e.g., "gpt-4o", "claude-sonnet-4-5" + validated?: boolean // Has this model been validated + validationError?: string // Error message if validation failed +} + +// Provider configuration +export interface ProviderConfig { + id: string // UUID for this provider config + provider: ProviderName + name?: string // Custom display name (e.g., "OpenAI Production") + apiKey: string + baseUrl?: string + // AWS Bedrock specific fields + awsAccessKeyId?: string + awsSecretAccessKey?: string + awsRegion?: string + awsSessionToken?: string // Optional, for temporary credentials + models: ModelConfig[] + validated?: boolean // Has API key been validated +} + +// The complete multi-model configuration +export interface MultiModelConfig { + version: 1 + providers: ProviderConfig[] + selectedModelId?: string // Currently selected model's UUID +} + +// Flattened model for dropdown display +export interface FlattenedModel { + id: string // Model config UUID + modelId: string // Actual model ID + provider: ProviderName + providerLabel: string // Provider display name + apiKey: string + baseUrl?: string + // AWS Bedrock specific fields + awsAccessKeyId?: string + awsSecretAccessKey?: string + awsRegion?: string + awsSessionToken?: string + validated?: boolean // Has this model been validated +} + +// Provider metadata +export const PROVIDER_INFO: Record< + ProviderName, + { label: string; defaultBaseUrl?: string } +> = { + openai: { label: "OpenAI" }, + anthropic: { + label: "Anthropic", + defaultBaseUrl: "https://api.anthropic.com/v1", + }, + google: { label: "Google" }, + azure: { label: "Azure OpenAI" }, + bedrock: { label: "Amazon Bedrock" }, + openrouter: { label: "OpenRouter" }, + deepseek: { label: "DeepSeek" }, + siliconflow: { + label: "SiliconFlow", + defaultBaseUrl: "https://api.siliconflow.com/v1", + }, + gateway: { label: "AI Gateway" }, +} + +// Suggested models per provider for quick add +export const SUGGESTED_MODELS: Record = { + openai: [ + // GPT-4o series (latest) + "gpt-4o", + "gpt-4o-mini", + "gpt-4o-2024-11-20", + // GPT-4 Turbo + "gpt-4-turbo", + "gpt-4-turbo-preview", + // o1/o3 reasoning models + "o1", + "o1-mini", + "o1-preview", + "o3-mini", + // GPT-4 + "gpt-4", + // GPT-3.5 + "gpt-3.5-turbo", + ], + anthropic: [ + // Claude 4.5 series (latest) + "claude-opus-4-5-20250514", + "claude-sonnet-4-5-20250514", + // Claude 4 series + "claude-opus-4-20250514", + "claude-sonnet-4-20250514", + // Claude 3.7 series + "claude-3-7-sonnet-20250219", + // Claude 3.5 series + "claude-3-5-sonnet-20241022", + "claude-3-5-haiku-20241022", + // Claude 3 series + "claude-3-opus-20240229", + "claude-3-sonnet-20240229", + "claude-3-haiku-20240307", + ], + google: [ + // Gemini 2.5 series + "gemini-2.5-pro", + "gemini-2.5-flash", + "gemini-2.5-flash-preview-05-20", + // Gemini 2.0 series + "gemini-2.0-flash", + "gemini-2.0-flash-exp", + "gemini-2.0-flash-lite", + // Gemini 1.5 series + "gemini-1.5-pro", + "gemini-1.5-flash", + // Legacy + "gemini-pro", + ], + azure: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-35-turbo"], + bedrock: [ + // Anthropic Claude + "anthropic.claude-opus-4-5-20250514-v1:0", + "anthropic.claude-sonnet-4-5-20250514-v1:0", + "anthropic.claude-opus-4-20250514-v1:0", + "anthropic.claude-sonnet-4-20250514-v1:0", + "anthropic.claude-3-7-sonnet-20250219-v1:0", + "anthropic.claude-3-5-sonnet-20241022-v2:0", + "anthropic.claude-3-5-haiku-20241022-v1:0", + "anthropic.claude-3-opus-20240229-v1:0", + "anthropic.claude-3-sonnet-20240229-v1:0", + "anthropic.claude-3-haiku-20240307-v1:0", + // Amazon Nova + "amazon.nova-pro-v1:0", + "amazon.nova-lite-v1:0", + "amazon.nova-micro-v1:0", + // Meta Llama + "meta.llama3-3-70b-instruct-v1:0", + "meta.llama3-1-405b-instruct-v1:0", + "meta.llama3-1-70b-instruct-v1:0", + // Mistral + "mistral.mistral-large-2411-v1:0", + "mistral.mistral-small-2503-v1:0", + ], + openrouter: [ + // Anthropic + "anthropic/claude-sonnet-4", + "anthropic/claude-opus-4", + "anthropic/claude-3.5-sonnet", + "anthropic/claude-3.5-haiku", + // OpenAI + "openai/gpt-4o", + "openai/gpt-4o-mini", + "openai/o1", + "openai/o3-mini", + // Google + "google/gemini-2.5-pro", + "google/gemini-2.5-flash", + "google/gemini-2.0-flash-exp:free", + // Meta Llama + "meta-llama/llama-3.3-70b-instruct", + "meta-llama/llama-3.1-405b-instruct", + "meta-llama/llama-3.1-70b-instruct", + // DeepSeek + "deepseek/deepseek-chat", + "deepseek/deepseek-r1", + // Qwen + "qwen/qwen-2.5-72b-instruct", + ], + deepseek: ["deepseek-chat", "deepseek-reasoner", "deepseek-coder"], + siliconflow: [ + // DeepSeek + "deepseek-ai/DeepSeek-V3", + "deepseek-ai/DeepSeek-R1", + "deepseek-ai/DeepSeek-V2.5", + // Qwen + "Qwen/Qwen2.5-72B-Instruct", + "Qwen/Qwen2.5-32B-Instruct", + "Qwen/Qwen2.5-Coder-32B-Instruct", + "Qwen/Qwen2.5-7B-Instruct", + "Qwen/Qwen2-VL-72B-Instruct", + ], + gateway: [ + "openai/gpt-4o", + "openai/gpt-4o-mini", + "anthropic/claude-sonnet-4-5", + "anthropic/claude-3-5-sonnet", + "google/gemini-2.0-flash", + ], +} + +// Helper to generate UUID +export function generateId(): string { + return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}` +} + +// Create empty config +export function createEmptyConfig(): MultiModelConfig { + return { + version: 1, + providers: [], + selectedModelId: undefined, + } +} + +// Create new provider config +export function createProviderConfig(provider: ProviderName): ProviderConfig { + return { + id: generateId(), + provider, + apiKey: "", + baseUrl: PROVIDER_INFO[provider].defaultBaseUrl, + models: [], + validated: false, + } +} + +// Create new model config +export function createModelConfig(modelId: string): ModelConfig { + return { + id: generateId(), + modelId, + } +} + +// Get all models as flattened list for dropdown +export function flattenModels(config: MultiModelConfig): FlattenedModel[] { + const models: FlattenedModel[] = [] + + for (const provider of config.providers) { + // Use custom name if provided, otherwise use default provider label + const providerLabel = + provider.name || PROVIDER_INFO[provider.provider].label + + for (const model of provider.models) { + models.push({ + id: model.id, + modelId: model.modelId, + provider: provider.provider, + providerLabel, + apiKey: provider.apiKey, + baseUrl: provider.baseUrl, + // AWS Bedrock fields + awsAccessKeyId: provider.awsAccessKeyId, + awsSecretAccessKey: provider.awsSecretAccessKey, + awsRegion: provider.awsRegion, + awsSessionToken: provider.awsSessionToken, + validated: model.validated, + }) + } + } + + return models +} + +// Find model by ID +export function findModelById( + config: MultiModelConfig, + modelId: string, +): FlattenedModel | undefined { + return flattenModels(config).find((m) => m.id === modelId) +} diff --git a/package-lock.json b/package-lock.json index 1731ee9f..ba8fd305 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,12 +26,14 @@ "@openrouter/ai-sdk-provider": "^1.2.3", "@opentelemetry/exporter-trace-otlp-http": "^0.208.0", "@opentelemetry/sdk-trace-node": "^2.2.0", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-use-controllable-state": "^1.2.2", @@ -40,6 +42,7 @@ "base-64": "^1.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "js-tiktoken": "^1.0.21", "jsdom": "^26.0.0", "jsonrepair": "^3.13.1", @@ -4340,6 +4343,111 @@ "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", "license": "MIT" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", @@ -4655,23 +4763,23 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", - "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, @@ -4690,14 +4798,214 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -4751,9 +5059,9 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", - "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4766,14 +5074,14 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", - "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -4790,6 +5098,77 @@ } } }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", @@ -4831,21 +5210,6 @@ } } }, - "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", @@ -4869,10 +5233,254 @@ } } }, - "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -4887,6 +5495,96 @@ } } }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", @@ -4990,6 +5688,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-scroll-area": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz", @@ -5171,46 +5887,6 @@ } } }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -5440,12 +6116,12 @@ "license": "MIT" }, "node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -5457,6 +6133,21 @@ } } }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-switch": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", @@ -5630,6 +6321,24 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", @@ -8853,6 +9562,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", diff --git a/package.json b/package.json index 4c39e269..7826186c 100644 --- a/package.json +++ b/package.json @@ -41,12 +41,14 @@ "@openrouter/ai-sdk-provider": "^1.2.3", "@opentelemetry/exporter-trace-otlp-http": "^0.208.0", "@opentelemetry/sdk-trace-node": "^2.2.0", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-use-controllable-state": "^1.2.2", @@ -55,6 +57,7 @@ "base-64": "^1.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "js-tiktoken": "^1.0.21", "jsdom": "^26.0.0", "jsonrepair": "^3.13.1", From 6e2d98e52dec2336a590466dcec09daad774479f Mon Sep 17 00:00:00 2001 From: Twelveeee <48245733+Twelveeee@users.noreply.github.com> Date: Mon, 22 Dec 2025 21:54:25 +0800 Subject: [PATCH 024/349] move Language Selector into SettingDialog (#352) * fix:custom model setting bug * refactor: consolidate aiProvider checks for cleaner code * fix:Integrated the language selection option into the `SettingsDialog` * fix:useSearchParams() should be wrapped in a suspense boundary at page * fix: improve semantic HTML and maintainability - Replace nested button>a with proper anchor element for GitHub link - Use i18n.locales.map() with LANGUAGE_LABELS for language options --------- Co-authored-by: dayuan.jiang --- components/chat-panel.tsx | 34 ++-- components/language-toggle.tsx | 108 ------------ components/settings-dialog.tsx | 310 +++++++++++++++++++++------------ lib/i18n/dictionaries/en.json | 3 + lib/i18n/dictionaries/ja.json | 3 + lib/i18n/dictionaries/zh.json | 3 + 6 files changed, 226 insertions(+), 235 deletions(-) delete mode 100644 components/language-toggle.tsx diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 7bfe8075..941769fc 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -21,6 +21,11 @@ import { ChatInput } from "@/components/chat-input" import { ModelConfigDialog } from "@/components/model-config-dialog" import { ResetWarningModal } from "@/components/reset-warning-modal" import { SettingsDialog } from "@/components/settings-dialog" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" import { useDiagram } from "@/contexts/diagram-context" import { useDictionary } from "@/hooks/use-dictionary" import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config" @@ -30,7 +35,6 @@ import { type FileData, useFileProcessor } from "@/lib/use-file-processor" import { useQuotaManager } from "@/lib/use-quota-manager" import { formatXML, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils" import { ChatMessageDisplay } from "./chat-message-display" -import LanguageToggle from "./language-toggle" // localStorage keys for persistence const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages" @@ -1304,16 +1308,23 @@ Continue from EXACTLY where you stopped.`, />
- - - + + + + + + + + {dict.nav.github} + +
- {!isMobile && ( = { - en: "EN", - zh: "中文", - ja: "日本語", -} - -function LanguageToggleInner({ className = "" }: { className?: string }) { - const router = useRouter() - const pathname = usePathname() || "/" - const search = useSearchParams() - const [open, setOpen] = useState(false) - const [value, setValue] = useState(i18n.defaultLocale) - const ref = useRef(null) - - useEffect(() => { - const seg = pathname.split("/").filter(Boolean) - const first = seg[0] - if (first && i18n.locales.includes(first as Locale)) - setValue(first as Locale) - else setValue(i18n.defaultLocale) - }, [pathname]) - - useEffect(() => { - function onDoc(e: MouseEvent) { - if (!ref.current) return - if (!ref.current.contains(e.target as Node)) setOpen(false) - } - if (open) document.addEventListener("mousedown", onDoc) - return () => document.removeEventListener("mousedown", onDoc) - }, [open]) - - const changeLocale = (lang: string) => { - const parts = pathname.split("/") - if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) { - parts[1] = lang - } else { - parts.splice(1, 0, lang) - } - const newPath = parts.join("/") || "/" - const searchStr = search?.toString() ? `?${search.toString()}` : "" - setOpen(false) - router.push(newPath + searchStr) - } - - return ( -
- - {open && ( -
-
- {i18n.locales.map((loc) => ( - - ))} -
-
- )} -
- ) -} - -export default function LanguageToggle({ - className = "", -}: { - className?: string -}) { - return ( - - - - } - > - - - ) -} diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx index 029fc8d9..720e0392 100644 --- a/components/settings-dialog.tsx +++ b/components/settings-dialog.tsx @@ -1,7 +1,8 @@ "use client" import { Moon, Sun } from "lucide-react" -import { useEffect, useState } from "react" +import { usePathname, useRouter, useSearchParams } from "next/navigation" +import { Suspense, useEffect, useState } from "react" import { Button } from "@/components/ui/button" import { Dialog, @@ -12,8 +13,22 @@ import { } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { useDictionary } from "@/hooks/use-dictionary" +import { i18n, type Locale } from "@/lib/i18n/config" + +const LANGUAGE_LABELS: Record = { + en: "English", + zh: "中文", + ja: "日本語", +} interface SettingsDialogProps { open: boolean @@ -36,7 +51,7 @@ function getStoredAccessCodeRequired(): boolean | null { return stored === "true" } -export function SettingsDialog({ +function SettingsContent({ open, onOpenChange, onCloseProtectionChange, @@ -46,6 +61,9 @@ export function SettingsDialog({ onToggleDarkMode, }: SettingsDialogProps) { const dict = useDictionary() + const router = useRouter() + const pathname = usePathname() || "/" + const search = useSearchParams() const [accessCode, setAccessCode] = useState("") const [closeProtection, setCloseProtection] = useState(true) const [isVerifying, setIsVerifying] = useState(false) @@ -53,6 +71,7 @@ export function SettingsDialog({ const [accessCodeRequired, setAccessCodeRequired] = useState( () => getStoredAccessCodeRequired() ?? false, ) + const [currentLang, setCurrentLang] = useState("en") useEffect(() => { // Only fetch if not cached in localStorage @@ -77,6 +96,17 @@ export function SettingsDialog({ }) }, []) + // Detect current language from pathname + useEffect(() => { + const seg = pathname.split("/").filter(Boolean) + const first = seg[0] + if (first && i18n.locales.includes(first as Locale)) { + setCurrentLang(first) + } else { + setCurrentLang(i18n.defaultLocale) + } + }, [pathname]) + useEffect(() => { if (open) { const storedCode = @@ -93,6 +123,18 @@ export function SettingsDialog({ } }, [open]) + const changeLanguage = (lang: string) => { + const parts = pathname.split("/") + if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) { + parts[1] = lang + } else { + parts.splice(1, 0, lang) + } + const newPath = parts.join("/") || "/" + const searchStr = search?.toString() ? `?${search.toString()}` : "" + router.push(newPath + searchStr) + } + const handleSave = async () => { if (!accessCodeRequired) return @@ -131,128 +173,166 @@ export function SettingsDialog({ } return ( - - - - {dict.settings.title} - - {dict.settings.description} - - -
- {accessCodeRequired && ( -
- -
- - setAccessCode(e.target.value) - } - onKeyDown={handleKeyDown} - placeholder={ - dict.settings.accessCodePlaceholder - } - autoComplete="off" - /> - -
-

- {dict.settings.accessCodeDescription} -

- {error && ( -

- {error} -

- )} + + + {dict.settings.title} + + {dict.settings.description} + + +
+ {accessCodeRequired && ( +
+ +
+ setAccessCode(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={ + dict.settings.accessCodePlaceholder + } + autoComplete="off" + /> +
- )} -
-
- -

- {dict.settings.themeDescription} +

+ {dict.settings.accessCodeDescription} +

+ {error && ( +

+ {error}

-
- + )}
+ )} -
-
- -

- {dict.settings.drawioStyleDescription}{" "} - {drawioUi === "min" - ? dict.settings.minimal - : dict.settings.sketch} -

-
- +
+ +
+
+ +

+ {dict.settings.drawioStyleDescription}{" "} {drawioUi === "min" - ? dict.settings.sketch - : dict.settings.minimal} - + ? dict.settings.minimal + : dict.settings.sketch} +

+ +
-
-
- -

- {dict.settings.closeProtectionDescription} -

-
- { - setCloseProtection(checked) - localStorage.setItem( - STORAGE_CLOSE_PROTECTION_KEY, - checked.toString(), - ) - onCloseProtectionChange?.(checked) - }} - /> +
+
+ +

+ {dict.settings.closeProtectionDescription} +

+ { + setCloseProtection(checked) + localStorage.setItem( + STORAGE_CLOSE_PROTECTION_KEY, + checked.toString(), + ) + onCloseProtectionChange?.(checked) + }} + />
-
-

- Version {process.env.APP_VERSION} -

-
- +
+
+

+ Version {process.env.APP_VERSION} +

+
+ + ) +} + +export function SettingsDialog(props: SettingsDialogProps) { + return ( + + +
+
+
+ + } + > + +
) } diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index 44912cf3..241c837b 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -14,6 +14,7 @@ "about": "About", "editor": "Editor", "newChat": "Start fresh chat", + "github": "GitHub", "settings": "Settings", "hidePanel": "Hide chat panel (Ctrl+B)", "showPanel": "Show chat panel (Ctrl+B)", @@ -87,6 +88,8 @@ "overrides": "Overrides", "clearSettings": "Clear Settings", "useServerDefault": "Use Server Default", + "language": "Language", + "languageDescription": "Choose your interface language.", "theme": "Theme", "themeDescription": "Dark/Light mode for interface and DrawIO canvas.", "drawioStyle": "DrawIO Style", diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json index 68001dd7..f16b0472 100644 --- a/lib/i18n/dictionaries/ja.json +++ b/lib/i18n/dictionaries/ja.json @@ -14,6 +14,7 @@ "about": "概要", "editor": "エディタ", "newChat": "新しいチャットを開始", + "github": "GitHub", "settings": "設定", "hidePanel": "チャットパネルを非表示 (Ctrl+B)", "showPanel": "チャットパネルを表示 (Ctrl+B)", @@ -87,6 +88,8 @@ "overrides": "上書き", "clearSettings": "設定をクリア", "useServerDefault": "サーバーデフォルトを使用", + "language": "言語", + "languageDescription": "インターフェース言語を選択します。", "theme": "テーマ", "themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。", "drawioStyle": "DrawIO スタイル", diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json index 43a8e7e6..f287f7d1 100644 --- a/lib/i18n/dictionaries/zh.json +++ b/lib/i18n/dictionaries/zh.json @@ -14,6 +14,7 @@ "about": "关于", "editor": "编辑器", "newChat": "开始新对话", + "github": "GitHub", "settings": "设置", "hidePanel": "隐藏聊天面板 (Ctrl+B)", "showPanel": "显示聊天面板 (Ctrl+B)", @@ -87,6 +88,8 @@ "overrides": "覆盖", "clearSettings": "清除设置", "useServerDefault": "使用服务器默认值", + "language": "语言", + "languageDescription": "选择界面语言。", "theme": "主题", "themeDescription": "界面和 DrawIO 画布的深色/浅色模式。", "drawioStyle": "DrawIO 样式", From deae5c2c38a2539f13aab5fb305eb761c8678d53 Mon Sep 17 00:00:00 2001 From: Biki Kalita <86558912+Biki-dev@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:30:20 +0530 Subject: [PATCH 025/349] Fix: Localize TPM rate-limit toast via i18n (#353) * TMP error toast hardcoded english fixed * fix: correct JA/ZH translations to use tokens instead of requests --------- Co-authored-by: dayuan.jiang --- lib/i18n/dictionaries/en.json | 1 + lib/i18n/dictionaries/ja.json | 1 + lib/i18n/dictionaries/zh.json | 1 + lib/use-quota-manager.tsx | 15 ++++++++++----- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index 241c837b..5fc1ac3b 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -150,6 +150,7 @@ "tokenLimit": "Daily Token Limit Reached", "tpmLimit": "Rate Limit", "tpmMessage": "Too many requests. Please wait a moment.", + "tpmMessageDetailed": "Rate limit reached ({limit} tokens/min). Please wait {seconds} seconds before sending another request.", "messageApi": "Oops — you've reached the daily API limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.", "messageToken": "Oops — you've reached the daily token limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.", "tip": "Tip: You can use your own API key (click the Settings icon) or self-host the project to bypass these limits.", diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json index f16b0472..d622337a 100644 --- a/lib/i18n/dictionaries/ja.json +++ b/lib/i18n/dictionaries/ja.json @@ -150,6 +150,7 @@ "tokenLimit": "1日のトークン制限に達しました", "tpmLimit": "レート制限", "tpmMessage": "リクエストが多すぎます。しばらくお待ちください。", + "tpmMessageDetailed": "レート制限に達しました({limit}トークン/分)。{seconds}秒待ってからもう一度リクエストしてください。", "messageApi": "おっと — このデモの1日の API 制限に達しました!個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。", "messageToken": "おっと — このデモの1日のトークン制限に達しました!個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。", "tip": "ヒント:独自の API キーを使用する(設定アイコンをクリック)か、プロジェクトをセルフホストしてこれらの制限を回避できます。", diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json index f287f7d1..eec9836a 100644 --- a/lib/i18n/dictionaries/zh.json +++ b/lib/i18n/dictionaries/zh.json @@ -150,6 +150,7 @@ "tokenLimit": "已达每日令牌限制", "tpmLimit": "速率限制", "tpmMessage": "请求过多。请稍等片刻。", + "tpmMessageDetailed": "达到速率限制({limit} 令牌/分钟)。请等待 {seconds} 秒后再发送请求。", "messageApi": "糟糕 — 您已达到此演示的每日 API 限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。", "messageToken": "糟糕 — 您已达到此演示的每日令牌限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。", "tip": "提示:您可以使用自己的 API 密钥(点击设置图标)或自托管项目来绕过这些限制。", diff --git a/lib/use-quota-manager.tsx b/lib/use-quota-manager.tsx index f13d51e9..687b8642 100644 --- a/lib/use-quota-manager.tsx +++ b/lib/use-quota-manager.tsx @@ -3,6 +3,8 @@ import { useCallback, useMemo } from "react" import { toast } from "sonner" import { QuotaLimitToast } from "@/components/quota-limit-toast" +import { useDictionary } from "@/hooks/use-dictionary" +import { formatMessage } from "@/lib/i18n/utils" import { STORAGE_KEYS } from "@/lib/storage" export interface QuotaConfig { @@ -40,6 +42,8 @@ export function useQuotaManager(config: QuotaConfig): { } { const { dailyRequestLimit, dailyTokenLimit, tpmLimit } = config + const dict = useDictionary() + // Check if user has their own API key configured (bypass limits) const hasOwnApiKey = useCallback((): boolean => { const provider = localStorage.getItem(STORAGE_KEYS.aiProvider) @@ -221,11 +225,12 @@ export function useQuotaManager(config: QuotaConfig): { const showTPMLimitToast = useCallback(() => { const limitDisplay = tpmLimit >= 1000 ? `${tpmLimit / 1000}k` : String(tpmLimit) - toast.error( - `Rate limit reached (${limitDisplay} tokens/min). Please wait 60 seconds before sending another request.`, - { duration: 8000 }, - ) - }, [tpmLimit]) + const message = formatMessage(dict.quota.tpmMessageDetailed, { + limit: limitDisplay, + seconds: 60, + }) + toast.error(message, { duration: 8000 }) + }, [tpmLimit, dict]) return { // Check functions From 9e9ea10bebf0085cd0637e7e5b70edb08545efef Mon Sep 17 00:00:00 2001 From: pointerhacker <145901472+pointerhacker@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:13:45 +0800 Subject: [PATCH 026/349] fix:feature/sglang-provider (#302) Co-authored-by: zhaochaojin Co-authored-by: dayuan.jiang --- env.example | 4 ++ lib/ai-providers.ts | 117 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/env.example b/env.example index 36c3d35b..4661bd07 100644 --- a/env.example +++ b/env.example @@ -68,6 +68,10 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0 # SILICONFLOW_API_KEY=sk-... # SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # Optional: switch to https://api.siliconflow.cn/v1 if needed +# SGLang Configuration (OpenAI-compatible) +# SGLANG_API_KEY=your-sglang-api-key +# SGLANG_BASE_URL=http://127.0.0.1:8000/v1 # Your SGLang endpoint + # Vercel AI Gateway Configuration # Get your API key from: https://vercel.com/ai-gateway # Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5" diff --git a/lib/ai-providers.ts b/lib/ai-providers.ts index 02ecb1ec..a20b92a9 100644 --- a/lib/ai-providers.ts +++ b/lib/ai-providers.ts @@ -19,6 +19,7 @@ export type ProviderName = | "openrouter" | "deepseek" | "siliconflow" + | "sglang" | "gateway" interface ModelConfig { @@ -50,6 +51,7 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [ "openrouter", "deepseek", "siliconflow", + "sglang", "gateway", ] @@ -343,6 +345,7 @@ function buildProviderOptions( case "deepseek": case "openrouter": case "siliconflow": + case "sglang": case "gateway": { // These providers don't have reasoning configs in AI SDK yet // Gateway passes through to underlying providers which handle their own configs @@ -367,6 +370,7 @@ const PROVIDER_ENV_VARS: Record = { openrouter: "OPENROUTER_API_KEY", deepseek: "DEEPSEEK_API_KEY", siliconflow: "SILICONFLOW_API_KEY", + sglang: "SGLANG_API_KEY", gateway: "AI_GATEWAY_API_KEY", } @@ -432,7 +436,7 @@ function validateProviderCredentials(provider: ProviderName): void { * Get the AI model based on environment variables * * Environment variables: - * - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow) + * - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway) * - AI_MODEL: The model ID/name for the selected provider * * Provider-specific env vars: @@ -448,6 +452,8 @@ function validateProviderCredentials(provider: ProviderName): void { * - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional) * - SILICONFLOW_API_KEY: SiliconFlow API key * - SILICONFLOW_BASE_URL: SiliconFlow endpoint (optional, defaults to https://api.siliconflow.com/v1) + * - SGLANG_API_KEY: SGLang API key + * - SGLANG_BASE_URL: SGLang endpoint (optional) */ export function getAIModel(overrides?: ClientOverrides): ModelConfig { // SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm) @@ -516,6 +522,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { `- OPENROUTER_API_KEY for OpenRouter\n` + `- AZURE_API_KEY for Azure\n` + `- SILICONFLOW_API_KEY for SiliconFlow\n` + + `- SGLANG_API_KEY for SGLang\n` + `Or set AI_PROVIDER=ollama for local Ollama.`, ) } else { @@ -698,6 +705,112 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { break } + case "sglang": { + const apiKey = overrides?.apiKey || process.env.SGLANG_API_KEY + const baseURL = overrides?.baseUrl || process.env.SGLANG_BASE_URL + + const sglangProvider = createOpenAI({ + apiKey, + baseURL, + // Add a custom fetch wrapper to intercept and fix the stream from sglang + fetch: async (url, options) => { + const response = await fetch(url, options) + if (!response.body) { + return response + } + + // Create a transform stream to fix the non-compliant sglang stream + let buffer = "" + const decoder = new TextDecoder() + + const transformStream = new TransformStream({ + transform(chunk, controller) { + buffer += decoder.decode(chunk, { stream: true }) + // Process all complete messages in the buffer + let messageEndPos + while ( + (messageEndPos = buffer.indexOf("\n\n")) !== -1 + ) { + const message = buffer.substring( + 0, + messageEndPos, + ) + buffer = buffer.substring(messageEndPos + 2) // Move past the '\n\n' + + if (message.startsWith("data: ")) { + const jsonStr = message.substring(6).trim() + if (jsonStr === "[DONE]") { + controller.enqueue( + new TextEncoder().encode( + message + "\n\n", + ), + ) + continue + } + try { + const data = JSON.parse(jsonStr) + const delta = data.choices?.[0]?.delta + + if (delta) { + // Fix 1: remove invalid empty role + if (delta.role === "") { + delete delta.role + } + // Fix 2: remove non-standard reasoning_content field + if ("reasoning_content" in delta) { + delete delta.reasoning_content + } + } + + // Re-serialize and forward the corrected data with the correct SSE format + controller.enqueue( + new TextEncoder().encode( + `data: ${JSON.stringify(data)}\n\n`, + ), + ) + } catch (e) { + // If parsing fails, forward the original message to avoid breaking the stream. + controller.enqueue( + new TextEncoder().encode( + message + "\n\n", + ), + ) + } + } else if (message.trim() !== "") { + // Pass through other message types (e.g., 'event: ...') + controller.enqueue( + new TextEncoder().encode( + message + "\n\n", + ), + ) + } + } + }, + flush(controller) { + // If there's anything left in the buffer, forward it. + if (buffer.trim()) { + controller.enqueue( + new TextEncoder().encode(buffer), + ) + } + }, + }) + + const transformedBody = + response.body.pipeThrough(transformStream) + + // Return a new response with the transformed body + return new Response(transformedBody, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }) + }, + }) + model = sglangProvider.chat(modelId) + break + } + case "gateway": { // Vercel AI Gateway - unified access to multiple AI providers // Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5" @@ -721,7 +834,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { default: throw new Error( - `Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, gateway`, + `Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway`, ) } From 84959637db2b9eadf27759f9e20a0788a61633d2 Mon Sep 17 00:00:00 2001 From: Biki Kalita <86558912+Biki-dev@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:58:55 +0530 Subject: [PATCH 027/349] Support subdirectory deployment and fix API path handling (#311) * feat: support subdirectory deployment (NEXT_PUBLIC_BASE_PATH) * removed unwanted check and fix favicon issue * Use getAssetUrl for manifest assets to avoid undefined NEXT_PUBLIC_BASE_PATH * Add validation warning for NEXT_PUBLIC_BASE_PATH format --------- Co-authored-by: dayuan.jiang --- app/manifest.ts | 8 +++---- components/chat-example-panel.tsx | 7 +++--- components/chat-message-display.tsx | 3 ++- components/chat-panel.tsx | 5 ++-- components/settings-dialog.tsx | 16 ++++++++----- contexts/diagram-context.tsx | 3 ++- docker-compose.yml | 5 ++++ env.example | 6 +++++ lib/base-path.ts | 37 +++++++++++++++++++++++++++++ next.config.ts | 3 +++ 10 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 lib/base-path.ts diff --git a/app/manifest.ts b/app/manifest.ts index 47162004..411107c3 100644 --- a/app/manifest.ts +++ b/app/manifest.ts @@ -1,24 +1,24 @@ import type { MetadataRoute } from "next" - +import { getAssetUrl } from "@/lib/base-path" export default function manifest(): MetadataRoute.Manifest { return { name: "Next AI Draw.io", short_name: "AIDraw.io", description: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.", - start_url: "/", + start_url: getAssetUrl("/"), display: "standalone", background_color: "#f9fafb", theme_color: "#171d26", icons: [ { - src: "/favicon-192x192.png", + src: getAssetUrl("/favicon-192x192.png"), sizes: "192x192", type: "image/png", purpose: "any", }, { - src: "/favicon-512x512.png", + src: getAssetUrl("/favicon-512x512.png"), sizes: "512x512", type: "image/png", purpose: "any", diff --git a/components/chat-example-panel.tsx b/components/chat-example-panel.tsx index f3508250..984601d0 100644 --- a/components/chat-example-panel.tsx +++ b/components/chat-example-panel.tsx @@ -9,6 +9,7 @@ import { Zap, } from "lucide-react" import { useDictionary } from "@/hooks/use-dictionary" +import { getAssetUrl } from "@/lib/base-path" interface ExampleCardProps { icon: React.ReactNode @@ -79,7 +80,7 @@ export default function ExamplePanel({ setInput("Replicate this flowchart.") try { - const response = await fetch("/example.png") + const response = await fetch(getAssetUrl("/example.png")) const blob = await response.blob() const file = new File([blob], "example.png", { type: "image/png" }) setFiles([file]) @@ -92,7 +93,7 @@ export default function ExamplePanel({ setInput("Replicate this in aws style") try { - const response = await fetch("/architecture.png") + const response = await fetch(getAssetUrl("/architecture.png")) const blob = await response.blob() const file = new File([blob], "architecture.png", { type: "image/png", @@ -107,7 +108,7 @@ export default function ExamplePanel({ setInput("Summarize this paper as a diagram") try { - const response = await fetch("/chain-of-thought.txt") + const response = await fetch(getAssetUrl("/chain-of-thought.txt")) const blob = await response.blob() const file = new File([blob], "chain-of-thought.txt", { type: "text/plain", diff --git a/components/chat-message-display.tsx b/components/chat-message-display.tsx index 58f2aaf7..68d72aec 100644 --- a/components/chat-message-display.tsx +++ b/components/chat-message-display.tsx @@ -27,6 +27,7 @@ import { ReasoningTrigger, } from "@/components/ai-elements/reasoning" import { ScrollArea } from "@/components/ui/scroll-area" +import { getApiEndpoint } from "@/lib/base-path" import { applyDiagramOperations, convertToLegalXml, @@ -291,7 +292,7 @@ export function ChatMessageDisplay({ setFeedback((prev) => ({ ...prev, [messageId]: value })) try { - await fetch("/api/log-feedback", { + await fetch(getApiEndpoint("/api/log-feedback"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 941769fc..b7a1929b 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -29,6 +29,7 @@ import { import { useDiagram } from "@/contexts/diagram-context" import { useDictionary } from "@/hooks/use-dictionary" import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config" +import { getApiEndpoint } from "@/lib/base-path" import { findCachedResponse } from "@/lib/cached-responses" import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { type FileData, useFileProcessor } from "@/lib/use-file-processor" @@ -172,7 +173,7 @@ export default function ChatPanel({ // Check config on mount useEffect(() => { - fetch("/api/config") + fetch(getApiEndpoint("/api/config")) .then((res) => res.json()) .then((data) => { setDailyRequestLimit(data.dailyRequestLimit || 0) @@ -243,7 +244,7 @@ export default function ChatPanel({ setMessages, } = useChat({ transport: new DefaultChatTransport({ - api: "/api/chat", + api: getApiEndpoint("/api/chat"), }), async onToolCall({ toolCall }) { if (DEBUG) { diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx index 720e0392..1d9f7100 100644 --- a/components/settings-dialog.tsx +++ b/components/settings-dialog.tsx @@ -22,6 +22,7 @@ import { } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { useDictionary } from "@/hooks/use-dictionary" +import { getApiEndpoint } from "@/lib/base-path" import { i18n, type Locale } from "@/lib/i18n/config" const LANGUAGE_LABELS: Record = { @@ -77,7 +78,7 @@ function SettingsContent({ // Only fetch if not cached in localStorage if (getStoredAccessCodeRequired() !== null) return - fetch("/api/config") + fetch(getApiEndpoint("/api/config")) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() @@ -142,12 +143,15 @@ function SettingsContent({ setIsVerifying(true) try { - const response = await fetch("/api/verify-access-code", { - method: "POST", - headers: { - "x-access-code": accessCode.trim(), + const response = await fetch( + getApiEndpoint("/api/verify-access-code"), + { + method: "POST", + headers: { + "x-access-code": accessCode.trim(), + }, }, - }) + ) const data = await response.json() diff --git a/contexts/diagram-context.tsx b/contexts/diagram-context.tsx index 5563e8bd..a43aeafa 100644 --- a/contexts/diagram-context.tsx +++ b/contexts/diagram-context.tsx @@ -5,6 +5,7 @@ import { createContext, useContext, useEffect, useRef, useState } from "react" import type { DrawIoEmbedRef } from "react-drawio" import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel" import type { ExportFormat } from "@/components/save-dialog" +import { getApiEndpoint } from "@/lib/base-path" import { extractDiagramXML, validateAndFixXml } from "../lib/utils" interface DiagramContextType { @@ -329,7 +330,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) { sessionId?: string, ) => { try { - await fetch("/api/log-save", { + await fetch(getApiEndpoint("/api/log-save"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename, format, sessionId }), diff --git a/docker-compose.yml b/docker-compose.yml index 84970bca..89e3a860 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,11 @@ services: context: . args: - NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080 + # Uncomment below for subdirectory deployment + # - NEXT_PUBLIC_BASE_PATH=/nextaidrawio ports: ["3000:3000"] env_file: .env + environment: + # For subdirectory deployment, uncomment and set your path: + # NEXT_PUBLIC_BASE_PATH: /nextaidrawio depends_on: [drawio] diff --git a/env.example b/env.example index 4661bd07..bc03d324 100644 --- a/env.example +++ b/env.example @@ -97,6 +97,12 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0 # NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net # Default: https://embed.diagrams.net # Use this to point to a self-hosted draw.io instance +# Subdirectory Deployment (Optional) +# For deploying to a subdirectory (e.g., https://example.com/nextaidrawio) +# Set this to your subdirectory path with leading slash (e.g., /nextaidrawio) +# Leave empty for root deployment (default) +# NEXT_PUBLIC_BASE_PATH=/nextaidrawio + # PDF Input Feature (Optional) # Enable PDF file upload to extract text and generate diagrams # Enabled by default. Set to "false" to disable. diff --git a/lib/base-path.ts b/lib/base-path.ts new file mode 100644 index 00000000..61278c75 --- /dev/null +++ b/lib/base-path.ts @@ -0,0 +1,37 @@ +/** + * Get the base path for API calls and static assets + * This is used for subdirectory deployment support + * + * Example: If deployed at https://example.com/nextaidrawio, this returns "/nextaidrawio" + * For root deployment, this returns "" + * + * Set NEXT_PUBLIC_BASE_PATH environment variable to your subdirectory path (e.g., /nextaidrawio) + */ +export function getBasePath(): string { + // Read from environment variable (must start with NEXT_PUBLIC_ to be available on client) + const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "" + if (basePath && !basePath.startsWith("/")) { + console.warn("NEXT_PUBLIC_BASE_PATH should start with /") + } + return basePath +} + +/** + * Get full API endpoint URL + * @param endpoint - API endpoint path (e.g., "/api/chat", "/api/config") + * @returns Full API path with base path prefix + */ +export function getApiEndpoint(endpoint: string): string { + const basePath = getBasePath() + return `${basePath}${endpoint}` +} + +/** + * Get full static asset URL + * @param assetPath - Asset path (e.g., "/example.png", "/chain-of-thought.txt") + * @returns Full asset path with base path prefix + */ +export function getAssetUrl(assetPath: string): string { + const basePath = getBasePath() + return `${basePath}${assetPath}` +} diff --git a/next.config.ts b/next.config.ts index a9579a29..e10ab303 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,9 @@ import packageJson from "./package.json" const nextConfig: NextConfig = { /* config options here */ output: "standalone", + // Support for subdirectory deployment (e.g., https://example.com/nextaidrawio) + // Set NEXT_PUBLIC_BASE_PATH environment variable to your subdirectory path (e.g., /nextaidrawio) + basePath: process.env.NEXT_PUBLIC_BASE_PATH || "", env: { APP_VERSION: packageJson.version, }, From 6446454cd7056b79aafb533b9bbdeba14a9d976e Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 00:26:01 +0900 Subject: [PATCH 028/349] fix: add SSRF protection to validate-model endpoint (#357) Block private IPs, localhost, cloud metadata endpoints (169.254.169.254), and internal hostnames in custom baseUrl parameter to prevent server-side request forgery attacks. --- app/api/validate-model/route.ts | 68 +++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/app/api/validate-model/route.ts b/app/api/validate-model/route.ts index 7beeaa47..13280359 100644 --- a/app/api/validate-model/route.ts +++ b/app/api/validate-model/route.ts @@ -11,6 +11,66 @@ import { createOllama } from "ollama-ai-provider-v2" export const runtime = "nodejs" +/** + * SECURITY: Check if URL points to private/internal network (SSRF protection) + * Blocks: localhost, private IPs, link-local, AWS metadata service + */ +function isPrivateUrl(urlString: string): boolean { + try { + const url = new URL(urlString) + const hostname = url.hostname.toLowerCase() + + // Block localhost + if ( + hostname === "localhost" || + hostname === "127.0.0.1" || + hostname === "::1" + ) { + return true + } + + // Block AWS/cloud metadata endpoints + if ( + hostname === "169.254.169.254" || + hostname === "metadata.google.internal" + ) { + return true + } + + // Check for private IPv4 ranges + const ipv4Match = hostname.match( + /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/, + ) + if (ipv4Match) { + const [, a, b] = ipv4Match.map(Number) + // 10.0.0.0/8 + if (a === 10) return true + // 172.16.0.0/12 + if (a === 172 && b >= 16 && b <= 31) return true + // 192.168.0.0/16 + if (a === 192 && b === 168) return true + // 169.254.0.0/16 (link-local) + if (a === 169 && b === 254) return true + // 127.0.0.0/8 (loopback) + if (a === 127) return true + } + + // Block common internal hostnames + if ( + hostname.endsWith(".local") || + hostname.endsWith(".internal") || + hostname.endsWith(".localhost") + ) { + return true + } + + return false + } catch { + // Invalid URL - block it + return true + } +} + interface ValidateRequest { provider: string apiKey: string @@ -42,6 +102,14 @@ export async function POST(req: Request) { ) } + // SECURITY: Block SSRF attacks via custom baseUrl + if (baseUrl && isPrivateUrl(baseUrl)) { + return NextResponse.json( + { valid: false, error: "Invalid base URL" }, + { status: 400 }, + ) + } + // Validate credentials based on provider if (provider === "bedrock") { if (!awsAccessKeyId || !awsSecretAccessKey || !awsRegion) { From 3521495ead5f2b79bf5cc82cdec6a3b10176327a Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 00:32:22 +0900 Subject: [PATCH 029/349] chore: conditionally show about and notice based on env var (#358) --- components/chat-panel.tsx | 54 +++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index b7a1929b..22545994 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -1269,32 +1269,36 @@ Continue from EXACTLY where you stopped.`, Next AI Drawio
- {!isMobile && ( - - About - - )} - {!isMobile && ( - - - - - - )} + About + + )} + {!isMobile && + process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE === + "true" && ( + + + + + + )}
Date: Tue, 23 Dec 2025 01:03:11 +0900 Subject: [PATCH 030/349] fix: include instrumentation.ts in standalone build for Langfuse (#359) Add outputFileTracingIncludes to next.config.ts to ensure instrumentation.ts is included in standalone builds (required for App Runner deployment) --- next.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/next.config.ts b/next.config.ts index e10ab303..69f0a740 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,6 +10,10 @@ const nextConfig: NextConfig = { env: { APP_VERSION: packageJson.version, }, + // Include instrumentation.ts in standalone build for Langfuse telemetry + outputFileTracingIncludes: { + "*": ["./instrumentation.ts"], + }, } export default nextConfig From 28f9a81e7bcf069d10797b5be9f0f6f5d4cdb7b8 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 01:06:42 +0900 Subject: [PATCH 031/349] chore: add build-time arg for showing About and Notice (#360) --- .github/workflows/docker-build.yml | 2 ++ Dockerfile | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 6bfe9c84..e26dcc4e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -63,6 +63,8 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64,linux/arm64 + build-args: | + NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=true # Push to AWS ECR for App Runner auto-deploy - name: Configure AWS credentials diff --git a/Dockerfile b/Dockerfile index d8612428..644da88b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,10 @@ ENV NEXT_TELEMETRY_DISABLED=1 ARG NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net ENV NEXT_PUBLIC_DRAWIO_BASE_URL=${NEXT_PUBLIC_DRAWIO_BASE_URL} +# Build-time argument to show About link and Notice icon +ARG NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=false +ENV NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=${NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE} + # Build Next.js application (standalone mode) RUN npm run build From b4fc259de811ffc0cef25499b7cd8bf69d669012 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 09:09:39 +0900 Subject: [PATCH 032/349] chore: bump version to 0.4.6 (#364) * chore: bump version to 0.4.6 * style: auto-format with Biome --------- Co-authored-by: github-actions[bot] --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba8fd305..81861401 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "next-ai-draw-io", - "version": "0.4.5", + "version": "0.4.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "next-ai-draw-io", - "version": "0.4.5", + "version": "0.4.6", "license": "Apache-2.0", "dependencies": { "@ai-sdk/amazon-bedrock": "^3.0.70", diff --git a/package.json b/package.json index 7826186c..e82dcdb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next-ai-draw-io", - "version": "0.4.5", + "version": "0.4.6", "license": "Apache-2.0", "private": true, "main": "dist-electron/main/index.js", From 87cdc53665f6a92d5910f17b44122dc9aec28084 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 09:47:23 +0900 Subject: [PATCH 033/349] fix: improve Langfuse span filter to exclude all Next.js infrastructure traces (#365) * debug: add log to verify instrumentation initialization * fix: improve Langfuse span filter to exclude all Next.js infrastructure traces --- instrumentation.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/instrumentation.ts b/instrumentation.ts index abd8c14a..7fe59fbc 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -19,10 +19,13 @@ export function register() { const spanName = otelSpan.name // Skip Next.js HTTP infrastructure spans if ( - spanName.startsWith("POST /") || - spanName.startsWith("GET /") || + spanName.startsWith("POST") || + spanName.startsWith("GET") || + spanName.startsWith("RSC") || spanName.includes("BaseServer") || - spanName.includes("handleRequest") + spanName.includes("handleRequest") || + spanName.includes("resolve page") || + spanName.includes("start response") ) { return false } @@ -36,4 +39,5 @@ export function register() { // Register globally so AI SDK's telemetry also uses this processor tracerProvider.register() + console.log("[Langfuse] Instrumentation initialized successfully") } From b5734aa5e1db3c8e657fa0d104616cfaa4f4196b Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 10:08:14 +0900 Subject: [PATCH 034/349] chore: hide notice icon from header (#366) --- components/chat-panel.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 22545994..dbb0ac36 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -1281,24 +1281,6 @@ Continue from EXACTLY where you stopped.`, About )} - {!isMobile && - process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE === - "true" && ( - - - - - - )}
Date: Tue, 23 Dec 2025 10:22:31 +0900 Subject: [PATCH 035/349] docs: update about page model info to Haiku 4.5 (#367) --- app/[lang]/about/cn/page.tsx | 4 ++-- app/[lang]/about/ja/page.tsx | 4 ++-- app/[lang]/about/page.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/[lang]/about/cn/page.tsx b/app/[lang]/about/cn/page.tsx index 6c74ae61..3f46f9ea 100644 --- a/app/[lang]/about/cn/page.tsx +++ b/app/[lang]/about/cn/page.tsx @@ -117,9 +117,9 @@ export default function AboutCN() { (TPS/TPM)。一旦超限,系统就会暂停,导致请求失败。

- 由于使用量过高,我已将模型从 Claude 更换为{" "} + 由于使用量过高,我已将模型从 Opus 4.5 更换为{" "} - minimax-m2 + Haiku 4.5 ,以降低成本。

diff --git a/app/[lang]/about/ja/page.tsx b/app/[lang]/about/ja/page.tsx index c8beb44c..9e6283f5 100644 --- a/app/[lang]/about/ja/page.tsx +++ b/app/[lang]/about/ja/page.tsx @@ -126,9 +126,9 @@ export default function AboutJA() {

利用量の増加に伴い、コスト削減のためモデルを - Claude から{" "} + Opus 4.5 から{" "} - minimax-m2 + Haiku 4.5 {" "} に変更しました。

diff --git a/app/[lang]/about/page.tsx b/app/[lang]/about/page.tsx index 7192bce5..5557f04c 100644 --- a/app/[lang]/about/page.tsx +++ b/app/[lang]/about/page.tsx @@ -129,9 +129,9 @@ export default function About() {

Due to the high usage, I have changed the - model from Claude to{" "} + model from Opus 4.5 to{" "} - minimax-m2 + Haiku 4.5 , which is more cost-effective.

From 8cb7494d163affe0abd56efcf751f72df349d28b Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:42:27 +0900 Subject: [PATCH 036/349] feat(i18n): add translations for model configuration UI (#368) - Add ~40 new translation keys for model-config-dialog and model-selector - Support English, Chinese, and Japanese translations - Replace all hardcoded strings with dictionary lookups --- components/model-config-dialog.tsx | 212 +++++++++++++++++++++-------- components/model-selector.tsx | 28 ++-- lib/i18n/dictionaries/en.json | 43 +++++- lib/i18n/dictionaries/ja.json | 43 +++++- lib/i18n/dictionaries/zh.json | 43 +++++- 5 files changed, 301 insertions(+), 68 deletions(-) diff --git a/components/model-config-dialog.tsx b/components/model-config-dialog.tsx index e94dcdb4..89f17a28 100644 --- a/components/model-config-dialog.tsx +++ b/components/model-config-dialog.tsx @@ -52,6 +52,7 @@ import { } from "@/components/ui/select" import { useDictionary } from "@/hooks/use-dictionary" import type { UseModelConfigReturn } from "@/hooks/use-model-config" +import { formatMessage } from "@/lib/i18n/utils" import type { ProviderConfig, ProviderName } from "@/lib/types/model-config" import { PROVIDER_INFO, SUGGESTED_MODELS } from "@/lib/types/model-config" import { cn } from "@/lib/utils" @@ -107,10 +108,12 @@ function ValidationButton({ status, onClick, disabled, + dict, }: { status: ValidationStatus onClick: () => void disabled: boolean + dict: ReturnType }) { return ( ) @@ -406,7 +409,7 @@ export function ModelConfigDialog({
- Providers + {dict.modelConfig.providers}
@@ -418,7 +421,7 @@ export function ModelConfigDialog({

- Add a provider to get started + {dict.modelConfig.addProviderHint}

) : ( @@ -484,7 +487,11 @@ export function ModelConfigDialog({ > - + {availableProviders.map((p) => ( @@ -552,15 +559,27 @@ export function ModelConfigDialog({

{selectedProvider.models .length === 0 - ? "No models configured" - : `${selectedProvider.models.length} model${selectedProvider.models.length > 1 ? "s" : ""} configured`} + ? dict.modelConfig + .noModelsConfigured + : formatMessage( + dict.modelConfig + .modelsConfiguredCount, + { + count: selectedProvider + .models + .length, + }, + )}

{selectedProvider.validated && (
- Verified + { + dict.modelConfig + .verified + }
)} @@ -570,7 +589,12 @@ export function ModelConfigDialog({
- Configuration + + { + dict.modelConfig + .configuration + } +
@@ -581,7 +605,10 @@ export function ModelConfigDialog({ className="text-xs font-medium flex items-center gap-1.5" > - Display Name + { + dict.modelConfig + .displayName + } - AWS Access Key - ID + { + dict + .modelConfig + .awsAccessKeyId + } - AWS Secret - Access Key + { + dict + .modelConfig + .awsSecretAccessKey + }
@@ -1395,11 +1494,10 @@ export function ModelConfigDialog({

- Configure AI Providers + {dict.modelConfig.configureProviders}

- Select a provider from the list or add a new - one to configure API keys and models + {dict.modelConfig.selectProviderHint}

)} @@ -1410,7 +1508,7 @@ export function ModelConfigDialog({

- API keys are stored locally in your browser + {dict.modelConfig.apiKeyStored}

@@ -1429,19 +1527,16 @@ export function ModelConfigDialog({
- Delete Provider + {dict.modelConfig.deleteProvider} - Are you sure you want to delete{" "} - - {selectedProvider + {formatMessage(dict.modelConfig.deleteConfirmDesc, { + name: selectedProvider ? selectedProvider.name || PROVIDER_INFO[selectedProvider.provider] .label - : "this provider"} - - ? This will remove all configured models and cannot - be undone. + : "this provider", + })} {selectedProvider && @@ -1451,11 +1546,16 @@ export function ModelConfigDialog({ htmlFor="delete-confirm" className="text-sm text-muted-foreground" > - Type " - {selectedProvider.name || - PROVIDER_INFO[selectedProvider.provider] - .label} - " to confirm + {formatMessage( + dict.modelConfig.typeToConfirm, + { + name: + selectedProvider.name || + PROVIDER_INFO[ + selectedProvider.provider + ].label, + }, + )} setDeleteConfirmText(e.target.value) } - placeholder="Type provider name..." + placeholder={ + dict.modelConfig.typeProviderName + } className="h-9" />
)} - Cancel + + {dict.modelConfig.cancel} + - Delete + {dict.modelConfig.delete} diff --git a/components/model-selector.tsx b/components/model-selector.tsx index 37baa4cf..cefab047 100644 --- a/components/model-selector.tsx +++ b/components/model-selector.tsx @@ -16,6 +16,7 @@ import { ModelSelectorTrigger, } from "@/components/ai-elements/model-selector" import { ButtonWithTooltip } from "@/components/button-with-tooltip" +import { useDictionary } from "@/hooks/use-dictionary" import type { FlattenedModel } from "@/lib/types/model-config" import { cn } from "@/lib/utils" @@ -67,6 +68,7 @@ export function ModelSelector({ onConfigure, disabled = false, }: ModelSelectorProps) { + const dict = useDictionary() const [open, setOpen] = useState(false) // Only show validated models in the selector const validatedModels = useMemo( @@ -96,8 +98,8 @@ export function ModelSelector({ } const tooltipContent = selectedModel - ? `${selectedModel.modelId} (click to change)` - : "Using server default model (click to change)" + ? `${selectedModel.modelId} ${dict.modelConfig.clickToChange}` + : `${dict.modelConfig.usingServerDefault} ${dict.modelConfig.clickToChange}` return ( @@ -111,22 +113,26 @@ export function ModelSelector({ > - {selectedModel ? selectedModel.modelId : "Default"} + {selectedModel + ? selectedModel.modelId + : dict.modelConfig.default} - - + + {validatedModels.length === 0 && models.length > 0 - ? "No verified models. Test your models first." - : "No models found."} + ? dict.modelConfig.noVerifiedModels + : dict.modelConfig.noModelsFound} {/* Server Default Option */} - + - Server Default + {dict.modelConfig.serverDefault} @@ -201,13 +207,13 @@ export function ModelSelector({ > - Configure Models... + {dict.modelConfig.configureModels} {/* Info text */}
- Only verified models are shown + {dict.modelConfig.onlyVerifiedShown}
diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index 5fc1ac3b..2d6b1fe2 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -202,6 +202,47 @@ "apiKeyStored": "API keys are stored locally in your browser", "test": "Test", "validationError": "Validation failed", - "addModelFirst": "Add at least one model to validate" + "addModelFirst": "Add at least one model to validate", + "providers": "Providers", + "addProviderHint": "Add a provider to get started", + "verified": "Verified", + "configuration": "Configuration", + "displayName": "Display Name", + "awsAccessKeyId": "AWS Access Key ID", + "awsSecretAccessKey": "AWS Secret Access Key", + "awsRegion": "AWS Region", + "selectRegion": "Select region", + "apiKey": "API Key", + "enterApiKey": "Enter your API key", + "enterSecretKey": "Enter your secret access key", + "baseUrl": "Base URL", + "optional": "(optional)", + "customEndpoint": "Custom endpoint URL", + "models": "Models", + "customModelId": "Custom model ID...", + "allAdded": "All added", + "suggested": "Suggested", + "noModelsConfigured": "No models configured", + "modelIdEmpty": "Model ID cannot be empty", + "modelIdExists": "This model ID already exists", + "configureProviders": "Configure AI Providers", + "selectProviderHint": "Select a provider from the list or add a new one to configure API keys and models", + "deleteConfirmDesc": "Are you sure you want to delete {name}? This will remove all configured models and cannot be undone.", + "typeToConfirm": "Type \"{name}\" to confirm", + "typeProviderName": "Type provider name...", + "modelsConfiguredCount": "{count} model(s) configured", + "validationFailedCount": "{count} model(s) failed validation", + "cancel": "Cancel", + "delete": "Delete", + "clickToChange": "(click to change)", + "usingServerDefault": "Using server default model", + "selectModel": "Select Model", + "searchModels": "Search models...", + "noVerifiedModels": "No verified models. Test your models first.", + "noModelsFound": "No models found.", + "default": "Default", + "serverDefault": "Server Default", + "configureModels": "Configure Models...", + "onlyVerifiedShown": "Only verified models are shown" } } diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json index d622337a..6b86e4e1 100644 --- a/lib/i18n/dictionaries/ja.json +++ b/lib/i18n/dictionaries/ja.json @@ -202,6 +202,47 @@ "apiKeyStored": "APIキーはブラウザにローカル保存されます", "test": "テスト", "validationError": "検証に失敗しました", - "addModelFirst": "検証するには少なくとも1つのモデルを追加してください" + "addModelFirst": "検証するには少なくとも1つのモデルを追加してください", + "providers": "プロバイダー", + "addProviderHint": "プロバイダーを追加して開始", + "verified": "検証済み", + "configuration": "設定", + "displayName": "表示名", + "awsAccessKeyId": "AWS アクセスキー ID", + "awsSecretAccessKey": "AWS シークレットアクセスキー", + "awsRegion": "AWS リージョン", + "selectRegion": "リージョンを選択", + "apiKey": "API キー", + "enterApiKey": "API キーを入力", + "enterSecretKey": "シークレットアクセスキーを入力", + "baseUrl": "ベース URL", + "optional": "(オプション)", + "customEndpoint": "カスタムエンドポイント URL", + "models": "モデル", + "customModelId": "カスタムモデル ID...", + "allAdded": "すべて追加済み", + "suggested": "おすすめ", + "noModelsConfigured": "モデルが設定されていません", + "modelIdEmpty": "モデル ID は空にできません", + "modelIdExists": "このモデル ID は既に存在します", + "configureProviders": "AI プロバイダーを設定", + "selectProviderHint": "リストからプロバイダーを選択するか、新規追加して API キーとモデルを設定", + "deleteConfirmDesc": "{name} を削除してもよろしいですか?設定されたすべてのモデルが削除され、元に戻せません。", + "typeToConfirm": "確認のため「{name}」と入力", + "typeProviderName": "プロバイダー名を入力...", + "modelsConfiguredCount": "{count} 個のモデルを設定済み", + "validationFailedCount": "{count} 個のモデルの検証に失敗", + "cancel": "キャンセル", + "delete": "削除", + "clickToChange": "(クリックして変更)", + "usingServerDefault": "サーバーデフォルトモデルを使用中", + "selectModel": "モデルを選択", + "searchModels": "モデルを検索...", + "noVerifiedModels": "検証済みのモデルがありません。先にモデルをテストしてください。", + "noModelsFound": "モデルが見つかりません。", + "default": "デフォルト", + "serverDefault": "サーバーデフォルト", + "configureModels": "モデルを設定...", + "onlyVerifiedShown": "検証済みのモデルのみ表示" } } diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json index eec9836a..7b3413cc 100644 --- a/lib/i18n/dictionaries/zh.json +++ b/lib/i18n/dictionaries/zh.json @@ -202,6 +202,47 @@ "apiKeyStored": "API 密钥存储在您的浏览器本地", "test": "测试", "validationError": "验证失败", - "addModelFirst": "请先添加至少一个模型以进行验证" + "addModelFirst": "请先添加至少一个模型以进行验证", + "providers": "提供商", + "addProviderHint": "添加提供商即可开始使用", + "verified": "已验证", + "configuration": "配置", + "displayName": "显示名称", + "awsAccessKeyId": "AWS 访问密钥 ID", + "awsSecretAccessKey": "AWS Secret Access Key", + "awsRegion": "AWS 区域", + "selectRegion": "选择区域", + "apiKey": "API 密钥", + "enterApiKey": "输入您的 API 密钥", + "enterSecretKey": "输入您的 Secret Key", + "baseUrl": "基础 URL", + "optional": "(可选)", + "customEndpoint": "自定义端点 URL", + "models": "模型", + "customModelId": "自定义模型 ID...", + "allAdded": "已全部添加", + "suggested": "推荐", + "noModelsConfigured": "尚未配置模型", + "modelIdEmpty": "模型 ID 不能为空", + "modelIdExists": "此模型 ID 已存在", + "configureProviders": "配置 AI 提供商", + "selectProviderHint": "从列表中选择提供商或添加新的以配置 API 密钥和模型", + "deleteConfirmDesc": "确定要删除 {name} 吗?这将移除所有配置的模型且无法撤销。", + "typeToConfirm": "输入 \"{name}\" 以确认", + "typeProviderName": "输入提供商名称...", + "modelsConfiguredCount": "已配置 {count} 个模型", + "validationFailedCount": "{count} 个模型验证失败", + "cancel": "取消", + "delete": "删除", + "clickToChange": "(点击更改)", + "usingServerDefault": "使用服务器默认模型", + "selectModel": "选择模型", + "searchModels": "搜索模型...", + "noVerifiedModels": "没有已验证的模型。请先测试您的模型。", + "noModelsFound": "未找到模型。", + "default": "默认", + "serverDefault": "服务器默认", + "configureModels": "配置模型...", + "onlyVerifiedShown": "仅显示已验证的模型" } } From 5262b7bfb2f56df2da51a9a903fccf0d07234bb7 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:31:42 +0900 Subject: [PATCH 037/349] chore: upgrade AI SDK to v6.0.1 (#369) - Upgrade ai package from ^5.0.89 to ^6.0.1 - Upgrade @ai-sdk/* provider packages to latest v3/v4 - Update convertToModelMessages call to async (new API) - Fix usage.cachedInputTokens to usage.inputTokenDetails?.cacheReadTokens --- app/api/chat/route.ts | 5 +- package-lock.json | 379 +++++++++++++----------------------------- package.json | 25 +-- 3 files changed, 138 insertions(+), 271 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index b9024e4e..92535821 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -248,7 +248,7 @@ ${userInputText} """` // Convert UIMessages to ModelMessages and add system message - const modelMessages = convertToModelMessages(messages) + const modelMessages = await convertToModelMessages(messages) // DEBUG: Log incoming messages structure console.log("[route.ts] Incoming messages count:", messages.length) @@ -686,7 +686,8 @@ Call this tool to get shape names and usage syntax for a specific library.`, // Total input = non-cached + cached (these are separate counts) // Note: cacheWriteInputTokens is not available on finish part const totalInputTokens = - (usage.inputTokens ?? 0) + (usage.cachedInputTokens ?? 0) + (usage.inputTokens ?? 0) + + (usage.inputTokenDetails?.cacheReadTokens ?? 0) return { inputTokens: totalInputTokens, outputTokens: usage.outputTokens ?? 0, diff --git a/package-lock.json b/package-lock.json index 81861401..d1aeec98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,21 +9,21 @@ "version": "0.4.6", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/amazon-bedrock": "^3.0.70", - "@ai-sdk/anthropic": "^2.0.44", - "@ai-sdk/azure": "^2.0.69", - "@ai-sdk/deepseek": "^1.0.30", - "@ai-sdk/gateway": "^2.0.21", - "@ai-sdk/google": "^2.0.0", - "@ai-sdk/openai": "^2.0.19", - "@ai-sdk/react": "^2.0.107", + "@ai-sdk/amazon-bedrock": "^4.0.1", + "@ai-sdk/anthropic": "^3.0.0", + "@ai-sdk/azure": "^3.0.0", + "@ai-sdk/deepseek": "^2.0.0", + "@ai-sdk/gateway": "^3.0.0", + "@ai-sdk/google": "^3.0.0", + "@ai-sdk/openai": "^3.0.0", + "@ai-sdk/react": "^3.0.1", "@aws-sdk/credential-providers": "^3.943.0", "@formatjs/intl-localematcher": "^0.7.2", "@langfuse/client": "^4.4.9", "@langfuse/otel": "^4.4.4", "@langfuse/tracing": "^4.4.9", "@next/third-parties": "^16.0.6", - "@openrouter/ai-sdk-provider": "^1.2.3", + "@openrouter/ai-sdk-provider": "^1.5.4", "@opentelemetry/exporter-trace-otlp-http": "^0.208.0", "@opentelemetry/sdk-trace-node": "^2.2.0", "@radix-ui/react-alert-dialog": "^1.1.15", @@ -38,7 +38,7 @@ "@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-use-controllable-state": "^1.2.2", "@xmldom/xmldom": "^0.9.8", - "ai": "^5.0.89", + "ai": "^6.0.1", "base-64": "^1.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -93,14 +93,14 @@ } }, "node_modules/@ai-sdk/amazon-bedrock": { - "version": "3.0.70", - "resolved": "https://registry.npmjs.org/@ai-sdk/amazon-bedrock/-/amazon-bedrock-3.0.70.tgz", - "integrity": "sha512-4NIBlwuS/iLKq2ynOqqyJ9imk/oyHuOzhBx88Bfm5I0ihQPKJ0dMMD1IKKuyDZvLRYKmlOEpa//P+/ZBp10drw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ai-sdk/amazon-bedrock/-/amazon-bedrock-4.0.1.tgz", + "integrity": "sha512-8Qu5wHTHYTwptZ1L4Sv8hvXMyKRNWUD6dH8wm+Zl8RDOEXZJNENS6zec7u/sWJKuvefL7j+xN8z3IVduhGfxig==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/anthropic": "2.0.56", - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.19", + "@ai-sdk/anthropic": "3.0.0", + "@ai-sdk/provider": "3.0.0", + "@ai-sdk/provider-utils": "4.0.0", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" @@ -112,48 +112,14 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/amazon-bedrock/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.19", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz", - "integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/@ai-sdk/anthropic": { - "version": "2.0.56", - "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-2.0.56.tgz", - "integrity": "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.0.tgz", + "integrity": "sha512-4BnxkXwRkvh+OB1ze0mHbskT90HL4MNrg6JUsRDkIsU9w5vitvGzxwc/XwlByUGMap/5I8/LZ3XZDzv6KViCuQ==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.19" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.19", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz", - "integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" + "@ai-sdk/provider": "3.0.0", + "@ai-sdk/provider-utils": "4.0.0" }, "engines": { "node": ">=18" @@ -163,31 +129,14 @@ } }, "node_modules/@ai-sdk/azure": { - "version": "2.0.69", - "resolved": "https://registry.npmjs.org/@ai-sdk/azure/-/azure-2.0.69.tgz", - "integrity": "sha512-0Y+f0XHviWw9ixB2Dkqyg07V67oczUh8adh4B/t0LgVMVkvOsf/WEzfYx2/LDqdvI/o8IYyJ6JzsCKpBwbS61g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/azure/-/azure-3.0.0.tgz", + "integrity": "sha512-bY1EfX4aisnpOYcuZkmkuBIUc0noNT/25ZoWispSiMZMS53yF55xkT1SFM+cxDjJ4M3HVJwVuU6r1szA8f9IPA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/openai": "2.0.67", - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.17" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/azure/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", - "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" + "@ai-sdk/openai": "3.0.0", + "@ai-sdk/provider": "3.0.0", + "@ai-sdk/provider-utils": "4.0.0" }, "engines": { "node": ">=18" @@ -197,14 +146,13 @@ } }, "node_modules/@ai-sdk/deepseek": { - "version": "1.0.30", - "resolved": "https://registry.npmjs.org/@ai-sdk/deepseek/-/deepseek-1.0.30.tgz", - "integrity": "sha512-pafNclW9L8Z3WimaRwlpHrGbdeaDE/UklT3rMi2aoRRyrA+s7zGcFuu1zbO2ViLNlKfaS91XZa9MFAPXbIftUA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/deepseek/-/deepseek-2.0.0.tgz", + "integrity": "sha512-qRX06mouHaF4OePE4S8W8+fLX7Iq4dWk7Ul4+SSPBzx6zBJKUqwHYW2XyPShNWhT4Rcogpl5yHJUrBaOExkiyg==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/openai-compatible": "1.0.28", - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.18" + "@ai-sdk/provider": "3.0.0", + "@ai-sdk/provider-utils": "4.0.0" }, "engines": { "node": ">=18" @@ -214,13 +162,13 @@ } }, "node_modules/@ai-sdk/gateway": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.21.tgz", - "integrity": "sha512-BwV7DU/lAm3Xn6iyyvZdWgVxgLu3SNXzl5y57gMvkW4nGhAOV5269IrJzQwGt03bb107sa6H6uJwWxc77zXoGA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.0.tgz", + "integrity": "sha512-JcjePYVpbezv+XOxkxPemwnorjWpgDiiKWMYy6FXTCG2rFABIK2Co1bFxIUSDT4vYO6f1448x9rKbn38vbhDiA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.19", + "@ai-sdk/provider": "3.0.0", + "@ai-sdk/provider-utils": "4.0.0", "@vercel/oidc": "3.0.5" }, "engines": { @@ -230,15 +178,14 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.19", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz", - "integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==", + "node_modules/@ai-sdk/google": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.0.tgz", + "integrity": "sha512-KFS9pR7KGDyt7p1OQibglS3amoLjCXxwF7DVg+gL2RLcwFRQV0s6Tp7Q+PvGNFSqPdrPYW8mHyvn8ODK4WTImA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" + "@ai-sdk/provider": "3.0.0", + "@ai-sdk/provider-utils": "4.0.0" }, "engines": { "node": ">=18" @@ -247,90 +194,14 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/google": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-2.0.0.tgz", - "integrity": "sha512-35uWKG+aWm0QClJV/kNhcyR9IVrDkZoI1UlWvUCjwoqbCxj4/L/1LKKbpM3JSRa9u74ghHzBB0UjLHdgcIoanw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4" - } - }, - "node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.0.tgz", - "integrity": "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.3", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4" - } - }, - "node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider-utils/node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - }, "node_modules/@ai-sdk/openai": { - "version": "2.0.67", - "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-2.0.67.tgz", - "integrity": "sha512-JhB3fUpY+IxAocyJt2PHuhfNwH+e+rDbZ8Q+d0hgSyNycuPRrV0xutLaf7mgDTvjr5FCrVEkXmM73tJprzZMiA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.0.tgz", + "integrity": "sha512-/o2xCQlRA+O0cAXIIBOfMeT35H6Fonzilz9r/IJojPOMQnmIL+0jPQVKOUPr5bouRqCjnwKpwuKEBRqm8jUZkQ==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.17" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/openai-compatible": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-1.0.28.tgz", - "integrity": "sha512-yKubDxLYtXyGUzkr9lNStf/lE/I+Okc8tmotvyABhsQHHieLKk6oV5fJeRJxhr67Ejhg+FRnwUOxAmjRoFM4dA==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.18" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/openai/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", - "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" + "@ai-sdk/provider": "3.0.0", + "@ai-sdk/provider-utils": "4.0.0" }, "engines": { "node": ">=18" @@ -340,9 +211,9 @@ } }, "node_modules/@ai-sdk/provider": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", - "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.0.tgz", + "integrity": "sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ==", "license": "Apache-2.0", "dependencies": { "json-schema": "^0.4.0" @@ -352,13 +223,13 @@ } }, "node_modules/@ai-sdk/provider-utils": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz", - "integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.0.tgz", + "integrity": "sha512-HyCyOls9I3a3e38+gtvOJOEjuw9KRcvbBnCL5GBuSmJvS9Jh9v3fz7pRC6ha1EUo/ZH1zwvLWYXBMtic8MTguA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", + "@ai-sdk/provider": "3.0.0", + "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "engines": { @@ -369,13 +240,13 @@ } }, "node_modules/@ai-sdk/react": { - "version": "2.0.107", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.107.tgz", - "integrity": "sha512-rv0u+tAi2r2zJu2uSLXcC3TBgGrkQIWXRM+i6us6qcGmYQ2kOu2VYg+lxviOSGPhL9PVebvTlN5x8mf3rDqX+w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.1.tgz", + "integrity": "sha512-XUPDMFgalNtqBQg+Q3UiiEmWE3PC5pAoc+Drs5Z1Mxqe57za+hKCEwViYADuqeZrc0q6PXTzbcFlQb3pjyGjcQ==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider-utils": "3.0.18", - "ai": "5.0.107", + "@ai-sdk/provider-utils": "4.0.0", + "ai": "6.0.1", "swr": "^2.2.5", "throttleit": "2.1.0" }, @@ -383,13 +254,7 @@ "node": ">=18" }, "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "zod": "^3.25.76 || ^4.1.8" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } + "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "node_modules/@alloc/quick-lru": { @@ -2182,7 +2047,7 @@ }, "node_modules/@electron/windows-sign": { "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", "dev": true, "license": "BSD-2-Clause", @@ -2204,7 +2069,7 @@ }, "node_modules/@electron/windows-sign/node_modules/fs-extra": { "version": "11.3.3", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.3.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", "dev": true, "license": "MIT", @@ -2221,7 +2086,7 @@ }, "node_modules/@electron/windows-sign/node_modules/jsonfile": { "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", @@ -2236,7 +2101,7 @@ }, "node_modules/@electron/windows-sign/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -4020,12 +3885,12 @@ } }, "node_modules/@openrouter/ai-sdk-provider": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-1.2.3.tgz", - "integrity": "sha512-a6Nc8dPRHakRH9966YJ/HZJhLOds7DuPTscNZDoAr+Aw+tEFUlacSJMvb/b3gukn74mgbuaJRji9YOn62ipfVg==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-1.5.4.tgz", + "integrity": "sha512-xrSQPUIH8n9zuyYZR0XK7Ba0h2KsjJcMkxnwaYfmv13pKs3sDkjPzVPPhlhzqBGddHb5cFEwJ9VFuFeDcxCDSw==", "license": "Apache-2.0", "dependencies": { - "@openrouter/sdk": "^0.1.8" + "@openrouter/sdk": "^0.1.27" }, "engines": { "node": ">=18" @@ -4036,28 +3901,12 @@ } }, "node_modules/@openrouter/sdk": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@openrouter/sdk/-/sdk-0.1.11.tgz", - "integrity": "sha512-OuPc8qqidL/PUM8+9WgrOfSR9+b6rKIWiezGcUJ54iPTdh+Gye5Qjut6hrLWlOCMZE7Z853gN90r1ft4iChj7Q==", + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@openrouter/sdk/-/sdk-0.1.27.tgz", + "integrity": "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ==", "license": "Apache-2.0", "dependencies": { "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependencies": { - "@tanstack/react-query": "^5", - "react": "^18 || ^19", - "react-dom": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@tanstack/react-query": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } } }, "node_modules/@opentelemetry/api": { @@ -7168,9 +7017,9 @@ } }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, "node_modules/@swc/helpers": { @@ -8287,14 +8136,14 @@ } }, "node_modules/ai": { - "version": "5.0.107", - "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.107.tgz", - "integrity": "sha512-laZlS9ZC/DZfSaxPgrBqI4mM+kxRvTPBBQfa74ceBFskkunZKEsaGVFNEs4cfyGa3nCCCl1WO/fjxixp4V8Zag==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.1.tgz", + "integrity": "sha512-g/jPakC6h4vUJKDww0d6+VaJmfMC38UqH3kKsngiP+coT0uvCUdQ7lpFDJ0mNmamaOyRMaY2zwEB2RnTAaJU/w==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "2.0.18", - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.18", + "@ai-sdk/gateway": "3.0.0", + "@ai-sdk/provider": "3.0.0", + "@ai-sdk/provider-utils": "4.0.0", "@opentelemetry/api": "1.9.0" }, "engines": { @@ -8304,23 +8153,6 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/ai/node_modules/@ai-sdk/gateway": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.18.tgz", - "integrity": "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.18", - "@vercel/oidc": "3.0.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -9791,7 +9623,7 @@ }, "node_modules/cross-dirname": { "version": "0.1.0", - "resolved": "https://registry.npmmirror.com/cross-dirname/-/cross-dirname-0.1.0.tgz", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", @@ -10343,7 +10175,7 @@ }, "node_modules/electron-builder-squirrel-windows": { "version": "26.0.12", - "resolved": "https://registry.npmmirror.com/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.0.12.tgz", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.0.12.tgz", "integrity": "sha512-kpwXM7c/ayRUbYVErQbsZ0nQZX4aLHQrPEG9C4h9vuJCXylwFH8a7Jgi2VpKIObzCXO7LKHiCw4KdioFLFOgqA==", "dev": true, "license": "MIT", @@ -10456,7 +10288,7 @@ }, "node_modules/electron-winstaller": { "version": "5.4.0", - "resolved": "https://registry.npmmirror.com/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", "dev": true, "hasInstallScript": true, @@ -10478,7 +10310,7 @@ }, "node_modules/electron-winstaller/node_modules/fs-extra": { "version": "7.0.1", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-7.0.1.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", @@ -15552,6 +15384,35 @@ "zod": "^4.0.16" } }, + "node_modules/ollama-ai-provider-v2/node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ollama-ai-provider-v2/node_modules/@ai-sdk/provider-utils": { + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz", + "integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", @@ -16040,7 +15901,7 @@ }, "node_modules/postject": { "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmmirror.com/postject/-/postject-1.0.0-alpha.6.tgz", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", "dev": true, "license": "MIT", @@ -16058,7 +15919,7 @@ }, "node_modules/postject/node_modules/commander": { "version": "9.5.0", - "resolved": "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, "license": "MIT", @@ -17872,7 +17733,7 @@ }, "node_modules/temp": { "version": "0.9.4", - "resolved": "https://registry.npmmirror.com/temp/-/temp-0.9.4.tgz", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", @@ -17936,7 +17797,7 @@ }, "node_modules/temp/node_modules/mkdirp": { "version": "0.5.6", - "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", @@ -17950,7 +17811,7 @@ }, "node_modules/temp/node_modules/rimraf": { "version": "2.6.3", - "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.6.3.tgz", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, diff --git a/package.json b/package.json index e82dcdb2..693069d4 100644 --- a/package.json +++ b/package.json @@ -24,21 +24,21 @@ "dist:all": "npm run electron:build && npm run electron:prepare && npx electron-builder --mac --win --linux" }, "dependencies": { - "@ai-sdk/amazon-bedrock": "^3.0.70", - "@ai-sdk/anthropic": "^2.0.44", - "@ai-sdk/azure": "^2.0.69", - "@ai-sdk/deepseek": "^1.0.30", - "@ai-sdk/gateway": "^2.0.21", - "@ai-sdk/google": "^2.0.0", - "@ai-sdk/openai": "^2.0.19", - "@ai-sdk/react": "^2.0.107", + "@ai-sdk/amazon-bedrock": "^4.0.1", + "@ai-sdk/anthropic": "^3.0.0", + "@ai-sdk/azure": "^3.0.0", + "@ai-sdk/deepseek": "^2.0.0", + "@ai-sdk/gateway": "^3.0.0", + "@ai-sdk/google": "^3.0.0", + "@ai-sdk/openai": "^3.0.0", + "@ai-sdk/react": "^3.0.1", "@aws-sdk/credential-providers": "^3.943.0", "@formatjs/intl-localematcher": "^0.7.2", "@langfuse/client": "^4.4.9", "@langfuse/otel": "^4.4.4", "@langfuse/tracing": "^4.4.9", "@next/third-parties": "^16.0.6", - "@openrouter/ai-sdk-provider": "^1.2.3", + "@openrouter/ai-sdk-provider": "^1.5.4", "@opentelemetry/exporter-trace-otlp-http": "^0.208.0", "@opentelemetry/sdk-trace-node": "^2.2.0", "@radix-ui/react-alert-dialog": "^1.1.15", @@ -53,7 +53,7 @@ "@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-use-controllable-state": "^1.2.2", "@xmldom/xmldom": "^0.9.8", - "ai": "^5.0.89", + "ai": "^6.0.1", "base-64": "^1.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -111,5 +111,10 @@ "tailwindcss": "^4", "typescript": "^5", "wait-on": "^9.0.3" + }, + "overrides": { + "@openrouter/ai-sdk-provider": { + "ai": "^6.0.1" + } } } From 0385c45a10215332e1d97c46b70707569f04136e Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:38:50 +0900 Subject: [PATCH 038/349] fix: OpenAI reasoning/thinking blocks not showing (#370) - Use Responses API instead of Chat Completions API for OpenAI (.chat() -> default call) to support reasoning events - Add o4 to reasoning model detection - Change default reasoningSummary from 'detailed' to 'auto' (not all models support 'detailed') - Update types to match AI SDK: 'auto' | 'detailed' --- lib/ai-providers.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/ai-providers.ts b/lib/ai-providers.ts index a20b92a9..da93069d 100644 --- a/lib/ai-providers.ts +++ b/lib/ai-providers.ts @@ -95,8 +95,8 @@ function parseIntSafe( * Supports various AI SDK providers with their unique configuration options * * Environment variables: - * - OPENAI_REASONING_EFFORT: OpenAI reasoning effort level (minimal/low/medium/high) - for o1/o3/gpt-5 - * - OPENAI_REASONING_SUMMARY: OpenAI reasoning summary (none/brief/detailed) - auto-enabled for o1/o3/gpt-5 + * - OPENAI_REASONING_EFFORT: OpenAI reasoning effort level (minimal/low/medium/high) - for o1/o3/o4/gpt-5 + * - OPENAI_REASONING_SUMMARY: OpenAI reasoning summary (auto/detailed) - auto-enabled for o1/o3/o4/gpt-5 * - ANTHROPIC_THINKING_BUDGET_TOKENS: Anthropic thinking budget in tokens (1024-64000) * - ANTHROPIC_THINKING_TYPE: Anthropic thinking type (enabled) * - GOOGLE_THINKING_BUDGET: Google Gemini 2.5 thinking budget in tokens (1024-100000) @@ -118,18 +118,19 @@ function buildProviderOptions( const reasoningEffort = process.env.OPENAI_REASONING_EFFORT const reasoningSummary = process.env.OPENAI_REASONING_SUMMARY - // OpenAI reasoning models (o1, o3, gpt-5) need reasoningSummary to return thoughts + // OpenAI reasoning models (o1, o3, o4, gpt-5) need reasoningSummary to return thoughts if ( modelId && (modelId.includes("o1") || modelId.includes("o3") || + modelId.includes("o4") || modelId.includes("gpt-5")) ) { options.openai = { - // Auto-enable reasoning summary for reasoning models (default: detailed) + // Auto-enable reasoning summary for reasoning models + // Use 'auto' as default since not all models support 'detailed' reasoningSummary: - (reasoningSummary as "none" | "brief" | "detailed") || - "detailed", + (reasoningSummary as "auto" | "detailed") || "auto", } // Optionally configure reasoning effort @@ -152,8 +153,7 @@ function buildProviderOptions( } if (reasoningSummary) { options.openai.reasoningSummary = reasoningSummary as - | "none" - | "brief" + | "auto" | "detailed" } } @@ -593,7 +593,9 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { apiKey, ...(baseURL && { baseURL }), }) - model = customOpenAI.chat(modelId) + // Use Responses API (default) instead of .chat() to support reasoning + // for gpt-5, o1, o3, o4 models. Chat Completions API does not emit reasoning events. + model = customOpenAI(modelId) } else { model = openai(modelId) } From a0fbc0ad33983fd301e16f0b0b6e90b0205b54db Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:43:28 +0900 Subject: [PATCH 039/349] fix: use last user message for Langfuse trace input (#371) In multi-step tool flows, messages array contains assistant messages from previous steps. Using messages[messages.length - 1] would record the assistant's response as trace input instead of the user's question. --- app/api/chat/route.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 92535821..226b45db 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -173,9 +173,12 @@ async function handleChatRequest(req: Request): Promise { : undefined // Extract user input text for Langfuse trace - const lastMessage = messages[messages.length - 1] + // Find the last USER message, not just the last message (which could be assistant in multi-step tool flows) + const lastUserMessage = [...messages] + .reverse() + .find((m: any) => m.role === "user") const userInputText = - lastMessage?.parts?.find((p: any) => p.type === "text")?.text || "" + lastUserMessage?.parts?.find((p: any) => p.type === "text")?.text || "" // Update Langfuse trace with input, session, and user setTraceInput({ @@ -237,9 +240,10 @@ async function handleChatRequest(req: Request): Promise { // Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5) const systemMessage = getSystemPrompt(modelId, minimalStyle) - // Extract file parts (images) from the last message + // Extract file parts (images) from the last user message const fileParts = - lastMessage.parts?.filter((part: any) => part.type === "file") || [] + lastUserMessage?.parts?.filter((part: any) => part.type === "file") || + [] // User input only - XML is now in a separate cached system message const formattedUserInput = `User input: From 9aec7eda79ae3963d58f30e8a04d1ddc15008d69 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:17:06 +0900 Subject: [PATCH 040/349] fix: add continuation retry limit for truncated diagrams (#372) Previously, continuation mode (for truncated XML) had unlimited client-side retries, relying only on server stepCountIs(5) limit. This could cause excessive API calls (495 observed) when XML truncation kept occurring. Added MAX_CONTINUATION_RETRY_COUNT=2 to limit continuation attempts: - After 2 failed continuation attempts, shows error toast and stops - Resets on successful completion or user-initiated message - Also resets when quota limits are hit --- components/chat-panel.tsx | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index dbb0ac36..a131a215 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -76,6 +76,7 @@ interface ChatPanelProps { const TOOL_ERROR_STATE = "output-error" as const const DEBUG = process.env.NODE_ENV === "development" const MAX_AUTO_RETRY_COUNT = 1 +const MAX_CONTINUATION_RETRY_COUNT = 2 // Limit for truncation continuation retries /** * Check if auto-resubmit should happen based on tool errors. @@ -216,6 +217,8 @@ export default function ChatPanel({ // Ref to track consecutive auto-retry count (reset on user action) const autoRetryCountRef = useRef(0) + // Ref to track continuation retry count (for truncation handling) + const continuationRetryCountRef = useRef(0) // Ref to accumulate partial XML when output is truncated due to maxOutputTokens // When partialXmlRef.current.length > 0, we're in continuation mode @@ -656,15 +659,25 @@ Continue from EXACTLY where you stopped.`, if (!shouldRetry) { // No error, reset retry count and clear state autoRetryCountRef.current = 0 + continuationRetryCountRef.current = 0 partialXmlRef.current = "" return false } - // Continuation mode: unlimited retries (truncation continuation, not real errors) - // Server limits to 5 steps via stepCountIs(5) + // Continuation mode: limited retries for truncation handling if (isInContinuationMode) { - // Don't count against retry limit for continuation - // Quota checks still apply below + if ( + continuationRetryCountRef.current >= + MAX_CONTINUATION_RETRY_COUNT + ) { + toast.error( + `Continuation retry limit reached (${MAX_CONTINUATION_RETRY_COUNT}). The diagram may be too complex.`, + ) + continuationRetryCountRef.current = 0 + partialXmlRef.current = "" + return false + } + continuationRetryCountRef.current++ } else { // Regular error: check retry count limit if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) { @@ -684,6 +697,7 @@ Continue from EXACTLY where you stopped.`, if (!tokenLimitCheck.allowed) { quotaManager.showTokenLimitToast(tokenLimitCheck.used) autoRetryCountRef.current = 0 + continuationRetryCountRef.current = 0 partialXmlRef.current = "" return false } @@ -692,6 +706,7 @@ Continue from EXACTLY where you stopped.`, if (!tpmCheck.allowed) { quotaManager.showTPMLimitToast() autoRetryCountRef.current = 0 + continuationRetryCountRef.current = 0 partialXmlRef.current = "" return false } @@ -1024,6 +1039,7 @@ Continue from EXACTLY where you stopped.`, ) => { // Reset all retry/continuation state on user-initiated message autoRetryCountRef.current = 0 + continuationRetryCountRef.current = 0 partialXmlRef.current = "" const config = getSelectedAIConfig() From 5ec05eb100b40c3c8fcadd4069c46b31001d0292 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:26:45 +0900 Subject: [PATCH 041/349] refactor: simplify Langfuse integration with AI SDK 6 (#375) - Remove manual token attribute setting (AI SDK 6 telemetry auto-reports) - Use totalTokens directly instead of inputTokens + outputTokens calculation - Fix sessionId bug in log-save/log-feedback (prevents wrong trace attachment) - Hash IP addresses for privacy instead of storing raw IPs - Fix isLangfuseEnabled() to check both keys for consistency --- app/api/chat/route.ts | 32 +++++++++++--------------------- app/api/log-feedback/route.ts | 13 +++++++++++-- app/api/log-save/route.ts | 5 +++++ components/chat-panel.tsx | 20 +++++++------------- lib/langfuse.ts | 32 ++++++++------------------------ 5 files changed, 42 insertions(+), 60 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 226b45db..8ed455d5 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -162,9 +162,13 @@ async function handleChatRequest(req: Request): Promise { const { messages, xml, previousXml, sessionId } = await req.json() - // Get user IP for Langfuse tracking + // Get user IP for Langfuse tracking (hashed for privacy) const forwardedFor = req.headers.get("x-forwarded-for") - const userId = forwardedFor?.split(",")[0]?.trim() || "anonymous" + const rawIp = forwardedFor?.split(",")[0]?.trim() || "anonymous" + const userId = + rawIp === "anonymous" + ? rawIp + : `user-${Buffer.from(rawIp).toString("base64url").slice(0, 8)}` // Validate sessionId for Langfuse (must be string, max 200 chars) const validSessionId = @@ -506,12 +510,9 @@ ${userInputText} userId, }), }), - onFinish: ({ text, usage }) => { - // Pass usage to Langfuse (Bedrock streaming doesn't auto-report tokens to telemetry) - setTraceOutput(text, { - promptTokens: usage?.inputTokens, - completionTokens: usage?.outputTokens, - }) + onFinish: ({ text }) => { + // AI SDK 6 telemetry auto-reports token usage on its spans + setTraceOutput(text) }, tools: { // Client-side tool that will be executed on the client @@ -681,20 +682,9 @@ Call this tool to get shape names and usage syntax for a specific library.`, messageMetadata: ({ part }) => { if (part.type === "finish") { const usage = (part as any).totalUsage - if (!usage) { - console.warn( - "[messageMetadata] No usage data in finish part", - ) - return undefined - } - // Total input = non-cached + cached (these are separate counts) - // Note: cacheWriteInputTokens is not available on finish part - const totalInputTokens = - (usage.inputTokens ?? 0) + - (usage.inputTokenDetails?.cacheReadTokens ?? 0) + // AI SDK 6 provides totalTokens directly return { - inputTokens: totalInputTokens, - outputTokens: usage.outputTokens ?? 0, + totalTokens: usage?.totalTokens ?? 0, finishReason: (part as any).finishReason, } } diff --git a/app/api/log-feedback/route.ts b/app/api/log-feedback/route.ts index 0ca1a9cf..82901ff6 100644 --- a/app/api/log-feedback/route.ts +++ b/app/api/log-feedback/route.ts @@ -27,9 +27,18 @@ export async function POST(req: Request) { const { messageId, feedback, sessionId } = data - // Get user IP for tracking + // Skip logging if no sessionId - prevents attaching to wrong user's trace + if (!sessionId) { + return Response.json({ success: true, logged: false }) + } + + // Get user IP for tracking (hashed for privacy) const forwardedFor = req.headers.get("x-forwarded-for") - const userId = forwardedFor?.split(",")[0]?.trim() || "anonymous" + const rawIp = forwardedFor?.split(",")[0]?.trim() || "anonymous" + const userId = + rawIp === "anonymous" + ? rawIp + : `user-${Buffer.from(rawIp).toString("base64url").slice(0, 8)}` try { // Find the most recent chat trace for this session to attach the score to diff --git a/app/api/log-save/route.ts b/app/api/log-save/route.ts index 28feded2..fc73fb2b 100644 --- a/app/api/log-save/route.ts +++ b/app/api/log-save/route.ts @@ -27,6 +27,11 @@ export async function POST(req: Request) { const { filename, format, sessionId } = data + // Skip logging if no sessionId - prevents attaching to wrong user's trace + if (!sessionId) { + return Response.json({ success: true, logged: false }) + } + try { const timestamp = new Date().toISOString() diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index a131a215..613cda81 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -632,21 +632,15 @@ Continue from EXACTLY where you stopped.`, // DEBUG: Log finish reason to diagnose truncation console.log("[onFinish] finishReason:", metadata?.finishReason) - console.log("[onFinish] metadata:", metadata) - if (metadata) { - // Use Number.isFinite to guard against NaN (typeof NaN === 'number' is true) - const inputTokens = Number.isFinite(metadata.inputTokens) - ? (metadata.inputTokens as number) + // AI SDK 6 provides totalTokens directly + const totalTokens = + metadata && Number.isFinite(metadata.totalTokens) + ? (metadata.totalTokens as number) : 0 - const outputTokens = Number.isFinite(metadata.outputTokens) - ? (metadata.outputTokens as number) - : 0 - const actualTokens = inputTokens + outputTokens - if (actualTokens > 0) { - quotaManager.incrementTokenCount(actualTokens) - quotaManager.incrementTPMCount(actualTokens) - } + if (totalTokens > 0) { + quotaManager.incrementTokenCount(totalTokens) + quotaManager.incrementTPMCount(totalTokens) } }, sendAutomaticallyWhen: ({ messages }) => { diff --git a/lib/langfuse.ts b/lib/langfuse.ts index 0cdfb1e4..040a296f 100644 --- a/lib/langfuse.ts +++ b/lib/langfuse.ts @@ -21,9 +21,11 @@ export function getLangfuseClient(): LangfuseClient | null { return langfuseClient } -// Check if Langfuse is configured +// Check if Langfuse is configured (both keys required) export function isLangfuseEnabled(): boolean { - return !!process.env.LANGFUSE_PUBLIC_KEY + return !!( + process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY + ) } // Update trace with input data at the start of request @@ -43,34 +45,16 @@ export function setTraceInput(params: { } // Update trace with output and end the span -export function setTraceOutput( - output: string, - usage?: { promptTokens?: number; completionTokens?: number }, -) { +// Note: AI SDK 6 telemetry automatically reports token usage on its spans, +// so we only need to set the output text and close our wrapper span +export function setTraceOutput(output: string) { if (!isLangfuseEnabled()) return updateActiveTrace({ output }) + // End the observe() wrapper span (AI SDK creates its own child spans with usage) const activeSpan = api.trace.getActiveSpan() if (activeSpan) { - // Manually set usage attributes since AI SDK Bedrock streaming doesn't provide them - if (usage?.promptTokens) { - activeSpan.setAttribute("ai.usage.promptTokens", usage.promptTokens) - activeSpan.setAttribute( - "gen_ai.usage.input_tokens", - usage.promptTokens, - ) - } - if (usage?.completionTokens) { - activeSpan.setAttribute( - "ai.usage.completionTokens", - usage.completionTokens, - ) - activeSpan.setAttribute( - "gen_ai.usage.output_tokens", - usage.completionTokens, - ) - } activeSpan.end() } } From 97ae9395cdbcaedd6ab4bf9cdaa196128fba1e1f Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:36:27 +0900 Subject: [PATCH 042/349] feat: add server-side quota tracking with DynamoDB (#379) - Add dynamo-quota-manager.ts for atomic quota checks using ConditionExpression - Enforce daily request limit, daily token limit, and TPM limit - Return 429 with quota details (type, used, limit) when exceeded - Quota is opt-in: only enabled when DYNAMODB_QUOTA_TABLE env var is set - Remove client-side quota enforcement (server is now source of truth) - Simplify use-quota-manager.tsx to only display toasts - Add @aws-sdk/client-dynamodb dependency --- app/api/chat/route.ts | 46 +- components/chat-panel.tsx | 84 +-- lib/dynamo-quota-manager.ts | 238 +++++++++ lib/use-quota-manager.tsx | 184 +------ package-lock.json | 991 ++++++++++++++++++++++++++++++------ package.json | 1 + 6 files changed, 1128 insertions(+), 416 deletions(-) create mode 100644 lib/dynamo-quota-manager.ts diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 8ed455d5..8db9eb1c 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -14,6 +14,11 @@ import path from "path" import { z } from "zod" import { getAIModel, supportsPromptCaching } from "@/lib/ai-providers" import { findCachedResponse } from "@/lib/cached-responses" +import { + checkAndIncrementRequest, + isQuotaEnabled, + recordTokenUsage, +} from "@/lib/dynamo-quota-manager" import { getTelemetryConfig, setTraceInput, @@ -191,6 +196,33 @@ async function handleChatRequest(req: Request): Promise { userId: userId, }) + // === SERVER-SIDE QUOTA CHECK START === + // Quota is opt-in: only enabled when DYNAMODB_QUOTA_TABLE env var is set + const hasOwnApiKey = !!( + req.headers.get("x-ai-provider") && req.headers.get("x-ai-api-key") + ) + + // Skip quota check if: quota disabled, user has own API key, or is anonymous + if (isQuotaEnabled() && !hasOwnApiKey && userId !== "anonymous") { + const quotaCheck = await checkAndIncrementRequest(userId, { + requests: Number(process.env.DAILY_REQUEST_LIMIT) || 10, + tokens: Number(process.env.DAILY_TOKEN_LIMIT) || 200000, + tpm: Number(process.env.TPM_LIMIT) || 20000, + }) + if (!quotaCheck.allowed) { + return Response.json( + { + error: quotaCheck.error, + type: quotaCheck.type, + used: quotaCheck.used, + limit: quotaCheck.limit, + }, + { status: 429 }, + ) + } + } + // === SERVER-SIDE QUOTA CHECK END === + // === FILE VALIDATION START === const fileValidation = validateFileParts(messages) if (!fileValidation.valid) { @@ -510,9 +542,21 @@ ${userInputText} userId, }), }), - onFinish: ({ text }) => { + onFinish: ({ text, usage }) => { // AI SDK 6 telemetry auto-reports token usage on its spans setTraceOutput(text) + + // Record token usage for server-side quota tracking (if enabled) + if ( + isQuotaEnabled() && + !hasOwnApiKey && + userId !== "anonymous" && + usage + ) { + const totalTokens = + (usage.inputTokens || 0) + (usage.outputTokens || 0) + recordTokenUsage(userId, totalTokens) + } }, tools: { // Client-side tool that will be executed on the client diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 613cda81..744fa3b3 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -556,6 +556,23 @@ Continue from EXACTLY where you stopped.`, } }, onError: (error) => { + // Handle server-side quota limit (429 response) + if (error.message.includes("Daily request limit")) { + quotaManager.showQuotaLimitToast() + return + } + if (error.message.includes("Daily token limit")) { + quotaManager.showTokenLimitToast(dailyTokenLimit) + return + } + if ( + error.message.includes("Rate limit exceeded") || + error.message.includes("tokens per minute") + ) { + quotaManager.showTPMLimitToast() + return + } + // Silence access code error in console since it's handled by UI if (!error.message.includes("Invalid or missing access code")) { console.error("Chat error:", error) @@ -632,16 +649,6 @@ Continue from EXACTLY where you stopped.`, // DEBUG: Log finish reason to diagnose truncation console.log("[onFinish] finishReason:", metadata?.finishReason) - - // AI SDK 6 provides totalTokens directly - const totalTokens = - metadata && Number.isFinite(metadata.totalTokens) - ? (metadata.totalTokens as number) - : 0 - if (totalTokens > 0) { - quotaManager.incrementTokenCount(totalTokens) - quotaManager.incrementTPMCount(totalTokens) - } }, sendAutomaticallyWhen: ({ messages }) => { const isInContinuationMode = partialXmlRef.current.length > 0 @@ -686,25 +693,6 @@ Continue from EXACTLY where you stopped.`, autoRetryCountRef.current++ } - // Check quota limits before auto-retry - const tokenLimitCheck = quotaManager.checkTokenLimit() - if (!tokenLimitCheck.allowed) { - quotaManager.showTokenLimitToast(tokenLimitCheck.used) - autoRetryCountRef.current = 0 - continuationRetryCountRef.current = 0 - partialXmlRef.current = "" - return false - } - - const tpmCheck = quotaManager.checkTPMLimit() - if (!tpmCheck.allowed) { - quotaManager.showTPMLimitToast() - autoRetryCountRef.current = 0 - continuationRetryCountRef.current = 0 - partialXmlRef.current = "" - return false - } - return true }, }) @@ -921,9 +909,6 @@ Continue from EXACTLY where you stopped.`, xmlSnapshotsRef.current.set(messageIndex, chartXml) saveXmlSnapshots() - // Check all quota limits - if (!checkAllQuotaLimits()) return - sendChatMessage(parts, chartXml, previousXml, sessionId) // Token count is tracked in onFinish with actual server usage @@ -1001,30 +986,7 @@ Continue from EXACTLY where you stopped.`, saveXmlSnapshots() } - // Check all quota limits (daily requests, tokens, TPM) - const checkAllQuotaLimits = (): boolean => { - const limitCheck = quotaManager.checkDailyLimit() - if (!limitCheck.allowed) { - quotaManager.showQuotaLimitToast() - return false - } - - const tokenLimitCheck = quotaManager.checkTokenLimit() - if (!tokenLimitCheck.allowed) { - quotaManager.showTokenLimitToast(tokenLimitCheck.used) - return false - } - - const tpmCheck = quotaManager.checkTPMLimit() - if (!tpmCheck.allowed) { - quotaManager.showTPMLimitToast() - return false - } - - return true - } - - // Send chat message with headers and increment quota + // Send chat message with headers const sendChatMessage = ( parts: any, xml: string, @@ -1074,7 +1036,6 @@ Continue from EXACTLY where you stopped.`, }, }, ) - quotaManager.incrementRequestCount() } // Process files and append content to user text (handles PDF, text, and optionally images) @@ -1162,13 +1123,8 @@ Continue from EXACTLY where you stopped.`, setMessages(newMessages) }) - // Check all quota limits - if (!checkAllQuotaLimits()) return - // Now send the message after state is guaranteed to be updated sendChatMessage(userParts, savedXml, previousXml, sessionId) - - // Token count is tracked in onFinish with actual server usage } const handleEditMessage = async (messageIndex: number, newText: string) => { @@ -1210,12 +1166,8 @@ Continue from EXACTLY where you stopped.`, setMessages(newMessages) }) - // Check all quota limits - if (!checkAllQuotaLimits()) return - // Now send the edited message after state is guaranteed to be updated sendChatMessage(newParts, savedXml, previousXml, sessionId) - // Token count is tracked in onFinish with actual server usage } // Collapsed view (desktop only) diff --git a/lib/dynamo-quota-manager.ts b/lib/dynamo-quota-manager.ts new file mode 100644 index 00000000..0e8d35ab --- /dev/null +++ b/lib/dynamo-quota-manager.ts @@ -0,0 +1,238 @@ +import { + ConditionalCheckFailedException, + DynamoDBClient, + GetItemCommand, + UpdateItemCommand, +} from "@aws-sdk/client-dynamodb" + +// Quota tracking is OPT-IN: only enabled if DYNAMODB_QUOTA_TABLE is explicitly set +// OSS users who don't need quota tracking can simply not set this env var +const TABLE = process.env.DYNAMODB_QUOTA_TABLE +const DYNAMODB_REGION = process.env.DYNAMODB_REGION || "ap-northeast-1" + +// Only create client if quota is enabled +const client = TABLE ? new DynamoDBClient({ region: DYNAMODB_REGION }) : null + +/** + * Check if server-side quota tracking is enabled. + * Quota is opt-in: only enabled when DYNAMODB_QUOTA_TABLE env var is set. + */ +export function isQuotaEnabled(): boolean { + return !!TABLE +} + +interface QuotaLimits { + requests: number // Daily request limit + tokens: number // Daily token limit + tpm: number // Tokens per minute +} + +interface QuotaCheckResult { + allowed: boolean + error?: string + type?: "request" | "token" | "tpm" + used?: number + limit?: number +} + +/** + * Check all quotas and increment request count atomically. + * Uses ConditionExpression to prevent race conditions. + * Returns which limit was exceeded if any. + */ +export async function checkAndIncrementRequest( + ip: string, + limits: QuotaLimits, +): Promise { + // Skip if quota tracking not enabled + if (!client || !TABLE) { + return { allowed: true } + } + + const today = new Date().toISOString().split("T")[0] + const currentMinute = Math.floor(Date.now() / 60000).toString() + const ttl = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60 + + try { + // Atomic check-and-increment with ConditionExpression + // This prevents race conditions by failing if limits are exceeded + await client.send( + new UpdateItemCommand({ + TableName: TABLE, + Key: { PK: { S: `IP#${ip}` } }, + // Reset counts if new day/minute, then increment request count + UpdateExpression: ` + SET lastResetDate = :today, + dailyReqCount = if_not_exists(dailyReqCount, :zero) + :one, + dailyTokenCount = if_not_exists(dailyTokenCount, :zero), + lastMinute = :minute, + tpmCount = if_not_exists(tpmCount, :zero), + #ttl = :ttl + `, + // Atomic condition: only succeed if ALL limits pass + // Uses attribute_not_exists for new items, then checks limits for existing items + ConditionExpression: ` + (attribute_not_exists(lastResetDate) OR lastResetDate < :today OR + ((attribute_not_exists(dailyReqCount) OR dailyReqCount < :reqLimit) AND + (attribute_not_exists(dailyTokenCount) OR dailyTokenCount < :tokenLimit))) AND + (attribute_not_exists(lastMinute) OR lastMinute <> :minute OR + attribute_not_exists(tpmCount) OR tpmCount < :tpmLimit) + `, + ExpressionAttributeNames: { "#ttl": "ttl" }, + ExpressionAttributeValues: { + ":today": { S: today }, + ":zero": { N: "0" }, + ":one": { N: "1" }, + ":minute": { S: currentMinute }, + ":ttl": { N: String(ttl) }, + ":reqLimit": { N: String(limits.requests || 999999) }, + ":tokenLimit": { N: String(limits.tokens || 999999) }, + ":tpmLimit": { N: String(limits.tpm || 999999) }, + }, + }), + ) + + return { allowed: true } + } catch (e: any) { + // Condition failed - need to determine which limit was exceeded + if (e instanceof ConditionalCheckFailedException) { + // Get current counts to determine which limit was hit + try { + const getResult = await client.send( + new GetItemCommand({ + TableName: TABLE, + Key: { PK: { S: `IP#${ip}` } }, + }), + ) + + const item = getResult.Item + const storedDate = item?.lastResetDate?.S + const storedMinute = item?.lastMinute?.S + const isNewDay = !storedDate || storedDate < today + + const dailyReqCount = isNewDay + ? 0 + : Number(item?.dailyReqCount?.N || 0) + const dailyTokenCount = isNewDay + ? 0 + : Number(item?.dailyTokenCount?.N || 0) + const tpmCount = + storedMinute !== currentMinute + ? 0 + : Number(item?.tpmCount?.N || 0) + + // Determine which limit was exceeded + if (limits.requests > 0 && dailyReqCount >= limits.requests) { + return { + allowed: false, + type: "request", + error: "Daily request limit exceeded", + used: dailyReqCount, + limit: limits.requests, + } + } + if (limits.tokens > 0 && dailyTokenCount >= limits.tokens) { + return { + allowed: false, + type: "token", + error: "Daily token limit exceeded", + used: dailyTokenCount, + limit: limits.tokens, + } + } + if (limits.tpm > 0 && tpmCount >= limits.tpm) { + return { + allowed: false, + type: "tpm", + error: "Rate limit exceeded (tokens per minute)", + used: tpmCount, + limit: limits.tpm, + } + } + + // Condition failed but no limit clearly exceeded - race condition edge case + // Fail safe by allowing (could be a reset race) + console.warn( + `[quota] Condition failed but no limit exceeded for IP prefix: ${ip.slice(0, 8)}...`, + ) + return { allowed: true } + } catch (getError: any) { + console.error( + `[quota] Failed to get quota details after condition failure, IP prefix: ${ip.slice(0, 8)}..., error: ${getError.message}`, + ) + return { allowed: true } // Fail open + } + } + + // Other DynamoDB errors - fail open + console.error( + `[quota] DynamoDB error (fail-open), IP prefix: ${ip.slice(0, 8)}..., error: ${e.message}`, + ) + return { allowed: true } + } +} + +/** + * Record token usage after response completes. + * Uses atomic operations to update both daily token count and TPM count. + * Handles minute boundaries atomically to prevent race conditions. + */ +export async function recordTokenUsage( + ip: string, + tokens: number, +): Promise { + // Skip if quota tracking not enabled + if (!client || !TABLE) return + if (!Number.isFinite(tokens) || tokens <= 0) return + + const currentMinute = Math.floor(Date.now() / 60000).toString() + const ttl = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60 + + try { + // Try to update assuming same minute (most common case) + // Uses condition to ensure we're in the same minute + await client.send( + new UpdateItemCommand({ + TableName: TABLE, + Key: { PK: { S: `IP#${ip}` } }, + UpdateExpression: + "SET #ttl = :ttl ADD dailyTokenCount :tokens, tpmCount :tokens", + ConditionExpression: "lastMinute = :minute", + ExpressionAttributeNames: { "#ttl": "ttl" }, + ExpressionAttributeValues: { + ":minute": { S: currentMinute }, + ":tokens": { N: String(tokens) }, + ":ttl": { N: String(ttl) }, + }, + }), + ) + } catch (e: any) { + if (e instanceof ConditionalCheckFailedException) { + // Different minute - reset TPM count and set new minute + try { + await client.send( + new UpdateItemCommand({ + TableName: TABLE, + Key: { PK: { S: `IP#${ip}` } }, + UpdateExpression: + "SET lastMinute = :minute, tpmCount = :tokens, #ttl = :ttl ADD dailyTokenCount :tokens", + ExpressionAttributeNames: { "#ttl": "ttl" }, + ExpressionAttributeValues: { + ":minute": { S: currentMinute }, + ":tokens": { N: String(tokens) }, + ":ttl": { N: String(ttl) }, + }, + }), + ) + } catch (retryError: any) { + console.error( + `[quota] Failed to record tokens (retry), IP prefix: ${ip.slice(0, 8)}..., tokens: ${tokens}, error: ${retryError.message}`, + ) + } + } else { + console.error( + `[quota] Failed to record tokens, IP prefix: ${ip.slice(0, 8)}..., tokens: ${tokens}, error: ${e.message}`, + ) + } + } +} diff --git a/lib/use-quota-manager.tsx b/lib/use-quota-manager.tsx index 687b8642..bda083bf 100644 --- a/lib/use-quota-manager.tsx +++ b/lib/use-quota-manager.tsx @@ -1,11 +1,10 @@ "use client" -import { useCallback, useMemo } from "react" +import { useCallback } from "react" import { toast } from "sonner" import { QuotaLimitToast } from "@/components/quota-limit-toast" import { useDictionary } from "@/hooks/use-dictionary" import { formatMessage } from "@/lib/i18n/utils" -import { STORAGE_KEYS } from "@/lib/storage" export interface QuotaConfig { dailyRequestLimit: number @@ -13,134 +12,19 @@ export interface QuotaConfig { tpmLimit: number } -export interface QuotaCheckResult { - allowed: boolean - remaining: number - used: number -} - /** - * Hook for managing request/token quotas and rate limiting. - * Handles three types of limits: - * - Daily request limit - * - Daily token limit - * - Tokens per minute (TPM) rate limit - * - * Users with their own API key bypass all limits. + * Hook for displaying quota limit toasts. + * Server-side handles actual quota enforcement via DynamoDB. + * This hook only provides UI feedback when limits are exceeded. */ export function useQuotaManager(config: QuotaConfig): { - hasOwnApiKey: () => boolean - checkDailyLimit: () => QuotaCheckResult - checkTokenLimit: () => QuotaCheckResult - checkTPMLimit: () => QuotaCheckResult - incrementRequestCount: () => void - incrementTokenCount: (tokens: number) => void - incrementTPMCount: (tokens: number) => void showQuotaLimitToast: () => void showTokenLimitToast: (used: number) => void showTPMLimitToast: () => void } { const { dailyRequestLimit, dailyTokenLimit, tpmLimit } = config - const dict = useDictionary() - // Check if user has their own API key configured (bypass limits) - const hasOwnApiKey = useCallback((): boolean => { - const provider = localStorage.getItem(STORAGE_KEYS.aiProvider) - const apiKey = localStorage.getItem(STORAGE_KEYS.aiApiKey) - return !!(provider && apiKey) - }, []) - - // Generic helper: Parse count from localStorage with NaN guard - const parseStorageCount = (key: string): number => { - const count = parseInt(localStorage.getItem(key) || "0", 10) - return Number.isNaN(count) ? 0 : count - } - - // Generic helper: Create quota checker factory - const createQuotaChecker = useCallback( - ( - getTimeKey: () => string, - timeStorageKey: string, - countStorageKey: string, - limit: number, - ) => { - return (): QuotaCheckResult => { - if (hasOwnApiKey()) - return { allowed: true, remaining: -1, used: 0 } - if (limit <= 0) return { allowed: true, remaining: -1, used: 0 } - - const currentTime = getTimeKey() - const storedTime = localStorage.getItem(timeStorageKey) - let count = parseStorageCount(countStorageKey) - - if (storedTime !== currentTime) { - count = 0 - localStorage.setItem(timeStorageKey, currentTime) - localStorage.setItem(countStorageKey, "0") - } - - return { - allowed: count < limit, - remaining: limit - count, - used: count, - } - } - }, - [hasOwnApiKey], - ) - - // Generic helper: Create quota incrementer factory - const createQuotaIncrementer = useCallback( - ( - getTimeKey: () => string, - timeStorageKey: string, - countStorageKey: string, - validateInput: boolean = false, - ) => { - return (tokens: number = 1): void => { - if (validateInput && (!Number.isFinite(tokens) || tokens <= 0)) - return - - const currentTime = getTimeKey() - const storedTime = localStorage.getItem(timeStorageKey) - let count = parseStorageCount(countStorageKey) - - if (storedTime !== currentTime) { - count = 0 - localStorage.setItem(timeStorageKey, currentTime) - } - - localStorage.setItem(countStorageKey, String(count + tokens)) - } - }, - [], - ) - - // Check daily request limit - const checkDailyLimit = useMemo( - () => - createQuotaChecker( - () => new Date().toDateString(), - STORAGE_KEYS.requestDate, - STORAGE_KEYS.requestCount, - dailyRequestLimit, - ), - [createQuotaChecker, dailyRequestLimit], - ) - - // Increment request count - const incrementRequestCount = useMemo( - () => - createQuotaIncrementer( - () => new Date().toDateString(), - STORAGE_KEYS.requestDate, - STORAGE_KEYS.requestCount, - false, - ), - [createQuotaIncrementer], - ) - // Show quota limit toast (request-based) const showQuotaLimitToast = useCallback(() => { toast.custom( @@ -155,30 +39,6 @@ export function useQuotaManager(config: QuotaConfig): { ) }, [dailyRequestLimit]) - // Check daily token limit - const checkTokenLimit = useMemo( - () => - createQuotaChecker( - () => new Date().toDateString(), - STORAGE_KEYS.tokenDate, - STORAGE_KEYS.tokenCount, - dailyTokenLimit, - ), - [createQuotaChecker, dailyTokenLimit], - ) - - // Increment token count - const incrementTokenCount = useMemo( - () => - createQuotaIncrementer( - () => new Date().toDateString(), - STORAGE_KEYS.tokenDate, - STORAGE_KEYS.tokenCount, - true, // Validate input tokens - ), - [createQuotaIncrementer], - ) - // Show token limit toast const showTokenLimitToast = useCallback( (used: number) => { @@ -197,30 +57,6 @@ export function useQuotaManager(config: QuotaConfig): { [dailyTokenLimit], ) - // Check TPM (tokens per minute) limit - const checkTPMLimit = useMemo( - () => - createQuotaChecker( - () => Math.floor(Date.now() / 60000).toString(), - STORAGE_KEYS.tpmMinute, - STORAGE_KEYS.tpmCount, - tpmLimit, - ), - [createQuotaChecker, tpmLimit], - ) - - // Increment TPM count - const incrementTPMCount = useMemo( - () => - createQuotaIncrementer( - () => Math.floor(Date.now() / 60000).toString(), - STORAGE_KEYS.tpmMinute, - STORAGE_KEYS.tpmCount, - true, // Validate input tokens - ), - [createQuotaIncrementer], - ) - // Show TPM limit toast const showTPMLimitToast = useCallback(() => { const limitDisplay = @@ -233,18 +69,6 @@ export function useQuotaManager(config: QuotaConfig): { }, [tpmLimit, dict]) return { - // Check functions - hasOwnApiKey, - checkDailyLimit, - checkTokenLimit, - checkTPMLimit, - - // Increment functions - incrementRequestCount, - incrementTokenCount, - incrementTPMCount, - - // Toast functions showQuotaLimitToast, showTokenLimitToast, showTPMLimitToast, diff --git a/package-lock.json b/package-lock.json index d1aeec98..7f53c05e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@ai-sdk/google": "^3.0.0", "@ai-sdk/openai": "^3.0.0", "@ai-sdk/react": "^3.0.1", + "@aws-sdk/client-dynamodb": "^3.957.0", "@aws-sdk/credential-providers": "^3.943.0", "@formatjs/intl-localematcher": "^0.7.2", "@langfuse/client": "^4.4.9", @@ -500,6 +501,515 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.957.0.tgz", + "integrity": "sha512-H/uHYgZTmFUq2qb4b/GTxT2F8Yyqyb3pMpI3mldGINZkPYQYN9pP246pqnf+OYOClPMxSMRchrbjZgZhADMi8Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-node": "3.957.0", + "@aws-sdk/dynamodb-codec": "3.957.0", + "@aws-sdk/middleware-endpoint-discovery": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/client-sso": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.957.0.tgz", + "integrity": "sha512-iRdRjd+IpOogqRPt8iNRcg30J53z4rRfMviGwpKgsEa/fx3inCUPOuca3Ap7ZDES0atnEg3KGSJ3V/NQiEJ4BA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/core": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.957.0.tgz", + "integrity": "sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@aws-sdk/xml-builder": "3.957.0", + "@smithy/core": "^3.20.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.957.0.tgz", + "integrity": "sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.957.0.tgz", + "integrity": "sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.957.0.tgz", + "integrity": "sha512-YuoZmIeE91YIeUfihh8SiSu546KtTvU+4rG5SaL30U9+nGq6P11GRRgqF0ANUyRseLC9ONHt+utar4gbO3++og==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-env": "3.957.0", + "@aws-sdk/credential-provider-http": "3.957.0", + "@aws-sdk/credential-provider-login": "3.957.0", + "@aws-sdk/credential-provider-process": "3.957.0", + "@aws-sdk/credential-provider-sso": "3.957.0", + "@aws-sdk/credential-provider-web-identity": "3.957.0", + "@aws-sdk/nested-clients": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.957.0.tgz", + "integrity": "sha512-XcD5NEQDWYk8B4gs89bkwf2d+DNF8oS2NR5RoHJEbX4l8KErVATUjpEYVn6/rAFEktungxlYTnQ5wh0cIQvP5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.957.0.tgz", + "integrity": "sha512-b9FT/7BQcJ001w+3JbTiJXfxHrWvPb7zDvvC1i1FKcNOvyCt3BGu04n4nO/b71a3iBnbfBXI89hCIZQsuLcEgw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.957.0", + "@aws-sdk/credential-provider-http": "3.957.0", + "@aws-sdk/credential-provider-ini": "3.957.0", + "@aws-sdk/credential-provider-process": "3.957.0", + "@aws-sdk/credential-provider-sso": "3.957.0", + "@aws-sdk/credential-provider-web-identity": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.957.0.tgz", + "integrity": "sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.957.0.tgz", + "integrity": "sha512-gTLPJFOkGtn3tVGglRhCar2oOobK1YctZRAT8nfJr17uaSRoAP46zIIHNYBZZUMqImb0qAHD9Ugm+Zd9sIqxyA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.957.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/token-providers": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.957.0.tgz", + "integrity": "sha512-x17xMeD7c+rKEsWachGIMifACqkugskrETWz18QDWismFcrmUuOcZu5rUa8s9y1pnITLKUQ1xU/qDLPH52jLlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.957.0.tgz", + "integrity": "sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/middleware-logger": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.957.0.tgz", + "integrity": "sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.957.0.tgz", + "integrity": "sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.957.0.tgz", + "integrity": "sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@smithy/core": "^3.20.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/nested-clients": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.957.0.tgz", + "integrity": "sha512-PZUFtaUTSZWO+mbgQGWSiwz3EqedsuKNb7Xoxjzh5rfJE352DD4/jScQEhVPxvdLw62IK9b5UDu5kZlxzBs9Ow==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.957.0.tgz", + "integrity": "sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/token-providers": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.957.0.tgz", + "integrity": "sha512-oSwo3BZ6gcvhjTg036V0UQmtENUeNwfCU35iDckX961CdI1alQ3TKRWLzKrwvXCbrOx+bZsuA1PHsTbNhI/+Fw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/types": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.957.0.tgz", + "integrity": "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/util-endpoints": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.957.0.tgz", + "integrity": "sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-endpoints": "^3.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.957.0.tgz", + "integrity": "sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/types": "^4.11.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.957.0.tgz", + "integrity": "sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/xml-builder": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.957.0.tgz", + "integrity": "sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-sso": { "version": "3.943.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.943.0.tgz", @@ -778,6 +1288,120 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/dynamodb-codec": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.957.0.tgz", + "integrity": "sha512-xds1mkwEGzXrNy/gT6/ehaJ+cbYn/QM7AkdwNrO1NBlwJVLo3imO6hOnOQ/0KWG2ck1dbKv9H9f2hka67bAzEA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@smithy/core": "^3.20.0", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.957.0" + } + }, + "node_modules/@aws-sdk/dynamodb-codec/node_modules/@aws-sdk/core": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.957.0.tgz", + "integrity": "sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@aws-sdk/xml-builder": "3.957.0", + "@smithy/core": "^3.20.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/dynamodb-codec/node_modules/@aws-sdk/types": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.957.0.tgz", + "integrity": "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/dynamodb-codec/node_modules/@aws-sdk/xml-builder": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.957.0.tgz", + "integrity": "sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.957.0.tgz", + "integrity": "sha512-QxvFejXYYBZp/GBfT7B15gvmvuq+0f2U8RPHqArf5IqBi51ZyBqUD805tQ8TlsVrlLoi+Z4fEFw4HEM5pGvPUg==", + "license": "Apache-2.0", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.957.0.tgz", + "integrity": "sha512-MJjlw4mVJNTyR5dW6wpzKLRzFPIYAMA8qUWqgG4hGscmm4GFHvWVJ9mhhdpDu7Ie4Uaikmzfy0C4xzZ+lkf1+w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/endpoint-cache": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery/node_modules/@aws-sdk/types": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.957.0.tgz", + "integrity": "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", @@ -6422,12 +7046,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", - "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz", + "integrity": "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6435,16 +7059,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", - "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.5.tgz", + "integrity": "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" }, "engines": { @@ -6452,18 +7076,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.18.7", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.7.tgz", - "integrity": "sha512-axG9MvKhMWOhFbvf5y2DuyTxQueO0dkedY9QC3mAfndLosRI/9LJv8WaL0mw7ubNhsO4IuXX9/9dYGPFvHrqlw==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.0.tgz", + "integrity": "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.6", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -6473,15 +7097,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", - "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.7.tgz", + "integrity": "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", "tslib": "^2.6.2" }, "engines": { @@ -6504,14 +7128,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", - "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.8.tgz", + "integrity": "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, @@ -6520,12 +7144,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", - "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.7.tgz", + "integrity": "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -6535,12 +7159,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", - "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.7.tgz", + "integrity": "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6560,13 +7184,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", - "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.7.tgz", + "integrity": "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6574,18 +7198,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.14.tgz", - "integrity": "sha512-v0q4uTKgBM8dsqGjqsabZQyH85nFaTnFcgpWU1uydKFsdyyMzfvOkNum9G7VK+dOP01vUnoZxIeRiJ6uD0kjIg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.1.tgz", + "integrity": "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.18.7", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-middleware": "^4.2.5", + "@smithy/core": "^3.20.0", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" }, "engines": { @@ -6593,18 +7217,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.14.tgz", - "integrity": "sha512-Z2DG8Ej7FyWG1UA+7HceINtSLzswUgs2np3sZX0YBBxCt+CXG4QUxv88ZDS3+2/1ldW7LqtSY1UO/6VQ1pND8Q==", + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.17.tgz", + "integrity": "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/service-error-classification": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/service-error-classification": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, @@ -6613,13 +7237,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", - "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.8.tgz", + "integrity": "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6627,12 +7251,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", - "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.7.tgz", + "integrity": "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6640,14 +7264,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", - "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz", + "integrity": "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6655,15 +7279,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", - "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.7.tgz", + "integrity": "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/abort-controller": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6671,12 +7295,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", - "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.7.tgz", + "integrity": "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6684,12 +7308,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", - "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.7.tgz", + "integrity": "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6697,12 +7321,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", - "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.7.tgz", + "integrity": "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, @@ -6711,12 +7335,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", - "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.7.tgz", + "integrity": "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6724,24 +7348,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", - "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.7.tgz", + "integrity": "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0" + "@smithy/types": "^4.11.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", - "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz", + "integrity": "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6749,16 +7373,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", - "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.7.tgz", + "integrity": "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-middleware": "^4.2.7", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -6768,17 +7392,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.9.10", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.10.tgz", - "integrity": "sha512-Jaoz4Jw1QYHc1EFww/E6gVtNjhoDU+gwRKqXP6C3LKYqqH2UQhP8tMP3+t/ePrhaze7fhLE8vS2q6vVxBANFTQ==", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.2.tgz", + "integrity": "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.18.7", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", + "@smithy/core": "^3.20.0", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" }, "engines": { @@ -6786,9 +7410,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", - "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.11.0.tgz", + "integrity": "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -6798,13 +7422,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", - "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.7.tgz", + "integrity": "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/querystring-parser": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6875,14 +7499,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.13", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.13.tgz", - "integrity": "sha512-hlVLdAGrVfyNei+pKIgqDTxfu/ZI2NSyqj4IDxKd5bIsIqwR/dSlkxlPaYxFiIaDVrBy0he8orsFy+Cz119XvA==", + "version": "4.3.16", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.16.tgz", + "integrity": "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6890,17 +7514,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.16.tgz", - "integrity": "sha512-F1t22IUiJLHrxW9W1CQ6B9PN+skZ9cqSuzB18Eh06HrJPbjsyZ7ZHecAKw80DQtyGTRcVfeukKaCRYebFwclbg==", + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.19.tgz", + "integrity": "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.3", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6908,13 +7532,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", - "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz", + "integrity": "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6934,12 +7558,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", - "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.7.tgz", + "integrity": "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6947,13 +7571,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", - "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.7.tgz", + "integrity": "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/service-error-classification": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -6961,14 +7585,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", - "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.8.tgz", + "integrity": "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/types": "^4.9.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", @@ -7004,6 +7628,20 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.7.tgz", + "integrity": "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/uuid": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", @@ -14943,6 +15581,15 @@ "node": ">=10" } }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "license": "MIT", + "dependencies": { + "obliterator": "^1.6.1" + } + }, "node_modules/motion": { "version": "12.23.25", "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.25.tgz", @@ -15368,6 +16015,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "license": "MIT" + }, "node_modules/ollama-ai-provider-v2": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/ollama-ai-provider-v2/-/ollama-ai-provider-v2-1.5.4.tgz", diff --git a/package.json b/package.json index 693069d4..921582e8 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@ai-sdk/google": "^3.0.0", "@ai-sdk/openai": "^3.0.0", "@ai-sdk/react": "^3.0.1", + "@aws-sdk/client-dynamodb": "^3.957.0", "@aws-sdk/credential-providers": "^3.943.0", "@formatjs/intl-localematcher": "^0.7.2", "@langfuse/client": "^4.4.9", From 7de192e1fa119c4531dd87ef4aaab45f4ebf3d2e Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:54:03 +0900 Subject: [PATCH 043/349] fix: enable progressive diagram rendering during streaming (#380) - Add extractCompleteMxCells() to extract only complete mxCell elements from partial XML - Remove useEffect cleanup that was killing debounce timeouts on every re-render - Wrap XML in tags for proper DOMParser validation Previously, diagrams only rendered after ALL XML finished streaming because: 1. useEffect cleanup cleared the 150ms debounce timeout on every message change 2. DOMParser rejected partial XML like ' { - const currentXml = xml || "" + let currentXml = xml || "" + const startTime = performance.now() + + // During streaming (showToast=false), extract only complete mxCell elements + // This allows progressive rendering even with partial/incomplete trailing XML + if (!showToast) { + const completeCells = extractCompleteMxCells(currentXml) + if (!completeCells) { + return + } + currentXml = completeCells + } + const convertedXml = convertToLegalXml(currentXml) if (convertedXml !== previousXML.current) { // Parse and validate XML BEFORE calling replaceNodes const parser = new DOMParser() - const testDoc = parser.parseFromString(convertedXml, "text/xml") + // Wrap in root element for parsing multiple mxCell elements + const testDoc = parser.parseFromString( + `${convertedXml}`, + "text/xml", + ) const parseError = testDoc.querySelector("parsererror") if (parseError) { @@ -347,7 +364,22 @@ export function ChatMessageDisplay({ `` const replacedXML = replaceNodes(baseXML, convertedXml) - // Validate and auto-fix the XML + const xmlProcessTime = performance.now() - startTime + + // During streaming (showToast=false), skip heavy validation for lower latency + // The quick DOM parse check above catches malformed XML + // Full validation runs on final output (showToast=true) + if (!showToast) { + previousXML.current = convertedXml + const loadStartTime = performance.now() + onDisplayChart(replacedXML, true) + console.log( + `[Streaming] XML processing: ${xmlProcessTime.toFixed(1)}ms, drawio load: ${(performance.now() - loadStartTime).toFixed(1)}ms`, + ) + return + } + + // Final output: run full validation and auto-fix const validation = validateAndFixXml(replacedXML) if (validation.valid) { previousXML.current = convertedXml @@ -360,18 +392,19 @@ export function ChatMessageDisplay({ ) } // Skip validation in loadDiagram since we already validated above + const loadStartTime = performance.now() onDisplayChart(xmlToLoad, true) + console.log( + `[Final] XML processing: ${xmlProcessTime.toFixed(1)}ms, validation+load: ${(performance.now() - loadStartTime).toFixed(1)}ms`, + ) } else { console.error( "[ChatMessageDisplay] XML validation failed:", validation.error, ) - // Only show toast if this is the final XML (not during streaming) - if (showToast) { - toast.error( - "Diagram validation failed. Please try regenerating.", - ) - } + toast.error( + "Diagram validation failed. Please try regenerating.", + ) } } catch (error) { console.error( @@ -603,17 +636,10 @@ export function ChatMessageDisplay({ } }) - // Cleanup: clear any pending debounce timeout on unmount - return () => { - if (debounceTimeoutRef.current) { - clearTimeout(debounceTimeoutRef.current) - debounceTimeoutRef.current = null - } - if (editDebounceTimeoutRef.current) { - clearTimeout(editDebounceTimeoutRef.current) - editDebounceTimeoutRef.current = null - } - } + // NOTE: Don't cleanup debounce timeouts here! + // The cleanup runs on every re-render (when messages changes), + // which would cancel the timeout before it fires. + // Let the timeouts complete naturally - they're harmless if component unmounts. }, [messages, handleDisplayChart, chartXML]) const renderToolPart = (part: ToolPartLike) => { diff --git a/lib/utils.ts b/lib/utils.ts index 1266d101..6ee7ac2f 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -61,6 +61,47 @@ export function isMxCellXmlComplete(xml: string | undefined | null): boolean { return trimmed.endsWith("/>") || trimmed.endsWith("") } +/** + * Extract only complete mxCell elements from partial/streaming XML. + * This allows progressive rendering during streaming by ignoring incomplete trailing elements. + * @param xml - The partial XML string (may contain incomplete trailing mxCell) + * @returns XML string containing only complete mxCell elements + */ +export function extractCompleteMxCells(xml: string | undefined | null): string { + if (!xml) return "" + + const completeCells: Array<{ index: number; text: string }> = [] + + // Match self-closing mxCell tags: + // Also match mxCell with nested mxGeometry: ... + const selfClosingPattern = /]*\/>/g + const nestedPattern = /]*>[\s\S]*?<\/mxCell>/g + + // Find all self-closing mxCell elements + let match: RegExpExecArray | null + while ((match = selfClosingPattern.exec(xml)) !== null) { + completeCells.push({ index: match.index, text: match[0] }) + } + + // Find all mxCell elements with nested content (like mxGeometry) + while ((match = nestedPattern.exec(xml)) !== null) { + completeCells.push({ index: match.index, text: match[0] }) + } + + // Sort by position to maintain order + completeCells.sort((a, b) => a.index - b.index) + + // Remove duplicates (a self-closing match might overlap with nested match) + const seen = new Set() + const uniqueCells = completeCells.filter((cell) => { + if (seen.has(cell.index)) return false + seen.add(cell.index) + return true + }) + + return uniqueCells.map((c) => c.text).join("\n") +} + // ============================================================================ // XML Parsing Helpers // ============================================================================ From c6b0e5ac622e3cc3b4db668ec22941c628d47d6f Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 20:19:28 +0900 Subject: [PATCH 044/349] fix: use totalUsage with all token types for accurate quota tracking (#381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The onFinish callback's 'usage' only contains the final step's tokens, which underreports usage for multi-step tool calls (like diagram generation). Changed to 'totalUsage' which provides cumulative counts across all steps. Include all 4 token types for accurate counting: 1. inputTokens - non-cached input tokens 2. outputTokens - generated output tokens 3. cachedInputTokens - tokens read from prompt cache 4. inputTokenDetails.cacheWriteTokens - tokens written to cache Tested locally: - Request 1 (cache write): 334 + 62 + 0 + 6671 = 7,067 tokens - Request 2 (cache read): 334 + 184 + 6551 + 120 = 7,189 tokens - DynamoDB total: 14,256 ✓ --- app/api/chat/route.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 8db9eb1c..68d6d482 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -542,19 +542,24 @@ ${userInputText} userId, }), }), - onFinish: ({ text, usage }) => { + onFinish: ({ text, totalUsage }) => { // AI SDK 6 telemetry auto-reports token usage on its spans setTraceOutput(text) // Record token usage for server-side quota tracking (if enabled) + // Use totalUsage (cumulative across all steps) instead of usage (final step only) + // Include all 4 token types: input, output, cache read, cache write if ( isQuotaEnabled() && !hasOwnApiKey && userId !== "anonymous" && - usage + totalUsage ) { const totalTokens = - (usage.inputTokens || 0) + (usage.outputTokens || 0) + (totalUsage.inputTokens || 0) + + (totalUsage.outputTokens || 0) + + (totalUsage.cachedInputTokens || 0) + + (totalUsage.inputTokenDetails?.cacheWriteTokens || 0) recordTokenUsage(userId, totalTokens) } }, From 72d647de7aae0e533d5467714c3075979c363883 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 20:29:48 +0900 Subject: [PATCH 045/349] fix: use Chat Completions API for OpenAI-compatible proxies (#382) Third-party OpenAI-compatible proxies typically don't support the /responses endpoint. Use .chat() for custom baseURLs while keeping Responses API for official OpenAI to preserve reasoning model support. Fixes #377 --- lib/ai-providers.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/ai-providers.ts b/lib/ai-providers.ts index da93069d..eddfe7a2 100644 --- a/lib/ai-providers.ts +++ b/lib/ai-providers.ts @@ -588,13 +588,15 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { case "openai": { const apiKey = overrides?.apiKey || process.env.OPENAI_API_KEY const baseURL = overrides?.baseUrl || process.env.OPENAI_BASE_URL - if (baseURL || overrides?.apiKey) { - const customOpenAI = createOpenAI({ - apiKey, - ...(baseURL && { baseURL }), - }) - // Use Responses API (default) instead of .chat() to support reasoning - // for gpt-5, o1, o3, o4 models. Chat Completions API does not emit reasoning events. + if (baseURL) { + // Custom base URL = third-party proxy, use Chat Completions API + // for compatibility (most proxies don't support /responses endpoint) + const customOpenAI = createOpenAI({ apiKey, baseURL }) + model = customOpenAI.chat(modelId) + } else if (overrides?.apiKey) { + // Custom API key but official OpenAI endpoint, use Responses API + // to support reasoning for gpt-5, o1, o3, o4 models + const customOpenAI = createOpenAI({ apiKey }) model = customOpenAI(modelId) } else { model = openai(modelId) From b2dfd5b890d4ddb527584415d0cdf74c9bd849eb Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:08:21 +0900 Subject: [PATCH 046/349] fix: display correct quota values in limit toast (#383) - Parse JSON error response from server to get actual used/limit values - Previously showed 0/0 due to race condition (config fetch vs error) - AI SDK puts full response body in error.message for non-OK responses - Updated all quota toasts (request, token, TPM) to use server values --- components/chat-panel.tsx | 22 +++++++++++++- lib/use-quota-manager.tsx | 63 ++++++++++++++++++++++----------------- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 744fa3b3..6bcfb77e 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -557,12 +557,32 @@ Continue from EXACTLY where you stopped.`, }, onError: (error) => { // Handle server-side quota limit (429 response) + // AI SDK puts the full response body in error.message for non-OK responses + try { + const data = JSON.parse(error.message) + if (data.type === "request") { + quotaManager.showQuotaLimitToast(data.used, data.limit) + return + } + if (data.type === "token") { + quotaManager.showTokenLimitToast(data.used, data.limit) + return + } + if (data.type === "tpm") { + quotaManager.showTPMLimitToast(data.limit) + return + } + } catch { + // Not JSON, fall through to string matching for backwards compatibility + } + + // Fallback to string matching if (error.message.includes("Daily request limit")) { quotaManager.showQuotaLimitToast() return } if (error.message.includes("Daily token limit")) { - quotaManager.showTokenLimitToast(dailyTokenLimit) + quotaManager.showTokenLimitToast() return } if ( diff --git a/lib/use-quota-manager.tsx b/lib/use-quota-manager.tsx index bda083bf..33a75272 100644 --- a/lib/use-quota-manager.tsx +++ b/lib/use-quota-manager.tsx @@ -18,36 +18,39 @@ export interface QuotaConfig { * This hook only provides UI feedback when limits are exceeded. */ export function useQuotaManager(config: QuotaConfig): { - showQuotaLimitToast: () => void - showTokenLimitToast: (used: number) => void - showTPMLimitToast: () => void + showQuotaLimitToast: (used?: number, limit?: number) => void + showTokenLimitToast: (used?: number, limit?: number) => void + showTPMLimitToast: (limit?: number) => void } { const { dailyRequestLimit, dailyTokenLimit, tpmLimit } = config const dict = useDictionary() // Show quota limit toast (request-based) - const showQuotaLimitToast = useCallback(() => { - toast.custom( - (t) => ( - toast.dismiss(t)} - /> - ), - { duration: 15000 }, - ) - }, [dailyRequestLimit]) + const showQuotaLimitToast = useCallback( + (used?: number, limit?: number) => { + toast.custom( + (t) => ( + toast.dismiss(t)} + /> + ), + { duration: 15000 }, + ) + }, + [dailyRequestLimit], + ) // Show token limit toast const showTokenLimitToast = useCallback( - (used: number) => { + (used?: number, limit?: number) => { toast.custom( (t) => ( toast.dismiss(t)} /> ), @@ -58,15 +61,21 @@ export function useQuotaManager(config: QuotaConfig): { ) // Show TPM limit toast - const showTPMLimitToast = useCallback(() => { - const limitDisplay = - tpmLimit >= 1000 ? `${tpmLimit / 1000}k` : String(tpmLimit) - const message = formatMessage(dict.quota.tpmMessageDetailed, { - limit: limitDisplay, - seconds: 60, - }) - toast.error(message, { duration: 8000 }) - }, [tpmLimit, dict]) + const showTPMLimitToast = useCallback( + (limit?: number) => { + const effectiveLimit = limit ?? tpmLimit + const limitDisplay = + effectiveLimit >= 1000 + ? `${effectiveLimit / 1000}k` + : String(effectiveLimit) + const message = formatMessage(dict.quota.tpmMessageDetailed, { + limit: limitDisplay, + seconds: 60, + }) + toast.error(message, { duration: 8000 }) + }, + [tpmLimit, dict], + ) return { showQuotaLimitToast, From d3be96de79cf4b93f0e1de5c1d36c06e1a2d25f3 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:52:04 +0900 Subject: [PATCH 047/349] refactor: redesign config panels with refined minimal aesthetic (#384) - Add CSS design system tokens (surfaces, borders, animations) to globals.css - Update dialog.tsx with rounded-2xl, shadow-dialog, refined close button - Enhance input.tsx with rounded-xl and refined focus states - Refactor settings-dialog with SettingItem pattern and consistent control sizing - Refactor model-config-dialog with ConfigSection/ConfigCard helpers - Replace emerald-* classes with success design tokens - Remove unused ValidationButton component and scrollState --- app/globals.css | 142 +++++++++++++ components/model-config-dialog.tsx | 331 ++++++++++++----------------- components/settings-dialog.tsx | 293 ++++++++++++++----------- components/ui/dialog.tsx | 38 +++- components/ui/input.tsx | 27 ++- 5 files changed, 503 insertions(+), 328 deletions(-) diff --git a/app/globals.css b/app/globals.css index feaea6da..b1f23dd3 100644 --- a/app/globals.css +++ b/app/globals.css @@ -144,6 +144,68 @@ --sidebar-ring: oklch(0.7 0.16 265); } +/* ============================================ + REFINED MINIMAL DESIGN SYSTEM + ============================================ */ + +:root { + /* Surface layers for depth */ + --surface-0: oklch(1 0 0); + --surface-1: oklch(0.985 0.002 240); + --surface-2: oklch(0.97 0.004 240); + --surface-elevated: oklch(1 0 0); + + /* Subtle borders */ + --border-subtle: oklch(0.94 0.008 260); + --border-default: oklch(0.91 0.012 260); + + /* Interactive states */ + --interactive-hover: oklch(0.96 0.015 260); + --interactive-active: oklch(0.93 0.02 265); + + /* Success state */ + --success: oklch(0.65 0.18 145); + --success-muted: oklch(0.95 0.03 145); + + /* Animation timing */ + --duration-fast: 120ms; + --duration-normal: 200ms; + --duration-slow: 300ms; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.dark { + --surface-0: oklch(0.15 0.015 260); + --surface-1: oklch(0.18 0.015 260); + --surface-2: oklch(0.22 0.015 260); + --surface-elevated: oklch(0.25 0.015 260); + + --border-subtle: oklch(0.25 0.012 260); + --border-default: oklch(0.3 0.015 260); + + --interactive-hover: oklch(0.25 0.02 265); + --interactive-active: oklch(0.3 0.025 270); + + --success: oklch(0.7 0.16 145); + --success-muted: oklch(0.25 0.04 145); +} + +/* Expose surface colors to Tailwind */ +@theme inline { + --color-surface-0: var(--surface-0); + --color-surface-1: var(--surface-1); + --color-surface-2: var(--surface-2); + --color-surface-elevated: var(--surface-elevated); + --color-border-subtle: var(--border-subtle); + --color-border-default: var(--border-default); + --color-interactive-hover: var(--interactive-hover); + --color-interactive-active: var(--interactive-active); + --color-success: var(--success); + --color-success-muted: var(--success-muted); +} + @layer base { * { @apply border-border outline-ring/50; @@ -257,3 +319,83 @@ -webkit-text-fill-color: transparent; background-clip: text; } + +/* ============================================ + REFINED DIALOG STYLES + ============================================ */ + +/* Refined dialog shadow - multi-layer soft shadow */ +.shadow-dialog { + box-shadow: + 0 0 0 1px oklch(0 0 0 / 0.03), + 0 2px 4px oklch(0 0 0 / 0.02), + 0 12px 24px oklch(0 0 0 / 0.06), + 0 24px 48px oklch(0 0 0 / 0.04); +} + +.dark .shadow-dialog { + box-shadow: + 0 0 0 1px oklch(1 0 0 / 0.05), + 0 2px 4px oklch(0 0 0 / 0.2), + 0 12px 24px oklch(0 0 0 / 0.3), + 0 24px 48px oklch(0 0 0 / 0.2); +} + +/* Dialog animations */ +@keyframes dialog-in { + from { + opacity: 0; + transform: translate(-50%, -48%) scale(0.96); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +@keyframes dialog-out { + from { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + to { + opacity: 0; + transform: translate(-50%, -48%) scale(0.96); + } +} + +.animate-dialog-in { + animation: dialog-in var(--duration-normal) var(--ease-out) forwards; +} + +.animate-dialog-out { + animation: dialog-out 150ms var(--ease-out) forwards; +} + +/* Check pop animation for validation success */ +@keyframes check-pop { + 0% { + transform: scale(0.8); + opacity: 0; + } + 50% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +.animate-check-pop { + animation: check-pop 0.25s var(--ease-spring) forwards; +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + .animate-dialog-in, + .animate-dialog-out, + .animate-check-pop { + animation: none; + } +} diff --git a/components/model-config-dialog.tsx b/components/model-config-dialog.tsx index 89f17a28..ccf3902b 100644 --- a/components/model-config-dialog.tsx +++ b/components/model-config-dialog.tsx @@ -103,41 +103,40 @@ function ProviderLogo({ ) } -// Reusable validation button component -function ValidationButton({ - status, - onClick, - disabled, - dict, +// Configuration section with title and optional action +function ConfigSection({ + title, + icon: Icon, + action, + children, }: { - status: ValidationStatus - onClick: () => void - disabled: boolean - dict: ReturnType + title: string + icon: React.ComponentType<{ className?: string }> + action?: React.ReactNode + children: React.ReactNode }) { return ( - +
+
+
+ + + {title} + +
+ {action} +
+ {children} +
+ ) +} + +// Card wrapper with subtle depth +function ConfigCard({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
) } @@ -154,7 +153,6 @@ export function ModelConfigDialog({ const [validationStatus, setValidationStatus] = useState("idle") const [validationError, setValidationError] = useState("") - const [scrollState, setScrollState] = useState({ top: false, bottom: true }) const [customModelInput, setCustomModelInput] = useState("") const scrollRef = useRef(null) const validationResetTimeoutRef = useRef p.id === selectedProviderId, ) - // Track scroll position for gradient shadows - useEffect(() => { - const scrollEl = scrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]", - ) as HTMLElement | null - if (!scrollEl) return - - const handleScroll = () => { - const { scrollTop, scrollHeight, clientHeight } = scrollEl - setScrollState({ - top: scrollTop > 10, - bottom: scrollTop < scrollHeight - clientHeight - 10, - }) - } - - handleScroll() // Initial check - scrollEl.addEventListener("scroll", handleScroll) - return () => scrollEl.removeEventListener("scroll", handleScroll) - }, [selectedProvider]) - // Cleanup validation reset timeout on unmount useEffect(() => { return () => { @@ -390,34 +368,35 @@ export function ModelConfigDialog({ return ( - - - -
+ + {/* Header */} + + +
{dict.modelConfig?.title || "AI Model Configuration"}
- + {dict.modelConfig?.description || "Configure multiple AI providers and models for your workspace"}
-
+
{/* Provider List (Left Sidebar) */} -
-
+
+
{dict.modelConfig.providers}
- -
+ +
{config.providers.length === 0 ? (
-
+

@@ -425,67 +404,78 @@ export function ModelConfigDialog({

) : ( -
- {config.providers.map((provider) => ( - - ))} -
+ + {provider.validated ? ( +
+ +
+ ) : ( + + )} + + )) )}
{/* Add Provider */} -
+
- {/* Show duplicate error for custom model input */} {duplicateError && (

{duplicateError} @@ -1132,7 +1090,7 @@ export function ModelConfigDialog({

-
- + } + > {/* Model List */} -
+
{selectedProvider.models .length === 0 ? ( -
-
+
+

@@ -1220,7 +1178,7 @@ export function ModelConfigDialog({

) : ( -
+
{selectedProvider.models.map( (model, index) => (
@@ -1264,8 +1213,8 @@ export function ModelConfigDialog({ ) : model.validated === true ? ( // Valid -
- +
+
) : model.validated === false ? ( @@ -1466,7 +1415,7 @@ export function ModelConfigDialog({
)}
-
+ {/* Danger Zone */}
@@ -1476,7 +1425,7 @@ export function ModelConfigDialog({ onClick={() => setDeleteConfirmOpen(true) } - className="text-muted-foreground hover:text-destructive hover:bg-destructive/10" + className="text-muted-foreground hover:text-destructive hover:bg-destructive/5 rounded-xl" > { @@ -1490,10 +1439,10 @@ export function ModelConfigDialog({ ) : (
-
- +
+
-

+

{dict.modelConfig.configureProviders}

@@ -1505,7 +1454,7 @@ export function ModelConfigDialog({

{/* Footer */} -
+

{dict.modelConfig.apiKeyStored} diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx index 1d9f7100..5ef91354 100644 --- a/components/settings-dialog.tsx +++ b/components/settings-dialog.tsx @@ -24,6 +24,32 @@ import { Switch } from "@/components/ui/switch" import { useDictionary } from "@/hooks/use-dictionary" import { getApiEndpoint } from "@/lib/base-path" import { i18n, type Locale } from "@/lib/i18n/config" +import { cn } from "@/lib/utils" + +// Reusable setting item component for consistent layout +function SettingItem({ + label, + description, + children, +}: { + label: string + description?: string + children: React.ReactNode +}) { + return ( +

+
+ + {description && ( +

+ {description} +

+ )} +
+
{children}
+
+ ) +} const LANGUAGE_LABELS: Record = { en: "English", @@ -177,145 +203,154 @@ function SettingsContent({ } return ( - - + + {/* Header */} + {dict.settings.title} - + {dict.settings.description} -
- {accessCodeRequired && ( -
- -
- setAccessCode(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={ - dict.settings.accessCodePlaceholder - } - autoComplete="off" - /> - + + {/* Content */} +
+
+ {/* Access Code (conditional) */} + {accessCodeRequired && ( +
+
+ +

+ {dict.settings.accessCodeDescription} +

+
+
+ + setAccessCode(e.target.value) + } + onKeyDown={handleKeyDown} + placeholder={ + dict.settings.accessCodePlaceholder + } + autoComplete="off" + className="h-9" + /> + +
+ {error && ( +

+ {error} +

+ )}
-

- {dict.settings.accessCodeDescription} -

- {error && ( -

- {error} -

- )} -
- )} + )} -
-
- -

- {dict.settings.languageDescription} -

-
- -
- -
-
- -

- {dict.settings.themeDescription} -

-
- -
+ + -
-
- -

- {dict.settings.drawioStyleDescription}{" "} - {drawioUi === "min" + {/* Theme */} + + + + + {/* Draw.io Style */} + -

- -
+ + -
-
- -

- {dict.settings.closeProtectionDescription} -

-
- { - setCloseProtection(checked) - localStorage.setItem( - STORAGE_CLOSE_PROTECTION_KEY, - checked.toString(), - ) - onCloseProtectionChange?.(checked) - }} - /> + {/* Close Protection */} + + { + setCloseProtection(checked) + localStorage.setItem( + STORAGE_CLOSE_PROTECTION_KEY, + checked.toString(), + ) + onCloseProtectionChange?.(checked) + }} + /> +
-
-

+ + {/* Footer */} +

+

Version {process.env.APP_VERSION}

@@ -328,9 +363,9 @@ export function SettingsDialog(props: SettingsDialogProps) { -
-
+ +
+
} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 7d7a9d31..77b681c0 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -38,7 +38,10 @@ function DialogOverlay({ {children} - + Close @@ -102,7 +124,10 @@ function DialogTitle({ return ( ) @@ -115,7 +140,10 @@ function DialogDescription({ return ( ) diff --git a/components/ui/input.tsx b/components/ui/input.tsx index 03295ca6..9605f29f 100644 --- a/components/ui/input.tsx +++ b/components/ui/input.tsx @@ -8,9 +8,30 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", - "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + // Base styles + "flex h-10 w-full min-w-0 rounded-xl px-3.5 py-2", + "border border-border-subtle bg-surface-1", + "text-sm text-foreground", + // Placeholder + "placeholder:text-muted-foreground/60", + // Selection + "selection:bg-primary selection:text-primary-foreground", + // Transitions + "transition-all duration-150 ease-out", + // Hover state + "hover:border-border-default", + // Focus state - refined ring + "focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/10", + // File input + "file:text-foreground file:inline-flex file:h-7 file:border-0", + "file:bg-transparent file:text-sm file:font-medium", + // Disabled + "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", + // Invalid state + "aria-invalid:border-destructive aria-invalid:ring-destructive/20", + "dark:aria-invalid:ring-destructive/40", + // Dark mode background + "dark:bg-surface-1", className )} {...props} From 3218ccc909a64df4fc07654c14bef468c64589df Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:29:29 +0900 Subject: [PATCH 048/349] feat: add dev XML streaming simulator for UI debugging (#385) --- components/chat-panel.tsx | 187 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 6bcfb77e..32931c33 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -164,6 +164,28 @@ export default function ChatPanel({ const [showNewChatDialog, setShowNewChatDialog] = useState(false) const [minimalStyle, setMinimalStyle] = useState(false) + // Dev simulation state (only used in development) + const [devXml, setDevXml] = useState("") + const [isSimulating, setIsSimulating] = useState(false) + const [devIntervalMs, setDevIntervalMs] = useState(20) + const [devChunkSize, setDevChunkSize] = useState(5) + const devStopRef = useRef(false) + const devXmlInitializedRef = useRef(false) + + // Restore dev XML from localStorage on mount (after hydration) + useEffect(() => { + const saved = localStorage.getItem("dev-xml-simulator") + if (saved) setDevXml(saved) + devXmlInitializedRef.current = true + }, []) + + // Save dev XML to localStorage (only after initial load) + useEffect(() => { + if (devXmlInitializedRef.current) { + localStorage.setItem("dev-xml-simulator", devXml) + } + }, [devXml]) + // Restore input from sessionStorage on mount (when ChatPanel remounts due to key change) useEffect(() => { const savedInput = sessionStorage.getItem(SESSION_STORAGE_INPUT_KEY) @@ -1190,6 +1212,85 @@ Continue from EXACTLY where you stopped.`, sendChatMessage(newParts, savedXml, previousXml, sessionId) } + // Dev: Simulate display_diagram streaming + const handleDevSimulate = async () => { + if (!devXml.trim() || isSimulating) return + + setIsSimulating(true) + devStopRef.current = false + const toolCallId = `dev-sim-${Date.now()}` + const xml = devXml.trim() + + // Add user message and initial assistant message with empty XML + const userMsg = { + id: `user-${Date.now()}`, + role: "user" as const, + parts: [ + { + type: "text" as const, + text: "[Dev] Simulating XML streaming", + }, + ], + } + const assistantMsg = { + id: `assistant-${Date.now()}`, + role: "assistant" as const, + parts: [ + { + type: "tool-display_diagram" as const, + toolCallId, + state: "input-streaming" as const, + input: { xml: "" }, + }, + ], + } + setMessages((prev) => [...prev, userMsg, assistantMsg] as any) + + // Stream characters progressively + for (let i = 0; i < xml.length; i += devChunkSize) { + if (devStopRef.current) { + setIsSimulating(false) + return + } + + const chunk = xml.slice(0, i + devChunkSize) + + setMessages((prev) => { + const updated = [...prev] + const lastMsg = updated[updated.length - 1] as any + if (lastMsg?.role === "assistant" && lastMsg.parts?.[0]) { + lastMsg.parts[0].input = { xml: chunk } + } + return updated + }) + + await new Promise((r) => setTimeout(r, devIntervalMs)) + } + + if (devStopRef.current) { + setIsSimulating(false) + return + } + + // Finalize: set state to output-available + setMessages((prev) => { + const updated = [...prev] + const lastMsg = updated[updated.length - 1] as any + if (lastMsg?.role === "assistant" && lastMsg.parts?.[0]) { + lastMsg.parts[0].state = "output-available" + lastMsg.parts[0].output = "Successfully displayed the diagram." + lastMsg.parts[0].input = { xml } + } + return updated + }) + + // Display the final diagram + const fullXml = wrapWithMxFile(xml) + onDisplayChart(fullXml) + + setIsSimulating(false) + } + // Collapsed view (desktop only) if (!isVisible && !isMobile) { return ( @@ -1330,6 +1431,7 @@ Continue from EXACTLY where you stopped.`, setFiles={handleFileChange} processedToolCallsRef={processedToolCallsRef} editDiagramOriginalXmlRef={editDiagramOriginalXmlRef} + partialXmlRef={partialXmlRef} sessionId={sessionId} onRegenerate={handleRegenerate} status={status} @@ -1337,6 +1439,91 @@ Continue from EXACTLY where you stopped.`, /> + {/* Dev XML Streaming Simulator - only in development */} + {DEBUG && ( +
+
+ + Dev: XML Streaming Simulator + +
+