From 9b2db68751bc9adba0fff031835890c59a447866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CBeeRad=E2=80=9D?= Date: Thu, 29 Jan 2026 08:52:15 +1100 Subject: [PATCH] =?UTF-8?q?chore:=20codebase=20audit=20=E2=80=94=20remove?= =?UTF-8?q?=20dead=20code,=20unused=20deps,=20clean=20configs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: Delete 19 dead component/service/tool files (4,200+ lines) Phase 2: Remove 13 unused npm packages (Radix UI, shadcn utilities, ytdl-core, etc.) Phase 3: Database schema — drop agent_delegations, add performance indexes Phase 4: Remove 12 dead types from database.ts/analytics.ts/helpers.ts Phase 5: Strip shadcn/ui theme from tailwind config Phase 6: Clean env vars, fix broken imports (LocalKeyGate, MapViewer) Co-Authored-By: Claude Opus 4.5 --- .env.example | 1 - app/api/analytics/usage/route.ts | 63 - app/api/cache-stats/route.ts | 67 -- app/api/tool-results/[id]/route.ts | 28 - app/page.tsx | 7 +- apps/mcp-server/server.js | 137 --- apps/mcp-server/stdio-server.js | 137 --- package-lock.json | 1049 +---------------- package.json | 13 - scripts/database/sqlite-ensure-app-schema.sh | 22 +- scripts/test-ts-migration.js | 225 ---- src/components/auth/LocalKeyGate.tsx | 99 -- src/components/common/EditableSection.tsx | 129 -- src/components/common/PanelHeader.tsx | 67 -- .../focus/edges/EdgeSearchModal.tsx | 400 ------- src/components/layout/CollapsedRail.tsx | 111 -- src/components/nodes/AddNodeButton.tsx | 43 - src/components/nodes/NodesPanel.tsx | 892 -------------- src/components/settings/MapViewer.tsx | 425 ------- src/components/settings/SettingsCog.tsx | 44 - src/components/settings/SettingsModal.tsx | 18 - src/components/views/ViewPanel.tsx | 286 ----- src/services/analytics/costs.ts | 313 ----- src/services/tools/resultCache.ts | 32 - src/styles/terminal-theme.css | 398 ------- src/styles/terminalTheme.ts | 250 ---- src/tools/database/lockDimension.ts | 62 - src/tools/database/unlockDimension.ts | 62 - src/tools/other/embedContent.ts | 41 - src/types/analytics.ts | 95 -- src/types/database.ts | 84 -- src/types/helpers.ts | 22 - tailwind.config.ts | 74 +- 33 files changed, 13 insertions(+), 5683 deletions(-) delete mode 100644 app/api/analytics/usage/route.ts delete mode 100644 app/api/cache-stats/route.ts delete mode 100644 app/api/tool-results/[id]/route.ts delete mode 100755 scripts/test-ts-migration.js delete mode 100644 src/components/auth/LocalKeyGate.tsx delete mode 100644 src/components/common/EditableSection.tsx delete mode 100644 src/components/common/PanelHeader.tsx delete mode 100644 src/components/focus/edges/EdgeSearchModal.tsx delete mode 100644 src/components/layout/CollapsedRail.tsx delete mode 100644 src/components/nodes/AddNodeButton.tsx delete mode 100644 src/components/nodes/NodesPanel.tsx delete mode 100644 src/components/settings/MapViewer.tsx delete mode 100644 src/components/settings/SettingsCog.tsx delete mode 100644 src/components/views/ViewPanel.tsx delete mode 100644 src/services/analytics/costs.ts delete mode 100644 src/services/tools/resultCache.ts delete mode 100644 src/styles/terminal-theme.css delete mode 100644 src/styles/terminalTheme.ts delete mode 100644 src/tools/database/lockDimension.ts delete mode 100644 src/tools/database/unlockDimension.ts delete mode 100644 src/tools/other/embedContent.ts delete mode 100644 src/types/helpers.ts diff --git a/.env.example b/.env.example index 9cf18b6..4374db3 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,6 @@ OPENAI_API_KEY=your-openai-api-key-here # Optional overrides specifically for ra-h orchestration/delegation RAH_ORCHESTRATOR_ANTHROPIC_API_KEY= RAH_DELEGATE_OPENAI_API_KEY= -RAH_WISE_RAH_OPENAI_API_KEY= # Optional: GPT-5 for wise ra-h (falls back to OPENAI_API_KEY) # Voice TTS defaults RAH_TTS_MODEL=gpt-4o-mini-tts diff --git a/app/api/analytics/usage/route.ts b/app/api/analytics/usage/route.ts deleted file mode 100644 index 98fce2a..0000000 --- a/app/api/analytics/usage/route.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { CostAnalytics } from '@/services/analytics/costs'; - -export async function GET(request: NextRequest) { - try { - const searchParams = request.nextUrl.searchParams; - const period = searchParams.get('period') || 'week'; - const agent = searchParams.get('agent'); - const traceId = searchParams.get('trace_id'); - const startDate = searchParams.get('start_date'); - const endDate = searchParams.get('end_date'); - - if (traceId) { - const traceCosts = CostAnalytics.getCostsByTrace(traceId); - return NextResponse.json(traceCosts); - } - - if (agent) { - const agentCosts = CostAnalytics.getCostsByAgent( - agent, - startDate || undefined, - endDate || undefined - ); - return NextResponse.json(agentCosts); - } - - const days = period === 'day' ? 1 : period === 'week' ? 7 : period === 'month' ? 30 : 7; - - if (startDate && endDate) { - const dateRangeCosts = CostAnalytics.getCostsByDateRange(startDate, endDate); - return NextResponse.json(dateRangeCosts); - } - - const now = new Date(); - const start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000); - const periodCosts = CostAnalytics.getCostsByDateRange( - start.toISOString(), - now.toISOString() - ); - - const cacheStats = CostAnalytics.getCacheEffectiveness( - start.toISOString(), - now.toISOString() - ); - - const dailyBreakdown = CostAnalytics.getDailyBreakdown(days); - - return NextResponse.json({ - period: period, - summary: periodCosts, - cache: cacheStats, - daily: dailyBreakdown, - }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error('❌ [Analytics] Error:', errorMessage); - - return NextResponse.json( - { error: errorMessage }, - { status: 500 } - ); - } -} diff --git a/app/api/cache-stats/route.ts b/app/api/cache-stats/route.ts deleted file mode 100644 index 5677fd2..0000000 --- a/app/api/cache-stats/route.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { NextResponse } from 'next/server'; -import type { CacheStats } from '@/types/prompts'; - -declare global { - // eslint-disable-next-line no-var - var lastCacheStats: CacheStats | undefined; -} - -export async function GET() { - try { - const stats = global.lastCacheStats; - - if (!stats) { - return NextResponse.json({ - error: 'No cache statistics available yet', - message: 'Send a message to ra-h to generate cache stats' - }, { status: 404 }); - } - - const hitRate = stats.cacheReadInputTokens > 0 ? 'HIT' : 'MISS'; - const totalInputTokens = stats.inputTokens + stats.cacheCreationInputTokens + stats.cacheReadInputTokens; - - // Cost calculation (Sonnet 4.5 pricing) - const baseCost = 3.0; // $3 per million input tokens - const writeCost = baseCost * 1.25; // $3.75 per million - const readCost = baseCost * 0.1; // $0.30 per million - - const actualCost = ( - (stats.inputTokens * baseCost) + - (stats.cacheCreationInputTokens * writeCost) + - (stats.cacheReadInputTokens * readCost) - ) / 1_000_000; - - const noCacheCost = (totalInputTokens * baseCost) / 1_000_000; - const costSavingsPercentage = noCacheCost > 0 - ? Math.round(((noCacheCost - actualCost) / noCacheCost) * 100) - : 0; - - return NextResponse.json({ - status: 'success', - timestamp: new Date().toISOString(), - lastRequest: { - hitRate, - tokens: { - cacheWrite: stats.cacheCreationInputTokens, - cacheRead: stats.cacheReadInputTokens, - regular: stats.inputTokens, - totalInput: totalInputTokens, - output: stats.outputTokens - }, - savings: { - tokenPercentage: stats.savingsPercentage, - costPercentage: costSavingsPercentage, - actualCostUSD: actualCost.toFixed(6), - noCacheCostUSD: noCacheCost.toFixed(6), - savedUSD: (noCacheCost - actualCost).toFixed(6) - } - } - }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return NextResponse.json( - { error: 'Failed to fetch cache stats', details: errorMessage }, - { status: 500 } - ); - } -} diff --git a/app/api/tool-results/[id]/route.ts b/app/api/tool-results/[id]/route.ts deleted file mode 100644 index 09ecb64..0000000 --- a/app/api/tool-results/[id]/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextRequest } from 'next/server'; -import { resultCache } from '@/services/tools/resultCache'; - -export async function GET( - request: NextRequest, - ctx: { params: Promise<{ id: string }> } -) { - try { - const { id } = await ctx.params; // Next.js 15: params is a Promise - const data = resultCache.get(id); - if (!data) { - return new Response(JSON.stringify({ success: false, error: 'Not found' }), { - status: 404, - headers: { 'Content-Type': 'application/json' } - }); - } - return new Response(JSON.stringify({ success: true, data }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - return new Response(JSON.stringify({ success: false, error: e?.message || 'Failed' }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); - } -} diff --git a/app/page.tsx b/app/page.tsx index 4b8f611..ab919a7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,10 +1,5 @@ import ThreePanelLayout from '@/components/layout/ThreePanelLayout'; -import { LocalKeyGate } from '@/components/auth/LocalKeyGate'; export default function Home() { - return ( - - - - ); + return ; } diff --git a/apps/mcp-server/server.js b/apps/mcp-server/server.js index 6de2348..eb6af05 100644 --- a/apps/mcp-server/server.js +++ b/apps/mcp-server/server.js @@ -253,47 +253,6 @@ const searchEmbeddingsOutputSchema = { ) }; -// rah_list_workflows schemas -const listWorkflowsOutputSchema = { - count: z.number(), - workflows: z.array( - z.object({ - key: z.string(), - displayName: z.string(), - description: z.string().nullable(), - enabled: z.boolean() - }) - ) -}; - -// rah_get_workflow schemas -const getWorkflowInputSchema = { - workflowKey: z.string().describe('Workflow key to fetch (e.g., "integrate", "prep")') -}; - -const getWorkflowOutputSchema = { - success: z.boolean(), - workflow: z.object({ - key: z.string(), - displayName: z.string(), - description: z.string().nullable(), - instructions: z.string(), - enabled: z.boolean() - }).nullable(), - message: z.string() -}; - -// rah_execute_workflow schemas -const executeWorkflowInputSchema = { - workflowKey: z.string().describe('Workflow key to execute (e.g., "integrate")'), - nodeId: z.number().int().positive().optional().describe('Target node ID for the workflow') -}; - -const executeWorkflowOutputSchema = { - success: z.boolean(), - message: z.string() -}; - // rah_extract_url schemas const extractUrlInputSchema = { url: z.string().url().describe('URL of the webpage to extract content from') @@ -763,102 +722,6 @@ mcpServer.registerTool( } ); -mcpServer.registerTool( - 'rah_list_workflows', - { - title: 'List RA-H workflows', - description: 'List all available workflows.', - inputSchema: {}, - outputSchema: listWorkflowsOutputSchema - }, - async () => { - const result = await callRaHApi('/api/workflows', { method: 'GET' }); - const workflows = Array.isArray(result.data) ? result.data : []; - - return { - content: [{ type: 'text', text: `Found ${workflows.length} workflow(s).` }], - structuredContent: { - count: workflows.length, - workflows: workflows.map(w => ({ - key: w.key, - displayName: w.displayName, - description: w.description || null, - enabled: w.enabled - })) - } - }; - } -); - -mcpServer.registerTool( - 'rah_get_workflow', - { - title: 'Get RA-H workflow', - description: 'Fetch a workflow definition by key (e.g., "integrate", "prep"). Returns instructions that can be followed.', - inputSchema: getWorkflowInputSchema, - outputSchema: getWorkflowOutputSchema - }, - async ({ workflowKey }) => { - try { - const result = await callRaHApi(`/api/workflows/${encodeURIComponent(workflowKey)}`, { - method: 'GET' - }); - - const workflow = result.data; - return { - content: [{ type: 'text', text: `Workflow "${workflowKey}": ${workflow?.displayName || 'Unknown'}` }], - structuredContent: { - success: true, - workflow: workflow ? { - key: workflow.key, - displayName: workflow.displayName, - description: workflow.description || null, - instructions: workflow.instructions, - enabled: workflow.enabled - } : null, - message: `Fetched workflow "${workflowKey}"` - } - }; - } catch (e) { - return { - content: [{ type: 'text', text: `Workflow "${workflowKey}" not found.` }], - structuredContent: { - success: false, - workflow: null, - message: `Workflow "${workflowKey}" not found` - } - }; - } - } -); - -mcpServer.registerTool( - 'rah_execute_workflow', - { - title: 'Execute RA-H workflow', - description: 'Execute a predefined workflow (e.g., "integrate" for connection discovery).', - inputSchema: executeWorkflowInputSchema, - outputSchema: executeWorkflowOutputSchema - }, - async ({ workflowKey, nodeId }) => { - const payload = { workflowKey }; - if (nodeId) payload.nodeId = nodeId; - - const result = await callRaHApi('/api/workflows/execute', { - method: 'POST', - body: JSON.stringify(payload) - }); - - return { - content: [{ type: 'text', text: `Workflow "${workflowKey}" initiated.` }], - structuredContent: { - success: true, - message: result.message || `Workflow "${workflowKey}" initiated.` - } - }; - } -); - mcpServer.registerTool( 'rah_extract_url', { diff --git a/apps/mcp-server/stdio-server.js b/apps/mcp-server/stdio-server.js index f0d4ccc..a6128cf 100644 --- a/apps/mcp-server/stdio-server.js +++ b/apps/mcp-server/stdio-server.js @@ -202,47 +202,6 @@ const searchEmbeddingsOutputSchema = { ) }; -// rah_list_workflows schemas -const listWorkflowsOutputSchema = { - count: z.number(), - workflows: z.array( - z.object({ - key: z.string(), - displayName: z.string(), - description: z.string().nullable(), - enabled: z.boolean() - }) - ) -}; - -// rah_get_workflow schemas -const getWorkflowInputSchema = { - workflowKey: z.string().describe('Workflow key to fetch (e.g., "integrate", "prep")') -}; - -const getWorkflowOutputSchema = { - success: z.boolean(), - workflow: z.object({ - key: z.string(), - displayName: z.string(), - description: z.string().nullable(), - instructions: z.string(), - enabled: z.boolean() - }).nullable(), - message: z.string() -}; - -// rah_execute_workflow schemas -const executeWorkflowInputSchema = { - workflowKey: z.string().describe('Workflow key to execute (e.g., "integrate")'), - nodeId: z.number().int().positive().optional().describe('Target node ID for the workflow') -}; - -const executeWorkflowOutputSchema = { - success: z.boolean(), - message: z.string() -}; - // rah_extract_url schemas const extractUrlInputSchema = { url: z.string().url().describe('URL of the webpage to extract content from') @@ -735,102 +694,6 @@ server.registerTool( } ); -server.registerTool( - 'rah_list_workflows', - { - title: 'List RA-H workflows', - description: 'List all available workflows.', - inputSchema: {}, - outputSchema: listWorkflowsOutputSchema - }, - async () => { - const result = await callRaHApi('/api/workflows', { method: 'GET' }); - const workflows = Array.isArray(result.data) ? result.data : []; - - return { - content: [{ type: 'text', text: `Found ${workflows.length} workflow(s).` }], - structuredContent: { - count: workflows.length, - workflows: workflows.map(w => ({ - key: w.key, - displayName: w.displayName, - description: w.description || null, - enabled: w.enabled - })) - } - }; - } -); - -server.registerTool( - 'rah_get_workflow', - { - title: 'Get RA-H workflow', - description: 'Fetch a workflow definition by key (e.g., "integrate", "prep"). Returns instructions that can be followed.', - inputSchema: getWorkflowInputSchema, - outputSchema: getWorkflowOutputSchema - }, - async ({ workflowKey }) => { - try { - const result = await callRaHApi(`/api/workflows/${encodeURIComponent(workflowKey)}`, { - method: 'GET' - }); - - const workflow = result.data; - return { - content: [{ type: 'text', text: `Workflow "${workflowKey}": ${workflow?.displayName || 'Unknown'}` }], - structuredContent: { - success: true, - workflow: workflow ? { - key: workflow.key, - displayName: workflow.displayName, - description: workflow.description || null, - instructions: workflow.instructions, - enabled: workflow.enabled - } : null, - message: `Fetched workflow "${workflowKey}"` - } - }; - } catch (e) { - return { - content: [{ type: 'text', text: `Workflow "${workflowKey}" not found.` }], - structuredContent: { - success: false, - workflow: null, - message: `Workflow "${workflowKey}" not found` - } - }; - } - } -); - -server.registerTool( - 'rah_execute_workflow', - { - title: 'Execute RA-H workflow', - description: 'Execute a predefined workflow (e.g., "integrate" for connection discovery).', - inputSchema: executeWorkflowInputSchema, - outputSchema: executeWorkflowOutputSchema - }, - async ({ workflowKey, nodeId }) => { - const payload = { workflowKey }; - if (nodeId) payload.nodeId = nodeId; - - const result = await callRaHApi('/api/workflows/execute', { - method: 'POST', - body: JSON.stringify(payload) - }); - - return { - content: [{ type: 'text', text: `Workflow "${workflowKey}" initiated.` }], - structuredContent: { - success: true, - message: result.message || `Workflow "${workflowKey}" initiated.` - } - }; - } -); - server.registerTool( 'rah_extract_url', { diff --git a/package-lock.json b/package-lock.json index 79fe106..988822a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,19 +14,12 @@ "@ai-sdk/react": "^3.0.29", "@langchain/core": "^1.0.1", "@langchain/textsplitters": "^1.0.1", - "@radix-ui/react-checkbox": "^1.1.1", - "@radix-ui/react-dropdown-menu": "^2.1.1", - "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-slot": "^1.1.0", "@xyflow/react": "^12.10.0", "ai": "^6.0.27", "better-sqlite3": "^12.2.0", "cheerio": "^1.1.2", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", "lucide-react": "^0.468.0", "next": "15.1.3", - "next-themes": "^0.4.3", "openai": "^4.103.0", "pdf-parse": "^1.1.1", "pdfjs-dist": "^5.4.296", @@ -34,12 +27,8 @@ "react-dom": "^19.0.1", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", - "tailwind-merge": "^2.5.2", - "uuid": "^11.1.0", - "youtube-captions-scraper": "^2.0.3", "youtube-transcript": "^1.2.1", "youtube-transcript-plus": "^1.1.1", - "ytdl-core": "^4.11.5", "zod": "^3.25.76" }, "devDependencies": { @@ -47,7 +36,6 @@ "@types/node": "22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "19.0.2", - "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.20", "esbuild": "^0.23.1", "eslint": "^8.57.0", @@ -55,7 +43,6 @@ "postcss": "^8.4.49", "prettier": "^3.3.3", "tailwindcss": "^3.4.17", - "tailwindcss-animate": "^1.0.7", "tsx": "^4.19.4", "typescript": "^5.7.2", "vitest": "^3.2.4" @@ -714,44 +701,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -1703,684 +1652,6 @@ "node": ">=8.0.0" } }, - "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-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-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", - "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-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "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-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@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-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-collection/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-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-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-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "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-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-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "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-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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-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-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-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-label": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", - "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.4" - }, - "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-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", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.4" - }, - "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-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@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-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "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-menu/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-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-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-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-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-primitive/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-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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-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==", - "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-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-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@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-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "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-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-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-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "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-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-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/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/@rollup/rollup-android-arm-eabi": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", @@ -2941,7 +2212,7 @@ "version": "19.0.2", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz", "integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -3771,18 +3042,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -4049,17 +3308,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "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", @@ -4568,18 +3816,6 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, "node_modules/classcat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", @@ -4592,15 +3828,6 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -5063,12 +4290,6 @@ "node": ">=8" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -6118,26 +5339,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "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", @@ -6305,15 +5506,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -6581,15 +5773,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -7496,12 +6679,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7548,19 +6725,6 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, - "node_modules/m3u8stream": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.6.tgz", - "integrity": "sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==", - "license": "MIT", - "dependencies": { - "miniget": "^4.2.2", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -8492,15 +7656,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/miniget": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.3.tgz", - "integrity": "sha512-SjbDPDICJ1zT+ZvQwK0hUcRY4wxlhhNpHL9nJOB2MEAXRGagTljsO8MEDzQMTFf0Q8g4QNi8P9lEm/g7e+qgzA==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -8658,16 +7813,6 @@ } } }, - "node_modules/next-themes": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", - "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" - } - }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -9530,12 +8675,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -9656,75 +8795,6 @@ "react": ">=18" } }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -10091,15 +9161,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -10628,12 +9689,6 @@ "dev": true, "license": "MIT" }, - "node_modules/striptags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.2.0.tgz", - "integrity": "sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==", - "license": "MIT" - }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -10736,16 +9791,6 @@ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/tailwind-merge": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", - "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -10784,16 +9829,6 @@ "node": ">=14.0.0" } }, - "node_modules/tailwindcss-animate": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, "node_modules/tailwindcss/node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -11861,49 +10896,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -11919,19 +10911,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -13393,18 +12372,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/youtube-captions-scraper": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/youtube-captions-scraper/-/youtube-captions-scraper-2.0.3.tgz", - "integrity": "sha512-XT6U5wFq35P5G99M+2uwNzJFkmGUrb38u8lMMQkv9PpfTOtG8Lpotka7jEqbDnQBshDw6/hgLQV597/tldiyUg==", - "license": "MIT", - "dependencies": { - "axios": "^1.6.7", - "he": "^1.1.1", - "lodash": "^4.17.4", - "striptags": "^3.1.0" - } - }, "node_modules/youtube-transcript": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/youtube-transcript/-/youtube-transcript-1.2.1.tgz", @@ -13423,20 +12390,6 @@ "node": ">=18.0.0" } }, - "node_modules/ytdl-core": { - "version": "4.11.5", - "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.11.5.tgz", - "integrity": "sha512-27LwsW4n4nyNviRCO1hmr8Wr5J1wLLMawHCQvH8Fk0hiRqrxuIu028WzbJetiYH28K8XDbeinYW4/wcHQD1EXA==", - "license": "MIT", - "dependencies": { - "m3u8stream": "^0.8.6", - "miniget": "^4.2.2", - "sax": "^1.1.3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index bb0a85a..26849ce 100644 --- a/package.json +++ b/package.json @@ -26,19 +26,12 @@ "@ai-sdk/react": "^3.0.29", "@langchain/core": "^1.0.1", "@langchain/textsplitters": "^1.0.1", - "@radix-ui/react-checkbox": "^1.1.1", - "@radix-ui/react-dropdown-menu": "^2.1.1", - "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-slot": "^1.1.0", "@xyflow/react": "^12.10.0", "ai": "^6.0.27", "better-sqlite3": "^12.2.0", "cheerio": "^1.1.2", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", "lucide-react": "^0.468.0", "next": "15.1.3", - "next-themes": "^0.4.3", "openai": "^4.103.0", "pdf-parse": "^1.1.1", "pdfjs-dist": "^5.4.296", @@ -46,12 +39,8 @@ "react-dom": "^19.0.1", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", - "tailwind-merge": "^2.5.2", - "uuid": "^11.1.0", - "youtube-captions-scraper": "^2.0.3", "youtube-transcript": "^1.2.1", "youtube-transcript-plus": "^1.1.1", - "ytdl-core": "^4.11.5", "zod": "^3.25.76" }, "devDependencies": { @@ -59,7 +48,6 @@ "@types/node": "22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "19.0.2", - "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.20", "esbuild": "^0.23.1", "eslint": "^8.57.0", @@ -67,7 +55,6 @@ "postcss": "^8.4.49", "prettier": "^3.3.3", "tailwindcss": "^3.4.17", - "tailwindcss-animate": "^1.0.7", "tsx": "^4.19.4", "typescript": "^5.7.2", "vitest": "^3.2.4" diff --git a/scripts/database/sqlite-ensure-app-schema.sh b/scripts/database/sqlite-ensure-app-schema.sh index 618da04..ac15325 100755 --- a/scripts/database/sqlite-ensure-app-schema.sh +++ b/scripts/database/sqlite-ensure-app-schema.sh @@ -264,20 +264,8 @@ CREATE INDEX IF NOT EXISTS idx_chat_memory_thread ON chat_memory_state(thread_id SQL fi -echo "Ensuring agent_delegations table exists..." -"$SQLITE_BIN" "$DB_PATH" <<'SQL' -CREATE TABLE IF NOT EXISTS agent_delegations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT UNIQUE NOT NULL, - task TEXT NOT NULL, - context TEXT, - expected_outcome TEXT, - status TEXT NOT NULL DEFAULT 'queued', - summary TEXT, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP -); -SQL +echo "Dropping legacy agent_delegations table if present..." +"$SQLITE_BIN" "$DB_PATH" "DROP TABLE IF EXISTS agent_delegations;" if ! has_table node_dimensions; then "$SQLITE_BIN" "$DB_PATH" <<'SQL' @@ -573,6 +561,12 @@ if has_table helpers && has_col helpers fluid_context; then "$SQLITE_BIN" "$DB_PATH" "ALTER TABLE helpers DROP COLUMN fluid_context;" || true fi +echo "Ensuring performance indexes exist..." +"$SQLITE_BIN" "$DB_PATH" <<'SQL' +CREATE INDEX IF NOT EXISTS idx_nodes_updated_at ON nodes(updated_at DESC); +CREATE INDEX IF NOT EXISTS idx_chats_created_at ON chats(created_at DESC); +SQL + echo "Running VACUUM and ANALYZE..." "$SQLITE_BIN" "$DB_PATH" "VACUUM; ANALYZE;" diff --git a/scripts/test-ts-migration.js b/scripts/test-ts-migration.js deleted file mode 100755 index a0fee78..0000000 --- a/scripts/test-ts-migration.js +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env node - -/** - * Test script for Python to TypeScript migration - * Tests extractors and embeddings using TypeScript modules - */ -const fs = require('fs'); -const path = require('path'); -const Module = require('module'); -const ts = require('typescript'); -const { getSubtitles } = require('youtube-captions-scraper'); - -function isNetworkError(error) { - if (!error) return false; - const message = typeof error === 'string' ? error : error.message || ''; - const code = error.code || ''; - return [ - 'fetch failed', - 'ENOTFOUND', - 'ECONNRESET', - 'EAI_AGAIN', - 'ECONNREFUSED', - 'ETIMEDOUT' - ].some(token => message.includes(token)) || ['ENOTFOUND', 'ECONNRESET', 'EAI_AGAIN', 'ECONNREFUSED', 'ETIMEDOUT'].includes(code); -} - -function requireTs(modulePath) { - const resolved = path.resolve(__dirname, '..', modulePath); - const source = fs.readFileSync(resolved, 'utf8'); - const transpiled = ts.transpileModule(source, { - compilerOptions: { - module: ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.ES2020, - esModuleInterop: true, - moduleResolution: ts.ModuleResolutionKind.Node16, - resolveJsonModule: true, - skipLibCheck: true - }, - fileName: resolved - }); - - const tempModule = new Module(resolved, module); - tempModule.filename = resolved; - tempModule.paths = Module._nodeModulePaths(path.dirname(resolved)); - tempModule._compile(transpiled.outputText, resolved); - return tempModule.exports; -} - -async function testTranscriptLibrary() { - console.log('🔍 Testing youtube-captions-scraper library directly...'); - - const testUrls = [ - 'dQw4w9WgXcQ', // Rick Roll - 'MnrJzXM7a6o', // TED Talk - 'jNQXAC9IVRw' // Popular video - ]; - - let sawNetworkError = false; - let sawEmptyTranscript = false; - - for (const videoId of testUrls) { - console.log(`\nTesting video: ${videoId}`); - try { - const transcript = await getSubtitles({ videoID: videoId }); - if (!transcript || transcript.length === 0) { - sawEmptyTranscript = true; - console.log('⚠️ Library returned 0 segments (likely geo-blocked). Trying next video...'); - continue; - } - console.log(`✅ Success! Got ${transcript.length} segments`); - console.log('First segment:', transcript[0]); - console.log('Sample formatted:', `[${transcript[0].start.toFixed(1)}s] ${transcript[0].text}`); - return 'passed'; // Exit on first success - } catch (error) { - if (isNetworkError(error)) { - sawNetworkError = true; - console.log('⚠️ Network unavailable, skipping this video'); - } else { - console.log('❌ Failed:', error.message); - } - } - } - - if (sawNetworkError) { - console.log('\n⚠️ Network unavailable for transcript tests, marking as SKIPPED'); - return 'skipped'; - } - - if (sawEmptyTranscript) { - console.log('\n⚠️ Library could not fetch transcripts for any sample video (likely YouTube changes). Marking as SKIPPED.'); - return 'skipped'; - } - - console.log('\n❌ No transcripts could be extracted from any test video'); - return 'failed'; -} - -console.log('🧪 Testing TypeScript Migration...\n'); - -async function testYouTubeExtractor() { - console.log('📺 Testing YouTube Extractor...'); - try { - const { extractYouTube } = requireTs('src/services/typescript/extractors/youtube.ts'); - const result = await extractYouTube('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); - console.log('✅ YouTube extraction successful'); - console.log(` Title: ${result.metadata.title}`); - console.log(` Channel: ${result.metadata.author_name}`); - console.log(` Transcript length: ${result.chunk.length} chars\n`); - return 'passed'; - } catch (error) { - if (isNetworkError(error)) { - console.warn('⚠️ Network unavailable, skipping YouTube extractor test\n'); - return 'skipped'; - } - console.error('❌ YouTube extraction failed:', error.message, '\n'); - return 'failed'; - } -} - -async function testWebsiteExtractor() { - console.log('🌐 Testing Website Extractor...'); - try { - const { extractWebsite } = requireTs('src/services/typescript/extractors/website.ts'); - const result = await extractWebsite('https://example.com'); - console.log('✅ Website extraction successful'); - console.log(` Title: ${result.metadata.title}`); - console.log(` Content length: ${result.chunk.length} chars\n`); - return 'passed'; - } catch (error) { - if (isNetworkError(error)) { - console.warn('⚠️ Network unavailable, skipping website extractor test\n'); - return 'skipped'; - } - console.error('❌ Website extraction failed:', error.message, '\n'); - return 'failed'; - } -} - -async function testPDFExtractor() { - console.log('📄 Testing PDF Extractor...'); - try { - const { extractPaper } = requireTs('src/services/typescript/extractors/paper.ts'); - // Using a sample PDF URL - const samplePdf = 'https://raw.githubusercontent.com/mozilla/pdf.js/master/web/compressed.tracemonkey-pldi-09.pdf'; - const result = await extractPaper(samplePdf); - console.log('✅ PDF extraction successful'); - console.log(` Pages: ${result.metadata.pages}`); - console.log(` Text length: ${result.metadata.text_length} chars\n`); - return 'passed'; - } catch (error) { - if (isNetworkError(error)) { - console.warn('⚠️ Network unavailable, skipping PDF extractor test\n'); - return 'skipped'; - } - console.error('❌ PDF extraction failed:', error.message, '\n'); - return 'failed'; - } -} - -async function testNodeEmbedding() { - console.log('🔮 Testing Node Embedding...'); - try { - // This would require a real node ID and database connection - console.log('⚠️ Skipping node embedding test (requires database and API key)\n'); - return 'skipped'; - } catch (error) { - console.error('❌ Node embedding failed:', error.message, '\n'); - return 'failed'; - } -} - -async function runTests() { - console.log('Environment:'); - console.log(` Node version: ${process.version}\n`); - - // First test the library directly - const libraryStatus = await testTranscriptLibrary(); - - const results = { - library: libraryStatus, - youtube: await testYouTubeExtractor(), - website: await testWebsiteExtractor(), - pdf: await testPDFExtractor(), - embedding: await testNodeEmbedding() - }; - - console.log('📊 Test Results:'); - console.log('─────────────────'); - - let passed = 0; - let failed = 0; - let skipped = 0; - - for (const [test, result] of Object.entries(results)) { - if (result === 'passed') { - console.log(` ${test}: ✅ PASSED`); - passed++; - } else if (result === 'skipped') { - console.log(` ${test}: ⚠️ SKIPPED`); - skipped++; - } else { - console.log(` ${test}: ❌ FAILED`); - failed++; - } - } - - console.log('\n📈 Summary:'); - console.log(` Passed: ${passed}`); - console.log(` Failed: ${failed}`); - console.log(` Skipped: ${skipped}`); - - if (failed === 0) { - console.log('\n🎉 No hard failures. Re-run skipped tests when network/database access is available.'); - } else { - console.log('\n⚠️ Some tests failed. Check the errors above.'); - } - - process.exit(failed > 0 ? 1 : 0); -} - -// Run tests -runTests().catch(error => { - console.error('Fatal error:', error); - process.exit(1); -}); diff --git a/src/components/auth/LocalKeyGate.tsx b/src/components/auth/LocalKeyGate.tsx deleted file mode 100644 index 202e1a4..0000000 --- a/src/components/auth/LocalKeyGate.tsx +++ /dev/null @@ -1,99 +0,0 @@ -"use client"; - -import { useEffect, useMemo, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { isLocalMode } from '@/config/runtime'; -import { apiKeyService } from '@/services/storage/apiKeys'; - -interface LocalKeyGateProps { - children: React.ReactNode; -} - -const panelStyle: React.CSSProperties = { - maxWidth: 420, - background: 'rgba(15, 15, 15, 0.92)', - border: '1px solid #2a2a2a', - borderRadius: 16, - padding: '28px 32px', - boxShadow: '0 18px 40px rgba(0,0,0,0.45)' -}; - -const buttonStyle: React.CSSProperties = { - background: '#22c55e', - color: '#0b1113', - border: 'none', - borderRadius: 6, - padding: '12px 18px', - fontWeight: 600, - cursor: 'pointer' -}; - -export function LocalKeyGate({ children }: LocalKeyGateProps) { - const isLocal = useMemo(() => isLocalMode(), []); - const [hasKeys, setHasKeys] = useState(() => (!isLocal) || apiKeyService.hasUserKeys()); - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - useEffect(() => { - if (!isLocal) return; - const handleUpdate = () => { - setHasKeys(apiKeyService.hasUserKeys()); - }; - handleUpdate(); - const listener = () => handleUpdate(); - window.addEventListener('api-keys:updated', listener); - return () => window.removeEventListener('api-keys:updated', listener); - }, [isLocal]); - - if (!isLocal || hasKeys) { - return <>{children}; - } - - const openApiKeySettings = () => { - window.dispatchEvent(new CustomEvent('settings:open', { detail: { tab: 'apikeys' } })); - }; - - const popupContent = ( -
-
-
-

Connect your AI keys

-

- Local mode needs an OpenAI or Anthropic key. Add one under Settings → API Keys - to unlock the workspace. -

-
-
- - -
-
-
- ); - - return ( - <> - {children} - {mounted && createPortal(popupContent, document.body)} - - ); -} diff --git a/src/components/common/EditableSection.tsx b/src/components/common/EditableSection.tsx deleted file mode 100644 index afc82d7..0000000 --- a/src/components/common/EditableSection.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import { ReactNode, CSSProperties } from 'react'; - -interface EditableSectionProps { - label: string; - summary: ReactNode; - expanded: boolean; - onToggle: () => void; - children: ReactNode; - metadata?: string; - disabled?: boolean; -} - -export default function EditableSection({ - label, - summary, - expanded, - onToggle, - children, - metadata, - disabled = false -}: EditableSectionProps) { - return ( -
-
{ - if (!disabled && !expanded) { - e.currentTarget.style.background = '#1f1f1f'; - } - }} - onMouseLeave={(e) => { - if (!disabled && !expanded) { - e.currentTarget.style.background = 'transparent'; - } - }} - > - - {label} - - -
- {summary} -
- - {metadata && ( - - {metadata} - - )} - - {!disabled && ( - - - - )} - - {disabled && ( - - locked - - )} -
- - {expanded && ( -
- {children} -
- )} -
- ); -} \ No newline at end of file diff --git a/src/components/common/PanelHeader.tsx b/src/components/common/PanelHeader.tsx deleted file mode 100644 index e62aec7..0000000 --- a/src/components/common/PanelHeader.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import { ReactNode, CSSProperties } from 'react'; - -interface PanelHeaderProps { - title: string; - leftContent?: ReactNode; - rightContent?: ReactNode; - className?: string; - style?: CSSProperties; -} - -export default function PanelHeader({ - title, - leftContent, - rightContent, - className = '', - style = {} -}: PanelHeaderProps) { - return ( -
-
- - {title} - - {leftContent} -
- - {rightContent && ( -
- {rightContent} -
- )} -
- ); -} \ No newline at end of file diff --git a/src/components/focus/edges/EdgeSearchModal.tsx b/src/components/focus/edges/EdgeSearchModal.tsx deleted file mode 100644 index 75ed16d..0000000 --- a/src/components/focus/edges/EdgeSearchModal.tsx +++ /dev/null @@ -1,400 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef } from 'react'; -import { createPortal } from 'react-dom'; - -interface EdgeSearchModalProps { - isOpen: boolean; - onClose: () => void; - onEdgeCreate: (targetNodeId: number, targetNodeTitle: string) => void; - sourceNodeId: number; -} - -interface NodeSuggestion { - id: number; - title: string; - dimensions?: string[]; -} - -export default function EdgeSearchModal({ - isOpen, - onClose, - onEdgeCreate, - sourceNodeId -}: EdgeSearchModalProps) { - const [searchQuery, setSearchQuery] = useState(''); - const [suggestions, setSuggestions] = useState([]); - const [selectedIndex, setSelectedIndex] = useState(0); - const inputRef = useRef(null); - const modalRef = useRef(null); - const returnFocusRef = useRef(null); - - // Store the element that triggered the modal for return focus - useEffect(() => { - if (isOpen && document.activeElement instanceof HTMLElement) { - returnFocusRef.current = document.activeElement; - } - }, [isOpen]); - - // Focus trap and accessibility - useEffect(() => { - if (!isOpen) return; - - // Autofocus input - inputRef.current?.focus(); - - // Lock body scroll - document.body.style.overflow = 'hidden'; - - // Handle Escape key - const handleEscape = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - onClose(); - } - }; - - // Focus trap: keep focus within modal - const handleTab = (e: KeyboardEvent) => { - if (e.key !== 'Tab') return; - - const focusableElements = modalRef.current?.querySelectorAll( - 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' - ); - - if (!focusableElements || focusableElements.length === 0) return; - - const firstElement = focusableElements[0] as HTMLElement; - const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement; - - if (e.shiftKey) { - if (document.activeElement === firstElement) { - e.preventDefault(); - lastElement.focus(); - } - } else { - if (document.activeElement === lastElement) { - e.preventDefault(); - firstElement.focus(); - } - } - }; - - document.addEventListener('keydown', handleEscape); - document.addEventListener('keydown', handleTab); - - return () => { - document.body.style.overflow = ''; - document.removeEventListener('keydown', handleEscape); - document.removeEventListener('keydown', handleTab); - - // Return focus to trigger element - if (returnFocusRef.current) { - returnFocusRef.current.focus(); - } - }; - }, [isOpen, onClose]); - - // Generate suggestions based on search query - useEffect(() => { - if (!searchQuery.trim()) { - setSuggestions([]); - return; - } - - const fetchSuggestions = async () => { - try { - const response = await fetch(`/api/nodes/search?q=${encodeURIComponent(searchQuery)}&limit=10`); - const result = await response.json(); - - if (result.success) { - const nodeSuggestions: NodeSuggestion[] = result.data - .filter((node: any) => node.id !== sourceNodeId) // Exclude source node - .map((node: any) => ({ - id: node.id, - title: node.title, - dimensions: node.dimensions || [] - })); - - setSuggestions(nodeSuggestions); - setSelectedIndex(0); - } - } catch (error) { - console.error('Error fetching suggestions:', error); - setSuggestions([]); - } - }; - - const timeoutId = setTimeout(fetchSuggestions, 200); - return () => clearTimeout(timeoutId); - }, [searchQuery, sourceNodeId]); - - // Handle keyboard navigation - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'ArrowDown') { - e.preventDefault(); - setSelectedIndex(prev => Math.min(prev + 1, suggestions.length - 1)); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - setSelectedIndex(prev => Math.max(prev - 1, 0)); - } else if (e.key === 'Enter' && suggestions[selectedIndex]) { - e.preventDefault(); - handleSelectSuggestion(suggestions[selectedIndex]); - } - }; - - const handleSelectSuggestion = (suggestion: NodeSuggestion) => { - onEdgeCreate(suggestion.id, suggestion.title); - setSearchQuery(''); - setSuggestions([]); - onClose(); - }; - - const handleBackdropClick = (e: React.MouseEvent) => { - if (e.target === e.currentTarget) { - onClose(); - } - }; - - if (!isOpen) return null; - - const modalContent = ( -
-
- {/* Search Input */} -
- - - - - setSearchQuery(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Connect to node..." - className="search-input" - /> - -
- esc -
-
- - {/* Results */} - {suggestions.length > 0 && ( -
- {suggestions.map((suggestion, index) => ( - - ))} -
- )} - - {/* Empty state */} - {searchQuery && suggestions.length === 0 && ( -
- No results for "{searchQuery}" -
- )} -
- - -
- ); - - return typeof window !== 'undefined' ? createPortal(modalContent, document.body) : null; -} diff --git a/src/components/layout/CollapsedRail.tsx b/src/components/layout/CollapsedRail.tsx deleted file mode 100644 index 5e6e433..0000000 --- a/src/components/layout/CollapsedRail.tsx +++ /dev/null @@ -1,111 +0,0 @@ -"use client"; - -import { ChevronLeft, ChevronRight, FileText, MessageSquare, Workflow, FolderOpen, Map, LayoutList } from 'lucide-react'; - -type RailPosition = 'left' | 'right'; -type PaneType = 'nodes' | 'node' | 'chat' | 'workflows' | 'dimensions' | 'map' | 'views'; - -interface CollapsedRailProps { - position: RailPosition; - paneType: PaneType; - onExpand: () => void; - shortcut?: string; -} - -const PANE_ICONS: Record = { - nodes: , - node: , - chat: , - workflows: , - dimensions: , - map: , - views: , -}; - -const PANE_LABELS: Record = { - nodes: 'Nodes', - node: 'Focus', - chat: 'Chat', - workflows: 'Workflows', - dimensions: 'Dimensions', - map: 'Map', - views: 'Feed', -}; - -export default function CollapsedRail({ position, paneType, onExpand, shortcut }: CollapsedRailProps) { - const isLeft = position === 'left'; - const ChevronIcon = isLeft ? ChevronRight : ChevronLeft; - - return ( -
{ - e.currentTarget.style.background = '#141414'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.background = '#0c0c0c'; - }} - title={`Expand ${PANE_LABELS[paneType]}${shortcut ? ` (${shortcut})` : ''}`} - > - {/* Icon representing the collapsed panel */} -
- {PANE_ICONS[paneType]} -
- - {/* Chevron indicator */} -
- -
- - {/* Vertical label */} -
- {PANE_LABELS[paneType]} -
-
- ); -} diff --git a/src/components/nodes/AddNodeButton.tsx b/src/components/nodes/AddNodeButton.tsx deleted file mode 100644 index 3924776..0000000 --- a/src/components/nodes/AddNodeButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -interface AddNodeButtonProps { - onAddNode: () => void; - loading?: boolean; -} - -export default function AddNodeButton({ onAddNode, loading = false }: AddNodeButtonProps) { - return ( - - ); -} \ No newline at end of file diff --git a/src/components/nodes/NodesPanel.tsx b/src/components/nodes/NodesPanel.tsx deleted file mode 100644 index 44ab5d1..0000000 --- a/src/components/nodes/NodesPanel.tsx +++ /dev/null @@ -1,892 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef, type DragEvent } from 'react'; -import { ChevronRight, ChevronDown, Folder, FolderOpen, Layers, List, Maximize2, Minimize2 } from 'lucide-react'; -import { Node } from '@/types/database'; -import AddNodeButton from './AddNodeButton'; -import Chip from '../common/Chip'; -import { getNodeIcon } from '@/utils/nodeIcons'; -import SearchModal from './SearchModal'; -import { DynamicIcon } from '../common/LucideIconPicker'; -import { usePersistentState } from '@/hooks/usePersistentState'; - -interface NodesPanelProps { - selectedNodes: Set; - onNodeSelect: (nodeId: number, multiSelect: boolean) => void; - onNodeCreated?: (node: Node) => void; - onNodeDeleted?: (nodeId: number) => void; - refreshTrigger?: number; - onToggleFolderView?: (isOpen?: boolean) => void; - folderViewOpen?: boolean; -} - -interface LockedDimension { - dimension: string; - count: number; - isPriority: boolean; -} - - -export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated, onNodeDeleted, refreshTrigger, onToggleFolderView, folderViewOpen = false }: NodesPanelProps) { - const [nodes, setNodes] = useState([]); - const [allNodes, setAllNodes] = useState([]); - const [loading, setLoading] = useState(true); - const [creating, setCreating] = useState(false); - const [deletingNode, setDeletingNode] = useState(null); - const [priorityDimensions, setPriorityDimensions] = useState([]); - const [lockedDimensions, setLockedDimensions] = useState([]); - const [expandedDimensions, setExpandedDimensions] = useState>(() => { - if (typeof window === 'undefined') return new Set(); - const saved = window.localStorage.getItem('expandedDimensions'); - return saved ? new Set(JSON.parse(saved)) : new Set(); - }); - const [dimensionNodes, setDimensionNodes] = useState>({}); - const [selectedFilters, setSelectedFilters] = useState<{type: 'dimension' | 'title', value: string}[]>([]); - const [dimensionsSectionCollapsed, setDimensionsSectionCollapsed] = useState(true); // Default: collapsed - const [allNodesSectionCollapsed, setAllNodesSectionCollapsed] = useState(false); - const [showSearchModal, setShowSearchModal] = useState(false); - const [dropFeedback, setDropFeedback] = useState(null); - const [dragHoverDimension, setDragHoverDimension] = useState(null); - const draggedNodeRef = useRef<{ id: number; dimensions?: string[] } | null>(null); - - // Dimension icons (shared with FolderViewOverlay via localStorage) - const [dimensionIcons] = usePersistentState>('ui.dimensionIcons', {}); - - useEffect(() => { - fetchNodes(); - fetchLockedDimensions(); - }, []); - - // Save expanded dimensions to localStorage - useEffect(() => { - if (typeof window === 'undefined') return; - window.localStorage.setItem('expandedDimensions', JSON.stringify([...expandedDimensions])); - }, [expandedDimensions]); - - // Refresh nodes and locked dimensions when SSE events trigger updates - useEffect(() => { - if (refreshTrigger !== undefined && refreshTrigger > 0) { - console.log('🔄 Refreshing nodes and locked dimensions due to SSE event'); - fetchNodes(); - fetchLockedDimensions(); - } - }, [refreshTrigger]); - - // Global Cmd+K / Ctrl+K keyboard shortcut for search - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault(); - setShowSearchModal(true); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, []); - - // Re-apply filters when allNodes changes - useEffect(() => { - if (selectedFilters.length > 0) { - applyFiltersWithSelection(selectedFilters); - } else { - setNodes(allNodes); - } - }, [allNodes, priorityDimensions]); - - const fetchNodes = async () => { - try { - const response = await fetch('/api/nodes?limit=100'); - const result = await response.json(); - const fetchedNodes = result.data || []; - setAllNodes(fetchedNodes); - setNodes(fetchedNodes); - } catch (error) { - console.error('Error fetching nodes:', error); - } finally { - setLoading(false); - } - }; - - const fetchLockedDimensions = async () => { - try { - const response = await fetch('/api/dimensions/popular'); - const result = await response.json(); - if (result.success) { - const locked = result.data.filter((d: any) => d.isPriority); - setLockedDimensions(locked); - const priority = locked.map((d: any) => d.dimension); - setPriorityDimensions(priority); - } - } catch (error) { - console.error('Error fetching locked dimensions:', error); - } - }; - - const toggleDimension = async (dimension: string) => { - const isCurrentlyExpanded = expandedDimensions.has(dimension); - - setExpandedDimensions(prev => { - const next = new Set(prev); - if (next.has(dimension)) { - next.delete(dimension); - } else { - next.add(dimension); - } - return next; - }); - - // Fetch nodes for this dimension when expanding (if not already fetched) - if (!isCurrentlyExpanded && !dimensionNodes[dimension]) { - await fetchNodesForDimension(dimension); - } - }; - - const fetchNodesForDimension = async (dimension: string) => { - try { - // Sort by edge count (most connected first), then updated_at - const response = await fetch(`/api/nodes?dimensions=${encodeURIComponent(dimension)}&limit=200&sortBy=edges`); - const result = await response.json(); - if (result.success) { - setDimensionNodes(prev => ({ - ...prev, - [dimension]: result.data || [] - })); - } - } catch (error) { - console.error(`Error fetching nodes for dimension ${dimension}:`, error); - } - }; - - const handleDimensionDragOver = (event: DragEvent) => { - if (event.dataTransfer.types.includes('application/node-info') || event.dataTransfer.types.includes('text/plain')) { - event.preventDefault(); - event.dataTransfer.dropEffect = 'copy'; - } - }; - - const handleDimensionDragEnter = (event: DragEvent, dimension: string) => { - if (event.dataTransfer.types.includes('application/node-info') || event.dataTransfer.types.includes('text/plain')) { - setDragHoverDimension(dimension); - } - }; - - const handleDimensionDragLeave = (event: DragEvent, dimension: string) => { - if (dragHoverDimension === dimension) { - setDragHoverDimension(null); - } - }; - - const handleNodeDropOnDimension = async (event: DragEvent, dimension: string) => { - event.preventDefault(); - event.stopPropagation(); - - // Try to get data from ref first (works in Electron/Tauri webviews) - let payload: { id: number; dimensions?: string[] } | null = draggedNodeRef.current; - - // Fallback to dataTransfer for browser compatibility - if (!payload) { - const raw = event.dataTransfer.getData('application/node-info') || event.dataTransfer.getData('text/plain'); - if (raw) { - try { - payload = JSON.parse(raw); - } catch (e) { - console.error('Failed to parse drag data:', e); - } - } - } - - // Clear the ref - draggedNodeRef.current = null; - - if (!payload?.id) { - console.warn('No valid node data in drop event'); - return; - } - - try { - const currentDimensions = payload.dimensions || []; - if (currentDimensions.some((dim) => dim.toLowerCase() === dimension.toLowerCase())) { - setDropFeedback(`Node already in ${dimension}`); - setTimeout(() => setDropFeedback(null), 2500); - return; - } - - const updatedDimensions = Array.from(new Set([...currentDimensions, dimension])); - const response = await fetch(`/api/nodes/${payload.id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ dimensions: updatedDimensions }) - }); - - if (!response.ok) { - throw new Error('Failed to update node dimensions'); - } - - setDropFeedback(`Added to ${dimension}`); - setTimeout(() => setDropFeedback(null), 2500); - if (expandedDimensions.has(dimension)) { - fetchNodesForDimension(dimension); - } - fetchLockedDimensions(); - setDragHoverDimension(null); - } catch (error) { - console.error('Error handling node drop:', error); - setDropFeedback('Failed to add dimension'); - setTimeout(() => setDropFeedback(null), 2500); - setDragHoverDimension(null); - } - }; - - const getNodesForDimension = (dimension: string): Node[] => { - // Use fetched dimension-specific nodes if available, otherwise filter from loaded nodes - return dimensionNodes[dimension] || nodes.filter(node => - node.dimensions?.some(d => d.toLowerCase() === dimension.toLowerCase()) - ); - }; - - const applyFilters = () => { - if (selectedFilters.length === 0) { - setNodes(allNodes); - return; - } - - const dimensionFilters = selectedFilters.filter(f => f.type === 'dimension').map(f => f.value); - const titleFilters = selectedFilters.filter(f => f.type === 'title').map(f => f.value); - - const filtered = allNodes.filter(node => { - // Must match ALL selected dimension filters - const dimensionMatch = dimensionFilters.length === 0 || - dimensionFilters.every(dim => - node.dimensions?.some(nodeDim => nodeDim.toLowerCase() === dim.toLowerCase()) - ); - - // Must match ANY selected title filter - const titleMatch = titleFilters.length === 0 || - titleFilters.some(title => - node.title?.toLowerCase().includes(title.toLowerCase()) - ); - - return dimensionMatch && titleMatch; - }); - - // Sort by priority dimensions first - const sorted = filtered.sort((a, b) => { - const aHasPriority = a.dimensions?.some(dim => priorityDimensions.includes(dim)) || false; - const bHasPriority = b.dimensions?.some(dim => priorityDimensions.includes(dim)) || false; - if (aHasPriority && !bHasPriority) return -1; - if (!aHasPriority && bHasPriority) return 1; - return 0; - }); - - setNodes(sorted); - }; - - const addFilter = (type: 'dimension' | 'title', value: string) => { - const newFilter = { type, value }; - const newFilters = [...selectedFilters, newFilter]; - setSelectedFilters(newFilters); - - // Apply filters immediately - setTimeout(() => { - if (newFilters.length === 0) { - setNodes(allNodes); - } else { - applyFiltersWithSelection(newFilters); - } - }, 0); - }; - - const removeFilter = (index: number) => { - const newFilters = selectedFilters.filter((_, i) => i !== index); - setSelectedFilters(newFilters); - - // Apply filters immediately - setTimeout(() => { - if (newFilters.length === 0) { - setNodes(allNodes); - } else { - applyFiltersWithSelection(newFilters); - } - }, 0); - }; - - const applyFiltersWithSelection = async (filters: {type: 'dimension' | 'title', value: string}[]) => { - const dimensionFilters = filters.filter(f => f.type === 'dimension').map(f => f.value); - const titleFilters = filters.filter(f => f.type === 'title').map(f => f.value); - - try { - // Build API query params - const params = new URLSearchParams(); - - if (dimensionFilters.length > 0) { - params.append('dimensions', dimensionFilters.join(',')); - } - - if (titleFilters.length > 0) { - // For title filters, we'll search each one and combine results - const titleResults: Node[] = []; - - for (const title of titleFilters) { - const response = await fetch(`/api/nodes?search=${encodeURIComponent(title)}&limit=50`); - const result = await response.json(); - if (result.success) { - titleResults.push(...result.data); - } - } - - // If we have dimension filters too, intersect the results - if (dimensionFilters.length > 0) { - const dimensionResponse = await fetch(`/api/nodes?${params.toString()}&limit=200`); - const dimensionResult = await dimensionResponse.json(); - - if (dimensionResult.success) { - // Find intersection: nodes that match dimensions AND titles - const dimensionNodeIds = new Set(dimensionResult.data.map((n: Node) => n.id)); - const intersectedNodes = titleResults.filter(node => dimensionNodeIds.has(node.id)); - setNodes(intersectedNodes); - } - } else { - // Only title filters - setNodes(titleResults); - } - } else if (dimensionFilters.length > 0) { - // Only dimension filters - const response = await fetch(`/api/nodes?${params.toString()}&limit=200`); - const result = await response.json(); - - if (result.success) { - setNodes(result.data); - } - } - } catch (error) { - console.error('Error applying filters:', error); - // Fallback to local filtering - const filtered = allNodes.filter(node => { - const dimensionMatch = dimensionFilters.length === 0 || - dimensionFilters.every(dim => - node.dimensions?.some(nodeDim => nodeDim.toLowerCase() === dim.toLowerCase()) - ); - - const titleMatch = titleFilters.length === 0 || - titleFilters.some(title => - node.title?.toLowerCase().includes(title.toLowerCase()) - ); - - return dimensionMatch && titleMatch; - }); - setNodes(filtered); - } - }; - - const clearSearch = () => { - setSelectedFilters([]); - setNodes(allNodes); - }; - - const collapseAllDimensions = () => { - setExpandedDimensions(new Set()); - }; - - const handleNodeDragStart = (event: DragEvent, node: Node) => { - event.dataTransfer.effectAllowed = 'copyMove'; - const nodeData = { - id: node.id, - title: node.title || 'Untitled', - dimensions: node.dimensions || [] - }; - // Store in ref for webview compatibility (dataTransfer.getData can fail in Electron/Tauri) - draggedNodeRef.current = nodeData; - // Set multiple MIME types for different drop targets - event.dataTransfer.setData('application/node-info', JSON.stringify(nodeData)); - // For chat input drops - includes title for [NODE:id:"title"] token - event.dataTransfer.setData('application/x-rah-node', JSON.stringify({ id: node.id, title: node.title || 'Untitled' })); - // Fallback for browsers/webviews that only support text/plain - event.dataTransfer.setData('text/plain', `[NODE:${node.id}:"${node.title || 'Untitled'}"]`); - - const preview = document.createElement('div'); - preview.textContent = node.title || `Node #${node.id}`; - preview.style.position = 'fixed'; - preview.style.top = '-1000px'; - preview.style.left = '-1000px'; - preview.style.padding = '4px 8px'; - preview.style.background = '#0f0f0f'; - preview.style.color = '#f8fafc'; - preview.style.fontSize = '11px'; - preview.style.fontWeight = '600'; - preview.style.borderRadius = '6px'; - preview.style.border = '1px solid #1f1f1f'; - document.body.appendChild(preview); - event.dataTransfer.setDragImage(preview, 6, 6); - setTimeout(() => { - if (preview.parentNode) { - preview.parentNode.removeChild(preview); - } - }, 0); - }; - - const handleNodeDragEnd = () => { - // Clear ref if drag ends without a drop - draggedNodeRef.current = null; - }; - - const renderNodeItem = (node: Node) => ( - - ); - - const handleAddNode = async () => { - setCreating(true); - try { - const response = await fetch('/api/nodes', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - title: 'New Node', - content: '', - link: null, - dimensions: [] - }), - }); - - const result = await response.json(); - - if (result.success) { - // Add new node to both lists immediately - setAllNodes(prevNodes => [result.data, ...prevNodes]); - setNodes(prevNodes => [result.data, ...prevNodes]); - - // Notify parent component about new node creation - if (onNodeCreated) { - onNodeCreated(result.data); - } - } else { - console.error('Failed to create node:', result.message); - } - } catch (error) { - console.error('Error creating node:', error); - } finally { - setCreating(false); - } - }; - - // handleDeleteNode removed - deletion only available in focus panel - - const handleNodeSelectFromSearch = (nodeId: number) => { - // Open node in focus panel without filtering the nodes panel - onNodeSelect(nodeId, false); - }; - - return ( -
- {/* Search Modal */} - setShowSearchModal(false)} - onNodeSelect={handleNodeSelectFromSearch} - existingFilters={selectedFilters} - /> - - {/* Nodes List */} -
- {loading ? ( -
- Loading nodes... -
- ) : ( -
- {/* + ADD NODE Button & folder expand */} -
- - -
- - {dropFeedback && ( -
- {dropFeedback} -
- )} - - {/* SEARCH Button */} - - - {/* DIMENSIONS Section Header */} - {lockedDimensions.length > 0 && ( - - )} - - {/* Locked Dimension Sections */} - {!dimensionsSectionCollapsed && lockedDimensions.map((lockedDim) => { - const dimNodes = getNodesForDimension(lockedDim.dimension); - const isExpanded = expandedDimensions.has(lockedDim.dimension); - - return ( -
- {/* Dimension Header */} - - - {/* Dimension Nodes (when expanded) */} - {isExpanded && ( -
- {dimNodes.length === 0 ? ( -
- No nodes in this dimension -
- ) : ( - dimNodes.map((node) => renderNodeItem(node)) - )} -
- )} -
- ); - })} - - {/* ALL NODES Section Header */} - - - {/* All Nodes List */} - {!allNodesSectionCollapsed && ( - nodes.length === 0 ? ( -
- {selectedFilters.length > 0 ? 'No nodes match filters' : 'No nodes yet - click "Add Node" above to get started'} -
- ) : ( - nodes.map((node) => renderNodeItem(node)) - ) - )} -
- )} -
- -
- ); -} diff --git a/src/components/settings/MapViewer.tsx b/src/components/settings/MapViewer.tsx deleted file mode 100644 index a1a849b..0000000 --- a/src/components/settings/MapViewer.tsx +++ /dev/null @@ -1,425 +0,0 @@ -"use client"; - -import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'; -import type { Edge, Node } from '@/types/database'; - -interface GraphNode extends Node { - edge_count?: number; - x: number; - y: number; - radius: number; -} - -interface LockedDimension { - name: string; -} - -const NODE_LIMIT = 200; -const LABEL_THRESHOLD = 15; // Top N nodes get labels - -export default function MapViewer() { - const containerRef = useRef(null); - const [containerSize, setContainerSize] = useState({ width: 800, height: 600 }); - const [nodes, setNodes] = useState([]); - const [edges, setEdges] = useState([]); - const [lockedDimensions, setLockedDimensions] = useState>(new Set()); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [selectedNode, setSelectedNode] = useState(null); - const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 }); - - // Resize observer - useEffect(() => { - const observer = new ResizeObserver(entries => { - const entry = entries[0]; - if (entry?.contentRect) { - setContainerSize({ - width: entry.contentRect.width, - height: entry.contentRect.height, - }); - } - }); - - if (containerRef.current) { - observer.observe(containerRef.current); - } - - return () => observer.disconnect(); - }, []); - - // Fetch data - useEffect(() => { - const fetchData = async () => { - setLoading(true); - setError(null); - try { - const [nodesRes, edgesRes, dimsRes] = await Promise.all([ - fetch(`/api/nodes?limit=${NODE_LIMIT}&sortBy=edges`), - fetch('/api/edges'), - fetch('/api/dimensions'), - ]); - - if (!nodesRes.ok || !edgesRes.ok) { - throw new Error('Failed to load data'); - } - - const nodesPayload = await nodesRes.json(); - const edgesPayload = await edgesRes.json(); - - setNodes(nodesPayload.data || []); - setEdges(edgesPayload.data || []); - - // Get locked dimensions - if (dimsRes.ok) { - const dimsPayload = await dimsRes.json(); - if (dimsPayload.success && dimsPayload.data) { - const locked = (dimsPayload.data as LockedDimension[]) - .filter((d: LockedDimension & { is_locked?: boolean }) => d.is_locked) - .map((d: LockedDimension) => d.name); - setLockedDimensions(new Set(locked)); - } - } - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - } finally { - setLoading(false); - } - }; - - fetchData(); - }, []); - - // Position nodes in a cluster layout - const graphNodes = useMemo(() => { - if (nodes.length === 0) return []; - - const { width, height } = containerSize; - const centerX = width / 2; - const centerY = height / 2; - - // Sort by edge count (highest first) - const sorted = [...nodes].sort((a, b) => (b.edge_count ?? 0) - (a.edge_count ?? 0)); - - // Find max edge count for scaling - const maxEdges = Math.max(...sorted.map(n => n.edge_count ?? 0), 1); - - // Position nodes using a spiral/cluster approach - // High-edge nodes get placed more centrally with more space - return sorted.map((node, index) => { - const edgeCount = node.edge_count ?? 0; - const edgeRatio = edgeCount / maxEdges; - - // Radius from center - higher edge count = closer to center - // Use golden angle for nice distribution - const goldenAngle = Math.PI * (3 - Math.sqrt(5)); - const angle = index * goldenAngle; - - // Distance from center inversely proportional to edge count - // Top nodes cluster in center, others spread out - // Extra spacing for labeled nodes (top 15) to prevent label overlap - const isLabeled = index < LABEL_THRESHOLD; - const labelSpacing = isLabeled ? 60 : 0; - const baseDistance = 80 + labelSpacing + (1 - edgeRatio) * Math.min(width, height) * 0.35; - const distance = baseDistance + (index * 4); // More spread between nodes - - const x = centerX + Math.cos(angle) * distance; - const y = centerY + Math.sin(angle) * distance; - - // Node size based on edge count - // Min 3px for tiny dots, max ~20px for top nodes - const minRadius = 3; - const maxRadius = 18; - const radius = minRadius + edgeRatio * (maxRadius - minRadius); - - return { - ...node, - x, - y, - radius, - }; - }); - }, [nodes, containerSize]); - - // Get edges between visible nodes - const graphEdges = useMemo(() => { - if (graphNodes.length === 0 || edges.length === 0) return []; - - const nodeMap = new Map(); - graphNodes.forEach(node => nodeMap.set(node.id, node)); - - return edges - .map(edge => { - const source = nodeMap.get(edge.from_node_id); - const target = nodeMap.get(edge.to_node_id); - if (!source || !target) return null; - return { id: edge.id, source, target }; - }) - .filter(Boolean) as Array<{ id: number; source: GraphNode; target: GraphNode }>; - }, [edges, graphNodes]); - - // Get connected node IDs for selected node - const connectedNodeIds = useMemo(() => { - if (!selectedNode) return new Set(); - const connected = new Set(); - edges.forEach(edge => { - if (edge.from_node_id === selectedNode.id) connected.add(edge.to_node_id); - if (edge.to_node_id === selectedNode.id) connected.add(edge.from_node_id); - }); - return connected; - }, [selectedNode, edges]); - - // Pan handling - const handlePanStart = (event: React.PointerEvent) => { - const startX = event.clientX; - const startY = event.clientY; - const originX = transform.x; - const originY = transform.y; - - const handleMove = (moveEvent: PointerEvent) => { - setTransform(prev => ({ - ...prev, - x: originX + (moveEvent.clientX - startX), - y: originY + (moveEvent.clientY - startY), - })); - }; - - const handleUp = () => { - window.removeEventListener('pointermove', handleMove); - window.removeEventListener('pointerup', handleUp); - }; - - window.addEventListener('pointermove', handleMove); - window.addEventListener('pointerup', handleUp); - }; - - const handleZoom = (direction: 'in' | 'out' | 'reset') => { - if (direction === 'reset') { - setTransform({ x: 0, y: 0, scale: 1 }); - return; - } - setTransform(prev => ({ - ...prev, - scale: direction === 'in' - ? Math.min(prev.scale + 0.2, 3) - : Math.max(prev.scale - 0.2, 0.5), - })); - }; - - if (loading) { - return ( -
- Loading map... -
- ); - } - - if (error) { - return ( -
- {error} -
- ); - } - - if (graphNodes.length === 0) { - return ( -
- No nodes to display -
- ); - } - - return ( -
- {/* Zoom controls */} -
- - - -
- - {/* Selected node info */} - {selectedNode && ( -
-
-
- {selectedNode.title || 'Untitled'} -
- -
-
- {connectedNodeIds.size} connected nodes · {selectedNode.edge_count ?? 0} total edges -
-
- Click a highlighted node to explore -
- {selectedNode.dimensions && selectedNode.dimensions.length > 0 && ( -
- {selectedNode.dimensions.slice(0, 5).map(dim => ( - - {dim} - - ))} -
- )} -
- )} - - {/* SVG Graph */} - - - - - {/* Edges */} - {graphEdges.map(edge => { - const isConnected = selectedNode && ( - edge.source.id === selectedNode.id || edge.target.id === selectedNode.id - ); - return ( - - ); - })} - - {/* Nodes */} - {graphNodes.map((node, index) => { - const isTop = index < LABEL_THRESHOLD; - const isSelected = selectedNode?.id === node.id; - const isConnectedToSelected = connectedNodeIds.has(node.id); - const isDimmed = selectedNode && !isSelected && !isConnectedToSelected; - - return ( - setSelectedNode(isSelected ? null : node)} - style={{ cursor: 'pointer' }} - opacity={isDimmed ? 0.25 : 1} - > - {/* Highlight ring for connected nodes */} - {isConnectedToSelected && !isSelected && ( - - )} - {/* Node circle */} - - - {/* Label for top nodes */} - {isTop && ( - <> - {/* Title */} - - {(node.title || 'Untitled').slice(0, 20)} - {(node.title?.length ?? 0) > 20 ? '…' : ''} - - - {/* Top dimensions (max 3) */} - {node.dimensions && node.dimensions.length > 0 && (() => { - const dims = node.dimensions.slice(0, 3).map(d => d.length > 10 ? d.slice(0, 9) + '…' : d).join(' · '); - const labelWidth = dims.length * 5 + 16; - return ( - - - - {dims} - - - ); - })()} - - )} - - ); - })} - - -
- ); -} - -const controlBtn: CSSProperties = { - width: 32, - height: 32, - borderRadius: 6, - border: '1px solid #262626', - background: '#141414', - color: '#888', - fontSize: 16, - cursor: 'pointer', -}; - -const infoPanel: CSSProperties = { - position: 'absolute', - bottom: 16, - left: 16, - width: 260, - background: '#0a0a0a', - border: '1px solid #1f1f1f', - borderRadius: 8, - padding: 14, - zIndex: 10, -}; diff --git a/src/components/settings/SettingsCog.tsx b/src/components/settings/SettingsCog.tsx deleted file mode 100644 index 468eb8e..0000000 --- a/src/components/settings/SettingsCog.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client"; - -import { User } from 'lucide-react'; - -interface SettingsCogProps { - onClick: () => void; -} - -export default function SettingsCog({ onClick }: SettingsCogProps) { - return ( - - ); -} diff --git a/src/components/settings/SettingsModal.tsx b/src/components/settings/SettingsModal.tsx index 336868a..57fc06f 100644 --- a/src/components/settings/SettingsModal.tsx +++ b/src/components/settings/SettingsModal.tsx @@ -7,7 +7,6 @@ import ToolsViewer from './ToolsViewer'; import WorkflowsViewer from './WorkflowsViewer'; import ApiKeysViewer from './ApiKeysViewer'; import DatabaseViewer from './DatabaseViewer'; -import MapViewer from './MapViewer'; import ExternalAgentsPanel from './ExternalAgentsPanel'; import ContextViewer from './ContextViewer'; import { apiKeyService } from '@/services/storage/apiKeys'; @@ -18,7 +17,6 @@ export type SettingsTab = | 'workflows' | 'apikeys' | 'database' - | 'map' | 'context' | 'agents'; @@ -197,20 +195,6 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM > Context -
setActiveTab('map')} - style={{ - padding: '12px 24px', - fontSize: '14px', - color: activeTab === 'map' ? '#fff' : '#888', - background: activeTab === 'map' ? '#1a3a2a' : 'transparent', - borderLeft: activeTab === 'map' ? '3px solid #22c55e' : '3px solid transparent', - cursor: 'pointer', - transition: 'all 0.2s' - }} - > - Map -
setActiveTab('agents')} style={{ @@ -315,7 +299,6 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM {activeTab === 'apikeys' && 'API Keys'} {activeTab === 'database' && 'Knowledge Database'} {activeTab === 'context' && 'Auto-Context'} - {activeTab === 'map' && 'Knowledge Map'} {activeTab === 'agents' && 'External Agents'}
diff --git a/src/components/views/ViewPanel.tsx b/src/components/views/ViewPanel.tsx deleted file mode 100644 index 1e67f85..0000000 --- a/src/components/views/ViewPanel.tsx +++ /dev/null @@ -1,286 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback } from 'react'; -import { Eye, List, Columns, LayoutGrid } from 'lucide-react'; -import { ViewType, ViewConfig, ViewFilter, DEFAULT_VIEW_CONFIG, KanbanColumn } from '@/types/views'; -import { Node } from '@/types/database'; -import ListView from './ListView'; -import KanbanView from './KanbanView'; -import GridView from './GridView'; -import ViewFilters from './ViewFilters'; - -interface ViewPanelProps { - viewMode: ViewType; - onViewModeChange: (mode: ViewType) => void; - onNodeClick: (nodeId: number) => void; - refreshTrigger?: number; -} - -export default function ViewPanel({ - viewMode, - onViewModeChange, - onNodeClick, - refreshTrigger -}: ViewPanelProps) { - const [nodes, setNodes] = useState([]); - const [loading, setLoading] = useState(true); - const [config, setConfig] = useState(DEFAULT_VIEW_CONFIG); - const [dimensions, setDimensions] = useState([]); - - // Fetch all nodes - const fetchNodes = useCallback(async () => { - setLoading(true); - try { - // Build query params from filters - const params = new URLSearchParams(); - params.set('limit', '500'); - - if (config.filters.length > 0) { - const includeDimensions = config.filters - .filter(f => f.operator === 'includes') - .map(f => f.dimension); - if (includeDimensions.length > 0) { - params.set('dimensions', includeDimensions.join(',')); - } - } - - if (config.sort) { - params.set('sortBy', config.sort.field); - params.set('sortOrder', config.sort.direction); - } - - const response = await fetch(`/api/nodes?${params.toString()}`); - if (response.ok) { - const data = await response.json(); - let filteredNodes = data.nodes || []; - - // Apply exclude filters client-side - const excludeDimensions = config.filters - .filter(f => f.operator === 'excludes') - .map(f => f.dimension); - if (excludeDimensions.length > 0) { - filteredNodes = filteredNodes.filter((node: Node) => - !excludeDimensions.some(dim => node.dimensions?.includes(dim)) - ); - } - - setNodes(filteredNodes); - } - } catch (error) { - console.error('Failed to fetch nodes:', error); - } finally { - setLoading(false); - } - }, [config.filters, config.sort]); - - // Fetch available dimensions - const fetchDimensions = useCallback(async () => { - try { - const response = await fetch('/api/dimensions/popular'); - if (response.ok) { - const data = await response.json(); - const dims = data.data?.map((d: { dimension: string }) => d.dimension) || []; - setDimensions(dims); - } - } catch (error) { - console.error('Failed to fetch dimensions:', error); - } - }, []); - - useEffect(() => { - fetchNodes(); - }, [fetchNodes, refreshTrigger]); - - useEffect(() => { - fetchDimensions(); - }, [fetchDimensions]); - - const handleFilterChange = (filters: ViewFilter[]) => { - setConfig(prev => ({ ...prev, filters })); - }; - - const handleFilterLogicChange = (logic: 'and' | 'or') => { - setConfig(prev => ({ ...prev, filterLogic: logic })); - }; - - const handleColumnChange = (columns: KanbanColumn[]) => { - setConfig(prev => ({ ...prev, columns })); - }; - - const handleNodeDimensionUpdate = async (nodeId: number, newDimension: string, oldDimension?: string) => { - // Update node's dimensions (add new, remove old if kanban move) - try { - const node = nodes.find(n => n.id === nodeId); - if (!node) return; - - let newDimensions = [...(node.dimensions || [])]; - - // Remove old dimension if moving in kanban - if (oldDimension) { - newDimensions = newDimensions.filter(d => d !== oldDimension); - } - - // Add new dimension if not already present - if (!newDimensions.includes(newDimension)) { - newDimensions.push(newDimension); - } - - const response = await fetch(`/api/nodes/${nodeId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ dimensions: newDimensions }) - }); - - if (response.ok) { - // Refresh nodes to get updated data - fetchNodes(); - } - } catch (error) { - console.error('Failed to update node dimensions:', error); - } - }; - - return ( -
- {/* Header with View Mode Selector */} -
-
- {nodes.length} nodes -
- - {/* View Mode Buttons */} -
- - - - -
-
- - {/* Filters Bar */} - - - {/* Content Area */} -
- {loading ? ( -
- Loading... -
- ) : ( - <> - {viewMode === 'list' && ( - - )} - {viewMode === 'kanban' && ( - - )} - {viewMode === 'grid' && ( - - )} - - )} -
-
- ); -} diff --git a/src/services/analytics/costs.ts b/src/services/analytics/costs.ts deleted file mode 100644 index bc87d08..0000000 --- a/src/services/analytics/costs.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { getSQLiteClient } from '@/services/database/sqlite-client'; -import { CostReport, TraceCostSummary, CacheEffectiveness } from '@/types/analytics'; - -export class CostAnalytics { - static getCostsByDateRange(startDate: string, endDate: string): CostReport { - const db = getSQLiteClient(); - - const chats = db.prepare(` - SELECT - id, - helper_name, - metadata, - created_at - FROM chats - WHERE created_at >= ? AND created_at <= ? - `).all(startDate, endDate) as Array<{ - id: number; - helper_name: string; - metadata: string; - created_at: string; - }>; - - let totalCostUsd = 0; - let totalTokens = 0; - let inputTokens = 0; - let outputTokens = 0; - let cacheReadTokens = 0; - let cacheWriteTokens = 0; - let cacheHitCount = 0; - let cacheSavingsUsd = 0; - - const costByAgent: Record = {}; - const costByModel: Record = {}; - - for (const chat of chats) { - let metadata: any = {}; - try { - metadata = JSON.parse(chat.metadata || '{}'); - } catch (e) { - continue; - } - - const costUsd = metadata.estimated_cost_usd || 0; - const tokens = metadata.total_tokens || 0; - const inputToks = metadata.input_tokens || 0; - const outputToks = metadata.output_tokens || 0; - const cacheRead = metadata.cache_read_tokens || 0; - const cacheWrite = metadata.cache_write_tokens || 0; - const cacheHit = metadata.cache_hit || false; - const model = metadata.model_used || 'unknown'; - - totalCostUsd += costUsd; - totalTokens += tokens; - inputTokens += inputToks; - outputTokens += outputToks; - cacheReadTokens += cacheRead; - cacheWriteTokens += cacheWrite; - if (cacheHit) cacheHitCount++; - - if (metadata.cache_savings_pct && cacheHit) { - const fullCost = costUsd / (1 - (metadata.cache_savings_pct / 100)); - cacheSavingsUsd += (fullCost - costUsd); - } - - if (!costByAgent[chat.helper_name]) { - costByAgent[chat.helper_name] = { costUsd: 0, chats: 0, tokens: 0 }; - } - costByAgent[chat.helper_name].costUsd += costUsd; - costByAgent[chat.helper_name].chats += 1; - costByAgent[chat.helper_name].tokens += tokens; - - if (!costByModel[model]) { - costByModel[model] = { costUsd: 0, chats: 0, tokens: 0 }; - } - costByModel[model].costUsd += costUsd; - costByModel[model].chats += 1; - costByModel[model].tokens += tokens; - } - - const cacheRequests = chats.filter(c => { - try { - const meta = JSON.parse(c.metadata || '{}'); - return meta.provider === 'anthropic'; - } catch { - return false; - } - }).length; - - return { - periodStart: startDate, - periodEnd: endDate, - totalCostUsd: parseFloat(totalCostUsd.toFixed(6)), - totalChats: chats.length, - totalTokens, - inputTokens, - outputTokens, - cacheReadTokens, - cacheWriteTokens, - cacheHitRate: cacheRequests > 0 ? cacheHitCount / cacheRequests : 0, - cacheSavingsUsd: parseFloat(cacheSavingsUsd.toFixed(6)), - avgCostPerChat: chats.length > 0 ? parseFloat((totalCostUsd / chats.length).toFixed(6)) : 0, - avgTokensPerChat: chats.length > 0 ? Math.round(totalTokens / chats.length) : 0, - costByAgent, - costByModel, - }; - } - - static getCostsByAgent(agentName: string, startDate?: string, endDate?: string): CostReport { - const db = getSQLiteClient(); - - let query = ` - SELECT - id, - helper_name, - metadata, - created_at - FROM chats - WHERE helper_name = ? - `; - const params: any[] = [agentName]; - - if (startDate && endDate) { - query += ` AND created_at >= ? AND created_at <= ?`; - params.push(startDate, endDate); - } - - const chats = db.prepare(query).all(...params) as Array<{ - id: number; - helper_name: string; - metadata: string; - created_at: string; - }>; - - if (chats.length === 0) { - return { - periodStart: startDate || '', - periodEnd: endDate || '', - totalCostUsd: 0, - totalChats: 0, - totalTokens: 0, - inputTokens: 0, - outputTokens: 0, - cacheReadTokens: 0, - cacheWriteTokens: 0, - cacheHitRate: 0, - cacheSavingsUsd: 0, - avgCostPerChat: 0, - avgTokensPerChat: 0, - costByAgent: {}, - costByModel: {}, - }; - } - - const start = startDate || chats[chats.length - 1].created_at; - const end = endDate || chats[0].created_at; - - return this.getCostsByDateRange(start, end); - } - - static getCostsByTrace(traceId: string): TraceCostSummary { - const db = getSQLiteClient(); - - const chats = db.prepare(` - SELECT - id, - helper_name, - agent_type, - metadata, - created_at - FROM chats - WHERE json_extract(metadata, '$.trace_id') = ? - ORDER BY created_at ASC - `).all(traceId) as Array<{ - id: number; - helper_name: string; - agent_type: string; - metadata: string; - created_at: string; - }>; - - let totalCostUsd = 0; - let orchestratorCost = 0; - let executorCost = 0; - let plannerCost = 0; - let totalTokens = 0; - - const interactions: TraceCostSummary['interactions'] = []; - - for (const chat of chats) { - let metadata: any = {}; - try { - metadata = JSON.parse(chat.metadata || '{}'); - } catch (e) { - continue; - } - - const costUsd = metadata.estimated_cost_usd || 0; - const tokens = metadata.total_tokens || 0; - - totalCostUsd += costUsd; - totalTokens += tokens; - - if (chat.agent_type === 'orchestrator') { - orchestratorCost += costUsd; - } else if (chat.agent_type === 'executor') { - executorCost += costUsd; - } else if (chat.agent_type === 'planner') { - plannerCost += costUsd; - } - - interactions.push({ - chatId: chat.id, - agentName: chat.helper_name, - costUsd: parseFloat(costUsd.toFixed(6)), - tokens, - createdAt: chat.created_at, - }); - } - - return { - traceId, - totalCostUsd: parseFloat(totalCostUsd.toFixed(6)), - chatCount: chats.length, - orchestratorCost: parseFloat(orchestratorCost.toFixed(6)), - executorCost: parseFloat(executorCost.toFixed(6)), - plannerCost: parseFloat(plannerCost.toFixed(6)), - totalTokens, - interactions, - }; - } - - static getCacheEffectiveness(startDate?: string, endDate?: string): CacheEffectiveness { - const db = getSQLiteClient(); - - let query = ` - SELECT - metadata - FROM chats - WHERE json_extract(metadata, '$.provider') = 'anthropic' - `; - const params: any[] = []; - - if (startDate && endDate) { - query += ` AND created_at >= ? AND created_at <= ?`; - params.push(startDate, endDate); - } - - const chats = db.prepare(query).all(...params) as Array<{ metadata: string }>; - - let totalRequests = 0; - let cacheHits = 0; - let cacheMisses = 0; - let totalCacheSavingsUsd = 0; - let totalTokensSaved = 0; - - for (const chat of chats) { - let metadata: any = {}; - try { - metadata = JSON.parse(chat.metadata || '{}'); - } catch (e) { - continue; - } - - totalRequests++; - - if (metadata.cache_hit) { - cacheHits++; - const cacheReadTokens = metadata.cache_read_tokens || 0; - totalTokensSaved += cacheReadTokens; - - if (metadata.cache_savings_pct && metadata.estimated_cost_usd) { - const fullCost = metadata.estimated_cost_usd / (1 - (metadata.cache_savings_pct / 100)); - totalCacheSavingsUsd += (fullCost - metadata.estimated_cost_usd); - } - } else { - cacheMisses++; - } - } - - return { - totalRequests, - cacheHits, - cacheMisses, - hitRate: totalRequests > 0 ? parseFloat((cacheHits / totalRequests).toFixed(4)) : 0, - totalCacheSavingsUsd: parseFloat(totalCacheSavingsUsd.toFixed(6)), - avgSavingsPerHit: cacheHits > 0 ? parseFloat((totalCacheSavingsUsd / cacheHits).toFixed(6)) : 0, - totalTokensSaved, - }; - } - - static getDailyBreakdown(days: number = 7): Array<{ date: string; cost: number; chats: number; tokens: number }> { - const db = getSQLiteClient(); - - const result = db.prepare(` - SELECT - DATE(created_at) as date, - SUM(COALESCE(json_extract(metadata, '$.estimated_cost_usd'), 0)) as cost, - COUNT(*) as chats, - SUM(COALESCE(json_extract(metadata, '$.total_tokens'), 0)) as tokens - FROM chats - WHERE created_at >= DATE('now', '-' || ? || ' days') - GROUP BY DATE(created_at) - ORDER BY date DESC - `).all(days) as Array<{ date: string; cost: number; chats: number; tokens: number }>; - - return result.map(row => ({ - date: row.date, - cost: parseFloat(Number(row.cost).toFixed(6)), - chats: Number(row.chats), - tokens: Number(row.tokens), - })); - } -} diff --git a/src/services/tools/resultCache.ts b/src/services/tools/resultCache.ts deleted file mode 100644 index bcb87fa..0000000 --- a/src/services/tools/resultCache.ts +++ /dev/null @@ -1,32 +0,0 @@ -type Entry = { data: any; ts: number }; - -class ResultCache { - private store = new Map(); - private ttlMs = 10 * 60 * 1000; // 10 minutes - - set(id: string, data: any) { - if (!id) return; - this.store.set(id, { data, ts: Date.now() }); - this.gc(); - } - - get(id: string): any | null { - const e = this.store.get(id); - if (!e) return null; - if (Date.now() - e.ts > this.ttlMs) { - this.store.delete(id); - return null; - } - return e.data; - } - - private gc() { - const now = Date.now(); - for (const [k, v] of this.store.entries()) { - if (now - v.ts > this.ttlMs) this.store.delete(k); - } - } -} - -export const resultCache = new ResultCache(); - diff --git a/src/styles/terminal-theme.css b/src/styles/terminal-theme.css deleted file mode 100644 index 171cd7f..0000000 --- a/src/styles/terminal-theme.css +++ /dev/null @@ -1,398 +0,0 @@ -/* RA-H Terminal Theme Design System */ - -:root { - /* Core Backgrounds - Ultra minimal dark */ - --bg-primary: #0a0a0a; /* Main app background */ - --bg-secondary: #0f0f0f; /* Panel backgrounds */ - --bg-surface: #151515; /* Elevated surfaces */ - --bg-hover: #1a1a1a; /* Hover states */ - - /* Borders & Dividers - Subtle definition */ - --border-subtle: #1f1f1f; /* Very subtle borders */ - --border-default: #2a2a2a; /* Default borders */ - --border-strong: #353535; /* Strong emphasis borders */ - - /* Text Hierarchy - High contrast */ - --text-primary: #e5e5e5; /* Primary text */ - --text-secondary: #a8a8a8; /* Secondary text */ - --text-muted: #6b6b6b; /* Muted/disabled text */ - --text-subtle: #4a4a4a; /* Very subtle text */ - - /* Terminal Accent Colors - Distinctive but minimal */ - --accent-blue: #5c9aff; /* Primary actions / User messages */ - --accent-green: #52d97a; /* Success / Assistant messages */ - --accent-yellow: #ffcc66; /* Warning / Tool usage */ - --accent-red: #ff6b6b; /* Error states */ - --accent-purple: #b794f6; /* Thinking / Processing */ - --accent-cyan: #69d2e7; /* Special highlights */ - - /* Message Status Indicators */ - --dot-user: #5c9aff; /* User message indicator */ - --dot-assistant: #52d97a; /* Assistant indicator */ - --dot-tool: #ffcc66; /* Tool usage indicator */ - --dot-thinking: #b794f6; /* Thinking indicator */ - --dot-system: #69d2e7; /* System messages */ - - /* Semantic Colors */ - --color-success: #52d97a; - --color-warning: #ffcc66; - --color-error: #ff6b6b; - --color-info: #5c9aff; - - /* Typography */ - --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', ui-monospace, Consolas, monospace; - --font-size-xs: 11px; - --font-size-sm: 12px; - --font-size-base: 13px; - --font-size-md: 14px; - --font-size-lg: 16px; - - /* Spacing */ - --spacing-xs: 4px; - --spacing-sm: 8px; - --spacing-md: 12px; - --spacing-lg: 16px; - --spacing-xl: 24px; - - /* Animations */ - --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Base Terminal Styles */ -.terminal-container { - background: var(--bg-primary); - color: var(--text-primary); - font-family: var(--font-mono); - font-size: var(--font-size-base); - line-height: 1.6; - letter-spacing: 0.025em; - font-feature-settings: 'liga' 1, 'calt' 1; -} - -/* Message Display Styles */ -.message-container { - display: flex; - gap: var(--spacing-md); - padding: var(--spacing-sm) 0; - font-size: var(--font-size-base); - line-height: 1.6; -} - -.message-indicator { - flex-shrink: 0; - width: 6px; - height: 6px; - margin-top: 7px; - border-radius: 50%; - transition: all var(--transition-fast); -} - -.message-indicator.user { - background: var(--dot-user); -} - -.message-indicator.assistant { - background: var(--dot-assistant); -} - -.message-indicator.tool { - background: var(--dot-tool); -} - -.message-indicator.thinking { - background: var(--dot-thinking); - animation: pulse 1.5s infinite; -} - -.message-indicator.system { - background: var(--dot-system); -} - -@keyframes pulse { - 0%, 100% { - opacity: 1; - transform: scale(1); - } - 50% { - opacity: 0.6; - transform: scale(1.2); - } -} - -.message-content { - flex: 1; - min-width: 0; -} - -.message-text { - color: var(--text-primary); - white-space: pre-wrap; - word-break: break-word; -} - -.message-timestamp { - display: inline-block; - margin-top: var(--spacing-xs); - color: var(--text-subtle); - font-size: var(--font-size-xs); -} - -/* Terminal Input Styles */ -.terminal-input-container { - display: flex; - align-items: flex-start; - gap: var(--spacing-sm); - padding: var(--spacing-md); - background: var(--bg-secondary); - border-top: 1px solid var(--border-default); -} - -.prompt-symbol { - color: var(--text-muted); - font-size: var(--font-size-md); - line-height: 1.5; - padding-top: 2px; - user-select: none; -} - -.terminal-input-wrapper { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} - -.terminal-input { - background: var(--bg-surface); - border: 1px solid var(--border-subtle); - color: var(--text-primary); - font-family: var(--font-mono); - font-size: var(--font-size-base); - padding: var(--spacing-sm) var(--spacing-md); - resize: none; - outline: none; - transition: all var(--transition-fast); - border-radius: 2px; - min-height: 32px; - line-height: 1.5; -} - -.terminal-input:focus { - border-color: var(--accent-blue); - background: var(--bg-primary); - box-shadow: 0 0 0 1px rgba(92, 154, 255, 0.1); -} - -.terminal-input:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.terminal-input::placeholder { - color: var(--text-subtle); -} - -.input-actions { - display: flex; - align-items: center; - gap: var(--spacing-md); -} - -.input-hints { - display: flex; - gap: var(--spacing-md); - color: var(--text-subtle); - font-size: var(--font-size-xs); - user-select: none; -} - -.input-hint { - display: flex; - align-items: center; - gap: var(--spacing-xs); -} - -.key-hint { - padding: 2px 4px; - background: var(--bg-surface); - border: 1px solid var(--border-subtle); - border-radius: 2px; - font-size: 10px; -} - -/* Tool Indicator Styles */ -.tool-indicator { - display: inline-flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-xs) 0; - color: var(--text-secondary); - font-size: var(--font-size-sm); -} - -.tool-spinner { - display: inline-block; - width: 12px; - height: 12px; - border: 2px solid var(--border-default); - border-top-color: var(--dot-tool); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.tool-name { - color: var(--dot-tool); - font-weight: 500; -} - -.tool-status { - color: var(--text-muted); -} - -.tool-complete { - color: var(--color-success); -} - -.tool-error { - color: var(--color-error); -} - -/* ASCII Banner Styles */ -.ascii-banner { - padding: var(--spacing-xl); - color: var(--text-muted); - font-size: var(--font-size-sm); - line-height: 1.2; - white-space: pre; - font-family: var(--font-mono); - opacity: 0; - animation: fadeIn 0.5s ease-out forwards; -} - -@keyframes fadeIn { - to { opacity: 1; } -} - -.ascii-banner-title { - color: var(--text-secondary); -} - -.ascii-banner-stats { - color: var(--text-subtle); -} - -/* Helper Tab Styles */ -.helper-tab { - position: relative; - padding: var(--spacing-sm) var(--spacing-md); - background: transparent; - border: none; - border-bottom: 2px solid transparent; - color: var(--text-muted); - font-family: var(--font-mono); - font-size: var(--font-size-sm); - cursor: pointer; - transition: all var(--transition-fast); - outline: none; - white-space: nowrap; -} - -.helper-tab:hover { - background: var(--bg-hover); - color: var(--text-secondary); -} - -.helper-tab.active { - background: var(--bg-primary); - border-bottom-color: var(--accent-blue); - color: var(--text-primary); -} - -/* Scrollbar Styles */ -.terminal-scrollbar::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -.terminal-scrollbar::-webkit-scrollbar-track { - background: var(--bg-primary); -} - -.terminal-scrollbar::-webkit-scrollbar-thumb { - background: var(--border-strong); - border-radius: 2px; -} - -.terminal-scrollbar::-webkit-scrollbar-thumb:hover { - background: var(--text-subtle); -} - -/* Loading States */ -.loading-dots { - display: inline-flex; - gap: 2px; -} - -.loading-dot { - width: 4px; - height: 4px; - border-radius: 50%; - background: var(--text-muted); - animation: loadingPulse 1.4s infinite ease-in-out both; -} - -.loading-dot:nth-child(1) { - animation-delay: -0.32s; -} - -.loading-dot:nth-child(2) { - animation-delay: -0.16s; -} - -@keyframes loadingPulse { - 0%, 80%, 100% { - transform: scale(0.8); - opacity: 0.5; - } - 40% { - transform: scale(1); - opacity: 1; - } -} - -/* Focus States for Accessibility */ -.terminal-focusable:focus-visible { - outline: 2px solid var(--accent-blue); - outline-offset: 2px; -} - -/* Utility Classes */ -.terminal-text-muted { - color: var(--text-muted); -} - -.terminal-text-subtle { - color: var(--text-subtle); -} - -.terminal-text-error { - color: var(--color-error); -} - -.terminal-text-success { - color: var(--color-success); -} - -.terminal-bg-surface { - background: var(--bg-surface); -} - -.terminal-border-default { - border-color: var(--border-default); -} \ No newline at end of file diff --git a/src/styles/terminalTheme.ts b/src/styles/terminalTheme.ts deleted file mode 100644 index 91f6b4a..0000000 --- a/src/styles/terminalTheme.ts +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Terminal Theme Constants for ra-h - * Centralized styling for terminal-inspired UI elements - */ - -import { CSSProperties } from 'react'; - -// Terminal Color Palette -export const TERMINAL_COLORS = { - // Message indicators (Claude Code style) - user: '#3b82f6', // Blue dot for user messages - assistant: '#10b981', // Green dot for assistant messages - tool: '#f59e0b', // Orange dot for tool use - processing: '#eab308', // Yellow dot for thinking/processing states - - // Terminal elements - prompt: '#10b981', // Terminal green for $ prompt - accent: '#10b981', // Primary terminal accent color - - // Base colors (terminal-inspired palette) - bg: { - primary: '#000', // Main background - secondary: '#000', // Secondary background (input area) - same as primary - elevated: '#000', // Elevated surfaces (message bubbles) - same as primary for borderless look - }, - - text: { - primary: '#d1d5db', // Primary text - secondary: '#666', // Muted text - tertiary: '#444', // Very muted text - }, - - border: { - primary: '#333', // Primary borders - secondary: '#222', // Subtle borders - } -} as const; - -// Terminal Typography -export const TERMINAL_FONTS = { - mono: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', - system: 'inherit' -} as const; - -// Terminal Component Styles -export const TERMINAL_STYLES = { - // Message header with colored dot indicator - messageHeader: (role: 'user' | 'assistant' | 'tool'): CSSProperties => ({ - fontSize: '10px', - color: TERMINAL_COLORS.text.secondary, - marginBottom: '4px', - textTransform: 'uppercase' as const, - letterSpacing: '0.05em', - display: 'flex', - alignItems: 'center', - gap: '6px', - fontFamily: TERMINAL_FONTS.mono - }), - - // Colored dot indicator - messageIndicator: (role: 'user' | 'assistant' | 'tool' | 'processing'): CSSProperties => ({ - fontSize: '8px', - color: TERMINAL_COLORS[role], - width: '6px', - height: '6px', - flexShrink: 0 - }), - - // Message bubble (borderless for cleaner terminal feel) - messageBubble: (role: 'user' | 'assistant'): CSSProperties => ({ - background: role === 'user' ? TERMINAL_COLORS.bg.elevated : TERMINAL_COLORS.bg.secondary, - border: 'none', // Removed borders for cleaner terminal look - borderRadius: '0px', // Sharp terminal corners - padding: '12px 16px', - maxWidth: '80%', - fontSize: '13px', - lineHeight: '1.5', - color: TERMINAL_COLORS.text.primary, - whiteSpace: 'pre-wrap' as const, - fontFamily: TERMINAL_FONTS.mono - }), - - // Message timestamp - messageTimestamp: (): CSSProperties => ({ - fontSize: '9px', - color: TERMINAL_COLORS.text.tertiary, - marginTop: '6px', - fontFamily: TERMINAL_FONTS.mono - }), - - // Terminal input container - terminalInputContainer: (): CSSProperties => ({ - position: 'relative' as const, - display: 'flex', - alignItems: 'center', - flex: 1 - }), - - // Terminal prompt symbol - terminalPrompt: (): CSSProperties => ({ - position: 'absolute' as const, - left: '12px', - color: TERMINAL_COLORS.prompt, - fontSize: '13px', - fontFamily: TERMINAL_FONTS.mono, - zIndex: 1, - pointerEvents: 'none' as const - }), - - // Terminal input field (subtle border, auto-expanding) - terminalInput: (): CSSProperties => ({ - width: '100%', - background: TERMINAL_COLORS.bg.elevated, - border: `1px solid ${TERMINAL_COLORS.border.secondary}`, // More subtle border - borderRadius: '4px', - padding: '8px 12px 8px 24px', // Extra left padding for $ symbol - fontSize: '13px', - color: TERMINAL_COLORS.text.primary, - fontFamily: TERMINAL_FONTS.mono, - resize: 'none' as const, - minHeight: '36px', - maxHeight: '120px', // Increased max height - outline: 'none', - transition: 'border-color 0.2s, height 0.1s ease', - overflow: 'hidden' // For auto-resize - }), - - // Terminal send button - terminalButton: (disabled: boolean = false): CSSProperties => ({ - padding: '8px 16px', - background: disabled ? TERMINAL_COLORS.border.primary : TERMINAL_COLORS.accent, - color: disabled ? TERMINAL_COLORS.text.secondary : '#fff', - border: `1px solid ${TERMINAL_COLORS.border.primary}`, - borderRadius: '4px', - fontSize: '13px', - cursor: disabled ? 'not-allowed' : 'pointer', - fontFamily: TERMINAL_FONTS.mono, - transition: 'all 0.2s', - display: 'flex', - alignItems: 'center', - gap: '4px' - }), - - // Chat messages container - messagesContainer: (): CSSProperties => ({ - flex: 1, - overflow: 'auto', - padding: '16px', - fontFamily: TERMINAL_FONTS.mono - }), - - // Thinking indicator - thinkingIndicator: (): CSSProperties => ({ - display: 'flex', - alignItems: 'center', - gap: '8px', - padding: '12px 16px', - background: TERMINAL_COLORS.bg.secondary, - borderRadius: '0px', - margin: '8px 0', - fontSize: '12px', - color: TERMINAL_COLORS.text.secondary, - fontFamily: TERMINAL_FONTS.mono, - fontStyle: 'italic' - }), - - // Tool use indicator (separate from messages) - toolIndicator: (): CSSProperties => ({ - display: 'flex', - alignItems: 'center', - gap: '8px', - padding: '8px 12px', - background: 'rgba(245, 158, 11, 0.1)', // Orange background with transparency - border: `1px solid ${TERMINAL_COLORS.tool}`, - borderRadius: '0px', - margin: '4px 0', - fontSize: '11px', - color: TERMINAL_COLORS.tool, - fontFamily: TERMINAL_FONTS.mono - }), - - // Input form container (remove border) - inputForm: (): CSSProperties => ({ - borderTop: 'none', // Remove border between chat and input - padding: '16px', - background: TERMINAL_COLORS.bg.primary // Same as main background for seamless look - }) -} as const; - -// Terminal Tool Indicators -export const TERMINAL_TOOLS = { - processing: '⟡ Processing...', - generating: '▸ Generating response...', - thinking: (displayName: string) => `${displayName} is thinking...`, - toolPrefix: '⟩', - thinkingDots: '⋯' -} as const; - -// Terminal Animations (for use in CSS) -export const TERMINAL_ANIMATIONS = { - pulse: 'pulse 1.5s ease-in-out infinite', - typing: 'typing 1s ease-in-out infinite' -} as const; - -// Helper function to get role-based styling -export const getMessageStyles = (role: 'user' | 'assistant', isLoading?: boolean) => ({ - header: TERMINAL_STYLES.messageHeader(role), - indicator: TERMINAL_STYLES.messageIndicator(isLoading ? 'processing' : role), - bubble: TERMINAL_STYLES.messageBubble(role), - timestamp: TERMINAL_STYLES.messageTimestamp() -}); - -// Helper function to get input styling with focus states -export const getTerminalInputStyles = (isFocused: boolean = false) => ({ - container: TERMINAL_STYLES.terminalInputContainer(), - prompt: TERMINAL_STYLES.terminalPrompt(), - input: { - ...TERMINAL_STYLES.terminalInput(), - borderColor: isFocused ? TERMINAL_COLORS.accent : TERMINAL_COLORS.border.secondary - } -}); - -// Auto-resize textarea helper function -export const autoResizeTextarea = (textarea: HTMLTextAreaElement) => { - textarea.style.height = 'auto'; - const newHeight = Math.min(textarea.scrollHeight, 120); // Max 120px - textarea.style.height = `${Math.max(newHeight, 36)}px`; // Min 36px -}; - -// Thinking indicator component props -export const getThinkingIndicator = (displayName: string, isVisible: boolean) => { - if (!isVisible) return null; - - return { - style: TERMINAL_STYLES.thinkingIndicator(), - content: TERMINAL_TOOLS.thinking(displayName), - dots: TERMINAL_TOOLS.thinkingDots - }; -}; - -// Tool use indicator component props -export const getToolIndicator = (toolName: string, isActive: boolean) => { - if (!isActive) return null; - - return { - style: TERMINAL_STYLES.toolIndicator(), - content: `${TERMINAL_TOOLS.toolPrefix} ${toolName}`, - isActive - }; -}; \ No newline at end of file diff --git a/src/tools/database/lockDimension.ts b/src/tools/database/lockDimension.ts deleted file mode 100644 index e62f67d..0000000 --- a/src/tools/database/lockDimension.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { tool } from 'ai'; -import { z } from 'zod'; - -export const lockDimensionTool = tool({ - description: 'Lock a dimension to enable auto-assignment to new nodes', - inputSchema: z.object({ - name: z.string().describe('Dimension name to lock') - }), - execute: async (params) => { - console.log('🔒 LockDimension tool called with params:', JSON.stringify(params, null, 2)); - try { - const trimmedName = params.name.trim(); - if (!trimmedName) { - return { - success: false, - error: 'Dimension name is required', - data: null - }; - } - - const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/dimensions`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: trimmedName, - isPriority: true - }) - }); - - if (!response.ok) { - let errorMessage = 'Failed to lock dimension'; - try { - const errorResult = await response.json(); - errorMessage = errorResult.error || errorMessage; - } catch { - // If response is not JSON (e.g., HTML error page), use status text - errorMessage = `Failed to lock dimension: ${response.status} ${response.statusText}`; - } - return { - success: false, - error: errorMessage, - data: null - }; - } - - const result = await response.json(); - - return { - success: true, - data: result.data, - message: `Locked dimension "${trimmedName}" - it will now be auto-assigned to new nodes` - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to lock dimension', - data: null - }; - } - } -}); - diff --git a/src/tools/database/unlockDimension.ts b/src/tools/database/unlockDimension.ts deleted file mode 100644 index 7ef79b0..0000000 --- a/src/tools/database/unlockDimension.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { tool } from 'ai'; -import { z } from 'zod'; - -export const unlockDimensionTool = tool({ - description: 'Unlock a dimension to disable auto-assignment to new nodes', - inputSchema: z.object({ - name: z.string().describe('Dimension name to unlock') - }), - execute: async (params) => { - console.log('🔓 UnlockDimension tool called with params:', JSON.stringify(params, null, 2)); - try { - const trimmedName = params.name.trim(); - if (!trimmedName) { - return { - success: false, - error: 'Dimension name is required', - data: null - }; - } - - const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/dimensions`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: trimmedName, - isPriority: false - }) - }); - - if (!response.ok) { - let errorMessage = 'Failed to unlock dimension'; - try { - const errorResult = await response.json(); - errorMessage = errorResult.error || errorMessage; - } catch { - // If response is not JSON (e.g., HTML error page), use status text - errorMessage = `Failed to unlock dimension: ${response.status} ${response.statusText}`; - } - return { - success: false, - error: errorMessage, - data: null - }; - } - - const result = await response.json(); - - return { - success: true, - data: result.data, - message: `Unlocked dimension "${trimmedName}" - it will no longer be auto-assigned to new nodes` - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to unlock dimension', - data: null - }; - } - } -}); - diff --git a/src/tools/other/embedContent.ts b/src/tools/other/embedContent.ts deleted file mode 100644 index bd57c90..0000000 --- a/src/tools/other/embedContent.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { tool } from 'ai'; -import { z } from 'zod'; - -export const embedContentTool = tool({ - description: 'Chunk and embed a node’s content so semantic search stays current', - inputSchema: z.object({ - nodeId: z.number().describe('The ID of the node to process') - }), - execute: async ({ nodeId }) => { - try { - // Call the same API endpoint that the manual embed button uses - const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/ingestion`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ nodeId }) - }); - - const result = await response.json(); - - if (!response.ok) { - return { - success: false, - error: result.error || 'Failed to embed content', - data: null - }; - } - - return { - success: true, - message: result.message || `Successfully chunked and embedded content for node ${nodeId}`, - data: { nodeId } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to embed content', - data: null - }; - } - } -}); diff --git a/src/types/analytics.ts b/src/types/analytics.ts index 027f5d8..52fc72d 100644 --- a/src/types/analytics.ts +++ b/src/types/analytics.ts @@ -23,92 +23,6 @@ export interface UsageData { mode?: 'easy' | 'hard'; } -export interface EnhancedChatMetadata { - timestamp: string; - session_id: string; - current_view: 'nodes' | 'memory'; - open_tab_count: number; - has_focused_node: boolean; - message_count: number; - input_tokens?: number; - output_tokens?: number; - total_tokens?: number; - cache_write_tokens?: number; - cache_read_tokens?: number; - cache_hit?: boolean; - cache_savings_pct?: number; - estimated_cost_usd?: number; - model_used?: string; - provider?: 'anthropic' | 'openai'; - tools_used?: string[]; - tool_calls_count?: number; - trace_id?: string; - parent_chat_id?: number; - voice_tts_chars?: number; - voice_tts_cost_usd?: number; - voice_tts_chars_total?: number; - voice_tts_cost_usd_total?: number; - voice_request_id?: string; - voice_tts_request_count?: number; - voice_usage?: Array<{ - request_id: string; - message_id?: string | null; - chars: number; - cost_usd: number; - voice?: string; - model?: string; - duration_ms?: number | null; - logged_at?: string; - }>; -} - -export interface CostReport { - periodStart: string; - periodEnd: string; - totalCostUsd: number; - totalChats: number; - totalTokens: number; - inputTokens: number; - outputTokens: number; - cacheReadTokens: number; - cacheWriteTokens: number; - cacheHitRate: number; - cacheSavingsUsd: number; - avgCostPerChat: number; - avgTokensPerChat: number; - costByAgent: { - [agentName: string]: { - costUsd: number; - chats: number; - tokens: number; - }; - }; - costByModel: { - [modelId: string]: { - costUsd: number; - chats: number; - tokens: number; - }; - }; -} - -export interface TraceCostSummary { - traceId: string; - totalCostUsd: number; - chatCount: number; - orchestratorCost: number; - executorCost: number; - plannerCost: number; - totalTokens: number; - interactions: Array<{ - chatId: number; - agentName: string; - costUsd: number; - tokens: number; - createdAt: string; - }>; -} - export interface ModelPricing { provider: 'anthropic' | 'openai'; inputPer1M: number; @@ -117,12 +31,3 @@ export interface ModelPricing { cacheReadPer1M?: number; } -export interface CacheEffectiveness { - totalRequests: number; - cacheHits: number; - cacheMisses: number; - hitRate: number; - totalCacheSavingsUsd: number; - avgSavingsPerHit: number; - totalTokensSaved: number; -} diff --git a/src/types/database.ts b/src/types/database.ts index b33feba..3b068fe 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -21,27 +21,6 @@ export interface Node { chunk_status?: 'not_chunked' | 'chunking' | 'chunked' | 'error' | null; } -// Legacy Item interface - DEPRECATED, use Node instead -// Kept temporarily for migration compatibility -export interface Item extends Node { - // Legacy fields for backwards compatibility during transition - description?: string; - abstract?: string; - type?: string; - legacyType?: string[]; - stage?: string; - segment?: string[]; - tags?: string[]; - sub_type?: any; - notes?: any; - extras?: any; - score?: number; - content_embedding?: number[]; - embedding_updated_at?: string; - chunk_status?: 'not_chunked' | 'chunking' | 'chunked' | 'error'; - chunk_updated_at?: string; -} - export interface Chunk { id: number; node_id: number; // Updated from item_id to node_id @@ -85,36 +64,6 @@ export interface EdgeContext { created_via: EdgeCreatedVia; } -export interface Chat { - id: number; - user_message?: string; - assistant_message?: string; - thread_id: string; - focused_node_id?: number; // Updated from focused_item_id - metadata?: any; - embedding?: number[]; // Renamed from content_embedding - created_at: string; -} - -export interface SessionContext { - id: number; - session_id: string; - focused_node_id: number; // Updated from focused_item_id - context_data: any; - created_at: string; - last_accessed: string; - expires_at: string; -} - -export interface SessionCache { - id: number; - session_id: string; - cache_key: string; - cache_data: any; - expires_at: string; - created_at: string; -} - // New NodeFilters interface replacing rigid ItemFilters export interface NodeFilters { dimensions?: string[]; // Filter by dimensions (replaces stage/type filtering) @@ -124,13 +73,6 @@ export interface NodeFilters { sortBy?: 'updated' | 'edges'; // Sort by updated_at or edge count } -// Legacy filters - DEPRECATED, use NodeFilters instead -export interface ItemFilters extends NodeFilters { - stage?: string; - type?: string; - tags?: string[]; -} - export interface ChunkData { node_id: number; // Updated from item_id chunk_idx?: number; @@ -158,13 +100,6 @@ export interface ChatData { embedding?: number[]; // Renamed from content_embedding } -export interface CachedContext { - sessionId: string; - focusedNodeId: number; // Updated from focusedItemId - contextData: any; - expiresAt: string; -} - // New NodeConnection interface export interface NodeConnection { id: number; @@ -172,25 +107,6 @@ export interface NodeConnection { edge: Edge; } -// Legacy connection - DEPRECATED, use NodeConnection instead -export interface ItemConnection { - id: number; - connected_item: Item; - edge: Edge; -} - -export interface DatabaseConfig { - host: string; - port: number; - database: string; - user: string; - password: string; - ssl?: boolean; - connectionTimeoutMillis?: number; - idleTimeoutMillis?: number; - max?: number; -} - export interface DatabaseError { message: string; code?: string; diff --git a/src/types/helpers.ts b/src/types/helpers.ts deleted file mode 100644 index 5087cd4..0000000 --- a/src/types/helpers.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface MemoryContext { - insights: { - user_patterns: string[]; - preferences: string[]; - common_queries: string[]; - knowledge_domains: string[]; - }; - context: string; -} - -export interface HelperConfig { - name: string; - display_name: string; - description: string; - system_prompt: string; - component_key: string; - available_tools: string[]; - enabled: boolean; - created_at: string; - updated_at: string; - memory?: MemoryContext; -} diff --git a/tailwind.config.ts b/tailwind.config.ts index 40d698a..09f5f15 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,80 +1,12 @@ import type { Config } from "tailwindcss"; const config = { - darkMode: ["class"], content: [ - "./pages/**/*.{ts,tsx}", - "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", ], - prefix: "", - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - keyframes: { - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, - }, - }, - plugins: [require("tailwindcss-animate")], + theme: { extend: {} }, + plugins: [], } satisfies Config; -export default config; \ No newline at end of file +export default config;