diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 776e516..b7d3f92 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,14 +21,28 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' - cache: 'npm' - - name: Install dependencies + - name: Install backend dependencies + working-directory: ./backend run: npm ci - - name: Run ESLint + - name: Install frontend dependencies + working-directory: ./frontend + run: npm ci + + - name: Run ESLint (backend) + working-directory: ./backend + run: npm run lint + + - name: Run ESLint (frontend) + working-directory: ./frontend run: npm run lint - - name: Check Prettier formatting + - name: Check Prettier formatting (backend) + working-directory: ./backend + run: npm run format:check + + - name: Check Prettier formatting (frontend) + working-directory: ./frontend run: npm run format:check diff --git a/.gitignore b/.gitignore index 82386e2..2d6f407 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,8 @@ backend/audio/ .DS_Store CLAUDE.md templates/ +.claude + +# Deployment files +deploy/ +.gcloudignore diff --git a/.prettierrc.json b/.prettierrc.json index 3171de5..63d982b 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -8,3 +8,4 @@ "arrowParens": "always", "endOfLine": "lf" } + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c024b46..563af65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,7 @@ Thank you for your interest in contributing to the Language Learning App! This d ```bash INWORLD_API_KEY=your_api_key_here + ASSEMBLY_AI_API_KEY=your_api_key_here ``` 5. **Verify the setup**: diff --git a/README.md b/README.md index 92c0284..281130a 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,20 @@ -# Language Learning - Aprendemo +# Inworld Language Tutor [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![Powered by Inworld AI](https://img.shields.io/badge/Powered_by-Inworld_AI-orange)](https://inworld.ai/runtime) [![Documentation](https://img.shields.io/badge/Documentation-Read_Docs-blue)](https://docs.inworld.ai/docs/node/overview) [![Model Providers](https://img.shields.io/badge/Model_Providers-See_Models-purple)](https://docs.inworld.ai/docs/models#llm) -A Node.js app where you can learn Spanish through conversation and flashcard studying, powered by Inworld AI Runtime. "Aprendemo" combines "Aprender" (Spanish for "to learn") with "demo" - it's both a language learning tool and a demonstration of the Inworld Runtime Node.js SDK. +A conversational language learning app powered by Inworld AI Runtime. Practice speaking with an AI tutor, get real-time feedback on your responses, and build vocabulary with auto-generated flashcards. ![App](screenshot.jpg) ## Prerequisites - Node.js (v20 or higher) +- npm - An Inworld AI account and API key +- An AssemblyAI account and API key (for speech-to-text) ## Get Started @@ -25,64 +27,170 @@ cd language-learning-node ### Step 2: Install Dependencies + +Frontend: +```bash +cd frontend +npm install +``` + +Backend: ```bash +cd backend npm install ``` ### Step 3: Configure Environment Variables -Create a `.env` file in the root directory: +Create a `.env` file in the /backend directory: ```bash -INWORLD_API_KEY=your_api_key_here +INWORLD_API_KEY=your_inworld_base64_key +ASSEMBLY_AI_API_KEY=your_assemblyai_key ``` -Get your Base64 API key from the [Inworld Portal](https://platform.inworld.ai/). +| Service | Get Key From | Purpose | +| -------------- | --------------------------------------------------- | --------------------------------- | +| **Inworld** | [platform.inworld.ai](https://platform.inworld.ai/) | AI conversations (Base64 API key) | +| **AssemblyAI** | [assemblyai.com](https://www.assemblyai.com/) | Speech-to-text | ### Step 4: Run the Application -For development: +**For development** (with auto-reload on file changes): +In frontend dir: ```bash npm run dev ``` -For production: +In backend dir: +```bash +npm run dev +``` + +Open [http://localhost:5173](http://localhost:5173) + +### Step 5 (Optional): Set Up Supabase for Auth & Memory + +Without Supabase, the app works in anonymous mode using localStorage. + +**a) Create a Supabase project** at [supabase.com](https://supabase.com) + +**b) Push the database schema:** + +```bash +npx supabase login +npx supabase link --project-ref YOUR_PROJECT_REF +npx supabase db push +``` + +This creates all tables, indexes, RLS policies, and the `match_memories` function for semantic search. + +Find your project ref in the Supabase dashboard URL: `supabase.com/dashboard/project/YOUR_PROJECT_REF` + +**c) Add Supabase variables to `.env` (backend):** ```bash -npm run build -npm start +SUPABASE_URL=https://YOUR_PROJECT.supabase.co +SUPABASE_SERVICE_ROLE_KEY=your_service_role_key ``` +**d) Create `frontend/.env.local`:** + +```bash +VITE_SUPABASE_URL=https://YOUR_PROJECT.supabase.co +VITE_SUPABASE_PUBLISHABLE_KEY=your_anon_key +``` + +Find these in: Supabase Dashboard > Settings > API + ## Repo Structure ``` language-learning-node/ ├── backend/ -│ ├── graphs/ # Graph definitions -│ │ ├── conversation-graph.ts -│ │ ├── flashcard-graph.ts -│ │ └── introduction-state-graph.ts -│ ├── helpers/ # Helper utilities -│ │ ├── anki-exporter.ts -│ │ ├── audio-buffer.ts -│ │ ├── audio-processor.ts -│ │ ├── flashcard-processor.ts -│ │ ├── introduction-state-processor.ts -│ │ ├── prompt-templates.ts -│ │ └── silero-vad.ts -│ ├── models/ # AI models -│ │ └── silero_vad.onnx -│ └── server.ts # Backend server -├── frontend/ # Frontend application -│ ├── js/ -│ ├── styles/ -│ └── index.html -├── flashcard-graph.json # Flashcard configuration -├── package.json # Dependencies -└── LICENSE # MIT License +│ ├── src/ +│ │ ├── __tests__/ # Backend unit tests +│ │ ├── config/ # Language & server configuration +│ │ ├── graphs/ # Inworld Runtime conversation graphs +│ │ │ ├── configs/ # Graph JSON configurations +│ │ │ └── nodes/ # Custom graph nodes +│ │ ├── helpers/ # Audio utils, connection management +│ │ ├── prompts/ # Nunjucks prompt templates +│ │ ├── services/ # Server components +│ │ ├── utils/ # Logger +│ │ └── server.ts # Entry point +│ ├── .env # Backend environment variables +│ └── vitest.config.ts # Backend test config +├── frontend/ +│ ├── src/ +│ │ ├── __tests__/ # Frontend unit tests +│ │ ├── components/ # React components +│ │ ├── context/ # App state & auth +│ │ ├── hooks/ # Custom React hooks +│ │ ├── services/ # WebSocket client, audio, storage +│ │ ├── styles/ # CSS +│ │ └── types/ # TypeScript types +│ ├── .env.local # Frontend environment variables +│ └── vitest.config.ts # Frontend test config +├── supabase/ +│ └── migrations/ # Database schema +└── deploy/ # Deployment configurations +``` + +## Architecture + +The app uses a real-time audio streaming architecture: + +1. **Frontend** captures microphone audio and streams it via WebSocket +2. **Backend** processes audio through an Inworld Runtime graph: + - AssemblyAI handles speech-to-text with voice activity detection + - LLM generates contextual responses in the target language + - TTS converts responses back to audio +3. **Flashcards** are auto-generated from conversation vocabulary +4. **Response feedback** provides grammar and usage corrections + +## Memory System + +When Supabase is configured, the app stores and retrieves user memories using semantic search: + +- **Automatic memory creation**: Every few conversation turns, the system extracts memorable facts +- **Semantic retrieval**: Relevant memories are retrieved using vector similarity search (pgvector) +- **Personalized responses**: The AI uses retrieved memories to personalize conversations + +Memory types: + +- `learning_progress`: Vocabulary struggles, grammar patterns, learning achievements +- `personal_context`: Interests, goals, preferences shared by the user + +Without Supabase, the app works in anonymous mode using localStorage (no memory persistence). + +## Environment Variables Reference + +| Variable | Required | Description | +| --------------------------- | -------- | ------------------------------------------------------------------ | +| `INWORLD_API_KEY` | Yes | Inworld AI Base64 API key | +| `ASSEMBLY_AI_API_KEY` | Yes | AssemblyAI API key | +| `PORT` | No | Server port (default: 3000) | +| `LOG_LEVEL` | No | `trace`, `debug`, `info`, `warn`, `error`, `fatal` (default: info) | +| `NODE_ENV` | No | `development` or `production` | +| `ASSEMBLY_AI_EAGERNESS` | No | Turn detection: `low`, `medium`, `high` (default: high) | +| `SUPABASE_URL` | No | Supabase project URL (enables memory feature) | +| `SUPABASE_SERVICE_ROLE_KEY` | No | Supabase service role key (for backend memory storage) | + +## Testing + +Run the test suite to verify core functionality: + +```bash +npm test # Run all tests +npm run test:backend # Backend tests only +npm run test:frontend # Frontend tests only +npm run test:watch # Watch mode for backend ``` +Tests cover critical paths: audio conversion, language configuration, storage persistence, and flashcard deduplication. + ## Troubleshooting **Bug Reports**: [GitHub Issues](https://github.com/inworld-ai/language-learning-node/issues) diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..41a06c1 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,5 @@ +INWORLD_API_KEY= +ASSEMBLY_AI_API_KEY= + +SUPABASE_URL= +SUPABASE_SERVICE_ROLE_KEY= \ No newline at end of file diff --git a/eslint.config.mjs b/backend/eslint.config.mjs similarity index 96% rename from eslint.config.mjs rename to backend/eslint.config.mjs index 358af7a..53b9311 100644 --- a/eslint.config.mjs +++ b/backend/eslint.config.mjs @@ -18,7 +18,7 @@ export default [ languageOptions: { parser: tseslint.parser, parserOptions: { - project: true, + project: './tsconfig.eslint.json', tsconfigRootDir: __dirname, }, }, diff --git a/backend/graphs/conversation-graph.ts b/backend/graphs/conversation-graph.ts deleted file mode 100644 index 9bface7..0000000 --- a/backend/graphs/conversation-graph.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { - GraphBuilder, - CustomNode, - ProcessContext, - ProxyNode, - RemoteLLMChatNode, - RemoteSTTNode, - RemoteTTSNode, - TextChunkingNode, -} from '@inworld/runtime/graph'; -import { GraphTypes } from '@inworld/runtime/common'; -import { renderJinja } from '@inworld/runtime/primitives/llm'; -import { AsyncLocalStorage } from 'async_hooks'; -import { conversationTemplate } from '../helpers/prompt-templates.js'; -import type { IntroductionState } from '../helpers/introduction-state-processor.js'; - -export interface ConversationGraphConfig { - apiKey: string; -} - -// Use AsyncLocalStorage to store state accessors directly per async execution context -// This eliminates the need for a registry and connectionId lookup -export const stateStorage = new AsyncLocalStorage<{ - getConversationState: () => { - messages: Array<{ role: string; content: string; timestamp: string }>; - }; - getIntroductionState: () => IntroductionState; -}>(); - -export function createConversationGraph(_config: ConversationGraphConfig) { - // Create the custom node class that gets state from AsyncLocalStorage - class EnhancedPromptBuilderNode extends CustomNode { - async process(_context: ProcessContext, currentInput: string) { - // Get state accessors directly from AsyncLocalStorage (set before graph execution) - const stateAccessors = stateStorage.getStore(); - - if (!stateAccessors) { - // Fallback to empty state if not available - const conversationState = { messages: [] }; - const introductionState = { - name: '', - level: '', - goal: '', - timestamp: '', - }; - const templateData = { - messages: conversationState.messages || [], - current_input: currentInput, - introduction_state: introductionState, - }; - - const renderedPrompt = await renderJinja( - conversationTemplate, - JSON.stringify(templateData) - ); - - return new GraphTypes.LLMChatRequest({ - messages: [{ role: 'user', content: renderedPrompt }], - }); - } - - // Get state directly from accessors - const conversationState = stateAccessors.getConversationState(); - const introductionState = stateAccessors.getIntroductionState(); - - const templateData = { - messages: conversationState.messages || [], - current_input: currentInput, - introduction_state: introductionState || { - name: '', - level: '', - goal: '', - }, - }; - - console.log( - 'ConversationGraph - Introduction state being used:', - JSON.stringify(introductionState, null, 2) - ); - console.log( - 'ConversationGraph - Number of messages in history:', - conversationState.messages?.length || 0 - ); - - const renderedPrompt = await renderJinja( - conversationTemplate, - JSON.stringify(templateData) - ); - - // Return LLMChatRequest for the LLM node - return new GraphTypes.LLMChatRequest({ - messages: [{ role: 'user', content: renderedPrompt }], - }); - } - } - - const sttNode = new RemoteSTTNode({ - id: 'stt_node', - sttConfig: { - languageCode: 'es', - }, - }); - const sttOutputNode = new ProxyNode({ - id: 'proxy_node', - reportToClient: true, - }); - const promptBuilderNode = new EnhancedPromptBuilderNode({ - id: 'enhanced_prompt_builder_node', - }); - const llmNode = new RemoteLLMChatNode({ - id: 'llm_node', - provider: 'openai', - modelName: 'gpt-4o-mini', - stream: true, - reportToClient: true, - textGenerationConfig: { - maxNewTokens: 250, - maxPromptLength: 2000, - repetitionPenalty: 1, - topP: 1, - temperature: 1, - frequencyPenalty: 0, - presencePenalty: 0, - }, - }); - const chunkerNode = new TextChunkingNode({ id: 'chunker_node' }); - const ttsNode = new RemoteTTSNode({ - id: 'tts_node', - speakerId: 'Diego', - modelId: 'inworld-tts-1', - sampleRate: 16000, - speakingRate: 1, - temperature: 0.7, - }); - - const executor = new GraphBuilder({ - id: 'conversation_graph', - enableRemoteConfig: false, - }) - .addNode(sttNode) - .addNode(sttOutputNode) - .addNode(promptBuilderNode) - .addNode(llmNode) - .addNode(chunkerNode) - .addNode(ttsNode) - .setStartNode(sttNode) - .addEdge(sttNode, sttOutputNode) - .addEdge(sttNode, promptBuilderNode, { - condition: async (input: string) => { - return input.trim().length > 0; - }, - }) - .addEdge(promptBuilderNode, llmNode) - .addEdge(llmNode, chunkerNode) - .addEdge(chunkerNode, ttsNode) - .setEndNode(sttOutputNode) - .setEndNode(ttsNode) - .build(); - - return executor; -} diff --git a/backend/graphs/introduction-state-graph.ts b/backend/graphs/introduction-state-graph.ts deleted file mode 100644 index ae078a9..0000000 --- a/backend/graphs/introduction-state-graph.ts +++ /dev/null @@ -1,142 +0,0 @@ -import 'dotenv/config'; -import { - GraphBuilder, - CustomNode, - ProcessContext, - RemoteLLMChatNode, -} from '@inworld/runtime/graph'; -import { GraphTypes } from '@inworld/runtime/common'; -import { renderJinja } from '@inworld/runtime/primitives/llm'; -import { introductionStatePromptTemplate } from '../helpers/prompt-templates.js'; - -type IntroductionStateLevel = 'beginner' | 'intermediate' | 'advanced' | ''; - -export interface IntroductionState { - name: string; - level: IntroductionStateLevel; - goal: string; - timestamp: string; -} - -class IntroductionPromptBuilderNode extends CustomNode { - async process( - _context: ProcessContext, - input: GraphTypes.Content | Record - ) { - const renderedPrompt = await renderJinja( - introductionStatePromptTemplate, - JSON.stringify(input) - ); - return renderedPrompt; - } -} - -class TextToChatRequestNode extends CustomNode { - process(_context: ProcessContext, renderedPrompt: string) { - return new GraphTypes.LLMChatRequest({ - messages: [{ role: 'user', content: renderedPrompt }], - }); - } -} - -function normalizeLevel(level: unknown): IntroductionStateLevel { - if (typeof level !== 'string') return ''; - const lower = level - .trim() - .toLowerCase() - .replace(/[.!?,;:]+$/g, ''); - const mapping: Record = { - beginner: 'beginner', - intermediate: 'intermediate', - advanced: 'advanced', - principiante: 'beginner', - intermedio: 'intermediate', - avanzado: 'advanced', - }; - return mapping[lower] || ''; -} - -class IntroductionStateParserNode extends CustomNode { - process(_context: ProcessContext, input: GraphTypes.Content) { - try { - const content = - (input && - typeof input === 'object' && - 'content' in input && - (input as { content?: unknown }).content) || - input; - const textContent = - typeof content === 'string' ? content : JSON.stringify(content); - console.log( - 'IntroductionStateParserNode - Raw LLM response:', - textContent - ); - - const jsonMatch = textContent.match(/\{[\s\S]*\}/); - if (jsonMatch) { - const parsed = JSON.parse(jsonMatch[0]); - console.log('IntroductionStateParserNode - Parsed JSON:', parsed); - - const name = typeof parsed.name === 'string' ? parsed.name.trim() : ''; - const level = normalizeLevel(parsed.level); - const goal = typeof parsed.goal === 'string' ? parsed.goal.trim() : ''; - const state: IntroductionState = { - name, - level, - goal, - timestamp: new Date().toISOString(), - }; - console.log('IntroductionStateParserNode - Returning state:', state); - return state; - } - } catch (error) { - console.error('Failed to parse introduction state JSON:', error); - } - - const fallback: IntroductionState = { - name: '', - level: '', - goal: '', - timestamp: new Date().toISOString(), - }; - console.log('IntroductionStateParserNode - Returning fallback state'); - return fallback; - } -} - -export function createIntroductionStateGraph() { - const apiKey = process.env.INWORLD_API_KEY; - if (!apiKey) { - throw new Error('INWORLD_API_KEY environment variable is required'); - } - - const promptBuilderNode = new IntroductionPromptBuilderNode({ - id: 'introduction-prompt-builder', - }); - const textToChatRequestNode = new TextToChatRequestNode({ - id: 'text-to-chat-request', - }); - const llmNode = new RemoteLLMChatNode({ - id: 'llm_node', - provider: 'openai', - modelName: 'gpt-4.1', - stream: false, - }); - const parserNode = new IntroductionStateParserNode({ - id: 'introduction-state-parser', - }); - - const executor = new GraphBuilder('introduction-state-graph') - .addNode(promptBuilderNode) - .addNode(textToChatRequestNode) - .addNode(llmNode) - .addNode(parserNode) - .addEdge(promptBuilderNode, textToChatRequestNode) - .addEdge(textToChatRequestNode, llmNode) - .addEdge(llmNode, parserNode) - .setStartNode(promptBuilderNode) - .setEndNode(parserNode) - .build(); - - return executor; -} diff --git a/backend/helpers/audio-processor.ts b/backend/helpers/audio-processor.ts deleted file mode 100644 index 59e2c51..0000000 --- a/backend/helpers/audio-processor.ts +++ /dev/null @@ -1,723 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; -import { GraphTypes } from '@inworld/runtime/common'; -import { UserContextInterface } from '@inworld/runtime/graph'; -import { Graph } from '@inworld/runtime/graph'; -import { WebSocket } from 'ws'; -import { SileroVAD, VADConfig } from './silero-vad.js'; -import { stateStorage } from '../graphs/conversation-graph.js'; -import type { IntroductionState } from './introduction-state-processor.js'; - -export class AudioProcessor { - private executor: Graph; - private vad: SileroVAD | null = null; - private isProcessing = false; - private isProcessingCancelled = false; // Track if current processing should be cancelled - private isReady = false; - private websocket: WebSocket | null = null; - private pendingSpeechSegments: Float32Array[] = []; // Accumulate speech segments - private conversationState: { - messages: Array<{ role: string; content: string; timestamp: string }>; - } = { - messages: [], - }; - private flashcardCallback: - | ((messages: Array<{ role: string; content: string }>) => Promise) - | null = null; - private introductionState: IntroductionState = { - name: '', - level: '', - goal: '', - timestamp: '', - }; - private introductionStateCallback: - | (( - messages: Array<{ role: string; content: string }> - ) => Promise) - | null = null; - private targetingKey: string | null = null; - private clientTimezone: string | null = null; - private graphStartTime: number = 0; - constructor(executor: Graph, websocket?: WebSocket) { - this.executor = executor; - this.websocket = websocket ?? null; - this.setupWebSocketMessageHandler(); - setTimeout(() => this.initialize(), 100); - } - - // Public methods for state registry access - getConversationState() { - return this.conversationState; - } - - getIntroductionState(): IntroductionState { - return this.introductionState; - } - - private trimConversationHistory(maxTurns: number = 40) { - const maxMessages = maxTurns * 2; - if (this.conversationState.messages.length > maxMessages) { - this.conversationState.messages = - this.conversationState.messages.slice(-maxMessages); - } - } - - private setupWebSocketMessageHandler() { - if (this.websocket) { - this.websocket.on('message', (data: Buffer | string) => { - try { - const raw = - typeof data === 'string' ? data : data?.toString?.() || ''; - const message = JSON.parse(raw); - - if (message.type === 'conversation_update') { - const incoming = - message.data && message.data.messages - ? message.data.messages - : []; - const existing = this.conversationState.messages || []; - const combined = [...existing, ...incoming]; - const seen = new Set(); - const deduped: Array<{ - role: string; - content: string; - timestamp: string; - }> = []; - for (const m of combined) { - const key = `${m.timestamp}|${m.role}|${m.content}`; - if (!seen.has(key)) { - seen.add(key); - deduped.push(m); - } - } - deduped.sort( - (a, b) => - new Date(a.timestamp).getTime() - - new Date(b.timestamp).getTime() - ); - this.conversationState = { messages: deduped }; - this.trimConversationHistory(40); - } else if (message.type === 'user_context') { - // Persist minimal attributes for user context - const tz = - message.timezone || (message.data && message.data.timezone) || ''; - const uid = - message.userId || (message.data && message.data.userId) || ''; - this.clientTimezone = tz || this.clientTimezone; - if (uid) this.targetingKey = uid; - } - } catch { - // Not a JSON message or conversation update, ignore - } - }); - } - } - - setFlashcardCallback( - callback: ( - messages: Array<{ role: string; content: string }> - ) => Promise - ) { - this.flashcardCallback = callback; - } - - setIntroductionStateCallback( - callback: ( - messages: Array<{ role: string; content: string }> - ) => Promise - ) { - this.introductionStateCallback = callback; - } - - private async initialize() { - try { - console.log('AudioProcessor: Starting initialization...'); - - // Initialize VAD - try { - const vadConfig: VADConfig = { - modelPath: 'backend/models/silero_vad.onnx', - threshold: 0.5, // Following working example SPEECH_THRESHOLD - minSpeechDuration: 0.2, // MIN_SPEECH_DURATION_MS / 1000 - minSilenceDuration: 0.4, // Reduced from 0.65 for faster response - speechResetSilenceDuration: 1.0, - minVolume: 0.01, // Lower threshold to start - can adjust based on testing - sampleRate: 16000, - }; - - console.log( - 'AudioProcessor: Creating SileroVAD with config:', - vadConfig - ); - this.vad = new SileroVAD(vadConfig); - - console.log('AudioProcessor: Initializing VAD...'); - await this.vad.initialize(); - console.log('AudioProcessor: VAD initialized successfully'); - - this.vad.on('speechStart', () => { - console.log('🎤 Speech started'); - - // Always notify frontend to stop audio playback when user starts speaking - if (this.websocket) { - try { - this.websocket.send( - JSON.stringify({ type: 'interrupt', reason: 'speech_start' }) - ); - // Notify frontend that speech was detected so it can show listening indicator - this.websocket.send( - JSON.stringify({ - type: 'speech_detected', - data: { text: '' }, // Empty text initially, will be updated with transcript - }) - ); - } catch { - // ignore send errors - } - } - - // If we're currently processing, set cancellation flag - if (this.isProcessing) { - console.log( - 'Setting cancellation flag - user started speaking during processing' - ); - this.isProcessingCancelled = true; - // Don't clear segments - we want to accumulate them - } - }); - - this.vad.on('speechEnd', async (event) => { - console.log( - '🔇 Speech ended, duration:', - event.speechDuration.toFixed(2) + 's' - ); - - // Always notify frontend that VAD stopped detecting speech - // This allows frontend to clear the real-time transcript bubble - if (this.websocket) { - try { - this.websocket.send( - JSON.stringify({ - type: 'speech_ended', - data: { - speechDuration: event.speechDuration, - hasSegment: - event.speechSegment !== null && - event.speechSegment.length > 0, - }, - }) - ); - } catch { - // ignore send errors - } - } - - try { - if (event.speechSegment && event.speechSegment.length > 0) { - // Add this segment to pending segments - this.pendingSpeechSegments.push(event.speechSegment); - - // Reset cancellation flag for new processing - this.isProcessingCancelled = false; - - // Process immediately if not already processing - if (!this.isProcessing && this.pendingSpeechSegments.length > 0) { - // Combine all pending segments - const totalLength = this.pendingSpeechSegments.reduce( - (sum, seg) => sum + seg.length, - 0 - ); - const combinedSegment = new Float32Array(totalLength); - let offset = 0; - for (const seg of this.pendingSpeechSegments) { - combinedSegment.set(seg, offset); - offset += seg.length; - } - - // Clear pending segments - this.pendingSpeechSegments = []; - - // Process immediately - console.log('Processing speech segment immediately'); - await this.processVADSpeechSegment(combinedSegment); - } - } - } catch (error) { - console.error('Error handling speech segment:', error); - } - }); - } catch (error) { - console.error('AudioProcessor: VAD initialization failed:', error); - this.vad = null; - } - - // Graph is already provided in constructor (shared instance) - // Just mark as ready - this.isReady = true; - console.log( - 'AudioProcessor: Using shared conversation graph, ready for audio processing' - ); - } catch (error) { - console.error( - 'AudioProcessor: Initialization failed with unexpected error:', - error - ); - this.isReady = false; - // Don't rethrow - prevent unhandled promise rejection - } - } - - addAudioChunk(base64Audio: string) { - try { - // Feed audio to VAD if available and ready - if (this.vad && this.isReady) { - this.vad.addAudioData(base64Audio); - } else { - // VAD not available - log error and ignore audio chunk - if (!this.vad) { - console.error('VAD not available - cannot process audio chunk'); - } else if (!this.isReady) { - console.warn('AudioProcessor not ready yet - ignoring audio chunk'); - } - } - } catch (error) { - console.error('Error processing audio chunk:', error); - } - } - - private amplifyAudio(audioData: Float32Array, gain: number): Float32Array { - const amplified = new Float32Array(audioData.length); - - let maxVal = 0; - for (let i = 0; i < audioData.length; i++) { - const absVal = Math.abs(audioData[i]); - if (absVal > maxVal) { - maxVal = absVal; - } - } - - // For normalized Float32 audio [-1, 1], prevent clipping at 1.0 - const safeGain = maxVal > 0 ? Math.min(gain, 0.95 / maxVal) : gain; - - for (let i = 0; i < audioData.length; i++) { - // Clamp to [-1, 1] range to prevent distortion - amplified[i] = Math.max(-1, Math.min(1, audioData[i] * safeGain)); - } - - return amplified; - } - - private async processVADSpeechSegment( - speechSegment: Float32Array - ): Promise { - if (this.isProcessing) { - return; - } - - this.isProcessing = true; - - try { - const amplifiedSegment = this.amplifyAudio(speechSegment, 2.0); - - // Save debug audio before sending to STT - // await this.saveAudioDebug(amplifiedSegment, 'vad-segment'); - - // Create Audio instance for STT node - const audioInput = new GraphTypes.Audio({ - data: Array.from(amplifiedSegment), - sampleRate: 16000, - }); - - // Build user context for experiments - const attributes: Record = { - timezone: this.clientTimezone || '', - }; - attributes.name = - (this.introductionState?.name && this.introductionState.name.trim()) || - 'unknown'; - attributes.level = - (this.introductionState?.level && - (this.introductionState.level as string)) || - 'unknown'; - attributes.goal = - (this.introductionState?.goal && this.introductionState.goal.trim()) || - 'unknown'; - - const targetingKey = this.targetingKey || uuidv4(); - const userContext: UserContextInterface = { - attributes: attributes, - targetingKey: targetingKey, - }; - console.log(userContext); - - this.graphStartTime = Date.now(); - - // Use AsyncLocalStorage to set state accessors for this execution context - // This allows the graph nodes to access state directly without needing connectionId - const executionResult = await stateStorage.run( - { - getConversationState: () => this.getConversationState(), - getIntroductionState: () => this.getIntroductionState(), - }, - async () => { - try { - const executionContext = { - executionId: uuidv4(), - userContext: userContext, - }; - return await this.executor.start(audioInput, executionContext); - } catch (err) { - console.warn( - 'Executor.start with ExecutionContext failed, falling back without context:', - err - ); - return await this.executor.start(audioInput); - } - } - ); - - let transcription = ''; - let llmResponse = ''; - - for await (const chunk of executionResult.outputStream) { - // Check if processing has been cancelled - if (this.isProcessingCancelled) { - console.log( - 'Processing cancelled by user speech, breaking from loop' - ); - break; - } - - console.log( - `Audio Processor:Chunk received - Type: ${chunk.typeName}, Has processResponse: ${typeof chunk.processResponse === 'function'}` - ); - console.log( - `Audio Processor:Time since graph started: ${Date.now() - this.graphStartTime}ms` - ); - - // Use processResponse for type-safe handling - await chunk.processResponse({ - // Handle string output (from ProxyNode with STT transcription) - string: (data: string) => { - transcription = data; - - if (transcription.trim() === '') { - return; - } - - console.log( - `VAD STT Transcription (via ProxyNode): "${transcription}"` - ); - - if (this.websocket) { - this.websocket.send( - JSON.stringify({ - type: 'transcription', - text: transcription.trim(), - timestamp: Date.now(), - }) - ); - } - // Don't add to conversation state yet - the prompt template will use it as current_input - // We'll add it after processing completes - - // Opportunistically run introduction-state extraction as soon as we have user input - const isIntroCompleteEarly = Boolean( - this.introductionState?.name && - this.introductionState?.level && - this.introductionState?.goal - ); - if (!isIntroCompleteEarly && this.introductionStateCallback) { - const recentMessages = this.conversationState.messages - .slice(-6) - .map((msg) => ({ - role: msg.role, - content: msg.content, - })); - this.introductionStateCallback(recentMessages) - .then((state) => { - if (state) { - this.introductionState = state; - if (this.websocket) { - this.websocket.send( - JSON.stringify({ - type: 'introduction_state_updated', - introduction_state: this.introductionState, - timestamp: Date.now(), - }) - ); - } - } - }) - .catch((error) => { - console.error( - 'Error in introduction-state callback (early):', - error - ); - }); - } - }, - - // Handle ContentStream (from LLM) - ContentStream: async (streamIterator: GraphTypes.ContentStream) => { - console.log('VAD Processing LLM ContentStream...'); - let currentLLMResponse = ''; - for await (const streamChunk of streamIterator) { - if (streamChunk.text) { - currentLLMResponse += streamChunk.text; - // console.log('VAD LLM chunk:', streamChunk.text); - if (this.websocket) { - this.websocket.send( - JSON.stringify({ - type: 'llm_response_chunk', - text: streamChunk.text, - timestamp: Date.now(), - }) - ); - } - } - } - if (currentLLMResponse.trim()) { - llmResponse = currentLLMResponse; - console.log(`VAD Complete LLM Response: "${llmResponse}"`); - if (this.websocket) { - this.websocket.send( - JSON.stringify({ - type: 'llm_response_complete', - text: llmResponse.trim(), - timestamp: Date.now(), - }) - ); - } - // Now update conversation state with both user and assistant messages - // Add user message first (it wasn't added earlier to avoid duplication) - this.conversationState.messages.push({ - role: 'user', - content: transcription.trim(), - timestamp: new Date().toISOString(), - }); - // Then add assistant message - this.conversationState.messages.push({ - role: 'assistant', - content: llmResponse.trim(), - timestamp: new Date().toISOString(), - }); - this.trimConversationHistory(40); - console.log('Updated conversation state with full exchange'); - - // Mark that we'll need to trigger flashcard generation after TTS completes - // We'll do this after all TTS chunks have been sent - } - }, - - // Handle TTS output stream - TTSOutputStream: async ( - ttsStreamIterator: GraphTypes.TTSOutputStream - ) => { - console.log('VAD Processing TTS audio stream...'); - let isFirstChunk = true; - for await (const ttsChunk of ttsStreamIterator) { - if (ttsChunk.audio && ttsChunk.audio.data) { - // Log first chunk for latency tracking - if (isFirstChunk) { - console.log('Sending first TTS chunk immediately'); - } - - // Decode base64 and create Float32Array - const decodedData = Buffer.from(ttsChunk.audio.data, 'base64'); - const float32Array = new Float32Array(decodedData.buffer); - - // Convert Float32 to Int16 for web audio playback - const int16Array = new Int16Array(float32Array.length); - for (let i = 0; i < float32Array.length; i++) { - int16Array[i] = Math.max( - -32768, - Math.min(32767, float32Array[i] * 32767) - ); - } - const base64Audio = Buffer.from(int16Array.buffer).toString( - 'base64' - ); - - // Send immediately without buffering - if (this.websocket) { - this.websocket.send( - JSON.stringify({ - type: 'audio_stream', - audio: base64Audio, - sampleRate: ttsChunk.audio.sampleRate || 16000, - timestamp: Date.now(), - text: ttsChunk.text || '', - isFirstChunk: isFirstChunk, - }) - ); - } - - // Mark that we've sent the first chunk - isFirstChunk = false; - } - } - // Send completion signal for iOS - console.log('VAD TTS stream complete, sending completion signal'); - if (this.websocket) { - this.websocket.send( - JSON.stringify({ - type: 'audio_stream_complete', - timestamp: Date.now(), - }) - ); - } - - // Now that TTS is complete, trigger flashcard generation and other post-processing - if (transcription && llmResponse) { - console.log( - 'Triggering flashcard generation after TTS completion' - ); - - // Send conversation update to frontend - if (this.websocket) { - this.websocket.send( - JSON.stringify({ - type: 'conversation_update', - messages: this.conversationState.messages, - timestamp: Date.now(), - }) - ); - } - - // Generate flashcards - fire and forget - if (this.flashcardCallback) { - const recentMessages = this.conversationState.messages - .slice(-6) - .map((msg) => ({ - role: msg.role, - content: msg.content, - })); - - this.flashcardCallback(recentMessages).catch((error) => { - console.error( - 'Error in flashcard generation callback:', - error - ); - }); - } - - // Run introduction-state extraction while incomplete - const isIntroComplete = Boolean( - this.introductionState?.name && - this.introductionState?.level && - this.introductionState?.goal - ); - if (!isIntroComplete && this.introductionStateCallback) { - const recentMessages = this.conversationState.messages - .slice(-6) - .map((msg) => ({ - role: msg.role, - content: msg.content, - })); - - this.introductionStateCallback(recentMessages) - .then((state) => { - if (state) { - this.introductionState = state; - if (this.websocket) { - this.websocket.send( - JSON.stringify({ - type: 'introduction_state_updated', - introduction_state: this.introductionState, - timestamp: Date.now(), - }) - ); - } - } - }) - .catch((error) => { - console.error( - 'Error in introduction-state callback:', - error - ); - }); - } - } - }, - - // Handle any other type - default: (data: unknown) => { - console.log( - `VAD Unknown/unhandled chunk type: ${chunk.typeName}`, - data - ); - }, - }); - - // Check again after processing each chunk - if (this.isProcessingCancelled) { - console.log('Processing cancelled after chunk processing'); - break; - } - } - } catch (error) { - console.error('Error processing VAD speech segment:', error); - } finally { - this.isProcessing = false; - - // If we have pending segments (user spoke while we were processing), process them now - if ( - this.pendingSpeechSegments.length > 0 && - !this.isProcessingCancelled - ) { - console.log( - 'Found pending segments after processing, processing them now' - ); - - // Combine all pending segments - const totalLength = this.pendingSpeechSegments.reduce( - (sum, seg) => sum + seg.length, - 0 - ); - const combinedSegment = new Float32Array(totalLength); - let offset = 0; - for (const seg of this.pendingSpeechSegments) { - combinedSegment.set(seg, offset); - offset += seg.length; - } - - // Clear pending segments - this.pendingSpeechSegments = []; - - // Process the combined segments recursively - await this.processVADSpeechSegment(combinedSegment); - } - } - } - - reset() { - // Clear conversation state - this.conversationState = { - messages: [], - }; - // Clear pending segments - this.pendingSpeechSegments = []; - // Reset introduction state - this.introductionState = { - name: '', - level: '', - goal: '', - timestamp: '', - }; - // Cancel any ongoing processing - this.isProcessingCancelled = true; - console.log('AudioProcessor: Conversation reset'); - } - - async destroy() { - if (this.vad) { - try { - this.vad.destroy(); - } catch (error) { - console.error('Error destroying VAD:', error); - // Continue with cleanup even if destroy fails - } - this.vad = null; - } - } -} diff --git a/backend/helpers/introduction-state-processor.ts b/backend/helpers/introduction-state-processor.ts deleted file mode 100644 index 678cfb3..0000000 --- a/backend/helpers/introduction-state-processor.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; -import { Graph } from '@inworld/runtime/graph'; -import { GraphTypes } from '@inworld/runtime/common'; -import { createIntroductionStateGraph } from '../graphs/introduction-state-graph.js'; - -export type IntroductionStateLevel = - | 'beginner' - | 'intermediate' - | 'advanced' - | ''; - -export interface IntroductionState { - name: string; - level: IntroductionStateLevel; - goal: string; - timestamp: string; -} - -export interface ConversationMessage { - role: string; - content: string; -} - -export class IntroductionStateProcessor { - private state: IntroductionState = { - name: '', - level: '', - goal: '', - timestamp: '', - }; - private executor: Graph; - - constructor() { - this.executor = createIntroductionStateGraph(); - } - - isComplete(): boolean { - return Boolean(this.state.name && this.state.level && this.state.goal); - } - - getState(): IntroductionState { - return this.state; - } - - private mergeState(newState: IntroductionState) { - const merged: IntroductionState = { ...this.state }; - // Only update fields if they have a non-empty value in the new state - // This preserves existing values when the LLM returns empty strings - if (newState.name && newState.name.trim()) { - merged.name = newState.name.trim(); - } - if (newState.level && newState.level.trim()) { - merged.level = newState.level as IntroductionStateLevel; - } - if (newState.goal && newState.goal.trim()) { - merged.goal = newState.goal.trim(); - } - merged.timestamp = new Date().toISOString(); - this.state = merged; - console.log( - 'IntroductionStateProcessor - After merge, state is:', - this.state - ); - } - - async update(messages: ConversationMessage[]): Promise { - try { - const input = { - messages, - existingState: this.state, - }; - - console.log( - 'IntroductionStateProcessor - Current state before update:', - this.state - ); - console.log( - 'IntroductionStateProcessor - Messages for extraction:', - messages - ); - - const executionContext = { - executionId: uuidv4(), - }; - const executionResult = await this.executor.start( - input, - executionContext - ); - let finalData: GraphTypes.Content | null = null; - for await (const res of executionResult.outputStream) { - finalData = res.data; - } - const parsed = finalData as unknown as IntroductionState; - console.log('IntroductionStateProcessor - Extracted state:', parsed); - this.mergeState(parsed); - console.log('IntroductionStateProcessor - Merged state:', this.state); - return this.state; - } catch (error) { - console.error('Error updating introduction state:', error); - return this.state; - } - } - - reset() { - this.state = { - name: '', - level: '', - goal: '', - timestamp: '', - }; - console.log('IntroductionStateProcessor: State reset'); - } -} diff --git a/backend/helpers/prompt-templates.ts b/backend/helpers/prompt-templates.ts deleted file mode 100644 index 4a2c64d..0000000 --- a/backend/helpers/prompt-templates.ts +++ /dev/null @@ -1,116 +0,0 @@ -export const conversationTemplate = ` - -# Context -- You are a Señor Gael Herrera, 35 year old 'Chilango' who has loaned their brain to AI. -- You are embedded in a Spanish learning app called 'Aprendemo', which is a demonstration of the Inworld AI Runtime. -- You can help the user learn Spanish by having natural (verbalized) conversations with them. -- The app generates flashcards for the user during the conversation. They are ANKI formatted and can be exported by the user. - -# Instructions -{% if introduction_state and (not introduction_state.name or not introduction_state.level or not introduction_state.goal) %} -- Your first priority is to collect missing onboarding info. Ask for exactly one missing item at a time, in Spanish, and keep it short and natural. -- Missing items to collect: - {% if not introduction_state.name %}- Ask their name. - {% endif %} - {% if not introduction_state.level %}- Ask their Spanish level (beginner, intermediate, or advanced). - {% endif %} - {% if not introduction_state.goal %}- Ask their goal for learning Spanish. - {% endif %} -- Do not assume or guess values. If a value was already collected, do not ask for it again. -- If the user's latest message appears to answer one of these (e.g., they state their name, level like "principiante/intermedio/avanzado" or "beginner/intermediate/advanced", or share a goal), acknowledge it and immediately move to the next missing item instead of repeating the same question. -- Use the name naturally as soon as they provide it. -{% else %} -- Greet the user and introduce yourself in Spanish -- Ask the user if they want a lesson or conversation on a specific topic, then proceed -- If they don't want anything in particular, lead them in a conversation or lesson about Mexico City, the Dunedin sound rock scene, gardening, the concept of brunch across cultures, Balkan travel, or any other topic which comes to mind -- You can advise the user that if they want specific flashcards, they should just ask -- Gently correct the user if they make mistakes -- Don't always ask the user questions, you can talk about yourself as well. Be natural! -{% endif %} - -# Communication Style -- Vary your conversation starters - don't always begin with "¡Hola!" or exclamations -- Respond naturally as if you're in the middle of an ongoing conversation -- Use varied sentence structures and beginnings -- Sometimes start with: direct responses, "Ah", "Bueno", "Claro", "Pues", "Sí", or simply dive into your response -- Only use "¡Hola!" when it's actually a greeting at the start of a new conversation -- Be conversational and natural, not overly enthusiastic with every response -- When available, naturally use the user's name. Adjust complexity to their level (beginner, intermediate, advanced) and align topics with their goal. - -{% if messages and messages|length > 0 %} -Previous conversation: -{% for message in messages %} -{{ message.role }}: {{ message.content }} -{% endfor %} -{% endif %} - -User just said: {{ current_input }} - -Please respond naturally and clearly in 1-2 sentences. Vary your response style and avoid starting every response with the same greeting or exclamation.`.trim(); - -export const flashcardPromptTemplate = ` - -You are a system that generates flashcards for interesting new vocabulary for a Spanish learning app. - -Based on the ongoing conversation between {{studentName}} and {{teacherName}}, generate one flashcard with the following things: - -- The word in Spanish -- The translation in English -- An example sentence in Spanish -- A mnemonic to help the student remember the word - -## Conversation - -{% for message in messages %} -{{message.role}}: {{message.content}}{% endfor %} - -## Already Created Flashcards - -{% for flashcard in flashcards %} -- Word: {{flashcard.spanish}} -{% endfor %} - -## Guidelines - -- The word must NOT have been used in the conversation yet -- The word must be related to the topics used in the conversation -- The word must be related to the learner's level (if they are sophisticated, so can the word be. but if they are a beginner, the word should be simple or common) -- The word should be useful to the learner so they can continue the conversation with new vocabulary - -Now, return JSON with the following format: - -{ - "spanish": "string", - "english": "string", - "example": "string", - "mnemonic": "string" -}`.trim(); - -export const introductionStatePromptTemplate = ` - -You extract onboarding information for a Spanish learning app. - -Your job is to collect the learner's name, level, and goal ONLY if they have been explicitly provided. Do not guess or infer. - -Input provides the recent conversation messages and the existing known onboarding state. Preserve existing non-empty values and only fill in fields when the user clearly states them. - -Return a single JSON object with this exact shape: -{ - "name": "string", // The learner's name or "" if unknown - "level": "beginner|intermediate|advanced|", // One of these values, or "" if unknown - "goal": "string" // The learner's goal or "" if unknown -} - -## Existing Known State -{{ existingState | tojson }} - -## Conversation (most recent first or last order is fine) -{% for message in messages %} -{{ message.role }}: {{ message.content }} -{% endfor %} - -## Rules -- Do not invent values. If not explicitly provided, leave the field as an empty string. -- Normalize level to exactly "beginner", "intermediate", or "advanced" when clearly stated; otherwise leave as "". -- If the existing state already has a non-empty value, keep it unless the user explicitly corrects it. -`.trim(); diff --git a/backend/helpers/silero-vad.ts b/backend/helpers/silero-vad.ts deleted file mode 100644 index 83c0eee..0000000 --- a/backend/helpers/silero-vad.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { EventEmitter } from 'events'; -import { VADFactory } from '@inworld/runtime/primitives/vad'; -import { DeviceRegistry, DeviceType } from '@inworld/runtime/core'; -import { AudioBuffer, AudioChunk } from './audio-buffer.js'; - -export interface VADConfig { - modelPath: string; - threshold: number; - minSpeechDuration: number; // seconds - minSilenceDuration: number; // seconds - speechResetSilenceDuration: number; // grace period before resetting speech timer - minVolume: number; // minimum RMS volume for speech (0.0-1.0) - sampleRate: number; -} - -export interface VADResult { - isSpeech: boolean; - confidence: number; - timestamp: number; -} - -// VAD type from the runtime library - using unknown since the exact type isn't exported -type VADInstance = { - detectVoiceActivity: (config: { - data: number[]; - sampleRate: number; - }) => Promise; - destroy: () => void; -}; - -export class SileroVAD extends EventEmitter { - private vad: VADInstance | null = null; - private config: VADConfig; - private audioBuffer: AudioBuffer; - private accumulatedSamples: Float32Array[] = []; - private isInitialized = false; - private isProcessingVAD = false; // Prevent concurrent VAD calls - - // Simplified state tracking following working example pattern - private isCapturingSpeech = false; - private speechBuffer: number[] = []; - private pauseDuration = 0; - private readonly FRAME_PER_BUFFER = 1024; - private readonly INPUT_SAMPLE_RATE = 16000; - - constructor(config: VADConfig) { - super(); - this.config = config; - this.audioBuffer = new AudioBuffer(20, config.sampleRate); - - // Listen to audio chunks from buffer - this.audioBuffer.on('audioChunk', this.processAudioChunk.bind(this)); - } - - async initialize(): Promise { - if (this.isInitialized) { - console.log('SileroVAD: Already initialized'); - return; - } - - try { - console.log( - 'SileroVAD: Starting initialization with model path:', - this.config.modelPath - ); - - // Try to find CUDA device - const availableDevices = DeviceRegistry.getAvailableDevices(); - console.log( - 'SileroVAD: Available devices:', - availableDevices.map((d) => d.getType()) - ); - - const cudaDevice = availableDevices.find( - (device) => device.getType() === DeviceType.CUDA - ); - console.log('SileroVAD: Using CUDA device:', !!cudaDevice); - - // Create local VAD instance - console.log('SileroVAD: Creating VAD instance...'); - this.vad = await VADFactory.createLocal({ - modelPath: this.config.modelPath, - device: cudaDevice, - }); - - this.isInitialized = true; - console.log('SileroVAD: Initialization complete'); - } catch (error) { - console.error('SileroVAD: Failed to initialize:', error); - throw error; - } - } - - addAudioData(base64Data: string): void { - try { - // Convert base64 to buffer (same as working example needs) - const binaryString = Buffer.from(base64Data, 'base64').toString('binary'); - const bytes = new Uint8Array(binaryString.length); - - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - - // Convert to Int16Array then normalize to Float32 range [-1, 1] - const int16Array = new Int16Array(bytes.buffer); - - // Convert to normalized Float32Array for consistent audio processing - const normalizedArray = new Float32Array(int16Array.length); - for (let i = 0; i < int16Array.length; i++) { - normalizedArray[i] = int16Array[i] / 32768.0; // Normalize to [-1, 1] range - } - - // Add to audio buffer with normalized Float32Array - this.audioBuffer.addChunk(normalizedArray); - } catch (error) { - console.error('SileroVAD: Error processing audio data:', error); - } - } - - private calculateRMSVolume(audioData: Float32Array): number { - let sumSquares = 0; - for (let i = 0; i < audioData.length; i++) { - sumSquares += audioData[i] * audioData[i]; - } - return Math.sqrt(sumSquares / audioData.length); - } - - reset(): void { - // Reset all state when interruption occurs - this.accumulatedSamples = []; - this.isProcessingVAD = false; - this.isCapturingSpeech = false; - this.speechBuffer = []; - this.pauseDuration = 0; - } - - private async processAudioChunk(chunk: AudioChunk): Promise { - if (!this.isInitialized || !this.vad) { - return; - } - - // Skip if already processing to prevent concurrent VAD calls - if (this.isProcessingVAD) { - return; - } - - // Accumulate samples until we have enough for VAD processing - this.accumulatedSamples.push(chunk.data); - - // Process when we have enough samples (using FRAME_PER_BUFFER like working example) - const totalSamples = this.accumulatedSamples.reduce( - (sum, arr) => sum + arr.length, - 0 - ); - if (totalSamples >= this.FRAME_PER_BUFFER) { - this.isProcessingVAD = true; // Set flag before async operation - try { - // Combine accumulated samples - const combinedAudio = new Float32Array(totalSamples); - let offset = 0; - for (const samples of this.accumulatedSamples) { - combinedAudio.set(samples, offset); - offset += samples.length; - } - - // Calculate RMS volume for noise filtering - const rmsVolume = this.calculateRMSVolume(combinedAudio); - - // Convert normalized Float32Array back to integer array for VAD model - const integerArray: number[] = []; - for (let i = 0; i < combinedAudio.length; i++) { - // Convert back to Int16 range for VAD processing - integerArray.push(Math.round(combinedAudio[i] * 32768)); - } - - // Process with Silero VAD - const result = await this.vad.detectVoiceActivity({ - data: integerArray, - sampleRate: this.config.sampleRate, - }); - - // Following working example: -1 = no voice activity, anything else = voice activity - // Add volume filtering to the working pattern - const hasVoiceActivity = - result !== -1 && rmsVolume >= this.config.minVolume; - - // Process using simplified state machine from working example - await this.processVADResult(hasVoiceActivity, integerArray, rmsVolume); - - // Clear accumulated samples - this.accumulatedSamples = []; - } catch (error) { - console.error('VAD processing error:', error); - this.accumulatedSamples = []; // Clear on error - } finally { - this.isProcessingVAD = false; // Always reset flag - } - } - } - - private async processVADResult( - hasVoiceActivity: boolean, - audioChunk: number[], - volume: number - ): Promise { - // Following the working example pattern exactly - if (this.isCapturingSpeech) { - this.speechBuffer.push(...audioChunk); - if (!hasVoiceActivity) { - // Already capturing speech but new chunk has no voice activity - this.pauseDuration += - (audioChunk.length * 1000) / this.INPUT_SAMPLE_RATE; // ms - - if (this.pauseDuration > this.config.minSilenceDuration * 1000) { - // Convert to ms - this.isCapturingSpeech = false; - - const speechDuration = - (this.speechBuffer.length * 1000) / this.INPUT_SAMPLE_RATE; // ms - - if (speechDuration > this.config.minSpeechDuration * 1000) { - // Convert to ms - await this.processCapturedSpeech(); - } else { - // Speech was too short, but still emit speechEnd to notify that VAD stopped detecting - this.emit('speechEnd', { - timestamp: Date.now() / 1000, - speechSegment: null, // No segment to process - speechDuration: speechDuration / 1000, - samplesCount: this.speechBuffer.length, - }); - } - - // Reset for next speech capture - this.speechBuffer = []; - this.pauseDuration = 0; - } - } else { - // Already capturing speech and new chunk has voice activity - this.pauseDuration = 0; - } - } else { - if (hasVoiceActivity) { - // Not capturing speech but new chunk has voice activity - start capturing - this.isCapturingSpeech = true; - this.speechBuffer = [...audioChunk]; // Start fresh - this.pauseDuration = 0; - - this.emit('speechStart', { - timestamp: Date.now() / 1000, - volume, - }); - } - // Not capturing speech and new chunk has no voice activity - do nothing - } - } - - private async processCapturedSpeech(): Promise { - if (this.speechBuffer.length === 0) return; - - // Convert integer array back to Float32Array for processing - const speechSegment = new Float32Array(this.speechBuffer.length); - for (let i = 0; i < this.speechBuffer.length; i++) { - speechSegment[i] = this.speechBuffer[i] / 32768.0; // Normalize back to [-1, 1] - } - - const speechDuration = - (this.speechBuffer.length * 1000) / this.INPUT_SAMPLE_RATE; - - this.emit('speechEnd', { - timestamp: Date.now() / 1000, - speechSegment, - speechDuration: speechDuration / 1000, // Convert back to seconds - samplesCount: this.speechBuffer.length, - }); - } - - destroy(): void { - if (this.vad) { - this.vad.destroy(); - this.vad = null; - } - - this.audioBuffer.clear(); - this.isInitialized = false; - } -} diff --git a/backend/models/silero_vad.onnx b/backend/models/silero_vad.onnx deleted file mode 100644 index b3e3a90..0000000 Binary files a/backend/models/silero_vad.onnx and /dev/null differ diff --git a/package-lock.json b/backend/package-lock.json similarity index 71% rename from package-lock.json rename to backend/package-lock.json index 626100b..22d389d 100644 --- a/package-lock.json +++ b/backend/package-lock.json @@ -9,33 +9,101 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@inworld/runtime": "^0.8.0", + "@inworld/runtime": "https://storage.googleapis.com/assets-inworld-ai/node-packages/inworld-runtime-0.9.0-rc.14.tgz", + "@supabase/supabase-js": "^2.89.0", "anki-apkg-export": "^4.0.3", + "cors": "^2.8.5", "dotenv": "^17.2.1", "express": "^4.19.2", + "pino": "^10.1.0", "uuid": "^11.1.0", "ws": "^8.18.0" }, "devDependencies": { "@eslint/js": "^9.0.0", "@tsconfig/node-lts": "^22.0.2", + "@types/cors": "^2.8.19", "@types/express": "^4.17.21", "@types/node": "^20.0.0", "@types/ws": "^8.5.12", + "@vitest/coverage-v8": "^4.0.16", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "nodemon": "^3.1.10", + "pino-pretty": "^13.1.3", "prettier": "^3.2.5", + "supabase": "^2.70.5", "tsx": "^4.7.0", "typescript": "^5.3.3", - "typescript-eslint": "^8.0.0" + "typescript-eslint": "^8.0.0", + "vitest": "^4.0.16" }, "engines": { - "node": ">=18.0.0", + "node": ">=20.0.0", "npm": ">=9.0.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -696,329 +764,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@fastify/accept-negotiator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", - "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/ajv-compiler": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", - "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0" - } - }, - "node_modules/@fastify/cors": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.1.0.tgz", - "integrity": "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "fastify-plugin": "^5.0.0", - "mnemonist": "0.40.0" - } - }, - "node_modules/@fastify/error": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", - "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/fast-json-stringify-compiler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", - "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "fast-json-stringify": "^6.0.0" - } - }, - "node_modules/@fastify/forwarded": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", - "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/merge-json-schemas": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", - "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/@fastify/proxy-addr": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", - "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/forwarded": "^3.0.0", - "ipaddr.js": "^2.1.0" - } - }, - "node_modules/@fastify/proxy-addr/node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@fastify/send": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", - "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@lukeed/ms": "^2.0.2", - "escape-html": "~1.0.3", - "fast-decode-uri-component": "^1.0.1", - "http-errors": "^2.0.0", - "mime": "^3" - } - }, - "node_modules/@fastify/send/node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@fastify/static": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.3.0.tgz", - "integrity": "sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/accept-negotiator": "^2.0.0", - "@fastify/send": "^4.0.0", - "content-disposition": "^0.5.4", - "fastify-plugin": "^5.0.0", - "fastq": "^1.17.1", - "glob": "^11.0.0" - } - }, - "node_modules/@fastify/swagger": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.6.1.tgz", - "integrity": "sha512-fKlpJqFMWoi4H3EdUkDaMteEYRCfQMEkK0HJJ0eaf4aRlKd8cbq0pVkOfXDXmtvMTXYcnx3E+l023eFDBsA1HA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "fastify-plugin": "^5.0.0", - "json-schema-resolver": "^3.0.0", - "openapi-types": "^12.1.3", - "rfdc": "^1.3.1", - "yaml": "^2.4.2" - } - }, - "node_modules/@fastify/swagger-ui": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-5.2.3.tgz", - "integrity": "sha512-e7ivEJi9EpFcxTONqICx4llbpB2jmlI+LI1NQ/mR7QGQnyDOqZybPK572zJtcdHZW4YyYTBHcP3a03f1pOh0SA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/static": "^8.0.0", - "fastify-plugin": "^5.0.0", - "openapi-types": "^12.1.3", - "rfdc": "^1.3.1", - "yaml": "^2.4.1" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.1.tgz", - "integrity": "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.3", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", - "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@grpc/reflection": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@grpc/reflection/-/reflection-1.0.4.tgz", - "integrity": "sha512-znA8v4AviOD3OPOxy11pxrtP8k8DanpefeTymS8iGW1fVr1U2cHuzfhYqDPHnVNDf4qvF9E25KtSihPy2DBWfQ==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.7.13", - "protobufjs": "^7.2.5" - }, - "peerDependencies": { - "@grpc/grpc-js": "^1.8.21" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1071,116 +816,73 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@inworld/graph-server": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@inworld/graph-server/-/graph-server-0.8.0.tgz", - "integrity": "sha512-mwAwgIPF23RbWECNfiAcugtMFQ1ronkRu3LMoqJN7fw6EKq3npQ2lJrYdAS7WI0aJbEIWbJ3/vio17z6TP5WLQ==", - "license": "MIT", - "dependencies": { - "@fastify/cors": "^10.0.1", - "@fastify/static": "^8.0.2", - "@fastify/swagger": "^9.4.0", - "@fastify/swagger-ui": "^5.2.0", - "@grpc/grpc-js": "^1.12.4", - "@grpc/proto-loader": "^0.7.13", - "@grpc/reflection": "^1.0.4", - "fastify": "^5.2.0", - "protobufjs": "^7.4.0" - } - }, "node_modules/@inworld/runtime": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@inworld/runtime/-/runtime-0.8.0.tgz", - "integrity": "sha512-bSOydjXGiuSTm8BZ1t/dsUOK7l/WCseHjNRCMdHUFkevDzbGXHPaf7jQEPM3r4c4nyRiVabjHpi2zvLSCa+jWw==", + "version": "0.9.0-rc.14", + "resolved": "https://storage.googleapis.com/assets-inworld-ai/node-packages/inworld-runtime-0.9.0-rc.14.tgz", + "integrity": "sha512-Ukh19V5giiuCYGOjM4m8e4BmgO5YTVAup/J9UNJb5IgYY8Ht8lAhtG2Ni/6Y5e2w0WoKvr1I3zD6ro2tT5KyDQ==", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE.md and LICENSE-CPP-BINARIES.md", "dependencies": { - "@inworld/graph-server": "0.8.0", "@types/lodash.snakecase": "^4.1.9", + "@types/protobufjs": "^6.0.0", + "@zod/core": "^0.11.6", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "assemblyai": "^4.17.0", + "chalk": "^4.1.2", "decompress": "^4.2.1", "decompress-targz": "^4.1.1", "decompress-unzip": "^4.0.1", - "dotenv": "^16.4.7", "groq-sdk": "^0.33.0", - "koffi": "^2.10.1", "lodash.snakecase": "^4.1.1", "node-record-lpcm16": "^1.0.1", - "uuid": "^11.1.0" + "protobufjs": "^7.5.4", + "uuid": "^11.1.0", + "zod-to-json-schema": "^3.25.0" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@inworld/runtime/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/@isaacs/balanced-match": { + "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, "license": "ISC", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "minipass": "^7.0.4" }, "engines": { - "node": ">=12" + "node": ">=18.0.0" } }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@lukeed/ms": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", - "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@nodelib/fs.scandir": { @@ -1304,497 +1006,665 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, - "node_modules/@tsconfig/node-lts": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node-lts/-/node-lts-22.0.2.tgz", - "integrity": "sha512-Kgq5yXTvnUnvlhob0xJpOH4na9PWtuFhHSf94MpDwnENWgiFeJKDNANQV2MT1WpXZYkK2WSWfVYKhVkR7bc8TA==", + "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", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", - "license": "MIT" - }, - "node_modules/@types/lodash.snakecase": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/lodash.snakecase/-/lodash.snakecase-4.1.9.tgz", - "integrity": "sha512-emBZJUiNlo+QPXr1junMKXwzHJK9zbFvTVdyAoorFcm1YRsbzkZCYPTVMM9AW+dlnA6utG7vpfvOs8alxv/TMw==", "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", - "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.4" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", - "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, + "license": "MIT" + }, + "node_modules/@supabase/auth-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.89.0.tgz", + "integrity": "sha512-wiWZdz8WMad8LQdJMWYDZ2SJtZP5MwMqzQq3ehtW2ngiI3UTgbKiFrvMUUS3KADiVlk4LiGfODB2mrYx7w2f8w==", "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/type-utils": "8.46.4", - "@typescript-eslint/utils": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "tslib": "2.8.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.46.4", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, + "node_modules/@supabase/functions-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.89.0.tgz", + "integrity": "sha512-XEueaC5gMe5NufNYfBh9kPwJlP5M2f+Ogr8rvhmRDAZNHgY6mI35RCkYDijd92pMcNM7g8pUUJov93UGUnqfyw==", "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, "engines": { - "node": ">= 4" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", - "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", - "dev": true, + "node_modules/@supabase/postgrest-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.89.0.tgz", + "integrity": "sha512-/b0fKrxV9i7RNOEXMno/I1862RsYhuUo+Q6m6z3ar1f4ulTMXnDfv0y4YYxK2POcgrOXQOgKYQx1eArybyNvtg==", "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", - "debug": "^4.3.4" + "tslib": "2.8.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "node_modules/@supabase/realtime-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.89.0.tgz", + "integrity": "sha512-aMOvfDb2a52u6PX6jrrjvACHXGV3zsOlWRzZsTIOAJa0hOVvRp01AwC1+nLTGUzxzezejrYeCX+KnnM1xHdl+w==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", - "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", - "dev": true, + "node_modules/@supabase/storage-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.89.0.tgz", + "integrity": "sha512-6zKcXofk/M/4Eato7iqpRh+B+vnxeiTumCIP+Tz26xEqIiywzD9JxHq+udRrDuv6hXE+pmetvJd8n5wcf4MFRQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.4", - "@typescript-eslint/types": "^8.46.4", - "debug": "^4.3.4" + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/project-service/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "node_modules/@supabase/supabase-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.89.0.tgz", + "integrity": "sha512-KlaRwSfFA0fD73PYVMHj5/iXFtQGCcX7PSx0FdQwYEEw9b2wqM7GxadY+5YwcmuEhalmjFB/YvqaoNVF+sWUlg==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "@supabase/auth-js": "2.89.0", + "@supabase/functions-js": "2.89.0", + "@supabase/postgrest-js": "2.89.0", + "@supabase/realtime-js": "2.89.0", + "@supabase/storage-js": "2.89.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/project-service/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/@tsconfig/node-lts": { + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node-lts/-/node-lts-22.0.2.tgz", + "integrity": "sha512-Kgq5yXTvnUnvlhob0xJpOH4na9PWtuFhHSf94MpDwnENWgiFeJKDNANQV2MT1WpXZYkK2WSWfVYKhVkR7bc8TA==", "dev": true, "license": "MIT" }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", - "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@types/connect": "*", + "@types/node": "*" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", - "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", - "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/utils": "8.46.4", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@types/node": "*" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "@types/node": "*" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, - "node_modules/@typescript-eslint/types": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", - "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", - "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.4", - "@typescript-eslint/tsconfig-utils": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "license": "MIT" + }, + "node_modules/@types/lodash.snakecase": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/lodash.snakecase/-/lodash.snakecase-4.1.9.tgz", + "integrity": "sha512-emBZJUiNlo+QPXr1junMKXwzHJK9zbFvTVdyAoorFcm1YRsbzkZCYPTVMM9AW+dlnA6utG7vpfvOs8alxv/TMw==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "@types/lodash": "*" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "undici-types": "~6.21.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "@types/node": "*", + "form-data": "^4.0.4" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/@types/phoenix": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", + "license": "MIT" + }, + "node_modules/@types/protobufjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/protobufjs/-/protobufjs-6.0.0.tgz", + "integrity": "sha512-A27RDExpAf3rdDjIrHKiJK6x8kqqJ4CmoChwtipfhVAn1p7+wviQFFP7dppn8FslSbHtQeVPvi8wNKkDjSYjHw==", + "deprecated": "This is a stub types definition for protobufjs (https://github.com/dcodeIO/ProtoBuf.js). protobufjs provides its own type definitions, so you don't need @types/protobufjs installed!", + "license": "MIT", + "dependencies": { + "protobufjs": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "dev": true, "license": "MIT" }, - "node_modules/@typescript-eslint/utils": { + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", - "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", + "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", + "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4" + "@typescript-eslint/type-utils": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1804,19 +1674,33 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { + "@typescript-eslint/parser": "^8.46.4", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", - "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", + "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", "dependencies": { + "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1824,1021 +1708,1473 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { - "event-target-shim": "^5.0.0" + "ms": "^2.1.3" }, "engines": { - "node": ">=6.5" - } - }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "license": "MIT" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "node": ">=6.0" }, - "engines": { - "node": ">= 0.6" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } + "license": "MIT" }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", + "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", "dependencies": { - "humanize-ms": "^1.2.1" + "@typescript-eslint/tsconfig-utils": "^8.46.4", + "@typescript-eslint/types": "^8.46.4", + "debug": "^4.3.4" }, "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "node_modules/@typescript-eslint/project-service/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0" + "ms": "^2.1.3" }, - "peerDependencies": { - "ajv": "^8.0.0" + "engines": { + "node": ">=6.0" }, "peerDependenciesMeta": { - "ajv": { + "supports-color": { "optional": true } } }, - "node_modules/anki-apkg-export": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/anki-apkg-export/-/anki-apkg-export-4.0.3.tgz", - "integrity": "sha512-r2DblNjY8L/Cqc7sKULqqibJAyLTy3EEVZkBbVPXbSf66qlYTYf939/NUhhOfKH6bMawe93NjlKKNT5eWo1+VA==", - "license": "MIT", - "dependencies": { - "jszip": "^3.2.2", - "sha1": "^1.1.1", - "sql.js": "^0.5.0" - } + "node_modules/@typescript-eslint/project-service/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", + "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", + "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4" + }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", + "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", + "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", + "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": ">= 8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/assemblyai": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/assemblyai/-/assemblyai-4.19.0.tgz", - "integrity": "sha512-3yMTZoipmae16Xj3htLKtG/m7PMOt/enIn16u3leX9iF+GiTpzuj60aUqA3zq19lkoDauVlpWVy6PIYssWN1Nw==", "license": "MIT", "dependencies": { - "ws": "^8.18.0" + "ms": "^2.1.3" }, "engines": { - "node": ">=18" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "node_modules/@typescript-eslint/types": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", + "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "dev": true, "license": "MIT", "dependencies": { - "possible-typed-array-names": "^1.0.0" + "@typescript-eslint/project-service": "8.46.4", + "@typescript-eslint/tsconfig-utils": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/avvio": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", - "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", "dependencies": { - "@fastify/error": "^4.0.0", - "fastq": "^1.17.1" + "balanced-match": "^1.0.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } - ], - "license": "MIT" + } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", + "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "dev": true, "license": "MIT", "dependencies": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", + "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "@typescript-eslint/types": "8.46.4", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/@vitest/coverage-v8": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.16.tgz", + "integrity": "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.16", + "ast-v8-to-istanbul": "^0.3.8", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.16", + "vitest": "4.0.16" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/@vitest/expect": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "node_modules/@vitest/mocker": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.16", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true }, - { - "type": "consulting", - "url": "https://feross.org/support" + "vite": { + "optional": true } - ], + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "dev": true, "license": "MIT", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "node_modules/@vitest/runner": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", + "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "dev": true, "license": "MIT", "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" + "@vitest/utils": "4.0.16", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "license": "MIT" - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "node_modules/@vitest/snapshot": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", + "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "dev": true, "license": "MIT", - "engines": { - "node": "*" + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/@vitest/spy": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "node_modules/@vitest/utils": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" + "@vitest/pretty-format": "4.0.16", + "tinyrainbow": "^3.0.3" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/vitest" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/@zod/core": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@zod/core/-/core-0.11.6.tgz", + "integrity": "sha512-03Bv82fFSfjDAvMfdHHdGSS6SOJs0iCcJlWJv1kJHRtoTT02hZpyip/2Lk6oo4l4FtjuwTrsEQTwg/LD8I7dJA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "event-target-shim": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=6.5" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.6" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=6" + "node": ">=0.4.0" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "humanize-ms": "^1.2.1" }, "engines": { - "node": ">=10" + "node": ">= 8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ajv": "^8.0.0" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "ajv": "^8.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "node_modules/anki-apkg-export": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/anki-apkg-export/-/anki-apkg-export-4.0.3.tgz", + "integrity": "sha512-r2DblNjY8L/Cqc7sKULqqibJAyLTy3EEVZkBbVPXbSf66qlYTYf939/NUhhOfKH6bMawe93NjlKKNT5eWo1+VA==", "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "jszip": "^3.2.2", + "sha1": "^1.1.1", + "sql.js": "^0.5.0" } }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "has-flag": "^4.0.0" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">=8" - } - }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" + "node": ">= 8" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/assemblyai": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/assemblyai/-/assemblyai-4.19.0.tgz", + "integrity": "sha512-3yMTZoipmae16Xj3htLKtG/m7PMOt/enIn16u3leX9iF+GiTpzuj60aUqA3zq19lkoDauVlpWVy6PIYssWN1Nw==", "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "ws": "^8.18.0" }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=18" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=12" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", + "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=8.0.0" } }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "possible-typed-array-names": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bin-links": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-6.0.0.tgz", + "integrity": "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w==", + "dev": true, + "license": "ISC", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "cmd-shim": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "proc-log": "^6.0.0", + "read-cmd-shim": "^6.0.0", + "write-file-atomic": "^7.0.0" }, "engines": { - "node": ">=8" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "fill-range": "^7.1.1" }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" } }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "license": "BSD-3-Clause", "engines": { "node": "*" } }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">= 0.8" } }, - "node_modules/decompress": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", - "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "license": "MIT", "dependencies": { - "decompress-tar": "^4.0.0", - "decompress-tarbz2": "^4.0.0", - "decompress-targz": "^4.0.0", - "decompress-unzip": "^4.0.1", - "graceful-fs": "^4.1.10", - "make-dir": "^1.0.0", - "pify": "^2.3.0", - "strip-dirs": "^2.0.0" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { - "file-type": "^5.2.0", - "is-stream": "^1.1.0", - "tar-stream": "^1.5.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">=4" + "node": ">= 0.4" } }, - "node_modules/decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "decompress-tar": "^4.1.0", - "file-type": "^6.1.0", - "is-stream": "^1.1.0", - "seek-bzip": "^1.0.5", - "unbzip2-stream": "^1.0.9" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/decompress-tarbz2/node_modules/file-type": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", - "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6" } }, - "node_modules/decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, "license": "MIT", - "dependencies": { - "decompress-tar": "^4.1.1", - "file-type": "^5.2.0", - "is-stream": "^1.1.0" - }, "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/decompress-unzip/node_modules/file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "color-convert": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { - "node": ">=0.4.0" + "node": ">=8" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": "*" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/dotenv": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", - "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", - "license": "BSD-2-Clause", + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" + "node": ">=18" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/cmd-shim": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-8.0.0.tgz", + "integrity": "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">= 0.4" + "node": ">=7.0.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, "engines": { "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", "dependencies": { - "once": "^1.4.0" + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 0.6" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 0.6" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "object-assign": "^4", + "vary": "^1" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.10" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 8" } }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", + "node": ">= 12" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "license": "MIT", + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "license": "MIT", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", @@ -2859,1209 +3195,1796 @@ "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/escalade": { + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-copy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fetch-blob": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, "engines": { - "node": ">=6" + "node": "^12.20 || >= 14.13" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" - }, + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", - "dev": true, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", - "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-config-prettier": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", - "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", - "dev": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", - "peer": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, - "peerDependencies": { - "eslint": ">=7.0.0" + "engines": { + "node": ">= 0.4" } }, - "node_modules/eslint-plugin-prettier": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", - "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", - "dev": true, + "node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.7" + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": ">=0.10.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" }, "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "BSD-2-Clause", + "license": "ISC", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "is-glob": "^4.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">= 6" } }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "engines": { + "node": ">= 0.4" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/groq-sdk": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/groq-sdk/-/groq-sdk-0.33.0.tgz", + "integrity": "sha512-wb7NrBq7LZDDhDPSpuAd9LpZ0MNjmWKGLfybYfjY3r63mSpfiP8+GQZQcSDJcX+jIMzSm+SwzxModDyVZ2T66Q==", + "license": "Apache-2.0", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", + "node_modules/groq-sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" + "undici-types": "~5.26.4" } }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "node_modules/groq-sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, - "node_modules/eslint/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "es-define-property": "^1.0.0" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { - "estraverse": "^5.1.0" + "has-symbols": "^1.0.3" }, "engines": { - "node": ">=0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=4.0" + "node": ">= 0.4" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } + "license": "MIT" }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } + "license": "MIT" }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "toidentifier": "1.0.1" }, "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.8" } }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">=8.6.0" + "node": ">= 14" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stringify": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.1.1.tgz", - "integrity": "sha512-DbgptncYEXZqDUOEl4krff4mUiVrTZZVI7BBrQR/T3BqMj/eM1flTC1Uk2uUoLcWCxjT95xKulV/Lc6hhOZsBQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], "license": "MIT", "dependencies": { - "@fastify/merge-json-schemas": "^0.2.0", - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0", - "json-schema-ref-resolver": "^3.0.0", - "rfdc": "^1.2.0" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, - "node_modules/fast-querystring": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", - "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "license": "MIT", "dependencies": { - "fast-decode-uri-component": "^1.0.1" + "ms": "^2.0.0" } }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastify": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.6.2.tgz", - "integrity": "sha512-dPugdGnsvYkBlENLhCgX8yhyGCsCPrpA8lFWbTNU428l+YOnLgYHR69hzV8HWPC79n536EqzqQtvhtdaCE0dKg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { - "@fastify/ajv-compiler": "^4.0.0", - "@fastify/error": "^4.0.0", - "@fastify/fast-json-stringify-compiler": "^5.0.0", - "@fastify/proxy-addr": "^5.0.0", - "abstract-logging": "^2.0.1", - "avvio": "^9.0.0", - "fast-json-stringify": "^6.0.0", - "find-my-way": "^9.0.0", - "light-my-request": "^6.0.0", - "pino": "^10.1.0", - "process-warning": "^5.0.0", - "rfdc": "^1.3.1", - "secure-json-parse": "^4.0.0", - "semver": "^7.6.0", - "toad-cache": "^3.7.0" + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/fastify-plugin": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", - "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", - "url": "https://github.com/sponsors/fastify" + "url": "https://github.com/sponsors/feross" }, { - "type": "opencollective", - "url": "https://opencollective.com/fastify" + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "BSD-3-Clause" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { - "pend": "~1.2.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, "engines": { - "node": ">=16.0.0" + "node": ">=0.8.19" } }, - "node_modules/file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.10" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "binary-extensions": "^2.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, - "node_modules/find-my-way": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.3.0.tgz", - "integrity": "sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-querystring": "^1.0.0", - "safe-regex2": "^5.0.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=20" + "node": ">=0.10.0" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "which-typed-array": "^1.1.16" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "MIT", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=16" + "node": ">=10" } }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.2.7" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=10" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "ms": "^2.1.3" }, "engines": { - "node": ">= 6" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "license": "MIT", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">= 12.20" + "node": ">=8" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=10" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/fs-constants": { + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "json-buffer": "3.0.1" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.8.0" } }, - "node_modules/get-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "license": "MIT", "dependencies": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "immediate": "~3.0.5" } }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", - "license": "BlueOak-1.0.0", + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/magicast": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "license": "BlueOak-1.0.0", + "node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "license": "MIT", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "pify": "^3.0.0" }, "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=4" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, + "node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/groq-sdk": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/groq-sdk/-/groq-sdk-0.33.0.tgz", - "integrity": "sha512-wb7NrBq7LZDDhDPSpuAd9LpZ0MNjmWKGLfybYfjY3r63mSpfiP8+GQZQcSDJcX+jIMzSm+SwzxModDyVZ2T66Q==", - "license": "Apache-2.0", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/groq-sdk/node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/groq-sdk/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.6" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8.6" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" + "bin": { + "mime": "cli.js" }, "engines": { - "node": ">= 0.4" + "node": ">=4" } }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "ms": "^2.0.0" + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 4" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", "engines": { - "node": ">=0.8.19" + "node": ">= 0.6" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=10.5.0" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=8" + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "node_modules/node-record-lpcm16": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/node-record-lpcm16/-/node-record-lpcm16-1.0.1.tgz", + "integrity": "sha512-H75GMOP8ErnF67m21+qSgj4USnzv5RLfm7OkEItdIi+soNKoJZpMQPX6umM8Cn9nVPSgd/dBUtc1msst5MmABA==", + "license": "ISC", + "dependencies": { + "debug": "^2.6.8" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/nodemon" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-natural-number": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=0.12.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, "engines": { "node": ">= 0.4" }, @@ -4069,1947 +4992,2252 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT" }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=14.0.0" } }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "ee-first": "1.1.1" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">= 0.8" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-ref-resolver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", - "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { - "dequal": "^2.0.3" + "wrappy": "1" } }, - "node_modules/json-schema-resolver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-3.0.0.tgz", - "integrity": "sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.1.1", - "fast-uri": "^3.0.5", - "rfdc": "^1.1.4" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/Eomm/json-schema-resolver?sponsor=1" + "node": ">= 0.8.0" } }, - "node_modules/json-schema-resolver/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6.0" + "node": ">=10" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/json-schema-resolver/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/koffi": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.13.0.tgz", - "integrity": "sha512-VqQzBC7XBVJHXA4DkmY68HbH8VTYVaBKq3MFlziI+pdJXIpd/lO4LeXKo2YpIhuTkLgXYra+dDjJOo2+yT1Tsg==", - "hasInstallScript": true, - "license": "MIT" + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "callsites": "^3.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=6" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/light-my-request": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", - "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause", - "dependencies": { - "cookie": "^1.0.1", - "process-warning": "^4.0.0", - "set-cookie-parser": "^2.6.0" + "engines": { + "node": ">= 0.8" } }, - "node_modules/light-my-request/node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/light-my-request/node_modules/process-warning": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", - "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, - "node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", - "license": "ISC", + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", "engines": { - "node": "20 || >=22" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "license": "MIT", "dependencies": { - "pify": "^3.0.0" + "pinkie": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/make-dir/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "split2": "^4.0.0" } }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" } }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "node_modules/pino-pretty/node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "split2": "^4.0.0" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.4" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=8.6" + "node": "^10 || ^12 || >=14" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "license": "MIT", - "bin": { - "mime": "cli.js" - }, "engines": { - "node": ">=4" + "node": ">= 0.8.0" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, "engines": { - "node": ">= 0.6" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "fast-diff": "^1.1.2" }, "engines": { - "node": ">= 0.6" + "node": ">=6.0.0" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, "engines": { - "node": "*" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=12.0.0" } }, - "node_modules/mnemonist": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.0.tgz", - "integrity": "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==", + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", "dependencies": { - "obliterator": "^2.0.4" + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" } }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, "license": "MIT" }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", - "url": "https://github.com/sponsors/jimmywarting" + "url": "https://github.com/sponsors/feross" }, { - "type": "github", - "url": "https://paypal.me/jimmywarting" + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { - "node": ">=10.5.0" + "node": ">= 0.6" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-record-lpcm16": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/node-record-lpcm16/-/node-record-lpcm16-1.0.1.tgz", - "integrity": "sha512-H75GMOP8ErnF67m21+qSgj4USnzv5RLfm7OkEItdIi+soNKoJZpMQPX6umM8Cn9nVPSgd/dBUtc1msst5MmABA==", - "license": "ISC", - "dependencies": { - "debug": "^2.6.8" + "node": ">= 0.8" } }, - "node_modules/nodemon": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", - "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", - "dev": true, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">=10" + "node": ">= 0.8" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/nodemon" + "url": "https://opencollective.com/express" } }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">= 0.8" } }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/read-cmd-shim": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz", + "integrity": "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==", "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8.10.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=4" } }, - "node_modules/obliterator": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", - "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", - "license": "MIT" + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=14.0.0" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, "license": "MIT", "dependencies": { - "ee-first": "1.1.1" + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">= 0.8" + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", "dependencies": { - "wrappy": "1" + "queue-microtask": "^1.2.2" } }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=10" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" + "commander": "^2.8.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", "dependencies": { - "callsites": "^3.0.0" + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" }, "engines": { - "node": ">=6" + "node": ">= 0.8.0" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, "engines": { - "node": ">=8" + "node": ">= 0.8.0" } }, - "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", - "license": "BlueOak-1.0.0", + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" }, "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 0.4" } }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, - "node_modules/pend": { + "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "license": "MIT", + "node_modules/sha1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", + "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", + "license": "BSD-3-Clause", "dependencies": { - "pinkie": "^2.0.0" + "charenc": ">= 0.0.1", + "crypt": ">= 0.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pino": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", - "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", - "license": "MIT", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" + "node": "*" } }, - "node_modules/pino-abstract-transport": { + "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", - "license": "MIT" - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", + "shebang-regex": "^3.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8.0" + "node": ">=8" } }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", - "peer": true, - "bin": { - "prettier": "bin/prettier.cjs" + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/prettier-linter-helpers": { + "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { - "fast-diff": "^1.1.2" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=6.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=12.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=6" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", "dependencies": { - "side-channel": "^1.0.6" + "semver": "^7.5.3" }, "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } }, - "node_modules/range-parser": { + "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql.js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-0.5.0.tgz", + "integrity": "sha512-E80yfgPSOvkbEweROP8kUg+8YfvuWht2RYhPMngkJuKD7IgFVWPP/pAhSAUVZMY9UPf5m10Pi0yW9e4CRnmYGg==", + "license": "MIT" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, "engines": { "node": ">= 0.8" } }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "safe-buffer": "~5.1.0" } }, - "node_modules/readable-stream/node_modules/safe-buffer": { + "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "node_modules/strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" + "is-natural-number": "^4.0.1" } }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 12.13.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "node_modules/supabase": { + "version": "2.70.5", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.70.5.tgz", + "integrity": "sha512-HtMGtYbcL9dxi2Yz4eGWWlxl9nQi76pOyGgCTBeE67p5AeIlv+KJhDOR8oinaNfHHMPKk0Uo1WW7Kxyrz+tdgg==", + "dev": true, + "hasInstallScript": true, "license": "MIT", + "dependencies": { + "bin-links": "^6.0.0", + "https-proxy-agent": "^7.0.2", + "node-fetch": "^3.3.2", + "tar": "7.5.2" + }, + "bin": { + "supabase": "bin/supabase" + }, "engines": { - "node": ">=0.10.0" + "npm": ">=8" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "node_modules/supabase/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, "engines": { - "node": ">=0.10.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, "engines": { "node": ">=4" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", "dev": true, "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "url": "https://opencollective.com/synckit" } }, - "node_modules/ret": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", - "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", - "license": "MIT", + "node_modules/tar": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "license": "MIT", + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" + "node": ">= 0.8.0" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "license": "MIT" - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", "license": "MIT", "dependencies": { - "queue-microtask": "^1.2.2" + "real-require": "^0.2.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, - "node_modules/safe-regex2": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", - "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "ret": "~0.5.0" - } + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/secure-json-parse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", - "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/seek-bzip": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", - "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", "dependencies": { - "commander": "^2.8.1" - }, - "bin": { - "seek-bunzip": "bin/seek-bunzip", - "seek-table": "bin/seek-bzip-table" - } - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=10" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=14.0.0" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", "license": "MIT", "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" }, "engines": { "node": ">= 0.4" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sha1": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", - "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", - "license": "BSD-3-Clause", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", "dependencies": { - "charenc": ">= 0.0.1", - "crypt": ">= 0.0.1" + "is-number": "^7.0.0" }, "engines": { - "node": "*" + "node": ">=8.0" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">=0.6" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">= 0.4" + "node": ">=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.8.0" } }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.6" } }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=14.17" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "node_modules/typescript-eslint": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", + "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.5.3" + "@typescript-eslint/eslint-plugin": "8.46.4", + "@typescript-eslint/parser": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4" }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "license": "MIT", "dependencies": { - "atomic-sleep": "^1.0.0" + "buffer": "^5.2.1", + "through": "^2.3.8" } }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { - "node": ">= 10.x" + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" } }, - "node_modules/sql.js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-0.5.0.tgz", - "integrity": "sha512-E80yfgPSOvkbEweROP8kUg+8YfvuWht2RYhPMngkJuKD7IgFVWPP/pAhSAUVZMY9UPf5m10Pi0yW9e4CRnmYGg==", + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.4.0" } }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "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", - "dependencies": { - "safe-buffer": "~5.1.0" + "bin": { + "uuid": "dist/esm/bin/uuid" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/strip-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", - "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "is-natural-number": "^4.0.1" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" + "node": ">=18" } }, - "node_modules/tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.8.0" + "node": ">=18" } }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "license": "MIT" - }, - "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, "license": "MIT", - "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/to-buffer/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8.0" + "node": ">=18" } }, - "node_modules/toad-cache": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", - "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.6" + "node": ">=18" } }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" + "node": ">=18" } }, - "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "node": ">=18" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 0.8.0" + "node": ">=18" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 0.6" + "node": ">=18" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=14.17" + "node": ">=18" } }, - "node_modules/typescript-eslint": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", - "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.4", - "@typescript-eslint/parser": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/utils": "8.46.4" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">=18" } }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8" + "node": ">=18" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "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", + "node_modules/vitest": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", + "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.16", + "@vitest/mocker": "4.0.16", + "@vitest/pretty-format": "4.0.16", + "@vitest/runner": "4.0.16", + "@vitest/snapshot": "4.0.16", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, "bin": { - "uuid": "dist/esm/bin/uuid" + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.16", + "@vitest/browser-preview": "4.0.16", + "@vitest/browser-webdriverio": "4.0.16", + "@vitest/ui": "4.0.16", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/web-streams-polyfill": { @@ -6041,6 +7269,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -6073,105 +7302,31 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "siginfo": "^2.0.0", + "stackback": "0.0.2" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "bin": { + "why-is-node-running": "cli.js" }, "engines": { "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/wrappy": { @@ -6180,6 +7335,20 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.0.tgz", + "integrity": "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -6210,93 +7379,14 @@ "node": ">=0.4" } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/yauzl": { @@ -6321,6 +7411,25 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/package.json b/backend/package.json similarity index 56% rename from package.json rename to backend/package.json index 5a52a59..302c0a0 100644 --- a/package.json +++ b/backend/package.json @@ -1,13 +1,17 @@ { "name": "language-learning", "version": "1.0.0", - "main": "dist/backend/server.js", + "main": "dist/server.js", "type": "module", "scripts": { - "build": "tsc", - "start": "node dist/backend/server.js", - "dev": "nodemon --exec \"tsx backend/server.ts\" --ext ts", - "test-stt": "tsx backend/test-stt.ts", + "build": "tsc && cp -r src/prompts dist/prompts", + "start": "node dist/server.js", + "dev": "nodemon --exec \"tsx src/server.ts\" --ext ts", + "test-stt": "tsx test-stt.ts", + "test": "npm run test:backend && npm run test:frontend", + "test:backend": "vitest run --config vitest.config.ts", + "test:frontend": "npm run test --prefix ../frontend", + "test:watch": "vitest --config vitest.config.ts", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", @@ -30,23 +34,31 @@ "devDependencies": { "@eslint/js": "^9.0.0", "@tsconfig/node-lts": "^22.0.2", + "@types/cors": "^2.8.19", "@types/express": "^4.17.21", "@types/node": "^20.0.0", "@types/ws": "^8.5.12", + "@vitest/coverage-v8": "^4.0.16", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "nodemon": "^3.1.10", + "pino-pretty": "^13.1.3", "prettier": "^3.2.5", + "supabase": "^2.70.5", "tsx": "^4.7.0", "typescript": "^5.3.3", - "typescript-eslint": "^8.0.0" + "typescript-eslint": "^8.0.0", + "vitest": "^4.0.16" }, "dependencies": { - "@inworld/runtime": "^0.8.0", + "@inworld/runtime": "https://storage.googleapis.com/assets-inworld-ai/node-packages/inworld-runtime-0.9.0-rc.14.tgz", + "@supabase/supabase-js": "^2.89.0", "anki-apkg-export": "^4.0.3", + "cors": "^2.8.5", "dotenv": "^17.2.1", "express": "^4.19.2", + "pino": "^10.1.0", "uuid": "^11.1.0", "ws": "^8.18.0" } diff --git a/backend/server.ts b/backend/server.ts deleted file mode 100644 index 30b441f..0000000 --- a/backend/server.ts +++ /dev/null @@ -1,385 +0,0 @@ -// Load environment variables FIRST -import dotenv from 'dotenv'; -dotenv.config(); -import path from 'path'; -import { fileURLToPath } from 'url'; -import { existsSync } from 'fs'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Basic imports -import express from 'express'; -import { createServer } from 'http'; -import { WebSocketServer } from 'ws'; -import { telemetry, stopInworldRuntime } from '@inworld/runtime'; -import { MetricType } from '@inworld/runtime/telemetry'; -import { UserContextInterface } from '@inworld/runtime/graph'; - -// Import our audio processor -import { AudioProcessor } from './helpers/audio-processor.js'; -import { FlashcardProcessor } from './helpers/flashcard-processor.js'; -import { AnkiExporter } from './helpers/anki-exporter.js'; -import { IntroductionStateProcessor } from './helpers/introduction-state-processor.js'; -import { createConversationGraph } from './graphs/conversation-graph.js'; - -const app = express(); -const server = createServer(app); -const wss = new WebSocketServer({ server }); - -// Add JSON parsing middleware -app.use(express.json()); - -const PORT = process.env.PORT || 3000; - -// Initialize telemetry once at startup -try { - const telemetryApiKey = process.env.INWORLD_API_KEY; - if (telemetryApiKey) { - telemetry.init({ - apiKey: telemetryApiKey, - appName: 'Aprendemo', - appVersion: '1.0.0', - }); - - telemetry.configureMetric({ - metricType: MetricType.CounterUInt, - name: 'flashcard_clicks_total', - description: 'Total flashcard clicks', - unit: 'clicks', - }); - } else { - console.warn( - '[Telemetry] INWORLD_API_KEY not set. Metrics will be disabled.' - ); - } -} catch (err) { - console.error('[Telemetry] Initialization failed:', err); -} - -// Create a single shared conversation graph instance -// State is passed directly through AsyncLocalStorage, no registry needed -let sharedConversationGraph: ReturnType | null = - null; - -// Store audio processors per connection -const audioProcessors = new Map(); -const flashcardProcessors = new Map(); -const introductionStateProcessors = new Map< - string, - IntroductionStateProcessor ->(); -// Store lightweight per-connection attributes provided by the client (e.g., timezone, userId) -const connectionAttributes = new Map< - string, - { timezone?: string; userId?: string } ->(); - -// Initialize shared graph once at startup -function initializeSharedGraph() { - if (!sharedConversationGraph) { - const apiKey = process.env.INWORLD_API_KEY || ''; - sharedConversationGraph = createConversationGraph({ apiKey }); - console.log('Shared conversation graph initialized'); - } - return sharedConversationGraph; -} - -// WebSocket handling with audio processing -wss.on('connection', (ws) => { - const connectionId = `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - console.log(`WebSocket connection established: ${connectionId}`); - - // Create audio processor for this connection - // Use the shared graph instance - state is passed via AsyncLocalStorage - const sharedGraph = initializeSharedGraph(); - const audioProcessor = new AudioProcessor(sharedGraph, ws); - const flashcardProcessor = new FlashcardProcessor(); - const introductionStateProcessor = new IntroductionStateProcessor(); - - // Register audio processor - audioProcessors.set(connectionId, audioProcessor); - flashcardProcessors.set(connectionId, flashcardProcessor); - introductionStateProcessors.set(connectionId, introductionStateProcessor); - - // Set up flashcard generation callback - audioProcessor.setFlashcardCallback(async (messages) => { - // Skip flashcard generation if we're shutting down - if (isShuttingDown) { - console.log('Skipping flashcard generation - server is shutting down'); - return; - } - - try { - // Build UserContext for flashcard graph execution - const introState = introductionStateProcessor.getState(); - const attrs = connectionAttributes.get(connectionId) || {}; - const userAttributes: Record = { - timezone: attrs.timezone || '', - }; - userAttributes.name = - (introState?.name && introState.name.trim()) || 'unknown'; - userAttributes.level = - (introState?.level && (introState.level as string)) || 'unknown'; - userAttributes.goal = - (introState?.goal && introState.goal.trim()) || 'unknown'; - - // Prefer a stable targeting key from client if available, fallback to connectionId - const targetingKey = attrs.userId || connectionId; - const userContext: UserContextInterface = { - attributes: userAttributes, - targetingKey: targetingKey, - }; - - const flashcards = await flashcardProcessor.generateFlashcards( - messages, - 1, - userContext - ); - if (flashcards.length > 0) { - ws.send( - JSON.stringify({ - type: 'flashcards_generated', - flashcards: flashcards, - }) - ); - } - } catch (error: unknown) { - // Suppress "Environment closed" errors during shutdown - they're expected - const err = error as { context?: string }; - if (isShuttingDown && err?.context === 'Environment closed') { - console.log('Flashcard generation cancelled due to shutdown'); - } else { - console.error('Error generating flashcards:', error); - } - } - }); - - // Set up introduction-state extraction callback (runs until complete) - audioProcessor.setIntroductionStateCallback(async (messages) => { - try { - const currentState = introductionStateProcessor.getState(); - console.log( - 'Server - Current introduction state before update:', - currentState - ); - console.log( - 'Server - Is complete?', - introductionStateProcessor.isComplete() - ); - - if (introductionStateProcessor.isComplete()) { - console.log( - 'Server - Introduction state is complete, returning:', - currentState - ); - return currentState; - } - - const state = await introductionStateProcessor.update(messages); - console.log('Server - Updated introduction state:', state); - return state; - } catch (error) { - console.error('Error generating introduction state:', error); - return null; - } - }); - - ws.on('message', (data) => { - try { - const message = JSON.parse(data.toString()); - - if (message.type === 'audio_chunk' && message.audio_data) { - // Process audio chunk - audioProcessor.addAudioChunk(message.audio_data); - } else if (message.type === 'reset_flashcards') { - // Reset flashcards for new conversation - const processor = flashcardProcessors.get(connectionId); - if (processor) { - processor.reset(); - } - } else if (message.type === 'restart_conversation') { - // Reset conversation state, introduction state, and flashcards - const audioProc = audioProcessors.get(connectionId); - const flashcardProc = flashcardProcessors.get(connectionId); - const introStateProc = introductionStateProcessors.get(connectionId); - - if (audioProc) { - audioProc.reset(); - } - if (flashcardProc) { - flashcardProc.reset(); - } - if (introStateProc) { - introStateProc.reset(); - } - - console.log(`Conversation restarted for connection: ${connectionId}`); - } else if (message.type === 'user_context') { - const timezone = - message.timezone || - (message.data && message.data.timezone) || - undefined; - const userId = - message.userId || (message.data && message.data.userId) || undefined; - connectionAttributes.set(connectionId, { timezone, userId }); - } else if (message.type === 'flashcard_clicked') { - try { - const card = message.card || {}; - const introState = introductionStateProcessors - .get(connectionId) - ?.getState(); - const attrs = connectionAttributes.get(connectionId) || {}; - telemetry.metric.recordCounterUInt('flashcard_clicks_total', 1, { - connectionId, - cardId: card.id || '', - spanish: card.spanish || card.word || '', - english: card.english || card.translation || '', - source: 'ui', - timezone: attrs.timezone || '', - name: (introState?.name && introState.name.trim()) || 'unknown', - level: - (introState?.level && (introState.level as string)) || 'unknown', - goal: (introState?.goal && introState.goal.trim()) || 'unknown', - }); - } catch (err) { - console.error('Error recording flashcard click metric:', err); - } - } else { - console.log('Received non-audio message:', message.type); - } - } catch (error) { - console.error('Error processing message:', error); - } - }); - - ws.on('error', (error) => { - console.error(`WebSocket error for ${connectionId}:`, error); - // Don't crash - errors are handled by close event - }); - - ws.on('close', async () => { - console.log(`WebSocket connection closed: ${connectionId}`); - - // Clean up audio processor - const processor = audioProcessors.get(connectionId); - if (processor) { - try { - await processor.destroy(); - } catch (error) { - console.error( - `Error destroying audio processor for ${connectionId}:`, - error - ); - // Continue with cleanup even if destroy fails - } - audioProcessors.delete(connectionId); - } - - // Clean up processors - flashcardProcessors.delete(connectionId); - introductionStateProcessors.delete(connectionId); - connectionAttributes.delete(connectionId); - }); -}); - -// API endpoint for ANKI export -app.post('/api/export-anki', async (req, res) => { - try { - const { flashcards, deckName } = req.body; - - if (!flashcards || !Array.isArray(flashcards)) { - return res.status(400).json({ error: 'Invalid flashcards data' }); - } - - const exporter = new AnkiExporter(); - const ankiBuffer = await exporter.exportFlashcards( - flashcards, - deckName || 'Aprendemo Spanish Cards' - ); - - // Set headers for file download - const filename = `${deckName || 'aprendemo_cards'}.apkg`; - res.setHeader('Content-Type', 'application/octet-stream'); - res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); - res.setHeader('Content-Length', ankiBuffer.length); - - // Send the file - res.send(ankiBuffer); - return; - } catch (error) { - console.error('Error exporting to ANKI:', error); - res.status(500).json({ error: 'Failed to export flashcards' }); - return; - } -}); - -// Serve static frontend files -// When running from dist/backend/server.js, go up two levels to project root -// When running from backend/server.ts (dev mode), go up one level to project root -const frontendPath = path.join(__dirname, '../../frontend'); -const devFrontendPath = path.join(__dirname, '../frontend'); -const staticPath = path.resolve(frontendPath); -const devStaticPath = path.resolve(devFrontendPath); -// Use the path that exists -const finalStaticPath = existsSync(devStaticPath) ? devStaticPath : staticPath; -app.use(express.static(finalStaticPath)); - -server.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); -}); - -// Graceful shutdown - prevent multiple calls -let isShuttingDown = false; - -async function gracefulShutdown() { - if (isShuttingDown) { - return; - } - isShuttingDown = true; - - console.log('Shutting down gracefully...'); - - try { - // Close all WebSocket connections immediately - console.log(`Closing ${wss.clients.size} WebSocket connections...`); - wss.clients.forEach((ws) => { - if (ws.readyState === ws.OPEN || ws.readyState === ws.CONNECTING) { - ws.close(); - } - }); - - // Close WebSocket server (non-blocking) - wss.close(); - - // Clean up processors (fire and forget - don't wait) - for (const processor of audioProcessors.values()) { - processor.destroy().catch(() => { - // Ignore errors during shutdown - }); - } - - // Close HTTP server (non-blocking) - server.close(() => { - console.log('HTTP server closed'); - }); - - // Stop Inworld Runtime (fire and forget - don't wait) - stopInworldRuntime() - .then(() => console.log('Inworld Runtime stopped')) - .catch(() => { - // Ignore errors during shutdown - }); - - console.log('Shutdown complete'); - } catch { - // Ignore errors during shutdown - } - - // Exit immediately - don't wait for anything - process.exitCode = 0; - process.exit(0); -} - -process.on('SIGTERM', gracefulShutdown); -process.on('SIGINT', gracefulShutdown); diff --git a/backend/src/__tests__/config/languages.test.ts b/backend/src/__tests__/config/languages.test.ts new file mode 100644 index 0000000..426d959 --- /dev/null +++ b/backend/src/__tests__/config/languages.test.ts @@ -0,0 +1,156 @@ +import { describe, it, expect } from 'vitest'; +import { + getLanguageConfig, + getSupportedLanguageCodes, + getLanguageOptions, + DEFAULT_LANGUAGE_CODE, + SUPPORTED_LANGUAGES, +} from '../../config/languages.js'; + +describe('languages config', () => { + describe('getLanguageConfig', () => { + it('returns Spanish config for "es"', () => { + const config = getLanguageConfig('es'); + expect(config.code).toBe('es'); + expect(config.name).toBe('Spanish'); + expect(config.teacherPersona.name).toBe('Señor Gael Herrera'); + }); + + it('returns English config for "en"', () => { + const config = getLanguageConfig('en'); + expect(config.code).toBe('en'); + expect(config.name).toBe('English'); + expect(config.ttsConfig.speakerId).toBe('Ashley'); + }); + + it('returns French config for "fr"', () => { + const config = getLanguageConfig('fr'); + expect(config.code).toBe('fr'); + expect(config.name).toBe('French'); + expect(config.ttsConfig.speakerId).toBe('Alain'); + }); + + it('returns fallback to Spanish for unknown language code', () => { + const config = getLanguageConfig('zz'); + expect(config.code).toBe('es'); + }); + + it('returns fallback to Spanish for empty string', () => { + const config = getLanguageConfig(''); + expect(config.code).toBe('es'); + }); + + it('has required fields for each supported language', () => { + const codes = getSupportedLanguageCodes(); + for (const code of codes) { + const config = getLanguageConfig(code); + expect(config.code).toBe(code); + expect(config.name).toBeTruthy(); + expect(config.nativeName).toBeTruthy(); + expect(config.flag).toBeTruthy(); + expect(config.sttLanguageCode).toBeTruthy(); + expect(config.ttsConfig).toBeDefined(); + expect(config.ttsConfig.speakerId).toBeTruthy(); + expect(config.ttsConfig.modelId).toBeTruthy(); + expect(config.teacherPersona).toBeDefined(); + expect(config.teacherPersona.name).toBeTruthy(); + expect(config.exampleTopics.length).toBeGreaterThan(0); + } + }); + }); + + describe('getSupportedLanguageCodes', () => { + it('returns array of supported language codes', () => { + const codes = getSupportedLanguageCodes(); + expect(Array.isArray(codes)).toBe(true); + expect(codes.length).toBeGreaterThan(0); + }); + + it('includes expected languages', () => { + const codes = getSupportedLanguageCodes(); + expect(codes).toContain('es'); + expect(codes).toContain('en'); + expect(codes).toContain('fr'); + expect(codes).toContain('de'); + }); + + it('matches SUPPORTED_LANGUAGES keys', () => { + const codes = getSupportedLanguageCodes(); + expect(codes.length).toBe(Object.keys(SUPPORTED_LANGUAGES).length); + for (const code of codes) { + expect(SUPPORTED_LANGUAGES[code]).toBeDefined(); + } + }); + }); + + describe('getLanguageOptions', () => { + it('returns options with code, name, nativeName, and flag', () => { + const options = getLanguageOptions(); + expect(Array.isArray(options)).toBe(true); + expect(options.length).toBeGreaterThan(0); + + for (const option of options) { + expect(option.code).toBeTruthy(); + expect(option.name).toBeTruthy(); + expect(option.nativeName).toBeTruthy(); + expect(option.flag).toBeTruthy(); + } + }); + + it('returns Spanish with correct properties', () => { + const options = getLanguageOptions(); + const spanish = options.find((o) => o.code === 'es'); + + expect(spanish).toBeDefined(); + expect(spanish!.name).toBe('Spanish'); + expect(spanish!.nativeName).toBe('Español'); + }); + + it('has same count as supported languages', () => { + const options = getLanguageOptions(); + const codes = getSupportedLanguageCodes(); + expect(options.length).toBe(codes.length); + }); + }); + + describe('DEFAULT_LANGUAGE_CODE', () => { + it('is a valid supported language', () => { + const codes = getSupportedLanguageCodes(); + expect(codes).toContain(DEFAULT_LANGUAGE_CODE); + }); + + it('is Spanish', () => { + expect(DEFAULT_LANGUAGE_CODE).toBe('es'); + }); + }); + + describe('TTS configurations', () => { + it('each language has valid TTS config', () => { + const codes = getSupportedLanguageCodes(); + for (const code of codes) { + const config = getLanguageConfig(code); + expect(config.ttsConfig.speakingRate).toBeGreaterThan(0); + expect(config.ttsConfig.temperature).toBeGreaterThan(0); + } + }); + }); + + describe('teacher personas', () => { + it('each language has a teacher with valid age', () => { + const codes = getSupportedLanguageCodes(); + for (const code of codes) { + const config = getLanguageConfig(code); + expect(config.teacherPersona.age).toBeGreaterThan(20); + expect(config.teacherPersona.age).toBeLessThan(100); + } + }); + + it('each language has a teacher with nationality', () => { + const codes = getSupportedLanguageCodes(); + for (const code of codes) { + const config = getLanguageConfig(code); + expect(config.teacherPersona.nationality).toBeTruthy(); + } + }); + }); +}); diff --git a/backend/src/__tests__/helpers/anki-exporter.test.ts b/backend/src/__tests__/helpers/anki-exporter.test.ts new file mode 100644 index 0000000..de4dbe3 --- /dev/null +++ b/backend/src/__tests__/helpers/anki-exporter.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from 'vitest'; +import { AnkiExporter } from '../../helpers/anki-exporter.js'; +import type { Flashcard } from '../../helpers/flashcard-processor.js'; + +// Helper to create test flashcards +function createFlashcard(overrides: Partial = {}): Flashcard { + return { + id: 'test-id', + targetWord: 'hola', + english: 'hello', + example: 'Hola, ¿cómo estás?', + mnemonic: 'Think of "hello" when you wave', + timestamp: new Date().toISOString(), + ...overrides, + }; +} + +describe('AnkiExporter', () => { + const exporter = new AnkiExporter(); + + describe('countValidFlashcards', () => { + it('counts flashcards with both targetWord and english', () => { + const flashcards = [ + createFlashcard({ targetWord: 'hola', english: 'hello' }), + createFlashcard({ targetWord: 'adios', english: 'goodbye' }), + ]; + expect(exporter.countValidFlashcards(flashcards)).toBe(2); + }); + + it('excludes flashcards missing targetWord', () => { + const flashcards = [ + createFlashcard({ targetWord: 'hola', english: 'hello' }), + createFlashcard({ targetWord: '', english: 'missing target' }), + ]; + expect(exporter.countValidFlashcards(flashcards)).toBe(1); + }); + + it('excludes flashcards missing english', () => { + const flashcards = [ + createFlashcard({ targetWord: 'hola', english: 'hello' }), + createFlashcard({ targetWord: 'test', english: '' }), + ]; + expect(exporter.countValidFlashcards(flashcards)).toBe(1); + }); + + it('excludes flashcards with whitespace-only fields', () => { + const flashcards = [ + createFlashcard({ targetWord: ' ', english: 'hello' }), + createFlashcard({ targetWord: 'test', english: ' ' }), + ]; + expect(exporter.countValidFlashcards(flashcards)).toBe(0); + }); + + it('returns 0 for empty array', () => { + expect(exporter.countValidFlashcards([])).toBe(0); + }); + + it('handles legacy "spanish" field', () => { + const flashcards = [ + { + id: '1', + spanish: 'hola', + targetWord: '', + english: 'hello', + example: '', + mnemonic: '', + timestamp: '', + } as Flashcard, + ]; + expect(exporter.countValidFlashcards(flashcards)).toBe(1); + }); + + it('prefers targetWord over spanish when both present', () => { + const flashcards = [ + { + id: '1', + targetWord: 'bonjour', + spanish: 'hola', + english: 'hello', + example: '', + mnemonic: '', + timestamp: '', + } as unknown as Flashcard, + ]; + // Should count as valid since targetWord is present + expect(exporter.countValidFlashcards(flashcards)).toBe(1); + }); + }); + + // Note: exportFlashcards tests are skipped because the anki-apkg-export + // package has ESM compatibility issues in vitest. The countValidFlashcards + // tests above cover the validation logic. The actual export functionality + // is tested manually and works in production. +}); diff --git a/backend/src/__tests__/helpers/audio-utils.test.ts b/backend/src/__tests__/helpers/audio-utils.test.ts new file mode 100644 index 0000000..ccbf994 --- /dev/null +++ b/backend/src/__tests__/helpers/audio-utils.test.ts @@ -0,0 +1,164 @@ +import { describe, it, expect } from 'vitest'; +import { + float32ToPCM16, + pcm16ToFloat32, + audioDataToPCM16, + decodeBase64ToFloat32, + convertToPCM16Base64, +} from '../../helpers/audio-utils.js'; + +describe('audio-utils', () => { + describe('float32ToPCM16', () => { + it('converts silence (0.0) to 0', () => { + const input = new Float32Array([0.0, 0.0, 0.0]); + const result = float32ToPCM16(input); + expect(result).toEqual(new Int16Array([0, 0, 0])); + }); + + it('converts max positive (1.0) to 32767', () => { + const input = new Float32Array([1.0]); + const result = float32ToPCM16(input); + expect(result[0]).toBe(32767); + }); + + it('converts max negative (-1.0) to -32768', () => { + const input = new Float32Array([-1.0]); + const result = float32ToPCM16(input); + expect(result[0]).toBe(-32768); + }); + + it('clamps values outside [-1, 1] range', () => { + const input = new Float32Array([2.0, -2.0]); + const result = float32ToPCM16(input); + expect(result[0]).toBe(32767); + expect(result[1]).toBe(-32768); + }); + + it('converts mid-range values correctly', () => { + const input = new Float32Array([0.5, -0.5]); + const result = float32ToPCM16(input); + // 0.5 * 32767 = 16383.5, rounded to 16383 + expect(result[0]).toBe(16383); + // -0.5 * 32768 = -16384 + expect(result[1]).toBe(-16384); + }); + }); + + describe('audioDataToPCM16', () => { + it('converts number array to PCM16', () => { + const input = [0.0, 1.0, -1.0]; + const result = audioDataToPCM16(input); + expect(result[0]).toBe(0); + expect(result[1]).toBe(32767); + expect(result[2]).toBe(-32768); + }); + + it('converts Float32Array to PCM16', () => { + const input = new Float32Array([0.5, -0.5]); + const result = audioDataToPCM16(input); + expect(result[0]).toBe(16383); + expect(result[1]).toBe(-16384); + }); + }); + + describe('pcm16ToFloat32', () => { + it('converts 0 to 0.0', () => { + const input = new Int16Array([0]); + const result = pcm16ToFloat32(input); + expect(result[0]).toBeCloseTo(0.0); + }); + + it('converts 32767 to approximately 1.0', () => { + const input = new Int16Array([32767]); + const result = pcm16ToFloat32(input); + expect(result[0]).toBeCloseTo(1.0, 2); + }); + + it('converts -32768 to -1.0', () => { + const input = new Int16Array([-32768]); + const result = pcm16ToFloat32(input); + expect(result[0]).toBe(-1.0); + }); + + it('round-trips values correctly', () => { + const original = new Float32Array([0.0, 0.5, -0.5, 0.75, -0.75]); + const pcm16 = float32ToPCM16(original); + const roundTripped = pcm16ToFloat32(pcm16); + + for (let i = 0; i < original.length; i++) { + // Allow small precision loss due to quantization + expect(roundTripped[i]).toBeCloseTo(original[i], 2); + } + }); + }); + + describe('decodeBase64ToFloat32', () => { + it('decodes base64 audio to Float32Array', () => { + // Create a known Float32Array, encode to base64, decode back + const original = new Float32Array([0.5, -0.5, 0.0, 1.0]); + const buffer = Buffer.from(original.buffer); + const base64 = buffer.toString('base64'); + + const decoded = decodeBase64ToFloat32(base64); + + expect(decoded.length).toBe(4); + expect(decoded[0]).toBeCloseTo(0.5); + expect(decoded[1]).toBeCloseTo(-0.5); + expect(decoded[2]).toBeCloseTo(0.0); + expect(decoded[3]).toBeCloseTo(1.0); + }); + + it('handles empty base64 string', () => { + const decoded = decodeBase64ToFloat32(''); + expect(decoded.length).toBe(0); + }); + }); + + describe('convertToPCM16Base64', () => { + it('returns null for undefined input', () => { + const result = convertToPCM16Base64(undefined, 16000); + expect(result).toBeNull(); + }); + + it('passes through already base64 encoded strings', () => { + const base64Input = 'SGVsbG8gV29ybGQ='; + const result = convertToPCM16Base64(base64Input, 16000); + expect(result).toBe(base64Input); + }); + + it('converts Float32Array to base64 PCM16', () => { + const input = new Float32Array([0.0, 1.0, -1.0]); + const result = convertToPCM16Base64(input, 16000); + + expect(result).not.toBeNull(); + expect(typeof result).toBe('string'); + + // Decode and verify the PCM16 values + const buffer = Buffer.from(result!, 'base64'); + const pcm16 = new Int16Array( + buffer.buffer, + buffer.byteOffset, + buffer.length / 2 + ); + expect(pcm16[0]).toBe(0); + expect(pcm16[1]).toBe(32767); + expect(pcm16[2]).toBe(-32768); + }); + + it('converts number array to base64 PCM16', () => { + const input = [0.5, -0.5]; + const result = convertToPCM16Base64(input, 16000); + + expect(result).not.toBeNull(); + + const buffer = Buffer.from(result!, 'base64'); + const pcm16 = new Int16Array( + buffer.buffer, + buffer.byteOffset, + buffer.length / 2 + ); + expect(pcm16[0]).toBe(16383); + expect(pcm16[1]).toBe(-16384); + }); + }); +}); diff --git a/backend/src/config/embedder.ts b/backend/src/config/embedder.ts new file mode 100644 index 0000000..91b8aae --- /dev/null +++ b/backend/src/config/embedder.ts @@ -0,0 +1,12 @@ +/** + * Embedder Configuration + * + * Uses Inworld's remote embedder with BAAI/bge-large-en-v1.5 model. + * This model produces 1024-dimensional embeddings. + */ + +export const embedderConfig = { + provider: 'inworld' as const, + modelName: 'BAAI/bge-large-en-v1.5', + dimensions: 1024, +}; diff --git a/backend/src/config/languages.ts b/backend/src/config/languages.ts new file mode 100644 index 0000000..b79fe8e --- /dev/null +++ b/backend/src/config/languages.ts @@ -0,0 +1,284 @@ +/** + * Language Configuration System + * + * This module provides a centralized configuration for all supported languages. + * To add a new language: + * 1. Add a new entry to SUPPORTED_LANGUAGES with all required fields + * 2. The rest of the app will automatically support the new language + */ + +import { createLogger } from '../utils/logger.js'; + +const logger = createLogger('Languages'); + +export interface TeacherPersona { + name: string; + age: number; + nationality: string; + description: string; +} + +export interface TTSConfig { + speakerId: string; + modelId: string; + speakingRate: number; + temperature: number; + languageCode?: string; // Optional TTS language code (e.g., 'ja-JP') +} + +export interface LanguageConfig { + // Identifier + code: string; // e.g., 'es', 'ja', 'fr' + + // Display names + name: string; // English name: "Spanish" + nativeName: string; // Native name: "Español" + flag: string; // Emoji flag + + // STT configuration + sttLanguageCode: string; // Language code for speech-to-text + + // TTS configuration + ttsConfig: TTSConfig; + + // Teacher persona for this language + teacherPersona: TeacherPersona; + + // Example conversation topics specific to this language's culture + exampleTopics: string[]; +} + +/** + * Supported Languages Configuration + * + * Each language defines everything needed for: + * - Speech recognition (STT) + * - Text-to-speech (TTS) + * - Teacher persona and conversation style + * - Cultural context and example topics + */ +export const SUPPORTED_LANGUAGES: Record = { + en: { + code: 'en', + name: 'English', + nativeName: 'English', + flag: '🇺🇸', + sttLanguageCode: 'en-US', + ttsConfig: { + speakerId: 'Ashley', + modelId: 'inworld-tts-1.5-max', + speakingRate: 1, + temperature: 1.1, + languageCode: 'en-US', + }, + teacherPersona: { + name: 'Ms. Sarah Mitchell', + age: 34, + nationality: 'American (New York)', + description: + 'a 34 year old New Yorker who loves teaching English through everyday conversations and pop culture', + }, + exampleTopics: [ + 'New York City life', + 'American movies and TV shows', + 'sports and outdoor activities', + 'American idioms and slang', + 'travel across the United States', + ], + }, + + es: { + code: 'es', + name: 'Spanish', + nativeName: 'Español', + flag: '🇲🇽', + sttLanguageCode: 'es-MX', // Mexican Spanish + ttsConfig: { + speakerId: 'Diego', + modelId: 'inworld-tts-1.5-max', + speakingRate: 1, + temperature: 1.1, + languageCode: 'es-MX', + }, + teacherPersona: { + name: 'Señor Gael Herrera', + age: 35, + nationality: 'Mexican (Chilango)', + description: + "a 35 year old 'Chilango' (from Mexico City) who has loaned their brain to AI", + }, + exampleTopics: [ + 'Mexico City', + 'the Dunedin sound rock scene', + 'gardening', + 'the concept of brunch across cultures', + 'Balkan travel', + ], + }, + + fr: { + code: 'fr', + name: 'French', + nativeName: 'Français', + flag: '🇫🇷', + sttLanguageCode: 'fr-FR', + ttsConfig: { + speakerId: 'Alain', + modelId: 'inworld-tts-1.5-max', + speakingRate: 1, + temperature: 1.1, + languageCode: 'fr-FR', + }, + teacherPersona: { + name: 'Monsieur Lucien Dubois', + age: 38, + nationality: 'French (Parisian)', + description: + 'a 38 year old Parisian who is passionate about French language, literature, and gastronomy', + }, + exampleTopics: [ + 'Parisian cafés and culture', + 'French cinema (nouvelle vague)', + 'wine regions and gastronomy', + 'French literature and philosophy', + 'travel in Provence and the French Riviera', + 'French music from Édith Piaf to modern artists', + ], + }, + + de: { + code: 'de', + name: 'German', + nativeName: 'Deutsch', + flag: '🇩🇪', + sttLanguageCode: 'de-DE', + ttsConfig: { + speakerId: 'Josef', + modelId: 'inworld-tts-1.5-max', + speakingRate: 1, + temperature: 0.7, + languageCode: 'de-DE', + }, + teacherPersona: { + name: 'Herr Klaus Weber', + age: 45, + nationality: 'German (Berlin)', + description: + 'a 45 year old Berliner who enjoys teaching German through history, philosophy, and modern culture', + }, + exampleTopics: [ + 'Berlin history and reunification', + 'German beer and food culture', + 'classical music and composers', + 'German engineering and innovation', + 'traveling through Bavaria and the Alps', + 'German literature from Goethe to modern authors', + ], + }, + + it: { + code: 'it', + name: 'Italian', + nativeName: 'Italiano', + flag: '🇮🇹', + sttLanguageCode: 'it-IT', + ttsConfig: { + speakerId: 'Orietta', + modelId: 'inworld-tts-1.5-max', + speakingRate: 1, + temperature: 1.1, + languageCode: 'it-IT', + }, + teacherPersona: { + name: 'Signora Maria Rossi', + age: 40, + nationality: 'Italian (Roman)', + description: + 'a 40 year old Roman who is passionate about Italian art, cuisine, and la dolce vita', + }, + exampleTopics: [ + 'Roman history and ancient sites', + 'Italian cuisine and regional specialties', + 'Renaissance art and architecture', + 'Italian cinema and neorealism', + 'fashion and design in Milan', + 'Italian music from opera to modern pop', + ], + }, + + pt: { + code: 'pt', + name: 'Portuguese', + nativeName: 'Português', + flag: '🇧🇷', + sttLanguageCode: 'pt-BR', // Brazilian Portuguese + ttsConfig: { + speakerId: 'Heitor', + modelId: 'inworld-tts-1.5-max', + speakingRate: 1, + temperature: 0.7, + languageCode: 'pt-BR', + }, + teacherPersona: { + name: 'Senhor João Silva', + age: 36, + nationality: 'Brazilian (Carioca)', + description: + 'a 36 year old Carioca from Rio de Janeiro who loves sharing Brazilian culture, music, and the joy of Portuguese', + }, + exampleTopics: [ + 'Rio de Janeiro and Brazilian beaches', + 'Brazilian music from bossa nova to funk', + 'Carnival and Brazilian festivals', + 'Brazilian cuisine and churrasco', + 'football (soccer) culture', + 'the Amazon and Brazilian nature', + ], + }, +}; + +/** + * Get configuration for a specific language + * @param code - Language code (e.g., 'es', 'ja', 'fr') + * @returns Language configuration or default (Spanish) if not found + */ +export function getLanguageConfig(code: string): LanguageConfig { + const config = SUPPORTED_LANGUAGES[code]; + if (!config) { + logger.warn( + { requestedCode: code, fallback: 'es' }, + 'language_not_found_using_fallback' + ); + return SUPPORTED_LANGUAGES['es']; + } + return config; +} + +/** + * Get all supported language codes + */ +export function getSupportedLanguageCodes(): string[] { + return Object.keys(SUPPORTED_LANGUAGES); +} + +/** + * Get language options for frontend dropdown + */ +export function getLanguageOptions(): Array<{ + code: string; + name: string; + nativeName: string; + flag: string; +}> { + return Object.values(SUPPORTED_LANGUAGES).map((lang) => ({ + code: lang.code, + name: lang.name, + nativeName: lang.nativeName, + flag: lang.flag, + })); +} + +/** + * Default language code + */ +export const DEFAULT_LANGUAGE_CODE = 'es'; diff --git a/backend/src/config/llm.ts b/backend/src/config/llm.ts new file mode 100644 index 0000000..615830a --- /dev/null +++ b/backend/src/config/llm.ts @@ -0,0 +1,101 @@ +/** + * LLM Configuration + * + * Centralized configuration for all LLM providers and models used in the application. + * To change models or parameters, update this file instead of modifying graph code. + */ + +export interface TextGenerationConfig { + maxNewTokens: number; + maxPromptLength: number; + temperature: number; + topP: number; + repetitionPenalty: number; + frequencyPenalty: number; + presencePenalty: number; +} + +export interface LLMNodeConfig { + provider: string; + model: string; + stream: boolean; + textGenerationConfig: TextGenerationConfig; +} + +export const llmConfig = { + /** + * Main conversation LLM - used for dialogue responses + * Streaming enabled for real-time TTS + */ + conversation: { + provider: 'openai', + model: 'gpt-4.1-nano', + stream: true, + textGenerationConfig: { + maxNewTokens: 250, + maxPromptLength: 2000, + temperature: 1, + topP: 1, + repetitionPenalty: 1, + frequencyPenalty: 0, + presencePenalty: 0, + }, + } satisfies LLMNodeConfig, + + /** + * Flashcard generation LLM - produces vocabulary cards + * Non-streaming, uses a more capable model for structured output + */ + flashcard: { + provider: 'openai', + model: 'gpt-4.1-nano', + stream: false, + textGenerationConfig: { + maxNewTokens: 2500, + maxPromptLength: 100, + temperature: 1, + topP: 1, + repetitionPenalty: 1, + frequencyPenalty: 0, + presencePenalty: 0, + }, + } satisfies LLMNodeConfig, + + /** + * Feedback generation LLM - provides learning feedback + * Non-streaming, lower temperature for consistent feedback + */ + feedback: { + provider: 'openai', + model: 'gpt-4.1-nano', + stream: false, + textGenerationConfig: { + maxNewTokens: 100, + maxPromptLength: 2000, + temperature: 0.7, + topP: 1, + repetitionPenalty: 1, + frequencyPenalty: 0, + presencePenalty: 0, + }, + } satisfies LLMNodeConfig, + + /** + * Memory generation LLM - creates memories from conversation context + * Non-streaming, moderate temperature for varied but accurate memories + */ + memoryGeneration: { + provider: 'openai', + model: 'gpt-4.1-nano', + stream: false, + textGenerationConfig: { + maxNewTokens: 200, + maxPromptLength: 2000, + temperature: 0.7, + topP: 1, + repetitionPenalty: 1, + frequencyPenalty: 0, + presencePenalty: 0, + }, + } satisfies LLMNodeConfig, +} as const; diff --git a/backend/src/config/server.ts b/backend/src/config/server.ts new file mode 100644 index 0000000..aa6f9ac --- /dev/null +++ b/backend/src/config/server.ts @@ -0,0 +1,113 @@ +/** + * Server Configuration + * + * Centralized configuration for server settings, audio processing, and external services. + * Environment variables can override defaults where appropriate. + */ + +export interface AssemblyAITurnDetectionSettings { + endOfTurnConfidenceThreshold: number; + minEndOfTurnSilenceWhenConfident: number; + maxTurnSilence: number; + description: string; +} + +export type AssemblyAIEagerness = 'low' | 'medium' | 'high'; + +/** + * AssemblyAI turn detection presets based on their documentation + * @see https://www.assemblyai.com/docs/speech-to-text/universal-streaming/turn-detection + */ +const assemblyAIPresets: Record< + AssemblyAIEagerness, + AssemblyAITurnDetectionSettings +> = { + /** + * Aggressive - Quick responses for rapid back-and-forth + * Use cases: Agent Assist, IVR replacements, Retail/E-commerce, Telecom + */ + high: { + endOfTurnConfidenceThreshold: 0.4, + minEndOfTurnSilenceWhenConfident: 160, + maxTurnSilence: 400, + description: + 'Aggressive - Quick responses for rapid back-and-forth (IVR, order confirmations)', + }, + + /** + * Balanced - Natural middle ground for most conversational turns + * Use cases: Customer Support, Tech Support, Financial Services, Travel + */ + medium: { + endOfTurnConfidenceThreshold: 0.4, + minEndOfTurnSilenceWhenConfident: 400, + maxTurnSilence: 1280, + description: + 'Balanced - Natural middle ground for most conversational turns', + }, + + /** + * Conservative - Patient, allows thinking pauses + * Use cases: Healthcare, Mental Health, Sales, Legal, Language Learning + */ + low: { + endOfTurnConfidenceThreshold: 0.7, + minEndOfTurnSilenceWhenConfident: 800, + maxTurnSilence: 3600, + description: + 'Conservative - Patient, allows thinking pauses (Language Learning, Healthcare)', + }, +}; + +export const serverConfig = { + /** + * HTTP server port + */ + port: Number(process.env.PORT) || 3000, + + /** + * Audio processing settings + */ + audio: { + /** Input sample rate from microphone (Hz) */ + inputSampleRate: 16000, + /** TTS output sample rate (Hz) - Inworld TTS standard */ + ttsSampleRate: 22050, + }, + + /** + * AssemblyAI speech-to-text configuration + */ + assemblyAI: { + /** Turn detection eagerness level */ + eagerness: (process.env.ASSEMBLY_AI_EAGERNESS || + 'high') as AssemblyAIEagerness, + /** Format turns in output (typically false for real-time processing) */ + formatTurns: false, + }, + + /** + * Telemetry configuration for Inworld Runtime + */ + telemetry: { + appName: 'inworld-language-tutor', + appVersion: '1.0.0', + }, +} as const; + +/** + * Get AssemblyAI turn detection settings for the configured eagerness level + */ +export function getAssemblyAISettings(): AssemblyAITurnDetectionSettings { + return assemblyAIPresets[serverConfig.assemblyAI.eagerness]; +} + +/** + * Get AssemblyAI turn detection settings for a specific eagerness level + * @param eagerness - The eagerness level ('low' | 'medium' | 'high') + */ +export function getAssemblyAISettingsForEagerness( + eagerness: AssemblyAIEagerness +): AssemblyAITurnDetectionSettings { + return assemblyAIPresets[eagerness]; +} diff --git a/backend/src/config/supabase.ts b/backend/src/config/supabase.ts new file mode 100644 index 0000000..ca4efb1 --- /dev/null +++ b/backend/src/config/supabase.ts @@ -0,0 +1,47 @@ +/** + * Supabase Backend Client Configuration + * + * Sets up Supabase client for server-side operations (memories, embeddings). + * Uses service role key for admin access (bypasses RLS). + */ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { createLogger } from '../utils/logger.js'; + +const logger = createLogger('Supabase'); + +let supabaseClient: SupabaseClient | null = null; + +/** + * Get or create the Supabase client singleton. + * Returns null if environment variables are not configured. + */ +export function getSupabaseClient(): SupabaseClient | null { + if (supabaseClient) { + return supabaseClient; + } + + const supabaseUrl = process.env.SUPABASE_URL; + const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseServiceKey) { + logger.debug('supabase_not_configured'); + return null; + } + + supabaseClient = createClient(supabaseUrl, supabaseServiceKey, { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + }); + + logger.info('supabase_client_initialized'); + return supabaseClient; +} + +/** + * Check if Supabase is configured and available. + */ +export function isSupabaseConfigured(): boolean { + return !!(process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY); +} diff --git a/flashcard-graph.json b/backend/src/graphs/configs/flashcard-generation-graph.json similarity index 92% rename from flashcard-graph.json rename to backend/src/graphs/configs/flashcard-generation-graph.json index 0f7f048..7cb5d7b 100644 --- a/flashcard-graph.json +++ b/backend/src/graphs/configs/flashcard-generation-graph.json @@ -1,5 +1,5 @@ { - "schema_version": "1.2.0", + "schema_version": "1.2.2", "main": { "id": "flashcard-generation-graph", "nodes": [ @@ -33,9 +33,9 @@ "text_generation_config": { "max_new_tokens": 2500, "max_prompt_length": 100, - "repetition_penalty": 1, - "top_p": 1, "temperature": 1, + "top_p": 1, + "repetition_penalty": 1, "frequency_penalty": 0, "presence_penalty": 0 }, @@ -70,8 +70,12 @@ "to_node": "flashcard-parser" } ], - "end_nodes": ["flashcard-parser"], - "start_nodes": ["flashcard-prompt-builder"] + "end_nodes": [ + "flashcard-parser" + ], + "start_nodes": [ + "flashcard-prompt-builder" + ] }, "components": [ { @@ -81,11 +85,11 @@ "type": "RemoteLLMConfig", "properties": { "provider": "openai", - "model_name": "gpt-5", + "model_name": "gpt-4.1-nano", "default_config": {}, "api_key": "{{INWORLD_API_KEY}}" } } } ] -} +} \ No newline at end of file diff --git a/backend/src/graphs/configs/lang-learning-conversation-graph.json b/backend/src/graphs/configs/lang-learning-conversation-graph.json new file mode 100644 index 0000000..43f141d --- /dev/null +++ b/backend/src/graphs/configs/lang-learning-conversation-graph.json @@ -0,0 +1,293 @@ +{ + "schema_version": "1.2.2", + "main": { + "id": "lang-learning-conversation-graph", + "nodes": [ + { + "id": "audio-input-proxy-lang-learning", + "type": "ProxyNode", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": false + } + } + }, + { + "type": "AssemblyAISTTWebSocketNodeType", + "id": "assembly-ai-stt-ws-node-lang-learning", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": false + } + } + }, + { + "type": "TranscriptExtractorNodeType", + "id": "transcript-extractor-node-lang-learning", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": true + } + } + }, + { + "type": "InteractionQueueNodeType", + "id": "interaction-queue-node-lang-learning", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": false + } + } + }, + { + "type": "TextInputNodeType", + "id": "text-input-node-lang-learning", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": true + } + } + }, + { + "type": "MemoryRetrievalNodeType", + "id": "memory-retrieval-node-lang-learning", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": false + } + } + }, + { + "type": "DialogPromptBuilderNodeType", + "id": "dialog-prompt-builder-node-lang-learning", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": false + } + } + }, + { + "id": "llm-node-lang-learning", + "type": "LLMChatNode", + "execution_config": { + "type": "LLMChatNodeExecutionConfig", + "properties": { + "llm_component_id": "llm-node-lang-learning_llm_component", + "text_generation_config": { + "max_new_tokens": 250, + "max_prompt_length": 2000, + "temperature": 1, + "top_p": 1, + "repetition_penalty": 1, + "frequency_penalty": 0, + "presence_penalty": 0 + }, + "stream": true, + "report_to_client": true, + "response_format": "text" + } + } + }, + { + "id": "text-chunking-node-lang-learning", + "type": "TextChunkingNode", + "execution_config": { + "type": "TextChunkingNodeExecutionConfig", + "properties": { + "report_to_client": false + } + } + }, + { + "id": "text-aggregator-node-lang-learning", + "type": "TextAggregatorNode", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": false + } + } + }, + { + "type": "TTSRequestBuilderNodeType", + "id": "tts-request-builder-node-lang-learning", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": false + } + } + }, + { + "id": "tts-node-lang-learning", + "type": "TTSNode", + "execution_config": { + "type": "TTSNodeExecutionConfig", + "properties": { + "tts_component_id": "tts-node-lang-learning_tts_component", + "voice": { + "id": "Diego", + "language_code": "es-MX" + }, + "synthesis_config": { + "type": "inworld", + "config": { + "model_id": "inworld-tts-1.5-max", + "inference": { + "speaking_rate": 1, + "temperature": 1.1 + }, + "postprocessing": { + "sample_rate": 22050 + } + } + }, + "report_to_client": true + } + } + }, + { + "type": "StateUpdateNodeType", + "id": "state-update-node-lang-learning", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": true + } + } + } + ], + "edges": [ + { + "from_node": "audio-input-proxy-lang-learning", + "to_node": "assembly-ai-stt-ws-node-lang-learning" + }, + { + "from_node": "assembly-ai-stt-ws-node-lang-learning", + "to_node": "assembly-ai-stt-ws-node-lang-learning", + "condition_id": "custom-condition-from-assembly-ai-stt-ws-node-lang-learning-to-assembly-ai-stt-ws-node-lang-learning", + "optional": true, + "loop": true + }, + { + "from_node": "assembly-ai-stt-ws-node-lang-learning", + "to_node": "transcript-extractor-node-lang-learning", + "condition_id": "custom-condition-from-assembly-ai-stt-ws-node-lang-learning-to-transcript-extractor-node-lang-learning" + }, + { + "from_node": "transcript-extractor-node-lang-learning", + "to_node": "interaction-queue-node-lang-learning" + }, + { + "from_node": "interaction-queue-node-lang-learning", + "to_node": "text-input-node-lang-learning", + "condition_id": "custom-condition-from-interaction-queue-node-lang-learning-to-text-input-node-lang-learning" + }, + { + "from_node": "text-input-node-lang-learning", + "to_node": "memory-retrieval-node-lang-learning" + }, + { + "from_node": "memory-retrieval-node-lang-learning", + "to_node": "dialog-prompt-builder-node-lang-learning" + }, + { + "from_node": "text-input-node-lang-learning", + "to_node": "tts-request-builder-node-lang-learning" + }, + { + "from_node": "dialog-prompt-builder-node-lang-learning", + "to_node": "llm-node-lang-learning" + }, + { + "from_node": "llm-node-lang-learning", + "to_node": "text-chunking-node-lang-learning" + }, + { + "from_node": "llm-node-lang-learning", + "to_node": "text-aggregator-node-lang-learning" + }, + { + "from_node": "text-chunking-node-lang-learning", + "to_node": "tts-request-builder-node-lang-learning" + }, + { + "from_node": "tts-request-builder-node-lang-learning", + "to_node": "tts-node-lang-learning" + }, + { + "from_node": "text-aggregator-node-lang-learning", + "to_node": "state-update-node-lang-learning" + }, + { + "from_node": "state-update-node-lang-learning", + "to_node": "interaction-queue-node-lang-learning", + "optional": true, + "loop": true + } + ], + "end_nodes": [ + "tts-node-lang-learning" + ], + "start_nodes": [ + "audio-input-proxy-lang-learning" + ] + }, + "components": [ + { + "id": "llm-node-lang-learning_llm_component", + "type": "LLMInterface", + "creation_config": { + "type": "RemoteLLMConfig", + "properties": { + "provider": "openai", + "model_name": "gpt-4.1-nano", + "default_config": {}, + "api_key": "{{INWORLD_API_KEY}}" + } + } + }, + { + "id": "tts-node-lang-learning_tts_component", + "type": "TTSInterface", + "creation_config": { + "type": "RemoteTTSConfig", + "properties": { + "synthesis_config": { + "type": "inworld", + "config": { + "model_id": "inworld-tts-1.5-max", + "inference": { + "speaking_rate": 1, + "temperature": 1.1 + }, + "postprocessing": { + "sample_rate": 22050 + } + } + }, + "api_key": "{{INWORLD_API_KEY}}" + } + } + }, + { + "id": "custom-condition-from-assembly-ai-stt-ws-node-lang-learning-to-assembly-ai-stt-ws-node-lang-learning", + "type": "custom-condition-from-assembly-ai-stt-ws-node-lang-learning-to-assembly-ai-stt-ws-node-lang-learning" + }, + { + "id": "custom-condition-from-assembly-ai-stt-ws-node-lang-learning-to-transcript-extractor-node-lang-learning", + "type": "custom-condition-from-assembly-ai-stt-ws-node-lang-learning-to-transcript-extractor-node-lang-learning" + }, + { + "id": "custom-condition-from-interaction-queue-node-lang-learning-to-text-input-node-lang-learning", + "type": "custom-condition-from-interaction-queue-node-lang-learning-to-text-input-node-lang-learning" + } + ] +} \ No newline at end of file diff --git a/backend/src/graphs/configs/response-feedback-graph.json b/backend/src/graphs/configs/response-feedback-graph.json new file mode 100644 index 0000000..978b019 --- /dev/null +++ b/backend/src/graphs/configs/response-feedback-graph.json @@ -0,0 +1,95 @@ +{ + "schema_version": "1.2.2", + "main": { + "id": "response-feedback-graph", + "nodes": [ + { + "type": "FeedbackPromptBuilderNodeType", + "id": "feedback-prompt-builder", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": false + } + } + }, + { + "type": "TextToChatRequestNodeType", + "id": "text-to-chat-request", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": false + } + } + }, + { + "id": "llm-node", + "type": "LLMChatNode", + "execution_config": { + "type": "LLMChatNodeExecutionConfig", + "properties": { + "llm_component_id": "llm-node_llm_component", + "text_generation_config": { + "max_new_tokens": 100, + "max_prompt_length": 2000, + "temperature": 0.7, + "top_p": 1, + "repetition_penalty": 1, + "frequency_penalty": 0, + "presence_penalty": 0 + }, + "stream": false, + "report_to_client": false, + "response_format": "text" + } + } + }, + { + "type": "FeedbackExtractorNodeType", + "id": "feedback-extractor", + "execution_config": { + "type": "NodeExecutionConfig", + "properties": { + "report_to_client": false + } + } + } + ], + "edges": [ + { + "from_node": "feedback-prompt-builder", + "to_node": "text-to-chat-request" + }, + { + "from_node": "text-to-chat-request", + "to_node": "llm-node" + }, + { + "from_node": "llm-node", + "to_node": "feedback-extractor" + } + ], + "end_nodes": [ + "feedback-extractor" + ], + "start_nodes": [ + "feedback-prompt-builder" + ] + }, + "components": [ + { + "id": "llm-node_llm_component", + "type": "LLMInterface", + "creation_config": { + "type": "RemoteLLMConfig", + "properties": { + "provider": "openai", + "model_name": "gpt-4.1-nano", + "default_config": {}, + "api_key": "{{INWORLD_API_KEY}}" + } + } + } + ] +} \ No newline at end of file diff --git a/backend/src/graphs/conversation-graph.ts b/backend/src/graphs/conversation-graph.ts new file mode 100644 index 0000000..1c4f578 --- /dev/null +++ b/backend/src/graphs/conversation-graph.ts @@ -0,0 +1,315 @@ +/** + * Conversation Graph for Language Learning App - Inworld Runtime 0.9 + * + * This is a long-running circular graph that: + * - Processes continuous audio streams via AssemblyAI STT with built-in VAD + * - Queues interactions for sequential processing + * - Uses language-specific prompts and TTS voices + * - Loops back for the next interaction automatically + * + * Graph Flow: + * AudioInput → AssemblyAI STT (loop) → TranscriptExtractor → InteractionQueue + * → TextInput → DialogPromptBuilder → LLM → TextChunking → TTSRequestBuilder → TTS + * → TextAggregator → StateUpdate → (loop back to InteractionQueue) + */ + +import { + Graph, + GraphBuilder, + ProxyNode, + RemoteLLMChatNode, + RemoteTTSNode, + TextChunkingNode, + TextAggregatorNode, +} from '@inworld/runtime/graph'; + +import { AssemblyAISTTWebSocketNode } from './nodes/assembly-ai-stt-ws-node.js'; +import { DialogPromptBuilderNode } from './nodes/dialog-prompt-builder-node.js'; +import { InteractionQueueNode } from './nodes/interaction-queue-node.js'; +import { MemoryRetrievalNode } from './nodes/memory-retrieval-node.js'; +import { StateUpdateNode } from './nodes/state-update-node.js'; +import { TextInputNode } from './nodes/text-input-node.js'; +import { TranscriptExtractorNode } from './nodes/transcript-extractor-node.js'; +import { TTSRequestBuilderNode } from './nodes/tts-request-builder-node.js'; +import { ConnectionsMap, TextInput } from '../types/index.js'; +import { + getLanguageConfig, + DEFAULT_LANGUAGE_CODE, +} from '../config/languages.js'; +import { llmConfig } from '../config/llm.js'; +import { serverConfig, getAssemblyAISettings } from '../config/server.js'; +import { graphLogger as logger } from '../utils/logger.js'; + +export interface ConversationGraphConfig { + assemblyAIApiKey: string; + connections: ConnectionsMap; + defaultLanguageCode?: string; +} + +/** + * Wrapper class for the conversation graph + * Provides access to the graph and the AssemblyAI node for session management + */ +export class ConversationGraphWrapper { + graph: Graph; + assemblyAINode: AssemblyAISTTWebSocketNode; + + private constructor(params: { + graph: Graph; + assemblyAINode: AssemblyAISTTWebSocketNode; + }) { + this.graph = params.graph; + this.assemblyAINode = params.assemblyAINode; + } + + async destroy(): Promise { + await this.assemblyAINode.destroy(); + await this.graph.stop(); + } + + /** + * Create the conversation graph + */ + static create(config: ConversationGraphConfig): ConversationGraphWrapper { + const { + connections, + assemblyAIApiKey, + defaultLanguageCode = DEFAULT_LANGUAGE_CODE, + } = config; + // Use provided language code or default to Spanish + const langConfig = getLanguageConfig(defaultLanguageCode); + const postfix = `-lang-learning`; + + logger.info( + { language: langConfig.name, languageCode: defaultLanguageCode }, + 'creating_conversation_graph' + ); + + // ============================================================ + // Create Nodes + // ============================================================ + + // Start node (audio input proxy) + const audioInputNode = new ProxyNode({ id: `audio-input-proxy${postfix}` }); + + // AssemblyAI STT with built-in VAD (always uses multilingual model) + const turnDetectionSettings = getAssemblyAISettings(); + const assemblyAISTTNode = new AssemblyAISTTWebSocketNode({ + id: `assembly-ai-stt-ws-node${postfix}`, + config: { + apiKey: assemblyAIApiKey, + connections: connections, + sampleRate: serverConfig.audio.inputSampleRate, + formatTurns: serverConfig.assemblyAI.formatTurns, + endOfTurnConfidenceThreshold: + turnDetectionSettings.endOfTurnConfidenceThreshold, + minEndOfTurnSilenceWhenConfident: + turnDetectionSettings.minEndOfTurnSilenceWhenConfident, + maxTurnSilence: turnDetectionSettings.maxTurnSilence, + }, + }); + + const transcriptExtractorNode = new TranscriptExtractorNode({ + id: `transcript-extractor-node${postfix}`, + reportToClient: true, + }); + + const interactionQueueNode = new InteractionQueueNode({ + id: `interaction-queue-node${postfix}`, + connections, + reportToClient: false, + }); + + const textInputNode = new TextInputNode({ + id: `text-input-node${postfix}`, + connections, + reportToClient: true, + }); + + const memoryRetrievalNode = new MemoryRetrievalNode({ + id: `memory-retrieval-node${postfix}`, + reportToClient: false, + }); + + const dialogPromptBuilderNode = new DialogPromptBuilderNode({ + id: `dialog-prompt-builder-node${postfix}`, + connections, + }); + + // LLM Node - uses Inworld Runtime's remote LLM + const llmNode = new RemoteLLMChatNode({ + id: `llm-node${postfix}`, + provider: llmConfig.conversation.provider, + modelName: llmConfig.conversation.model, + stream: llmConfig.conversation.stream, + textGenerationConfig: llmConfig.conversation.textGenerationConfig, + reportToClient: true, + }); + + const textChunkingNode = new TextChunkingNode({ + id: `text-chunking-node${postfix}`, + }); + + const textAggregatorNode = new TextAggregatorNode({ + id: `text-aggregator-node${postfix}`, + }); + + const stateUpdateNode = new StateUpdateNode({ + id: `state-update-node${postfix}`, + connections, + reportToClient: true, + }); + + const ttsRequestBuilderNode = new TTSRequestBuilderNode({ + id: `tts-request-builder-node${postfix}`, + connections, + defaultVoiceId: langConfig.ttsConfig.speakerId, + reportToClient: false, + }); + + const ttsNode = new RemoteTTSNode({ + id: `tts-node${postfix}`, + speakerId: langConfig.ttsConfig.speakerId, + modelId: langConfig.ttsConfig.modelId, + sampleRate: serverConfig.audio.ttsSampleRate, + temperature: langConfig.ttsConfig.temperature, + speakingRate: langConfig.ttsConfig.speakingRate, + languageCode: langConfig.ttsConfig.languageCode, + reportToClient: true, + }); + + // ============================================================ + // Build the Graph + // ============================================================ + + const graphBuilder = new GraphBuilder({ + id: `lang-learning-conversation-graph`, + enableRemoteConfig: false, + }); + + graphBuilder + // Add all nodes + .addNode(audioInputNode) + .addNode(assemblyAISTTNode) + .addNode(transcriptExtractorNode) + .addNode(interactionQueueNode) + .addNode(textInputNode) + .addNode(memoryRetrievalNode) + .addNode(dialogPromptBuilderNode) + .addNode(llmNode) + .addNode(textChunkingNode) + .addNode(textAggregatorNode) + .addNode(ttsRequestBuilderNode) + .addNode(ttsNode) + .addNode(stateUpdateNode) + + // ============================================================ + // Audio Input Flow (STT with VAD) + // ============================================================ + .addEdge(audioInputNode, assemblyAISTTNode) + + // AssemblyAI loops back to itself while stream is active + .addEdge(assemblyAISTTNode, assemblyAISTTNode, { + condition: async (input: unknown) => { + const data = input as { stream_exhausted?: boolean }; + return data?.stream_exhausted !== true; + }, + loop: true, + optional: true, + }) + + // When interaction is complete, extract transcript + .addEdge(assemblyAISTTNode, transcriptExtractorNode, { + condition: async (input: unknown) => { + const data = input as { interaction_complete?: boolean }; + return data?.interaction_complete === true; + }, + }) + + // Transcript goes to interaction queue + .addEdge(transcriptExtractorNode, interactionQueueNode) + + // ============================================================ + // Processing Flow + // ============================================================ + + // InteractionQueue → TextInput only when there's text to process + .addEdge(interactionQueueNode, textInputNode, { + condition: (input: TextInput) => { + return Boolean(input.text && input.text.trim().length > 0); + }, + }) + + // TextInput updates state, retrieves memories, then builds prompt + .addEdge(textInputNode, memoryRetrievalNode) + .addEdge(memoryRetrievalNode, dialogPromptBuilderNode) + + // Also pass state to TTS builder for voice selection + .addEdge(textInputNode, ttsRequestBuilderNode) + + // Prompt builder → LLM + .addEdge(dialogPromptBuilderNode, llmNode) + + // LLM output splits: one for TTS, one for state update + .addEdge(llmNode, textChunkingNode) + .addEdge(llmNode, textAggregatorNode) + + // Text chunking → TTS request builder → TTS + .addEdge(textChunkingNode, ttsRequestBuilderNode) + .addEdge(ttsRequestBuilderNode, ttsNode) + + // Text aggregator → state update + .addEdge(textAggregatorNode, stateUpdateNode) + + // ============================================================ + // Loop Back + // ============================================================ + + // State update loops back to interaction queue for next turn + .addEdge(stateUpdateNode, interactionQueueNode, { + loop: true, + optional: true, + }) + + // ============================================================ + // Start and End Nodes + // ============================================================ + .setStartNode(audioInputNode) + .setEndNode(ttsNode); + + const graph = graphBuilder.build(); + + logger.info('conversation_graph_built'); + + return new ConversationGraphWrapper({ + graph, + assemblyAINode: assemblyAISTTNode, + }); + } +} + +// Cache for the conversation graph wrapper instance +let conversationGraphWrapper: ConversationGraphWrapper | null = null; + +/** + * Get or create the conversation graph wrapper + */ +export function getConversationGraph( + config: ConversationGraphConfig +): ConversationGraphWrapper { + if (!conversationGraphWrapper) { + logger.info('creating_conversation_graph_wrapper'); + conversationGraphWrapper = ConversationGraphWrapper.create(config); + } + return conversationGraphWrapper; +} + +/** + * Destroy the conversation graph wrapper and clear the cache + */ +export async function destroyConversationGraph(): Promise { + if (conversationGraphWrapper) { + await conversationGraphWrapper.destroy(); + conversationGraphWrapper = null; + } +} diff --git a/backend/graphs/flashcard-graph.ts b/backend/src/graphs/flashcard-graph.ts similarity index 67% rename from backend/graphs/flashcard-graph.ts rename to backend/src/graphs/flashcard-graph.ts index 8452137..6f8b108 100644 --- a/backend/graphs/flashcard-graph.ts +++ b/backend/src/graphs/flashcard-graph.ts @@ -1,26 +1,28 @@ import 'dotenv/config'; -import fs from 'fs'; import { GraphBuilder, CustomNode, ProcessContext, RemoteLLMChatNode, + Graph, } from '@inworld/runtime/graph'; import { GraphTypes } from '@inworld/runtime/common'; -import { renderJinja } from '@inworld/runtime/primitives/llm'; +import { PromptBuilder } from '@inworld/runtime/primitives/llm'; import { flashcardPromptTemplate } from '../helpers/prompt-templates.js'; import { v4 } from 'uuid'; import { Flashcard } from '../helpers/flashcard-processor.js'; +import { llmConfig } from '../config/llm.js'; +import { flashcardLogger as logger } from '../utils/logger.js'; class FlashcardPromptBuilderNode extends CustomNode { async process( _context: ProcessContext, input: GraphTypes.Content | Record ) { - const renderedPrompt = await renderJinja( - flashcardPromptTemplate, - JSON.stringify(input) + const builder = await PromptBuilder.create(flashcardPromptTemplate); + const renderedPrompt = await builder.build( + input as Record ); return renderedPrompt; } @@ -51,7 +53,8 @@ class FlashcardParserNode extends CustomNode { const parsed = JSON.parse(jsonMatch[0]); return { id: v4(), - spanish: parsed.spanish ?? '', + // Support both new 'targetWord' format and legacy 'spanish' format + targetWord: parsed.targetWord ?? parsed.spanish ?? '', english: parsed.english ?? '', example: parsed.example ?? '', mnemonic: parsed.mnemonic ?? '', @@ -59,12 +62,12 @@ class FlashcardParserNode extends CustomNode { }; } } catch (error) { - console.error('Failed to parse flashcard JSON:', error); + logger.error({ err: error }, 'failed_to_parse_flashcard_json'); } return { id: v4(), - spanish: '', + targetWord: '', english: '', example: '', mnemonic: '', @@ -74,7 +77,10 @@ class FlashcardParserNode extends CustomNode { } } -export function createFlashcardGraph() { +/** + * Creates a flashcard generation graph (language-agnostic) + */ +function createFlashcardGraph(): Graph { const apiKey = process.env.INWORLD_API_KEY; if (!apiKey) { throw new Error('INWORLD_API_KEY environment variable is required'); @@ -88,24 +94,16 @@ export function createFlashcardGraph() { }); const llmNode = new RemoteLLMChatNode({ id: 'llm_node', - provider: 'openai', - modelName: 'gpt-5', - stream: false, - textGenerationConfig: { - maxNewTokens: 2500, - maxPromptLength: 100, - repetitionPenalty: 1, - topP: 1, - temperature: 1, - frequencyPenalty: 0, - presencePenalty: 0, - }, + provider: llmConfig.flashcard.provider, + modelName: llmConfig.flashcard.model, + stream: llmConfig.flashcard.stream, + textGenerationConfig: llmConfig.flashcard.textGenerationConfig, }); const parserNode = new FlashcardParserNode({ id: 'flashcard-parser' }); const executor = new GraphBuilder({ id: 'flashcard-generation-graph', - enableRemoteConfig: true, + enableRemoteConfig: false, }) .addNode(promptBuilderNode) .addNode(textToChatRequestNode) @@ -118,7 +116,19 @@ export function createFlashcardGraph() { .setEndNode(parserNode) .build(); - fs.writeFileSync('flashcard-graph.json', executor.toJSON()); - return executor; } + +// Cache for the single flashcard graph instance +let flashcardGraph: Graph | null = null; + +/** + * Get or create the flashcard graph (language-agnostic, uses input params for language) + */ +export function getFlashcardGraph(): Graph { + if (!flashcardGraph) { + logger.info('creating_flashcard_graph'); + flashcardGraph = createFlashcardGraph(); + } + return flashcardGraph; +} diff --git a/backend/src/graphs/memory-generation-graph.ts b/backend/src/graphs/memory-generation-graph.ts new file mode 100644 index 0000000..eae75ae --- /dev/null +++ b/backend/src/graphs/memory-generation-graph.ts @@ -0,0 +1,74 @@ +/** + * Memory Generation Graph + * + * A simple graph that generates memories from conversation history. + * Used by the MemoryProcessor to create memory records in the background. + * + * Graph Flow: + * Input → MemoryPromptBuilder → LLM → Output + */ + +import { + Graph, + GraphBuilder, + ProxyNode, + RemoteLLMChatNode, +} from '@inworld/runtime/graph'; + +import { MemoryPromptBuilderNode } from './nodes/memory-prompt-builder-node.js'; +import { llmConfig } from '../config/llm.js'; +import { graphLogger as logger } from '../utils/logger.js'; + +export interface MemoryGenerationGraphConfig { + graphId?: string; +} + +/** + * Create a memory generation graph + */ +export function createMemoryGenerationGraph( + config: MemoryGenerationGraphConfig = {} +): Graph { + const { graphId = 'memory-generation-graph' } = config; + + logger.info({ graphId }, 'creating_memory_generation_graph'); + + // Input proxy node + const inputNode = new ProxyNode({ id: `${graphId}-input` }); + + // Memory prompt builder + const memoryPromptBuilderNode = new MemoryPromptBuilderNode({ + id: `${graphId}-prompt-builder`, + }); + + // LLM node for memory generation + const llmNode = new RemoteLLMChatNode({ + id: `${graphId}-llm`, + provider: llmConfig.memoryGeneration.provider, + modelName: llmConfig.memoryGeneration.model, + stream: false, + textGenerationConfig: llmConfig.memoryGeneration.textGenerationConfig, + reportToClient: false, + }); + + // Build the graph + const graphBuilder = new GraphBuilder({ + id: graphId, + enableRemoteConfig: false, + }); + + graphBuilder + .addNode(inputNode) + .addNode(memoryPromptBuilderNode) + .addNode(llmNode) + .addEdge(inputNode, memoryPromptBuilderNode) + .addEdge(memoryPromptBuilderNode, llmNode) + .setStartNode(inputNode) + .setEndNode(llmNode); + + const graph = graphBuilder.build(); + + logger.info({ graphId }, 'memory_generation_graph_created'); + + return graph; +} diff --git a/backend/src/graphs/nodes/assembly-ai-stt-ws-node.ts b/backend/src/graphs/nodes/assembly-ai-stt-ws-node.ts new file mode 100644 index 0000000..3b566e9 --- /dev/null +++ b/backend/src/graphs/nodes/assembly-ai-stt-ws-node.ts @@ -0,0 +1,707 @@ +import { DataStreamWithMetadata } from '@inworld/runtime'; +import { CustomNode, GraphTypes, ProcessContext } from '@inworld/runtime/graph'; +import WebSocket from 'ws'; +import { v4 as uuidv4 } from 'uuid'; + +import { Connection } from '../../types/index.js'; +import { audioDataToPCM16 } from '../../helpers/audio-utils.js'; +import { createLogger } from '../../utils/logger.js'; + +const logger = createLogger('AssemblyAI'); + +/** + * Configuration interface for AssemblyAISTTWebSocketNode + */ +export interface AssemblyAISTTWebSocketNodeConfig { + /** Assembly.AI API key */ + apiKey: string; + /** Connections map to access session state */ + connections: { [sessionId: string]: Connection }; + /** Sample rate of the audio stream in Hz */ + sampleRate?: number; + /** Enable turn formatting from Assembly.AI */ + formatTurns?: boolean; + /** End of turn confidence threshold (0-1) */ + endOfTurnConfidenceThreshold?: number; + /** Minimum silence duration when confident (in milliseconds) */ + minEndOfTurnSilenceWhenConfident?: number; + /** Maximum turn silence (in milliseconds) */ + maxTurnSilence?: number; +} + +/** + * Manages a persistent WebSocket connection to Assembly.AI for a single session. + */ +class AssemblyAISession { + private ws: WebSocket | null = null; + private wsReady: boolean = false; + private wsConnectionPromise: Promise | null = null; + + public assemblySessionId: string = ''; + public sessionExpiresAt: number = 0; + public shouldStopProcessing: boolean = false; + + private inactivityTimeout: NodeJS.Timeout | null = null; + private lastActivityTime: number = Date.now(); + private readonly INACTIVITY_TIMEOUT_MS = 60000; // 60 seconds + + constructor( + public readonly sessionId: string, + private apiKey: string, + private url: string + ) {} + + /** + * Ensure WebSocket connection is ready, reconnecting if needed + */ + public async ensureConnection(): Promise { + const now = Math.floor(Date.now() / 1000); + const isExpired = this.sessionExpiresAt > 0 && now >= this.sessionExpiresAt; + + if ( + !this.ws || + !this.wsReady || + this.ws.readyState !== WebSocket.OPEN || + isExpired + ) { + if (isExpired) { + logger.info( + { sessionId: this.sessionId }, + 'session_expired_reconnecting' + ); + } + this.closeWebSocket(); + this.initializeWebSocket(); + } + + if (this.wsConnectionPromise) { + await this.wsConnectionPromise; + } + + this.shouldStopProcessing = false; + this.resetInactivityTimer(); + } + + private initializeWebSocket(): void { + logger.debug({ sessionId: this.sessionId }, 'initializing_websocket'); + + this.wsConnectionPromise = new Promise((resolve, reject) => { + this.ws = new WebSocket(this.url, { + headers: { Authorization: this.apiKey }, + }); + + this.ws.on('open', () => { + logger.debug({ sessionId: this.sessionId }, 'websocket_opened'); + this.wsReady = true; + resolve(); + }); + + // Permanent message handler for session metadata + this.ws.on('message', (data: WebSocket.Data) => { + try { + const message = JSON.parse(data.toString()); + if (message.type === 'Begin') { + this.assemblySessionId = message.id || message.session_id || ''; + this.sessionExpiresAt = message.expires_at || 0; + logger.debug( + { assemblySessionId: this.assemblySessionId }, + 'session_began' + ); + } + } catch { + // Ignore parsing errors + } + }); + + this.ws.on('error', (error: Error) => { + logger.error({ err: error }, 'websocket_error'); + this.wsReady = false; + reject(error); + }); + + this.ws.on('close', (code: number, reason: Buffer) => { + logger.debug({ code, reason: reason.toString() }, 'websocket_closed'); + this.wsReady = false; + }); + }); + } + + public onMessage(listener: (data: WebSocket.Data) => void): void { + if (this.ws) { + this.ws.on('message', listener); + } + } + + public offMessage(listener: (data: WebSocket.Data) => void): void { + if (this.ws) { + this.ws.off('message', listener); + } + } + + public sendAudio(pcm16Data: Int16Array): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(Buffer.from(pcm16Data.buffer)); + this.resetInactivityTimer(); + } + } + + private resetInactivityTimer(): void { + if (this.inactivityTimeout) { + clearTimeout(this.inactivityTimeout); + } + this.lastActivityTime = Date.now(); + this.inactivityTimeout = setTimeout(() => { + this.closeDueToInactivity(); + }, this.INACTIVITY_TIMEOUT_MS); + } + + /** + * Clear the inactivity timer without closing the connection. + * Used when we know no audio will be coming (e.g., text-only interactions). + */ + public clearInactivityTimer(): void { + if (this.inactivityTimeout) { + clearTimeout(this.inactivityTimeout); + this.inactivityTimeout = null; + } + } + + private closeDueToInactivity(): void { + const inactiveFor = Date.now() - this.lastActivityTime; + logger.info( + { sessionId: this.sessionId, inactiveMs: inactiveFor }, + 'closing_due_to_inactivity' + ); + // Only close the WebSocket to stop billing, but keep the session reusable. + // Don't set shouldStopProcessing - this allows the graph to continue waiting + // for input and reconnect when audio arrives. + this.closeWebSocket(); + // Note: We intentionally do NOT call onCleanup here anymore. + // The session stays in the map and can be reactivated by ensureConnection(). + } + + private closeWebSocket(): void { + if (this.ws) { + try { + this.ws.removeAllListeners(); + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.close(); + } + } catch (e) { + logger.warn({ err: e }, 'error_closing_socket'); + } + this.ws = null; + this.wsReady = false; + } + } + + public async close(): Promise { + if (this.inactivityTimeout) { + clearTimeout(this.inactivityTimeout); + } + + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + try { + this.ws.send(JSON.stringify({ type: 'Terminate' })); + await new Promise((resolve) => setTimeout(resolve, 100)); + } catch { + // Ignore + } + } + + this.closeWebSocket(); + } +} + +/** + * AssemblyAISTTWebSocketNode processes continuous multimodal streams using Assembly.AI's + * streaming Speech-to-Text service via direct WebSocket connection. + * + * This node: + * - Receives MultimodalContent stream (audio and/or text) + * - For audio: extracts audio and feeds to Assembly.AI streaming transcriber + * - For text: bypasses STT and returns text directly + * - Detects turn endings using Assembly.AI's neural turn detection + * - Returns DataStreamWithMetadata with transcribed text when a turn completes + */ +export class AssemblyAISTTWebSocketNode extends CustomNode { + private apiKey: string; + private connections: { [sessionId: string]: Connection }; + private sampleRate: number; + private formatTurns: boolean; + private endOfTurnConfidenceThreshold: number; + private minEndOfTurnSilenceWhenConfident: number; + private maxTurnSilence: number; + private wsEndpointBaseUrl: string = 'wss://streaming.assemblyai.com/v3/ws'; + + private sessions: Map = new Map(); + private readonly TURN_COMPLETION_TIMEOUT_MS = 2000; + private readonly MAX_TRANSCRIPTION_DURATION_MS = 40000; + + constructor(props: { + id?: string; + config: AssemblyAISTTWebSocketNodeConfig; + }) { + const { config, ...nodeProps } = props; + + if (!config.apiKey) { + throw new Error('AssemblyAISTTWebSocketNode requires an API key.'); + } + if (!config.connections) { + throw new Error( + 'AssemblyAISTTWebSocketNode requires a connections object.' + ); + } + + super({ id: nodeProps.id || 'assembly-ai-stt-ws-node' }); + + this.apiKey = config.apiKey; + this.connections = config.connections; + this.sampleRate = config.sampleRate || 16000; + this.formatTurns = config.formatTurns ?? false; + this.endOfTurnConfidenceThreshold = + config.endOfTurnConfidenceThreshold ?? 0.7; + this.minEndOfTurnSilenceWhenConfident = + config.minEndOfTurnSilenceWhenConfident ?? 800; + this.maxTurnSilence = config.maxTurnSilence ?? 3600; + + logger.info( + { + threshold: this.endOfTurnConfidenceThreshold, + minSilenceMs: this.minEndOfTurnSilenceWhenConfident, + maxSilenceMs: this.maxTurnSilence, + }, + 'stt_node_configured' + ); + } + + /** + * Build WebSocket URL with query parameters + */ + private buildWebSocketUrl(): string { + const params = new URLSearchParams({ + sample_rate: this.sampleRate.toString(), + encoding: 'pcm_s16le', + format_turns: this.formatTurns.toString(), + end_of_turn_confidence_threshold: + this.endOfTurnConfidenceThreshold.toString(), + min_end_of_turn_silence_when_confident: + this.minEndOfTurnSilenceWhenConfident.toString(), + max_turn_silence: this.maxTurnSilence.toString(), + speech_model: 'universal-streaming-multilingual', + language_detection: 'true', + }); + + const url = `${this.wsEndpointBaseUrl}?${params.toString()}`; + logger.debug( + { + model: 'universal-streaming-multilingual', + threshold: this.endOfTurnConfidenceThreshold, + maxSilenceMs: this.maxTurnSilence, + }, + 'connecting_to_assemblyai' + ); + + return url; + } + + /** + * Process multimodal stream and transcribe using Assembly.AI WebSocket + */ + async process( + context: ProcessContext, + input0: AsyncIterableIterator, + input: DataStreamWithMetadata + ): Promise { + const multimodalStream = + input !== undefined && + input !== null && + input instanceof DataStreamWithMetadata + ? (input.toStream() as unknown as AsyncIterableIterator) + : input0; + + const sessionId = context.getDatastore().get('sessionId') as string; + const connection = this.connections[sessionId]; + + if (connection?.unloaded) { + throw Error(`Session unloaded for sessionId: ${sessionId}`); + } + if (!connection) { + throw Error(`Failed to read connection for sessionId: ${sessionId}`); + } + + // Get iteration from metadata or parse from interactionId + const metadata = input?.getMetadata?.() || {}; + let previousIteration = (metadata.iteration as number) || 0; + + if ( + !connection.state.interactionId || + connection.state.interactionId === '' + ) { + connection.state.interactionId = uuidv4(); + } + + const currentId = connection.state.interactionId; + const delimiterIndex = currentId.indexOf('#'); + + if (previousIteration === 0 && delimiterIndex !== -1) { + const iterationStr = currentId.substring(delimiterIndex + 1); + const parsedIteration = parseInt(iterationStr, 10); + if (!isNaN(parsedIteration) && /^\d+$/.test(iterationStr)) { + previousIteration = parsedIteration; + } + } + + const iteration = previousIteration + 1; + const baseId = + delimiterIndex !== -1 + ? currentId.substring(0, delimiterIndex) + : currentId; + const nextInteractionId = `${baseId}#${iteration}`; + + logger.debug({ iteration }, 'starting_transcription'); + + // State tracking + let transcriptText = ''; + let turnDetected = false; + let speechDetected = false; + let audioChunkCount = 0; + let totalAudioSamples = 0; + let isStreamExhausted = false; + let errorOccurred = false; + let errorMessage = ''; + let maxDurationReached = false; + let isTextInput = false; + let textContent: string | undefined; + + // Get or create session + let session = this.sessions.get(sessionId); + if (!session) { + session = new AssemblyAISession( + sessionId, + this.apiKey, + this.buildWebSocketUrl() + ); + this.sessions.set(sessionId, session); + } + + // Promise to capture turn result + let turnResolve: (value: string) => void = () => {}; + let turnReject: (error: Error) => void = () => {}; + let turnCompleted = false; + const turnPromise = new Promise((resolve, reject) => { + turnResolve = resolve; + turnReject = reject; + }); + const turnPromiseWithState = turnPromise.then((value) => { + turnCompleted = true; + return value; + }); + + // AssemblyAI message handler for this process() call + const messageHandler = (data: WebSocket.Data) => { + try { + const message = JSON.parse(data.toString()); + const msgType = message.type; + + if (msgType === 'Turn') { + if (session?.shouldStopProcessing) { + return; + } + + const transcript = message.transcript || ''; + const utterance = message.utterance || ''; + const isFinal = message.end_of_turn; + + if (!transcript) return; + + if (!isFinal) { + // Partial transcript + const textToSend = utterance || transcript; + if (textToSend) { + this.sendPartialTranscript( + sessionId, + nextInteractionId, + textToSend + ); + + if (connection?.onSpeechDetected && !speechDetected) { + logger.debug({ iteration }, 'speech_detected'); + speechDetected = true; + connection.onSpeechDetected(nextInteractionId); + } + } + return; + } + + // Final transcript - check for pending transcript to stitch + let finalTranscript = transcript; + + if (connection?.pendingTranscript) { + // Stitch the pending transcript with the new one + finalTranscript = + `${connection.pendingTranscript} ${transcript}`.trim(); + logger.debug( + { + iteration, + transcriptSnippet: finalTranscript.substring(0, 80), + }, + 'stitched_transcript' + ); + // Clear the pending transcript + connection.pendingTranscript = undefined; + } else { + logger.debug( + { iteration, transcriptSnippet: transcript.substring(0, 50) }, + 'turn_detected' + ); + } + + // Clear interrupt flag for new processing + if (connection) { + connection.isProcessingInterrupted = false; + } + + transcriptText = finalTranscript; + turnDetected = true; + if (session) session.shouldStopProcessing = true; + turnResolve(finalTranscript); + } else if (msgType === 'Termination') { + logger.debug({ iteration }, 'session_terminated'); + } + } catch (error) { + logger.error({ err: error }, 'error_handling_message'); + } + }; + + try { + await session.ensureConnection(); + session.onMessage(messageHandler); + + // Process multimodal content (audio chunks) + const audioProcessingPromise = (async () => { + let maxDurationTimeout: NodeJS.Timeout | null = null; + try { + // Safety timer: prevent infinite loops + maxDurationTimeout = setTimeout(() => { + maxDurationReached = true; + }, this.MAX_TRANSCRIPTION_DURATION_MS); + + while (true) { + if (session?.shouldStopProcessing) break; + + if (maxDurationReached && !transcriptText) { + logger.warn( + { maxDurationMs: this.MAX_TRANSCRIPTION_DURATION_MS }, + 'max_transcription_duration_reached' + ); + break; + } + + const result = await multimodalStream.next(); + + if (result.done) { + logger.debug( + { iteration, audioChunkCount }, + 'multimodal_stream_exhausted' + ); + isStreamExhausted = true; + break; + } + + if (session?.shouldStopProcessing) break; + + const content = result.value as GraphTypes.MultimodalContent; + + // Handle text input + if (content.text !== undefined && content.text !== null) { + logger.debug( + { iteration, textSnippet: content.text.substring(0, 50) }, + 'text_input_detected' + ); + isTextInput = true; + textContent = content.text; + transcriptText = content.text; + turnDetected = true; + if (session) { + session.shouldStopProcessing = true; + // Clear inactivity timer since we're handling text, not audio + // This prevents the 60s timeout from firing and disrupting the session + session.clearInactivityTimer(); + } + turnResolve(transcriptText); + break; + } + + // Extract audio + if (content.audio === undefined || content.audio === null) continue; + + const audioData = content.audio.data; + if (!audioData || audioData.length === 0) continue; + + audioChunkCount++; + totalAudioSamples += audioData.length; + + const pcm16Data = audioDataToPCM16(audioData); + session?.sendAudio(pcm16Data); + } + } catch (error) { + logger.error({ err: error }, 'error_processing_audio'); + errorOccurred = true; + errorMessage = error instanceof Error ? error.message : String(error); + throw error; + } finally { + if (maxDurationTimeout) { + clearTimeout(maxDurationTimeout); + } + } + })(); + + const raceResult = await Promise.race([ + turnPromiseWithState.then(() => ({ winner: 'turn' as const })), + audioProcessingPromise.then(() => ({ winner: 'audio' as const })), + ]); + + if ( + raceResult.winner === 'audio' && + !turnCompleted && + !maxDurationReached + ) { + logger.debug( + { waitMs: this.TURN_COMPLETION_TIMEOUT_MS }, + 'audio_ended_before_turn_waiting' + ); + + // Send silence to keep connection alive - AssemblyAI needs continuous audio + const silenceIntervalMs = 100; + const silenceSamples = Math.floor( + (silenceIntervalMs / 1000) * this.sampleRate + ); + const silenceFrame = new Int16Array(silenceSamples); + const silenceTimer = setInterval(() => { + if (session && !session.shouldStopProcessing) { + session.sendAudio(silenceFrame); + } + }, silenceIntervalMs); + + const timeoutPromise = new Promise<{ winner: 'timeout' }>((resolve) => + setTimeout( + () => resolve({ winner: 'timeout' }), + this.TURN_COMPLETION_TIMEOUT_MS + ) + ); + + const waitResult = await Promise.race([ + turnPromiseWithState.then(() => ({ winner: 'turn' as const })), + timeoutPromise, + ]); + + clearInterval(silenceTimer); + + if (waitResult.winner === 'timeout' && !turnCompleted) { + logger.warn('timed_out_waiting_for_turn'); + turnReject?.(new Error('Timed out waiting for turn completion')); + } + } + + await audioProcessingPromise.catch(() => {}); + + logger.debug( + { iteration, transcriptSnippet: transcriptText?.substring(0, 50) }, + 'transcription_complete' + ); + + if (turnDetected) { + connection.state.interactionId = ''; + } + + const taggedStream = Object.assign(multimodalStream, { + type: 'MultimodalContent', + abort: () => {}, + getMetadata: () => ({}), + }); + + return new DataStreamWithMetadata(taggedStream, { + elementType: 'MultimodalContent', + iteration: iteration, + interactionId: nextInteractionId, + session_id: sessionId, + assembly_session_id: session.assemblySessionId, + transcript: transcriptText, + turn_detected: turnDetected, + audio_chunk_count: audioChunkCount, + total_audio_samples: totalAudioSamples, + sample_rate: this.sampleRate, + stream_exhausted: isStreamExhausted, + interaction_complete: turnDetected && transcriptText.length > 0, + error_occurred: errorOccurred, + error_message: errorMessage, + is_text_input: isTextInput, + text_content: textContent, + }); + } catch (error) { + logger.error({ err: error, iteration }, 'transcription_failed'); + + const taggedStream = Object.assign(multimodalStream, { + type: 'MultimodalContent', + abort: () => {}, + getMetadata: () => ({}), + }); + + return new DataStreamWithMetadata(taggedStream, { + elementType: 'MultimodalContent', + iteration: iteration, + interactionId: nextInteractionId, + session_id: sessionId, + transcript: '', + turn_detected: false, + stream_exhausted: isStreamExhausted, + interaction_complete: false, + error_occurred: true, + error_message: error instanceof Error ? error.message : String(error), + is_text_input: isTextInput, + text_content: textContent, + }); + } finally { + if (session) { + session.offMessage(messageHandler); + } + } + } + + private sendPartialTranscript( + sessionId: string, + interactionId: string, + text: string + ): void { + const connection = this.connections[sessionId]; + if (!connection?.onPartialTranscript) return; + + try { + connection.onPartialTranscript(text, interactionId); + } catch (error) { + logger.error({ err: error }, 'error_sending_partial_transcript'); + } + } + + async closeSession(sessionId: string): Promise { + const session = this.sessions.get(sessionId); + if (session) { + logger.debug({ sessionId }, 'closing_session'); + await session.close(); + this.sessions.delete(sessionId); + } + } + + async destroy(): Promise { + logger.info({ sessionCount: this.sessions.size }, 'destroying_node'); + + const promises: Promise[] = []; + for (const session of this.sessions.values()) { + promises.push(session.close()); + } + + await Promise.all(promises); + this.sessions.clear(); + } +} diff --git a/backend/src/graphs/nodes/dialog-prompt-builder-node.ts b/backend/src/graphs/nodes/dialog-prompt-builder-node.ts new file mode 100644 index 0000000..2d65bda --- /dev/null +++ b/backend/src/graphs/nodes/dialog-prompt-builder-node.ts @@ -0,0 +1,103 @@ +/** + * DialogPromptBuilderNode builds a LLM chat request from the state. + * + * This node is specifically designed for the language learning app and: + * - Receives the current conversation state + * - Applies the language-specific prompt template + * - Uses Jinja to render the prompt with conversation history + * - Returns a formatted LLMChatRequest for the LLM node + */ + +import { CustomNode, GraphTypes, ProcessContext } from '@inworld/runtime/graph'; +import { PromptBuilder } from '@inworld/runtime/primitives/llm'; +import { ConnectionsMap, StateWithMemories } from '../../types/index.js'; +import { + getLanguageConfig, + DEFAULT_LANGUAGE_CODE, +} from '../../config/languages.js'; +import { conversationTemplate } from '../../helpers/prompt-templates.js'; +import { graphLogger as logger } from '../../utils/logger.js'; + +export class DialogPromptBuilderNode extends CustomNode { + constructor(props: { + id: string; + connections: ConnectionsMap; + reportToClient?: boolean; + }) { + super({ + id: props.id, + reportToClient: props.reportToClient, + }); + // connections passed for future use (e.g., accessing global state) + } + + async process( + context: ProcessContext, + state: StateWithMemories + ): Promise { + // Get language code from state, fallback to default + const languageCode = state.languageCode || DEFAULT_LANGUAGE_CODE; + if (!state.languageCode) { + const sessionId = context.getDatastore().get('sessionId') as string; + logger.warn({ sessionId }, 'missing_language_code_using_default'); + } + + logger.debug( + { + languageCode, + messageCount: state.messages?.length || 0, + memoryCount: state.relevantMemories?.length || 0, + }, + 'building_prompt' + ); + + // Get language config from state + const langConfig = getLanguageConfig(languageCode); + + // Build template variables from language config + const templateVars = { + target_language: langConfig.name, + target_language_native: langConfig.nativeName, + teacher_name: langConfig.teacherPersona.name, + teacher_description: langConfig.teacherPersona.description, + example_topics: langConfig.exampleTopics.join(', '), + }; + + // Get all messages except the last user message (that's our current_input) + const messages = state.messages || []; + const historyMessages = messages.slice(0, -1); // All except last + const lastMessage = messages[messages.length - 1]; + const currentInput = + lastMessage?.role === 'user' ? lastMessage.content : ''; + + logger.debug( + { language: langConfig.name, historyLength: historyMessages.length }, + 'prompt_context' + ); + + const templateData = { + messages: historyMessages.map((m) => ({ + role: m.role, + content: m.content, + })), + current_input: currentInput, + relevant_memories: state.relevantMemories || [], + ...templateVars, + }; + + // Render the prompt using PromptBuilder (Jinja2) + const builder = await PromptBuilder.create(conversationTemplate); + const renderedPrompt = await builder.build(templateData); + + // Debug: Log a snippet of the rendered prompt + logger.debug( + { promptSnippet: renderedPrompt.substring(0, 400) }, + 'prompt_rendered' + ); + + // Return LLMChatRequest for the LLM node + return new GraphTypes.LLMChatRequest({ + messages: [{ role: 'user', content: renderedPrompt }], + }); + } +} diff --git a/backend/src/graphs/nodes/interaction-queue-node.ts b/backend/src/graphs/nodes/interaction-queue-node.ts new file mode 100644 index 0000000..7cdfebf --- /dev/null +++ b/backend/src/graphs/nodes/interaction-queue-node.ts @@ -0,0 +1,178 @@ +/** + * InteractionQueueNode manages the queue of user interactions. + * + * This node: + * - Receives interaction info from STT processing + * - Manages a queue of interactions to ensure sequential processing + * - Prevents race conditions when multiple interactions arrive + * - Returns TextInput when ready to process, or empty when waiting + */ + +import { CustomNode, ProcessContext } from '@inworld/runtime/graph'; +import { + ConnectionsMap, + InteractionInfo, + State, + TextInput, +} from '../../types/index.js'; +import { graphLogger as logger } from '../../utils/logger.js'; + +export class InteractionQueueNode extends CustomNode { + private connections: ConnectionsMap; + + constructor(props?: { + id?: string; + connections?: ConnectionsMap; + reportToClient?: boolean; + }) { + super({ + id: props?.id || 'interaction-queue-node', + reportToClient: props?.reportToClient, + }); + this.connections = props?.connections || {}; + } + + process( + context: ProcessContext, + interactionInfo: InteractionInfo, + state: State + ): TextInput { + const sessionId = interactionInfo.sessionId; + logger.debug( + { interactionId: interactionInfo.interactionId }, + 'processing_interaction' + ); + + // Get current voiceId from connection state + const connection = this.connections[sessionId]; + const currentVoiceId = connection?.state?.voiceId || state?.voiceId; + + const dataStore = context.getDatastore(); + const QUEUED_PREFIX = 'q'; + const RUNNING_PREFIX = 'r'; + const COMPLETED_PREFIX = 'c'; + + // Register interaction in the queue + if (!dataStore.has(QUEUED_PREFIX + interactionInfo.interactionId)) { + dataStore.add( + QUEUED_PREFIX + interactionInfo.interactionId, + interactionInfo.text + ); + logger.debug( + { interactionId: interactionInfo.interactionId }, + 'interaction_queued' + ); + } + + // Get all keys and categorize them + const allKeys = dataStore.keys(); + const queuedIds: string[] = []; + let completedCount = 0; + let runningCount = 0; + + for (const key of allKeys) { + if (key.startsWith(QUEUED_PREFIX)) { + const idStr = key.substring(QUEUED_PREFIX.length); + queuedIds.push(idStr); + } else if (key.startsWith(COMPLETED_PREFIX)) { + completedCount++; + } else if (key.startsWith(RUNNING_PREFIX)) { + runningCount++; + } + } + + // Sort queued IDs by iteration number + queuedIds.sort((a, b) => { + const getIteration = (id: string): number => { + const hashIndex = id.indexOf('#'); + if (hashIndex === -1) return 0; + const iter = parseInt(id.substring(hashIndex + 1), 10); + return isNaN(iter) ? 0 : iter; + }; + return getIteration(a) - getIteration(b); + }); + + logger.debug( + { + queued: queuedIds.length, + completed: completedCount, + running: runningCount, + }, + 'queue_state' + ); + + // Decide if we should start processing the next interaction + if (queuedIds.length === 0) { + logger.debug('no_interactions_to_process'); + return { + text: '', + sessionId: sessionId, + interactionId: '', + voiceId: currentVoiceId, + }; + } + + if (queuedIds.length === completedCount) { + logger.debug('all_interactions_completed'); + return { + text: '', + sessionId: sessionId, + interactionId: '', + voiceId: currentVoiceId, + }; + } + + // There are unprocessed interactions + if (runningCount === completedCount) { + // No interaction is currently running, start the next one + const nextId = queuedIds[completedCount]; + const runningKey = RUNNING_PREFIX + nextId; + + // Try to mark as running + if (dataStore.has(runningKey) || !dataStore.add(runningKey, '')) { + logger.debug({ interactionId: nextId }, 'interaction_already_started'); + return { + text: '', + sessionId: sessionId, + interactionId: '', + voiceId: currentVoiceId, + }; + } + + const queuedText = dataStore.get(QUEUED_PREFIX + nextId) as string; + if (!queuedText) { + logger.error({ interactionId: nextId }, 'failed_to_retrieve_text'); + return { + text: '', + sessionId: sessionId, + interactionId: '', + voiceId: currentVoiceId, + }; + } + + logger.debug( + { textSnippet: queuedText.substring(0, 50) }, + 'starting_llm_processing' + ); + + return { + text: queuedText, + sessionId: sessionId, + interactionId: nextId, + voiceId: currentVoiceId, + }; + } else { + // An interaction is currently running, wait + logger.debug( + { waitingFor: queuedIds[completedCount] }, + 'waiting_for_interaction' + ); + return { + text: '', + sessionId: sessionId, + interactionId: '', + voiceId: currentVoiceId, + }; + } + } +} diff --git a/backend/src/graphs/nodes/memory-prompt-builder-node.ts b/backend/src/graphs/nodes/memory-prompt-builder-node.ts new file mode 100644 index 0000000..32b479e --- /dev/null +++ b/backend/src/graphs/nodes/memory-prompt-builder-node.ts @@ -0,0 +1,56 @@ +/** + * MemoryPromptBuilderNode builds the prompt for memory generation. + * + * Takes conversation messages and target language, renders the memory + * generation template, and outputs an LLMChatRequest. + */ + +import { CustomNode, ProcessContext } from '@inworld/runtime/graph'; +import { GraphTypes } from '@inworld/runtime/common'; +import { PromptBuilder } from '@inworld/runtime/primitives/llm'; +import { readFile } from 'fs/promises'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const promptsDir = join(__dirname, '..', '..', 'prompts'); + +// Cache for the template +let memoryGenerationTemplate: string | null = null; + +async function loadMemoryTemplate(): Promise { + if (!memoryGenerationTemplate) { + memoryGenerationTemplate = await readFile( + join(promptsDir, 'memory-generation.njk'), + 'utf-8' + ); + } + return memoryGenerationTemplate; +} + +export interface MemoryPromptInput { + messages: Array<{ role: string; content: string }>; + target_language: string; +} + +export class MemoryPromptBuilderNode extends CustomNode { + constructor(props: { id: string }) { + super({ id: props.id }); + } + + async process( + _context: ProcessContext, + input: MemoryPromptInput + ): Promise { + const template = await loadMemoryTemplate(); + const builder = await PromptBuilder.create(template); + const renderedPrompt = await builder.build({ + messages: input.messages, + target_language: input.target_language, + }); + return new GraphTypes.LLMChatRequest({ + messages: [{ role: 'user', content: renderedPrompt }], + }); + } +} diff --git a/backend/src/graphs/nodes/memory-retrieval-node.ts b/backend/src/graphs/nodes/memory-retrieval-node.ts new file mode 100644 index 0000000..4ed0a5a --- /dev/null +++ b/backend/src/graphs/nodes/memory-retrieval-node.ts @@ -0,0 +1,175 @@ +/** + * MemoryRetrievalNode retrieves relevant memories before prompt building. + * + * This node: + * - Receives the current state with user input + * - Embeds the user's current message + * - Queries Supabase for similar memories using vector similarity + * - Returns state enriched with relevant memories + */ + +import { CustomNode, ProcessContext } from '@inworld/runtime/graph'; +import { TextEmbedder } from '@inworld/runtime/primitives/embeddings'; +import { State, StateWithMemories } from '../../types/index.js'; +import { getMemoryService } from '../../services/memory-service.js'; +import { isSupabaseConfigured } from '../../config/supabase.js'; +import { embedderConfig } from '../../config/embedder.js'; +import { graphLogger as logger } from '../../utils/logger.js'; + +// Re-export for backwards compatibility +export type { StateWithMemories } from '../../types/index.js'; + +/** Maximum number of memories to retrieve */ +const MEMORY_RETRIEVAL_LIMIT = 3; + +/** Minimum similarity threshold for memory matches */ +const MEMORY_SIMILARITY_THRESHOLD = 0.5; + +// Singleton embedder - shared across all MemoryRetrievalNode instances +let sharedEmbedder: TextEmbedder | null = null; +let sharedInitPromise: Promise | null = null; + +/** + * Initialize shared embedder lazily + */ +async function initSharedEmbedder(): Promise { + if (sharedEmbedder) { + return; + } + + if (sharedInitPromise) { + return sharedInitPromise; + } + + sharedInitPromise = (async () => { + try { + const apiKey = process.env.INWORLD_API_KEY; + if (!apiKey) { + throw new Error('INWORLD_API_KEY environment variable is required'); + } + + sharedEmbedder = await TextEmbedder.create({ + remoteConfig: { + apiKey, + provider: embedderConfig.provider, + modelName: embedderConfig.modelName, + }, + }); + logger.info('memory_retrieval_embedder_initialized'); + } catch (error) { + logger.error({ err: error }, 'memory_retrieval_embedder_init_failed'); + sharedInitPromise = null; // Allow retry + throw error; + } + })(); + + return sharedInitPromise; +} + +export class MemoryRetrievalNode extends CustomNode { + constructor(props: { id: string; reportToClient?: boolean }) { + super({ + id: props.id, + reportToClient: props.reportToClient, + }); + // Note: embedder is initialized lazily on first use, not during construction + } + + /** + * Cleanup method for pattern consistency with other nodes + */ + destroy(): void { + // Embedder is shared singleton, not destroyed per-node + } + + async process( + _context: ProcessContext, + state: State + ): Promise { + logger.info( + { nodeId: this.id, hasState: !!state }, + 'memory_retrieval_node_entered' + ); + + // Get user ID from state (flows through the graph from connection.state) + const userId = state.userId; + logger.info( + { userId: userId?.substring(0, 8) }, + 'memory_retrieval_got_userId' + ); + + // If no userId or Supabase not configured, skip memory retrieval + if (!userId || !isSupabaseConfigured()) { + logger.info( + { hasUserId: !!userId, supabaseConfigured: isSupabaseConfigured() }, + 'skipping_memory_retrieval' + ); + return { ...state, relevantMemories: [] }; + } + + // Get the last user message for embedding + const lastUserMessage = [...state.messages] + .reverse() + .find((m) => m.role === 'user'); + + if (!lastUserMessage) { + return { ...state, relevantMemories: [] }; + } + + try { + logger.info( + { + userId: userId.substring(0, 8), + userMessage: lastUserMessage.content.substring(0, 50), + }, + 'memory_retrieval_starting' + ); + + // Initialize embedder lazily (only when actually needed) + await initSharedEmbedder(); + + if (!sharedEmbedder) { + logger.warn('embedder_not_available'); + return { ...state, relevantMemories: [] }; + } + + // Embed user message + logger.debug('embedding_user_message'); + const embedResponse = await sharedEmbedder.embed(lastUserMessage.content); + const queryEmbedding = TextEmbedder.toArray(embedResponse); + logger.debug( + { embeddingLength: queryEmbedding.length }, + 'embedding_generated' + ); + + // Retrieve similar memories + const memoryService = getMemoryService(); + const memories = await memoryService.retrieveMemories( + userId, + queryEmbedding, + MEMORY_RETRIEVAL_LIMIT, + MEMORY_SIMILARITY_THRESHOLD + ); + + logger.info( + { + memoriesFound: memories.length, + userId: userId.substring(0, 8), + memories: memories.map((m) => ({ + content: m.content.substring(0, 50), + similarity: m.similarity, + })), + }, + 'memories_retrieved_for_prompt' + ); + + return { + ...state, + relevantMemories: memories, + }; + } catch (error) { + logger.error({ err: error }, 'memory_retrieval_error'); + return { ...state, relevantMemories: [] }; + } + } +} diff --git a/backend/src/graphs/nodes/state-update-node.ts b/backend/src/graphs/nodes/state-update-node.ts new file mode 100644 index 0000000..cfae532 --- /dev/null +++ b/backend/src/graphs/nodes/state-update-node.ts @@ -0,0 +1,72 @@ +/** + * StateUpdateNode updates the state with the LLM's response. + * + * This node: + * - Receives the LLM output text + * - Updates the connection state with the assistant message + * - Marks the interaction as completed in the datastore + * - Returns the updated state + */ + +import { CustomNode, ProcessContext } from '@inworld/runtime/graph'; +import { v4 as uuidv4 } from 'uuid'; +import { ConnectionsMap, State } from '../../types/index.js'; +import { graphLogger as logger } from '../../utils/logger.js'; + +export class StateUpdateNode extends CustomNode { + private connections: ConnectionsMap; + + constructor(props: { + id: string; + connections: ConnectionsMap; + reportToClient?: boolean; + }) { + super({ + id: props.id, + reportToClient: props.reportToClient, + }); + this.connections = props.connections; + } + + process(context: ProcessContext, llmOutput: string): State { + const sessionId = context.getDatastore().get('sessionId') as string; + logger.debug( + { outputLength: llmOutput?.length || 0 }, + 'state_update_processing' + ); + + const connection = this.connections[sessionId]; + if (connection?.unloaded) { + throw Error(`Session unloaded for sessionId:${sessionId}`); + } + if (!connection) { + throw Error(`Failed to read connection for sessionId:${sessionId}`); + } + + // Only add assistant message if there's actual content + if (llmOutput && llmOutput.trim().length > 0) { + logger.debug( + { messageSnippet: llmOutput.substring(0, 50) }, + 'adding_assistant_message' + ); + connection.state.messages.push({ + role: 'assistant', + content: llmOutput, + id: connection.state.interactionId || uuidv4(), + timestamp: new Date().toISOString(), + }); + } else { + logger.debug('skipping_empty_message'); + } + + // Mark interaction as completed + const dataStore = context.getDatastore(); + dataStore.add('c' + connection.state.interactionId, ''); + logger.debug( + { interactionId: connection.state.interactionId }, + 'interaction_completed' + ); + + return connection.state; + } +} diff --git a/backend/src/graphs/nodes/text-input-node.ts b/backend/src/graphs/nodes/text-input-node.ts new file mode 100644 index 0000000..1508884 --- /dev/null +++ b/backend/src/graphs/nodes/text-input-node.ts @@ -0,0 +1,65 @@ +/** + * TextInputNode updates the state with the user's input this turn. + * + * This node: + * - Receives user text input with interaction and session IDs + * - Updates the connection state with the user message + * - Returns the updated state for downstream processing + */ + +import { CustomNode, ProcessContext } from '@inworld/runtime/graph'; +import { v4 as uuidv4 } from 'uuid'; +import { ConnectionsMap, State, TextInput } from '../../types/index.js'; +import { graphLogger as logger } from '../../utils/logger.js'; + +export class TextInputNode extends CustomNode { + private connections: ConnectionsMap; + + constructor(props: { + id: string; + connections: ConnectionsMap; + reportToClient?: boolean; + }) { + super({ + id: props.id, + reportToClient: props.reportToClient, + }); + this.connections = props.connections; + } + + process(_context: ProcessContext, input: TextInput): State { + logger.debug( + { textSnippet: input.text?.substring(0, 50) }, + 'text_input_processing' + ); + + const { text, interactionId, sessionId } = input; + + const connection = this.connections[sessionId]; + if (connection?.unloaded) { + throw Error(`Session unloaded for sessionId:${sessionId}`); + } + if (!connection) { + throw Error(`Failed to read connection for sessionId:${sessionId}`); + } + const state = connection.state; + if (!state) { + throw Error( + `Failed to read state from connection for sessionId:${sessionId}` + ); + } + + // Update interactionId + connection.state.interactionId = interactionId; + + // Add user message to conversation + connection.state.messages.push({ + role: 'user', + content: text, + id: interactionId || uuidv4(), + timestamp: new Date().toISOString(), + }); + + return connection.state; + } +} diff --git a/backend/src/graphs/nodes/transcript-extractor-node.ts b/backend/src/graphs/nodes/transcript-extractor-node.ts new file mode 100644 index 0000000..75d6132 --- /dev/null +++ b/backend/src/graphs/nodes/transcript-extractor-node.ts @@ -0,0 +1,57 @@ +/** + * TranscriptExtractorNode extracts transcript information from + * DataStreamWithMetadata (output from AssemblyAISTTNode) and converts + * it to InteractionInfo for downstream processing. + */ + +import { DataStreamWithMetadata } from '@inworld/runtime'; +import { CustomNode, ProcessContext } from '@inworld/runtime/graph'; +import { InteractionInfo } from '../../types/index.js'; +import { graphLogger as logger } from '../../utils/logger.js'; + +export class TranscriptExtractorNode extends CustomNode { + constructor(props?: { id?: string; reportToClient?: boolean }) { + super({ + id: props?.id || 'transcript-extractor-node', + reportToClient: props?.reportToClient, + }); + } + + /** + * Extract transcript from metadata and return as InteractionInfo + */ + process( + context: ProcessContext, + streamWithMetadata: DataStreamWithMetadata + ): InteractionInfo { + const metadata = streamWithMetadata.getMetadata(); + const sessionId = context.getDatastore().get('sessionId') as string; + + // Extract transcript and related info from metadata + const transcript = (metadata.transcript as string) || ''; + const interactionComplete = + (metadata.interaction_complete as boolean) || false; + const iteration = (metadata.iteration as number) || 1; + const interactionId = String(metadata.interactionId || iteration); + + logger.debug( + { + iteration, + transcriptSnippet: transcript?.substring(0, 50), + interactionComplete, + }, + 'transcript_extracted' + ); + + return { + sessionId, + interactionId: interactionId, + text: transcript, + interactionComplete, + }; + } + + async destroy(): Promise { + // No cleanup needed + } +} diff --git a/backend/src/graphs/nodes/tts-request-builder-node.ts b/backend/src/graphs/nodes/tts-request-builder-node.ts new file mode 100644 index 0000000..b1c5226 --- /dev/null +++ b/backend/src/graphs/nodes/tts-request-builder-node.ts @@ -0,0 +1,65 @@ +/** + * TTSRequestBuilderNode builds a TTSRequest with dynamic voiceId. + * + * For long-running graphs, it reads voiceId from connection state at processing time + * to ensure voice changes via language switching are reflected immediately. + */ + +import { CustomNode, GraphTypes, ProcessContext } from '@inworld/runtime/graph'; +import { ConnectionsMap } from '../../types/index.js'; +import { getLanguageConfig } from '../../config/languages.js'; +import { graphLogger as logger } from '../../utils/logger.js'; + +export class TTSRequestBuilderNode extends CustomNode { + private connections: ConnectionsMap; + private defaultVoiceId: string; + + constructor(props: { + id: string; + connections: ConnectionsMap; + defaultVoiceId: string; + reportToClient?: boolean; + }) { + super({ + id: props.id, + reportToClient: props.reportToClient, + }); + this.connections = props.connections; + this.defaultVoiceId = props.defaultVoiceId; + } + + /** + * Build a TTSRequest with the current voiceId from connection state + * Receives two inputs: + * 1. input - Graph input with sessionId (TextInput or State) + * 2. textStream - The text stream from TextChunkingNode + */ + process( + context: ProcessContext, + _input: unknown, + textStream: GraphTypes.TextStream + ): GraphTypes.TTSRequest { + const sessionId = context.getDatastore().get('sessionId') as string; + + // Get voice from connection state (which is updated when language changes) + const connection = this.connections[sessionId]; + + // Get voice from state, or fall back to language config, or default + let voiceId = connection?.state?.voiceId; + if (!voiceId && connection?.state?.languageCode) { + const langConfig = getLanguageConfig(connection.state.languageCode); + voiceId = langConfig.ttsConfig.speakerId; + } + voiceId = voiceId || this.defaultVoiceId; + + logger.debug({ voiceId }, 'building_tts_request'); + + return GraphTypes.TTSRequest.withStream(textStream, { + id: voiceId, + }); + } + + async destroy(): Promise { + // No cleanup needed + } +} diff --git a/backend/src/graphs/response-feedback-graph.ts b/backend/src/graphs/response-feedback-graph.ts new file mode 100644 index 0000000..a81d7c9 --- /dev/null +++ b/backend/src/graphs/response-feedback-graph.ts @@ -0,0 +1,111 @@ +import 'dotenv/config'; + +import { + GraphBuilder, + CustomNode, + ProcessContext, + RemoteLLMChatNode, + Graph, +} from '@inworld/runtime/graph'; +import { GraphTypes } from '@inworld/runtime/common'; +import { PromptBuilder } from '@inworld/runtime/primitives/llm'; +import { llmConfig } from '../config/llm.js'; +import { feedbackLogger as logger } from '../utils/logger.js'; +import { responseFeedbackPromptTemplate } from '../helpers/prompt-templates.js'; + +export interface ResponseFeedbackInput { + messages: Array<{ role: string; content: string }>; + currentTranscript: string; + targetLanguage: string; +} + +class FeedbackPromptBuilderNode extends CustomNode { + async process(_context: ProcessContext, input: ResponseFeedbackInput) { + const builder = await PromptBuilder.create(responseFeedbackPromptTemplate); + const renderedPrompt = await builder.build( + input as unknown as Record + ); + return renderedPrompt; + } +} + +class TextToChatRequestNode extends CustomNode { + process(_context: ProcessContext, renderedPrompt: string) { + return new GraphTypes.LLMChatRequest({ + messages: [{ role: 'user', content: renderedPrompt }], + }); + } +} + +class FeedbackExtractorNode extends CustomNode { + process(_context: ProcessContext, input: GraphTypes.Content) { + const content = + (input && + typeof input === 'object' && + 'content' in input && + (input as { content?: unknown }).content) || + input; + const textContent = + typeof content === 'string' ? content : JSON.stringify(content); + + // Return just the feedback string, trimmed + return textContent.trim(); + } +} + +/** + * Creates a response feedback graph that analyzes user utterances + * and provides one-sentence feedback in English + */ +function createResponseFeedbackGraph(): Graph { + const apiKey = process.env.INWORLD_API_KEY; + if (!apiKey) { + throw new Error('INWORLD_API_KEY environment variable is required'); + } + + const promptBuilderNode = new FeedbackPromptBuilderNode({ + id: 'feedback-prompt-builder', + }); + const textToChatRequestNode = new TextToChatRequestNode({ + id: 'text-to-chat-request', + }); + const llmNode = new RemoteLLMChatNode({ + id: 'llm-node', + provider: llmConfig.feedback.provider, + modelName: llmConfig.feedback.model, + stream: llmConfig.feedback.stream, + textGenerationConfig: llmConfig.feedback.textGenerationConfig, + }); + const extractorNode = new FeedbackExtractorNode({ id: 'feedback-extractor' }); + + const executor = new GraphBuilder({ + id: 'response-feedback-graph', + enableRemoteConfig: false, + }) + .addNode(promptBuilderNode) + .addNode(textToChatRequestNode) + .addNode(llmNode) + .addNode(extractorNode) + .addEdge(promptBuilderNode, textToChatRequestNode) + .addEdge(textToChatRequestNode, llmNode) + .addEdge(llmNode, extractorNode) + .setStartNode(promptBuilderNode) + .setEndNode(extractorNode) + .build(); + + return executor; +} + +// Cache for the single response feedback graph instance +let responseFeedbackGraph: Graph | null = null; + +/** + * Get or create the response feedback graph + */ +export function getResponseFeedbackGraph(): Graph { + if (!responseFeedbackGraph) { + logger.info('creating_response_feedback_graph'); + responseFeedbackGraph = createResponseFeedbackGraph(); + } + return responseFeedbackGraph; +} diff --git a/backend/src/graphs/simple-tts-graph.ts b/backend/src/graphs/simple-tts-graph.ts new file mode 100644 index 0000000..cdae811 --- /dev/null +++ b/backend/src/graphs/simple-tts-graph.ts @@ -0,0 +1,120 @@ +/** + * Simple TTS Graph + * + * A lightweight graph that takes text and produces TTS audio. + * Used for pronouncing individual words (e.g., flashcard pronunciation). + * + * Graph Flow: + * Input { text } → TextExtractor → RemoteTTS → Audio Output + * + * RemoteTTSNode accepts String directly as input, so we just need to + * extract the text from our input object. + */ + +import { + Graph, + GraphBuilder, + CustomNode, + ProcessContext, + RemoteTTSNode, +} from '@inworld/runtime/graph'; +import { getLanguageConfig, getSupportedLanguageCodes } from '../config/languages.js'; +import { serverConfig } from '../config/server.js'; +import { graphLogger as logger } from '../utils/logger.js'; + +export interface SimpleTTSInput { + text: string; +} + +/** + * Simple node that extracts text string from input object + */ +class TextExtractorNode extends CustomNode { + process(_context: ProcessContext, input: SimpleTTSInput): string { + logger.info({ text: input.text }, 'simple_tts_extracting_text'); + return input.text; + } +} + +/** + * Creates a simple TTS graph for pronouncing words + * RemoteTTSNode accepts String directly as input + */ +function createSimpleTTSGraph(languageCode: string): Graph { + const langConfig = getLanguageConfig(languageCode); + + logger.info( + { + languageCode, + speakerId: langConfig.ttsConfig.speakerId, + modelId: langConfig.ttsConfig.modelId, + }, + 'creating_simple_tts_graph_with_config' + ); + + const textExtractorNode = new TextExtractorNode({ + id: `simple-tts-text-extractor-${languageCode}`, + }); + + // RemoteTTSNode accepts String directly as input + const ttsNode = new RemoteTTSNode({ + id: `simple-tts-node-${languageCode}`, + speakerId: langConfig.ttsConfig.speakerId, + modelId: langConfig.ttsConfig.modelId, + sampleRate: serverConfig.audio.ttsSampleRate, + temperature: langConfig.ttsConfig.temperature, + speakingRate: langConfig.ttsConfig.speakingRate, + languageCode: langConfig.ttsConfig.languageCode, + reportToClient: true, + }); + + const graphBuilder = new GraphBuilder({ + id: `simple-tts-graph-${languageCode}`, + enableRemoteConfig: false, + }); + + graphBuilder + .addNode(textExtractorNode) + .addNode(ttsNode) + .addEdge(textExtractorNode, ttsNode) + .setStartNode(textExtractorNode) + .setEndNode(ttsNode); + + return graphBuilder.build(); +} + +// Cache for simple TTS graphs per language +const simpleTTSGraphs = new Map(); + +/** + * Initialize TTS graphs for all supported languages + */ +export function initializeTTSGraphs(): void { + const languageCodes = getSupportedLanguageCodes(); + + logger.info({ languageCount: languageCodes.length }, 'initializing_tts_graphs'); + + for (const languageCode of languageCodes) { + if (!simpleTTSGraphs.has(languageCode)) { + logger.info({ languageCode }, 'creating_tts_graph'); + simpleTTSGraphs.set(languageCode, createSimpleTTSGraph(languageCode)); + } + } + + logger.info( + { graphCount: simpleTTSGraphs.size }, + 'tts_graphs_initialized' + ); +} + +/** + * Get TTS graph for a specific language + */ +export function getSimpleTTSGraph(languageCode: string): Graph { + // If graph doesn't exist, create it (fallback for new languages) + if (!simpleTTSGraphs.has(languageCode)) { + logger.info({ languageCode }, 'creating_tts_graph_on_demand'); + simpleTTSGraphs.set(languageCode, createSimpleTTSGraph(languageCode)); + } + return simpleTTSGraphs.get(languageCode)!; +} diff --git a/backend/helpers/anki-exporter.ts b/backend/src/helpers/anki-exporter.ts similarity index 76% rename from backend/helpers/anki-exporter.ts rename to backend/src/helpers/anki-exporter.ts index bc78c88..4db3e75 100644 --- a/backend/helpers/anki-exporter.ts +++ b/backend/src/helpers/anki-exporter.ts @@ -8,28 +8,38 @@ export class AnkiExporter { */ async exportFlashcards( flashcards: Flashcard[], - deckName: string = 'Aprendemo Spanish Cards' + deckName: string = 'Inworld Language Tutor Cards' ): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const apkg = new (AnkiExport as any).default(deckName); // Add each flashcard as a card flashcards.forEach((flashcard) => { + // Support both new 'targetWord' and legacy 'spanish' field + const targetWord = + flashcard.targetWord || (flashcard as { spanish?: string }).spanish; + // Skip empty or error flashcards if ( - !flashcard.spanish || + !targetWord || !flashcard.english || - flashcard.spanish.trim() === '' || + targetWord.trim() === '' || flashcard.english.trim() === '' ) { return; } - const front = flashcard.spanish.trim(); + const front = targetWord.trim(); const back = this.formatCardBack(flashcard); // Add tags for organization - const tags = ['aprendemo', 'spanish-learning']; + const tags = ['inworld-language-tutor']; + + // Add language tag if available + if (flashcard.languageCode) { + tags.push(`language-${flashcard.languageCode}`); + } + if (flashcard.timestamp) { const date = new Date(flashcard.timestamp).toISOString().split('T')[0]; tags.push(`created-${date}`); @@ -76,12 +86,15 @@ export class AnkiExporter { * Count valid flashcards (ones that can be exported) */ countValidFlashcards(flashcards: Flashcard[]): number { - return flashcards.filter( - (flashcard) => - flashcard.spanish && + return flashcards.filter((flashcard) => { + const targetWord = + flashcard.targetWord || (flashcard as { spanish?: string }).spanish; + return ( + targetWord && flashcard.english && - flashcard.spanish.trim() !== '' && + targetWord.trim() !== '' && flashcard.english.trim() !== '' - ).length; + ); + }).length; } } diff --git a/backend/helpers/audio-buffer.ts b/backend/src/helpers/audio-buffer.ts similarity index 100% rename from backend/helpers/audio-buffer.ts rename to backend/src/helpers/audio-buffer.ts diff --git a/backend/src/helpers/audio-utils.ts b/backend/src/helpers/audio-utils.ts new file mode 100644 index 0000000..0f0d86e --- /dev/null +++ b/backend/src/helpers/audio-utils.ts @@ -0,0 +1,89 @@ +/** + * Audio utility functions for format conversion + */ + +/** + * Convert Float32Array audio data to Int16Array (PCM16) + */ +export function float32ToPCM16(float32Data: Float32Array): Int16Array { + const pcm16 = new Int16Array(float32Data.length); + for (let i = 0; i < float32Data.length; i++) { + // Clamp to [-1, 1] range and convert to Int16 range [-32768, 32767] + const s = Math.max(-1, Math.min(1, float32Data[i])); + pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7fff; + } + return pcm16; +} + +/** + * Convert number[] or Float32Array audio data to Int16Array (PCM16) + * This is an optimized version that handles both types to avoid + * intermediate allocations in the audio pipeline. + */ +export function audioDataToPCM16( + audioData: number[] | Float32Array +): Int16Array { + const pcm16 = new Int16Array(audioData.length); + for (let i = 0; i < audioData.length; i++) { + // Clamp to [-1, 1] range and convert to Int16 range [-32768, 32767] + const s = Math.max(-1, Math.min(1, audioData[i])); + pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7fff; + } + return pcm16; +} + +/** + * Convert Int16Array (PCM16) to Float32Array + */ +export function pcm16ToFloat32(pcm16Data: Int16Array): Float32Array { + const float32 = new Float32Array(pcm16Data.length); + for (let i = 0; i < pcm16Data.length; i++) { + float32[i] = pcm16Data[i] / 32768.0; + } + return float32; +} + +/** + * Convert audio data to PCM16 base64 string for WebSocket transmission + */ +export function convertToPCM16Base64( + audioData: number[] | Float32Array | string | undefined, + _sampleRate: number | undefined, + _logPrefix: string = 'Audio' +): string | null { + if (!audioData) { + return null; + } + + let base64Data: string; + + if (typeof audioData === 'string') { + // Already base64 encoded + base64Data = audioData; + } else { + // Convert Float32 array to PCM16 base64 + const float32Data = Array.isArray(audioData) + ? new Float32Array(audioData) + : audioData; + const pcm16Data = float32ToPCM16(float32Data); + base64Data = Buffer.from(pcm16Data.buffer).toString('base64'); + } + + return base64Data; +} + +/** + * Decode base64 audio to Float32Array + * Frontend sends Float32 audio data directly (4 bytes per sample) + * Note: Node.js Buffer objects share ArrayBuffers with offsets, so we need to copy + */ +export function decodeBase64ToFloat32(base64Audio: string): Float32Array { + const buffer = Buffer.from(base64Audio, 'base64'); + // Create a clean copy to avoid Node.js Buffer's internal ArrayBuffer sharing + const cleanArray = new Uint8Array(buffer.length); + for (let i = 0; i < buffer.length; i++) { + cleanArray[i] = buffer[i]; + } + // Interpret bytes directly as Float32 (4 bytes per sample) + return new Float32Array(cleanArray.buffer); +} diff --git a/backend/src/helpers/connection-manager.ts b/backend/src/helpers/connection-manager.ts new file mode 100644 index 0000000..20867c1 --- /dev/null +++ b/backend/src/helpers/connection-manager.ts @@ -0,0 +1,1065 @@ +/** + * ConnectionManager - Manages WebSocket connections and graph execution + * + * This replaces the AudioProcessor for Inworld Runtime 0.9. + * Key differences from AudioProcessor: + * - Uses MultimodalStreamManager to feed audio to a long-running graph + * - VAD is handled inside the graph by AssemblyAI (not external Silero) + * - Graph runs continuously for the session duration + */ + +import { WebSocket } from 'ws'; +import { GraphTypes } from '@inworld/runtime/graph'; + +import { ConversationGraphWrapper } from '../graphs/conversation-graph.js'; +import { MultimodalStreamManager } from './multimodal-stream-manager.js'; +import { decodeBase64ToFloat32 } from './audio-utils.js'; +import { ConnectionsMap } from '../types/index.js'; +import { + getLanguageConfig, + DEFAULT_LANGUAGE_CODE, + LanguageConfig, +} from '../config/languages.js'; +import { serverConfig } from '../config/server.js'; +import { createSessionLogger } from '../utils/logger.js'; + +export class ConnectionManager { + private sessionId: string; + private ws: WebSocket; + private graphWrapper: ConversationGraphWrapper; + private multimodalStreamManager: MultimodalStreamManager; + private connections: ConnectionsMap; + private graphExecution: Promise | null = null; + private isDestroyed = false; + private languageCode: string; + private languageConfig: LanguageConfig; + private logger: ReturnType; + private conversationId: string | null = null; + private pendingFlashcardGeneration: Promise | null = null; + private pendingFeedbackGeneration: Promise | null = null; + private pendingMemoryGeneration: Promise | null = null; + private isSwitchingConversation: boolean = false; + private restartAttempts = 0; + private readonly MAX_RESTART_ATTEMPTS = 3; + private lastRestartTime = 0; + private readonly RESTART_COOLDOWN_MS = 5000; // Prevent rapid restart loops + private readonly RESTART_RESET_THRESHOLD_MS = 30000; // Reset attempts after stable operation + + // Callback for flashcard processing + private flashcardCallback: + | ((messages: Array<{ role: string; content: string }>) => Promise) + | null = null; + + // Callback for feedback generation + private feedbackCallback: + | (( + messages: Array<{ role: string; content: string }>, + currentTranscript: string + ) => Promise) + | null = null; + + // Callback for memory creation + private memoryCallback: + | (( + messages: Array<{ role: string; content: string }> + ) => Promise) + | null = null; + + // User ID for memory retrieval/creation + private userId: string | undefined = undefined; + + // Processing state tracking for utterance stitching + private isProcessingResponse: boolean = false; + private currentTranscript: string = ''; + + constructor( + sessionId: string, + ws: WebSocket, + graphWrapper: ConversationGraphWrapper, + connections: ConnectionsMap, + languageCode: string = DEFAULT_LANGUAGE_CODE + ) { + this.sessionId = sessionId; + this.ws = ws; + this.graphWrapper = graphWrapper; + this.connections = connections; + // Default language is Spanish, but can be changed via conversation_switch + this.languageCode = languageCode; + this.languageConfig = getLanguageConfig(languageCode); + this.multimodalStreamManager = new MultimodalStreamManager(); + this.logger = createSessionLogger('ConnectionManager', sessionId); + + // Initialize connection state + this.connections[sessionId] = { + ws: ws, + state: { + interactionId: '', + messages: [], + userName: '', + targetLanguage: this.languageConfig.name, + languageCode: this.languageCode, + voiceId: this.languageConfig.ttsConfig.speakerId, + output_modalities: ['audio', 'text'], + conversationId: undefined, + }, + multimodalStreamManager: this.multimodalStreamManager, + onSpeechDetected: (interactionId) => + this.handleSpeechDetected(interactionId), + onPartialTranscript: (text, interactionId) => + this.handlePartialTranscript(text, interactionId), + }; + + this.logger.info( + { language: this.languageConfig.name }, + 'connection_manager_created' + ); + } + + /** + * Start the long-running graph execution + */ + async start(): Promise { + this.logger.info('starting_graph'); + + // Create the multimodal stream generator + const multimodalStream = this.createMultimodalStreamGenerator(); + + // Start graph execution (runs in background) + this.graphExecution = this.executeGraph(multimodalStream); + + // Don't await - the graph runs continuously + this.graphExecution.catch((error) => { + if (!this.isDestroyed) { + this.logger.error({ err: error }, 'graph_execution_error'); + } + }); + } + + /** + * Create an async generator that yields multimodal content from the stream manager + */ + private async *createMultimodalStreamGenerator(): AsyncGenerator { + for await (const content of this.multimodalStreamManager.createStream()) { + yield content; + } + } + + /** + * Execute the graph with the multimodal stream + */ + private async executeGraph( + stream: AsyncGenerator + ): Promise { + const connection = this.connections[this.sessionId]; + if (!connection) { + throw new Error(`No connection found for session ${this.sessionId}`); + } + + // Tag the stream for the runtime + const taggedStream = Object.assign(stream, { + type: 'MultimodalContent', + }); + + this.logger.info('graph_execution_started'); + + // Build dataStoreContent - only include userId if defined + const dataStoreContent: Record = { + sessionId: this.sessionId, + state: connection.state, + }; + if (this.userId) { + dataStoreContent.userId = this.userId; + } + + const { outputStream } = await this.graphWrapper.graph.start(taggedStream, { + executionId: this.sessionId, + dataStoreContent, + userContext: { + attributes: { + languageCode: this.languageCode, + language: this.languageConfig.name, + }, + targetingKey: this.sessionId, + }, + }); + + // Store the output stream for potential cancellation + connection.currentAudioExecutionStream = outputStream; + + // Process graph outputs + try { + for await (const result of outputStream) { + if (this.isDestroyed) break; + await this.processGraphOutput(result); + } + } catch (error) { + if (!this.isDestroyed) { + this.logger.error({ err: error }, 'output_processing_error'); + } + } finally { + connection.currentAudioExecutionStream = undefined; + } + + this.logger.info('graph_execution_completed'); + + // Auto-restart if the graph completed but we weren't destroyed + // This handles timeout scenarios (DEADLINE_EXCEEDED) gracefully + if (!this.isDestroyed) { + await this.handleGraphCompletion(); + } + } + + /** + * Handle unexpected graph completion by restarting + */ + private async handleGraphCompletion(): Promise { + const now = Date.now(); + const timeSinceLastRestart = now - this.lastRestartTime; + + // Reset restart attempts if enough time has passed (successful operation) + if (timeSinceLastRestart > this.RESTART_RESET_THRESHOLD_MS) { + this.restartAttempts = 0; + } + + // Check if we should attempt restart + if (this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) { + this.logger.warn( + { attempts: this.restartAttempts }, + 'max_restart_attempts_reached' + ); + this.sendToClient({ + type: 'error', + message: + 'Connection lost. Please refresh the page to continue the conversation.', + code: 'GRAPH_RESTART_FAILED', + timestamp: Date.now(), + }); + return; + } + + // Cooldown check to prevent rapid restart loops + if (timeSinceLastRestart < this.RESTART_COOLDOWN_MS) { + this.logger.debug( + { cooldownMs: this.RESTART_COOLDOWN_MS - timeSinceLastRestart }, + 'restart_cooldown_active' + ); + await new Promise((resolve) => + setTimeout(resolve, this.RESTART_COOLDOWN_MS - timeSinceLastRestart) + ); + } + + this.restartAttempts++; + this.lastRestartTime = Date.now(); + + this.logger.info( + { attempt: this.restartAttempts }, + 'auto_restarting_graph_after_timeout' + ); + + // Create a fresh multimodal stream manager + this.multimodalStreamManager = new MultimodalStreamManager(); + + // Update the connection's reference to the new stream manager + const connection = this.connections[this.sessionId]; + if (connection) { + connection.multimodalStreamManager = this.multimodalStreamManager; + } + + // Notify client that we're recovering + this.sendToClient({ + type: 'connection_recovered', + message: 'Connection restored after idle timeout.', + timestamp: Date.now(), + }); + + // Restart graph execution + const multimodalStream = this.createMultimodalStreamGenerator(); + this.graphExecution = this.executeGraph(multimodalStream); + + this.graphExecution.catch((error) => { + if (!this.isDestroyed) { + this.logger.error({ err: error }, 'graph_restart_error'); + } + }); + } + + /** + * Process a single output from the graph + */ + private async processGraphOutput(result: unknown): Promise { + const connection = this.connections[this.sessionId]; + if (!connection) return; + + let transcription = ''; + let llmResponse = ''; + + try { + // Cast to any to work around strict typing issues with processResponse handlers + // The handlers receive typed data at runtime even though the type system says unknown + const resultWithProcess = result as { + processResponse: ( + handlers: Record Promise | void> + ) => Promise; + }; + await resultWithProcess.processResponse({ + // Handle string output (transcription from proxy node) + string: (data: unknown) => { + transcription = String(data); + if (transcription.trim()) { + this.logger.debug({ transcription }, 'transcription_received'); + this.sendToClient({ + type: 'transcription', + text: transcription.trim(), + conversationId: this.conversationId, + timestamp: Date.now(), + }); + } + }, + + // Handle Custom data (transcription from transcript extractor) + // InteractionInfo has: sessionId, interactionId, text, interactionComplete + Custom: async (customData: unknown) => { + const data = customData as { + text?: string; + interactionId?: string; + interactionComplete?: boolean; + }; + // Only send final transcriptions (interactionComplete=true) to avoid duplicates + if (data.text && data.interactionComplete) { + transcription = data.text; + this.logger.debug({ transcription }, 'transcription_final'); + + // Mark start of response processing for utterance stitching + this.markProcessingStart(transcription); + + this.sendToClient({ + type: 'transcription', + text: transcription.trim(), + conversationId: this.conversationId, + timestamp: Date.now(), + }); + } + }, + + // Handle LLM response stream + ContentStream: async (streamData: unknown) => { + const stream = streamData as GraphTypes.ContentStream; + this.logger.debug('processing_llm_content_stream'); + // Use array + join instead of string concatenation for O(n) vs O(n²) + const responseChunks: string[] = []; + let wasInterrupted = false; + + for await (const chunk of stream) { + if (this.isDestroyed) break; + + // Check for interruption (user started speaking again for continuation) + if (connection.isProcessingInterrupted) { + this.logger.debug('llm_stream_interrupted_for_continuation'); + wasInterrupted = true; + break; + } + + if (chunk.text) { + responseChunks.push(chunk.text); + this.sendToClient({ + type: 'llm_response_chunk', + text: chunk.text, + conversationId: this.conversationId, + timestamp: Date.now(), + }); + } + } + + // Only send completion if not interrupted + if (!wasInterrupted) { + const currentResponse = responseChunks.join(''); + if (currentResponse.trim()) { + llmResponse = currentResponse; + this.logger.debug( + { responseSnippet: llmResponse.substring(0, 50) }, + 'llm_response_complete' + ); + this.sendToClient({ + type: 'llm_response_complete', + text: llmResponse.trim(), + conversationId: this.conversationId, + timestamp: Date.now(), + }); + } + } else { + this.logger.debug('llm_stream_interrupted_skipping_completion'); + } + }, + + // Handle TTS output stream + TTSOutputStream: async (ttsData: unknown) => { + const ttsStream = ttsData as GraphTypes.TTSOutputStream; + this.logger.debug('processing_tts_stream'); + let isFirstChunk = true; + let wasInterrupted = false; + + for await (const chunk of ttsStream) { + if (this.isDestroyed) break; + + // Check for interruption (user started speaking again for continuation) + if (connection.isProcessingInterrupted) { + this.logger.debug('tts_interrupted_for_continuation'); + wasInterrupted = true; + break; + } + + if (chunk.audio?.data) { + // Log sample rate on first chunk + if (isFirstChunk) { + this.logger.debug( + { + sampleRate: + chunk.audio.sampleRate || + serverConfig.audio.ttsSampleRate, + bytes: Array.isArray(chunk.audio.data) + ? chunk.audio.data.length + : 'N/A', + }, + 'tts_audio_chunk' + ); + } + + // Block audio during conversation switch + if (this.isSwitchingConversation) { + this.logger.debug('blocking_audio_during_conversation_switch'); + continue; + } + + // Convert audio to base64 for WebSocket transmission + // Use ttsSampleRate as fallback (not inputSampleRate which is for microphone input) + const audioResult = this.convertAudioToBase64(chunk.audio); + if (audioResult) { + this.sendToClient({ + type: 'audio_stream', + audio: audioResult.base64, + audioFormat: audioResult.format, + sampleRate: + chunk.audio.sampleRate || serverConfig.audio.ttsSampleRate, + text: chunk.text || '', + isFirstChunk: isFirstChunk, + conversationId: this.conversationId, + timestamp: Date.now(), + }); + isFirstChunk = false; + } + } + } + + // Only send completion signals if not interrupted + if (!wasInterrupted) { + // Send completion signals + if (!this.isSwitchingConversation) { + this.logger.debug('tts_stream_complete'); + this.sendToClient({ + type: 'audio_stream_complete', + conversationId: this.conversationId, + timestamp: Date.now(), + }); + + // Send conversation update with conversationId + this.sendToClient({ + type: 'conversation_update', + messages: connection.state.messages, + conversationId: this.conversationId, + timestamp: Date.now(), + }); + } + + // Trigger flashcard, feedback, and memory generation after TTS completes + this.triggerFlashcardGeneration(); + this.triggerFeedbackGeneration(); + this.triggerMemoryGeneration(); + } else { + this.logger.debug('tts_interrupted_skipping_completion'); + } + this.markProcessingComplete(); + }, + + // Handle errors + error: async (error: unknown) => { + const err = error as { message?: string }; + this.logger.error({ err }, 'graph_error'); + if (!err.message?.includes('recognition produced no text')) { + this.sendToClient({ + type: 'error', + message: err.message || 'Unknown error', + timestamp: Date.now(), + }); + } + }, + + // Default handler for unknown types + default: (_data: unknown) => { + // Ignore unknown output types + }, + }); + } catch (error) { + this.logger.error({ err: error }, 'graph_output_processing_error'); + } + } + + /** + * Add an audio chunk from the WebSocket + */ + addAudioChunk(base64Audio: string): void { + if (this.isDestroyed) return; + + try { + // Decode base64 to Float32Array + const float32Data = decodeBase64ToFloat32(base64Audio); + + // Push to multimodal stream - pass Float32Array directly, + // MultimodalStreamManager will handle conversion when needed + this.multimodalStreamManager.pushAudio({ + data: float32Data, + sampleRate: serverConfig.audio.inputSampleRate, + }); + } catch (error) { + this.logger.error({ err: error }, 'audio_chunk_error'); + } + } + + /** + * Handle speech detected event from AssemblyAI + */ + private handleSpeechDetected(interactionId: string): void { + this.logger.debug({ interactionId }, 'speech_detected'); + + // Check if we're currently processing a response - if so, this is a continuation + if (this.isProcessingResponse && this.currentTranscript) { + this.logger.debug('new_speech_during_processing_interrupting'); + this.interruptForContinuation(this.currentTranscript); + + // Send interrupt signal with continuation reason so frontend discards partial response + this.sendToClient({ + type: 'interrupt', + reason: 'continuation_detected', + conversationId: this.conversationId, + timestamp: Date.now(), + }); + } else { + // Normal case - send regular interrupt signal + this.sendToClient({ + type: 'interrupt', + reason: 'speech_start', + conversationId: this.conversationId, + timestamp: Date.now(), + }); + } + + // Always send speech_detected for UI feedback + this.sendToClient({ + type: 'speech_detected', + interactionId, + data: { text: '' }, + conversationId: this.conversationId, + timestamp: Date.now(), + }); + } + + /** + * Handle partial transcript from AssemblyAI + */ + private handlePartialTranscript(text: string, interactionId: string): void { + this.sendToClient({ + type: 'partial_transcript', + text, + interactionId, + conversationId: this.conversationId, + timestamp: Date.now(), + }); + } + + /** + * Interrupt current processing for utterance continuation/stitching. + * Called when user starts speaking again while we're processing the first utterance. + */ + private interruptForContinuation(partialTranscript: string): void { + const connection = this.connections[this.sessionId]; + if (connection) { + connection.isProcessingInterrupted = true; + connection.pendingTranscript = partialTranscript; + this.logger.debug( + { transcriptSnippet: partialTranscript.substring(0, 50) }, + 'interrupting_for_continuation' + ); + + // Remove the last user message and any assistant response that was added + // before the continuation was detected + const messages = connection.state.messages; + let removedCount = 0; + + // Remove the last assistant message if it exists (the interrupted response) + if ( + messages.length > 0 && + messages[messages.length - 1].role === 'assistant' + ) { + const removed = messages.pop(); + removedCount++; + this.logger.debug( + { contentSnippet: removed?.content.substring(0, 50) }, + 'removed_interrupted_assistant_message' + ); + } + + // Remove the last user message (the partial utterance that will be stitched) + if ( + messages.length > 0 && + messages[messages.length - 1].role === 'user' + ) { + const removed = messages.pop(); + removedCount++; + this.logger.debug( + { contentSnippet: removed?.content.substring(0, 50) }, + 'removed_partial_user_message' + ); + } + + // Notify frontend to update its conversation history + if (removedCount > 0) { + this.sendToClient({ + type: 'conversation_rollback', + removedCount, + messages: messages, + conversationId: this.conversationId, + timestamp: Date.now(), + }); + } + } + } + + /** + * Mark the start of response processing (LLM/TTS) + */ + private markProcessingStart(transcript: string): void { + this.isProcessingResponse = true; + this.currentTranscript = transcript; + } + + /** + * Mark the end of response processing + */ + private markProcessingComplete(): void { + this.isProcessingResponse = false; + this.currentTranscript = ''; + } + + /** + * Convert audio data to base64 string for WebSocket transmission + * Inworld TTS returns Float32 PCM in [-1.0, 1.0] range - send directly to preserve quality + */ + private convertAudioToBase64(audio: { + data?: string | number[] | Float32Array; + sampleRate?: number; + }): { base64: string; format: 'float32' | 'int16' } | null { + if (!audio.data) return null; + + if (typeof audio.data === 'string') { + // Already base64 - assume Int16 format for backwards compatibility + return { base64: audio.data, format: 'int16' }; + } + + // Inworld SDK returns audio.data as an array of raw bytes (0-255) + // These bytes ARE the Float32 PCM data in IEEE 754 format (4 bytes per sample) + // Simply pass them through as-is, and frontend interprets as Float32Array + const audioBuffer = Array.isArray(audio.data) + ? Buffer.from(audio.data) // Treat each array element as a byte + : Buffer.from( + audio.data.buffer, + audio.data.byteOffset, + audio.data.byteLength + ); + + return { + base64: audioBuffer.toString('base64'), + format: 'float32', // Frontend will interpret bytes as Float32Array + }; + } + + /** + * Send message to WebSocket client + */ + private sendToClient(message: Record): void { + if (this.ws.readyState === WebSocket.OPEN) { + try { + this.ws.send(JSON.stringify(message)); + } catch (error) { + this.logger.error({ err: error }, 'send_to_client_error'); + } + } + } + + /** + * Trigger flashcard generation + */ + private triggerFlashcardGeneration(): void { + if (!this.flashcardCallback) return; + + const connection = this.connections[this.sessionId]; + if (!connection) return; + + const recentMessages = connection.state.messages.slice(-6).map((m) => ({ + role: m.role, + content: m.content, + })); + + // Track pending flashcard generation + this.pendingFlashcardGeneration = this.flashcardCallback(recentMessages) + .catch((error) => { + this.logger.error({ err: error }, 'flashcard_generation_trigger_error'); + }) + .finally(() => { + this.pendingFlashcardGeneration = null; + }); + } + + /** + * Trigger feedback generation for the user's last utterance + */ + private triggerFeedbackGeneration(): void { + if (!this.feedbackCallback) return; + + const connection = this.connections[this.sessionId]; + if (!connection) return; + + // Find the last user message + const messages = connection.state.messages; + const lastUserMessage = [...messages] + .reverse() + .find((m) => m.role === 'user'); + + if (!lastUserMessage) return; + + const recentMessages = messages.slice(-6).map((m) => ({ + role: m.role, + content: m.content, + })); + + // Track pending feedback generation + this.pendingFeedbackGeneration = this.feedbackCallback( + recentMessages, + lastUserMessage.content + ) + .catch((error) => { + this.logger.error({ err: error }, 'feedback_generation_trigger_error'); + }) + .finally(() => { + this.pendingFeedbackGeneration = null; + }); + } + + /** + * Trigger memory generation + * The callback handles turn counting and decides whether to create a memory + */ + private triggerMemoryGeneration(): void { + if (!this.memoryCallback) return; + + const connection = this.connections[this.sessionId]; + if (!connection) return; + + const recentMessages = connection.state.messages.slice(-10).map((m) => ({ + role: m.role, + content: m.content, + })); + + // Track pending memory generation + this.pendingMemoryGeneration = this.memoryCallback(recentMessages) + .catch((error) => { + this.logger.error({ err: error }, 'memory_generation_trigger_error'); + }) + .finally(() => { + this.pendingMemoryGeneration = null; + }); + } + + // ============================================================ + // Public API (compatible with AudioProcessor) + // ============================================================ + + setFlashcardCallback( + callback: ( + messages: Array<{ role: string; content: string }> + ) => Promise + ): void { + this.flashcardCallback = callback; + } + + setFeedbackCallback( + callback: ( + messages: Array<{ role: string; content: string }>, + currentTranscript: string + ) => Promise + ): void { + this.feedbackCallback = callback; + } + + setMemoryCallback( + callback: ( + messages: Array<{ role: string; content: string }> + ) => Promise + ): void { + this.memoryCallback = callback; + } + + /** + * Set the user ID for memory retrieval and creation + * This should be called when user context is received + */ + setUserId(userId: string): void { + this.userId = userId; + // Also update connection state so it flows through the graph + const connection = this.connections[this.sessionId]; + if (connection) { + connection.state.userId = userId; + } + this.logger.info({ userId: userId.substring(0, 8) }, 'user_id_set'); + } + + getConversationState(): { + messages: Array<{ role: string; content: string; timestamp: string }>; + } { + const connection = this.connections[this.sessionId]; + return { + messages: + connection?.state.messages.map((m) => ({ + role: m.role, + content: m.content, + timestamp: m.timestamp || new Date().toISOString(), + })) || [], + }; + } + + getLanguageCode(): string { + return this.languageCode; + } + + getLanguageConfig(): LanguageConfig { + return this.languageConfig; + } + + /** + * Update language for this connection + */ + setLanguage(languageCode: string): void { + if (this.languageCode === languageCode) return; + + this.logger.info( + { from: this.languageCode, to: languageCode }, + 'changing_language' + ); + + this.languageCode = languageCode; + this.languageConfig = getLanguageConfig(languageCode); + + const connection = this.connections[this.sessionId]; + if (connection) { + connection.state.languageCode = languageCode; + connection.state.targetLanguage = this.languageConfig.name; + connection.state.voiceId = this.languageConfig.ttsConfig.speakerId; + } + } + + /** + * Reset conversation state + */ + reset(): void { + const connection = this.connections[this.sessionId]; + if (connection) { + connection.state.messages = []; + connection.state.interactionId = ''; + } + this.logger.info('conversation_reset'); + } + + /** + * Load conversation history from client + * Called when switching to an existing conversation + */ + loadConversationHistory( + messages: Array<{ role: string; content: string; timestamp?: string }> + ): void { + const connection = this.connections[this.sessionId]; + if (!connection) { + this.logger.warn('connection_not_found_for_history_load'); + return; + } + + // Convert client messages to backend format + const chatMessages = messages.map((m, index) => ({ + id: `msg_${Date.now()}_${index}_${Math.random().toString(36).substring(2, 9)}`, + role: m.role as 'user' | 'assistant' | 'system', + content: m.content, + timestamp: m.timestamp || new Date().toISOString(), + })); + + connection.state.messages = chatMessages; + + this.logger.info( + { messageCount: chatMessages.length }, + 'conversation_history_loaded' + ); + } + + /** + * Wait for all pending operations to complete + * Used before switching conversations + */ + async waitForPendingOperations(): Promise { + // FIRST: Wait for graph processing to complete (isProcessingResponse becomes false) + // This ensures flashcard/feedback generation has been triggered before we check for them + if (this.isProcessingResponse) { + this.logger.info('waiting_for_graph_processing_to_complete'); + await new Promise((resolve) => { + const checkInterval = setInterval(() => { + if (!this.isProcessingResponse) { + clearInterval(checkInterval); + resolve(); + } + }, 100); + + // Timeout after 5 seconds + setTimeout(() => { + clearInterval(checkInterval); + resolve(); + }, 5000); + }); + this.logger.info('graph_processing_completed'); + } + + // SECOND: After graph processing completes, check for pending flashcard/feedback/memory generation + // By this point, triggerFlashcardGeneration(), triggerFeedbackGeneration(), and triggerMemoryGeneration() have been called + const promises: Promise[] = []; + + if (this.pendingFlashcardGeneration) { + this.logger.info('waiting_for_flashcard_generation'); + promises.push(this.pendingFlashcardGeneration); + } + + if (this.pendingFeedbackGeneration) { + this.logger.info('waiting_for_feedback_generation'); + promises.push(this.pendingFeedbackGeneration); + } + + if (this.pendingMemoryGeneration) { + this.logger.info('waiting_for_memory_generation'); + promises.push(this.pendingMemoryGeneration); + } + + if (promises.length > 0) { + this.logger.info( + { pendingCount: promises.length }, + 'waiting_for_pending_operations' + ); + await Promise.all(promises); + this.logger.info('pending_operations_completed'); + } + } + + /** + * Switch to a new conversation + * Waits for pending operations, then updates language, history, and conversationId + */ + async switchConversation( + conversationId: string, + languageCode: string, + messages: Array<{ role: string; content: string; timestamp?: string }> + ): Promise { + if (this.isSwitchingConversation) { + this.logger.warn('conversation_switch_already_in_progress'); + return; + } + + this.isSwitchingConversation = true; + this.logger.info( + { conversationId, languageCode, messageCount: messages.length }, + 'switching_conversation' + ); + + try { + // Wait for pending operations to complete + await this.waitForPendingOperations(); + + // Update language + const newLanguageConfig = getLanguageConfig(languageCode); + this.languageCode = languageCode; + this.languageConfig = newLanguageConfig; + + // Update connection state + const connection = this.connections[this.sessionId]; + if (connection) { + connection.state.languageCode = languageCode; + connection.state.targetLanguage = newLanguageConfig.name; + connection.state.voiceId = newLanguageConfig.ttsConfig.speakerId; + connection.state.conversationId = conversationId; + + // Load conversation history + const chatMessages = messages.map((m, index) => ({ + id: `msg_${Date.now()}_${index}_${Math.random().toString(36).substring(2, 9)}`, + role: m.role as 'user' | 'assistant' | 'system', + content: m.content, + timestamp: m.timestamp || new Date().toISOString(), + })); + + connection.state.messages = chatMessages; + } + + // Update conversationId + this.conversationId = conversationId; + + this.logger.info( + { conversationId, languageCode, messageCount: messages.length }, + 'conversation_switched' + ); + } finally { + this.isSwitchingConversation = false; + } + } + + /** + * Get current conversation ID + */ + getConversationId(): string | null { + return this.conversationId; + } + + /** + * Send a text message (bypasses audio/STT, goes directly to LLM) + */ + sendTextMessage(text: string): void { + if (this.isDestroyed) return; + + const trimmedText = text.trim(); + if (!trimmedText) return; + + this.logger.debug( + { textSnippet: trimmedText.substring(0, 50) }, + 'sending_text_message' + ); + this.multimodalStreamManager.pushText(trimmedText); + } + + /** + * Clean up resources + */ + async destroy(): Promise { + this.logger.info('destroying_session'); + this.isDestroyed = true; + + // End the multimodal stream + this.multimodalStreamManager.end(); + + // Close AssemblyAI session + await this.graphWrapper.assemblyAINode.closeSession(this.sessionId); + + // Remove from connections map + delete this.connections[this.sessionId]; + + this.logger.info('session_destroyed'); + } +} diff --git a/backend/src/helpers/feedback-processor.ts b/backend/src/helpers/feedback-processor.ts new file mode 100644 index 0000000..c0458c9 --- /dev/null +++ b/backend/src/helpers/feedback-processor.ts @@ -0,0 +1,92 @@ +import { v4 } from 'uuid'; +import { GraphTypes } from '@inworld/runtime/common'; +import { UserContextInterface } from '@inworld/runtime/graph'; +import { + getResponseFeedbackGraph, + ResponseFeedbackInput, +} from '../graphs/response-feedback-graph.js'; +import { + LanguageConfig, + getLanguageConfig, + DEFAULT_LANGUAGE_CODE, +} from '../config/languages.js'; +import { feedbackLogger as logger } from '../utils/logger.js'; + +export interface ConversationMessage { + role: string; + content: string; +} + +export class FeedbackProcessor { + private languageCode: string = DEFAULT_LANGUAGE_CODE; + private languageConfig: LanguageConfig; + + constructor(languageCode: string = DEFAULT_LANGUAGE_CODE) { + this.languageCode = languageCode; + this.languageConfig = getLanguageConfig(languageCode); + } + + setLanguage(languageCode: string): void { + if (this.languageCode !== languageCode) { + this.languageCode = languageCode; + this.languageConfig = getLanguageConfig(languageCode); + logger.info({ language: this.languageConfig.name }, 'language_changed'); + } + } + + getLanguageCode(): string { + return this.languageCode; + } + + async generateFeedback( + messages: ConversationMessage[], + currentTranscript: string, + userContext?: UserContextInterface + ): Promise { + const executor = getResponseFeedbackGraph(); + + // Remove the last assistant message so conversation ends with user's utterance + let conversationMessages = messages; + if ( + messages.length > 0 && + messages[messages.length - 1].role === 'assistant' + ) { + conversationMessages = messages.slice(0, -1); + } + + try { + const input: ResponseFeedbackInput = { + messages: conversationMessages, + currentTranscript: currentTranscript, + targetLanguage: this.languageConfig.name, + }; + + let executionResult; + try { + const executionContext = { + executionId: v4(), + userContext: userContext, + }; + executionResult = await executor.start(input, executionContext); + } catch (err) { + logger.warn({ err }, 'executor_start_with_context_failed_falling_back'); + executionResult = await executor.start(input); + } + + let finalData: GraphTypes.Content | null = null; + for await (const res of executionResult.outputStream) { + finalData = res.data; + } + + const feedback = finalData as unknown as string; + return feedback || ''; + } catch (error) { + logger.error({ err: error }, 'feedback_generation_error'); + return ''; + } + } + + reset() { + // No state to reset for feedback processor + } +} diff --git a/backend/helpers/flashcard-processor.ts b/backend/src/helpers/flashcard-processor.ts similarity index 62% rename from backend/helpers/flashcard-processor.ts rename to backend/src/helpers/flashcard-processor.ts index ab1b028..b08fdec 100644 --- a/backend/helpers/flashcard-processor.ts +++ b/backend/src/helpers/flashcard-processor.ts @@ -2,15 +2,22 @@ import { v4 } from 'uuid'; import { Graph } from '@inworld/runtime/graph'; import { GraphTypes } from '@inworld/runtime/common'; import { UserContextInterface } from '@inworld/runtime/graph'; -import { createFlashcardGraph } from '../graphs/flashcard-graph.js'; +import { getFlashcardGraph } from '../graphs/flashcard-graph.js'; +import { + LanguageConfig, + getLanguageConfig, + DEFAULT_LANGUAGE_CODE, +} from '../config/languages.js'; +import { flashcardLogger as logger } from '../utils/logger.js'; export interface Flashcard { id: string; - spanish: string; + targetWord: string; // The word in the target language (was 'spanish') english: string; example: string; mnemonic: string; timestamp: string; + languageCode?: string; // Track which language this card belongs to } export interface ConversationMessage { @@ -20,9 +27,30 @@ export interface ConversationMessage { export class FlashcardProcessor { private existingFlashcards: Flashcard[] = []; + private languageCode: string = DEFAULT_LANGUAGE_CODE; + private languageConfig: LanguageConfig; - constructor() { - // Initialize with empty flashcard array + constructor(languageCode: string = DEFAULT_LANGUAGE_CODE) { + this.languageCode = languageCode; + this.languageConfig = getLanguageConfig(languageCode); + } + + /** + * Update the language for this processor + */ + setLanguage(languageCode: string): void { + if (this.languageCode !== languageCode) { + this.languageCode = languageCode; + this.languageConfig = getLanguageConfig(languageCode); + logger.info({ language: this.languageConfig.name }, 'language_changed'); + } + } + + /** + * Get current language code + */ + getLanguageCode(): string { + return this.languageCode; } async generateFlashcards( @@ -30,7 +58,7 @@ export class FlashcardProcessor { count: number = 1, userContext?: UserContextInterface ): Promise { - const executor = createFlashcardGraph(); + const executor = getFlashcardGraph(); // Generate flashcards in parallel const promises: Promise[] = []; @@ -46,7 +74,7 @@ export class FlashcardProcessor { // Filter out any failed generations and duplicates const validFlashcards = flashcards.filter( - (card) => card.spanish && card.english + (card) => card.targetWord && card.english ); // Add to existing flashcards to track for future duplicates @@ -54,7 +82,7 @@ export class FlashcardProcessor { return validFlashcards; } catch (error) { - console.error('Error generating flashcards:', error); + logger.error({ err: error }, 'flashcard_batch_generation_error'); return []; } } @@ -67,7 +95,8 @@ export class FlashcardProcessor { try { const input = { studentName: 'Student', - teacherName: 'Señor Rosales', + teacherName: this.languageConfig.teacherPersona.name, + target_language: this.languageConfig.name, messages: messages, flashcards: this.existingFlashcards, }; @@ -80,10 +109,7 @@ export class FlashcardProcessor { }; executionResult = await executor.start(input, executionContext); } catch (err) { - console.warn( - 'Flashcard executor.start with ExecutionContext failed, falling back without context:', - err - ); + logger.warn({ err }, 'executor_start_with_context_failed_falling_back'); executionResult = await executor.start(input); } let finalData: GraphTypes.Content | null = null; @@ -92,10 +118,14 @@ export class FlashcardProcessor { } const flashcard = finalData as unknown as Flashcard; + // Add language code to the flashcard + flashcard.languageCode = this.languageCode; + // Check if this is a duplicate const isDuplicate = this.existingFlashcards.some( (existing) => - existing.spanish?.toLowerCase() === flashcard.spanish?.toLowerCase() + existing.targetWord?.toLowerCase() === + flashcard.targetWord?.toLowerCase() ); if (isDuplicate) { @@ -103,24 +133,26 @@ export class FlashcardProcessor { // For simplicity, we'll just return an empty flashcard if duplicate return { id: v4(), - spanish: '', + targetWord: '', english: '', example: '', mnemonic: '', timestamp: new Date().toISOString(), + languageCode: this.languageCode, } as Flashcard & { error?: string }; } return flashcard; } catch (error) { - console.error('Error generating single flashcard:', error); + logger.error({ err: error }, 'single_flashcard_generation_error'); return { id: v4(), - spanish: '', + targetWord: '', english: '', example: '', mnemonic: '', timestamp: new Date().toISOString(), + languageCode: this.languageCode, } as Flashcard & { error?: string }; } } diff --git a/backend/src/helpers/memory-processor.ts b/backend/src/helpers/memory-processor.ts new file mode 100644 index 0000000..e09ed5f --- /dev/null +++ b/backend/src/helpers/memory-processor.ts @@ -0,0 +1,301 @@ +/** + * Memory Processor + * + * Generates, embeds, and stores memories from conversations. + * Designed for non-blocking (fire-and-forget) execution. + * + * Features: + * - Turn counting to trigger memory creation every N turns + * - LLM-based memory generation from conversation context + * - Embedding generation using BAAI/bge-large-en-v1.5 + * - Storage in Supabase with pgvector + */ + +import { v4 as uuidv4 } from 'uuid'; +import { Graph } from '@inworld/runtime/graph'; +import { TextEmbedder } from '@inworld/runtime/primitives/embeddings'; +import { getMemoryService } from '../services/memory-service.js'; +import { isSupabaseConfigured } from '../config/supabase.js'; +import { embedderConfig } from '../config/embedder.js'; +import { createMemoryGenerationGraph } from '../graphs/memory-generation-graph.js'; +import { + getLanguageConfig, + DEFAULT_LANGUAGE_CODE, +} from '../config/languages.js'; +import { + MemoryRecord, + MemoryType, + MemoryGenerationOutput, +} from '../types/memory.js'; +import { createLogger } from '../utils/logger.js'; + +const logger = createLogger('MemoryProcessor'); + +/** Number of recent messages to include for memory generation context */ +const MEMORY_MESSAGE_CONTEXT_LIMIT = 10; + +// Singleton graph instance - shared across all MemoryProcessor instances +let sharedGraph: Graph | null = null; +let sharedEmbedder: TextEmbedder | null = null; +let sharedInitPromise: Promise | null = null; + +/** + * Initialize shared resources (graph and embedder) - called lazily + */ +async function initSharedResources(): Promise { + if (sharedGraph && sharedEmbedder) { + return; + } + + if (sharedInitPromise) { + return sharedInitPromise; + } + + sharedInitPromise = (async () => { + try { + const apiKey = process.env.INWORLD_API_KEY; + if (!apiKey) { + throw new Error('INWORLD_API_KEY environment variable is required'); + } + + // Initialize embedder + sharedEmbedder = await TextEmbedder.create({ + remoteConfig: { + apiKey, + provider: embedderConfig.provider, + modelName: embedderConfig.modelName, + }, + }); + + // Initialize memory generation graph + sharedGraph = createMemoryGenerationGraph(); + + logger.info('memory_processor_initialized'); + } catch (error) { + logger.error({ err: error }, 'memory_processor_init_failed'); + sharedInitPromise = null; // Allow retry + throw error; + } + })(); + + return sharedInitPromise; +} + +/** + * Memory Processor class for generating and storing user memories + */ +export class MemoryProcessor { + private turnCount: number = 0; + private readonly turnInterval: number = 3; + private languageCode: string = DEFAULT_LANGUAGE_CODE; + + constructor(languageCode: string = DEFAULT_LANGUAGE_CODE) { + this.languageCode = languageCode; + logger.info('memory_processor_created'); + } + + /** + * Update the language for this processor + */ + setLanguage(languageCode: string): void { + if (this.languageCode !== languageCode) { + this.languageCode = languageCode; + logger.debug({ languageCode }, 'memory_processor_language_changed'); + } + } + + /** + * Increment turn count - call after each completed turn + */ + incrementTurn(): void { + this.turnCount++; + logger.debug({ turnCount: this.turnCount }, 'memory_turn_incremented'); + } + + /** + * Check if we should create a memory this turn + */ + shouldCreateMemory(): boolean { + return this.turnCount > 0 && this.turnCount % this.turnInterval === 0; + } + + /** + * Get current turn count + */ + getTurnCount(): number { + return this.turnCount; + } + + /** + * Asynchronous Memory creation + */ + async createMemoryAsync( + userId: string, + messages: Array<{ role: string; content: string }> + ): Promise { + // Wait for memory creation to complete + try { + await this.processMemoryCreation(userId, messages); + } catch (error) { + logger.error({ err: error }, 'memory_creation_failed'); + throw error; // Re-throw so caller can handle it + } + } + + /** + * Internal method to process memory creation + */ + private async processMemoryCreation( + userId: string, + messages: Array<{ role: string; content: string }> + ): Promise { + if (!isSupabaseConfigured()) { + logger.debug('supabase_not_configured_skipping_memory_creation'); + return; + } + + // Initialize shared resources lazily (only when first memory is created) + await initSharedResources(); + + if (!sharedGraph || !sharedEmbedder) { + logger.warn('memory_processor_not_ready'); + return; + } + + try { + const langConfig = getLanguageConfig(this.languageCode); + + // Step 1: Generate memory using LLM + const input = { + messages: messages.slice(-MEMORY_MESSAGE_CONTEXT_LIMIT), + target_language: langConfig.name, + }; + + const executionResult = await sharedGraph.start(input, { + executionId: uuidv4(), + }); + + let llmOutput: string = ''; + for await (const res of executionResult.outputStream) { + if (res.data) { + // Extract content from LLM response + const data = res.data as { content?: string } | string; + llmOutput = typeof data === 'string' ? data : data.content || ''; + } + } + + if (!llmOutput) { + logger.warn('no_llm_output_for_memory'); + return; + } + + // Step 2: Parse JSON response + const memoryData = this.parseMemoryOutput(llmOutput); + if (!memoryData || !memoryData.memory) { + logger.debug( + { llmOutput: llmOutput.substring(0, 100) }, + 'no_valid_memory_generated' + ); + return; + } + + // Step 3: Generate embedding for the memory text + const embedResponse = await sharedEmbedder.embed(memoryData.memory); + const embedding = TextEmbedder.toArray(embedResponse); + + // Step 4: Store memory in Supabase + const memoryRecord: MemoryRecord = { + userId, + content: memoryData.memory, + memoryType: memoryData.type, + topics: memoryData.topics, + importance: memoryData.importance, + embedding, + }; + + const memoryService = getMemoryService(); + const memoryId = await memoryService.storeMemory(memoryRecord); + + if (memoryId) { + logger.info( + { + memoryId, + type: memoryData.type, + topics: memoryData.topics, + importance: memoryData.importance, + }, + 'memory_created_successfully' + ); + } + } catch (error) { + logger.error({ err: error }, 'memory_processing_error'); + } + } + + /** + * Parse LLM output to extract memory data + */ + private parseMemoryOutput(llmOutput: string): MemoryGenerationOutput | null { + try { + // Find JSON in the output + const jsonMatch = llmOutput.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + return null; + } + + const parsed = JSON.parse(jsonMatch[0]); + + // Validate required fields + if (!parsed.memory || typeof parsed.memory !== 'string') { + return null; + } + + // Empty memory means nothing memorable + if (parsed.memory.trim() === '') { + return null; + } + + // Validate and normalize type + const validTypes: MemoryType[] = [ + 'learning_progress', + 'personal_context', + ]; + const type: MemoryType = validTypes.includes(parsed.type) + ? parsed.type + : 'personal_context'; + + // Validate topics + const topics: string[] = Array.isArray(parsed.topics) + ? parsed.topics + .filter((t: unknown) => typeof t === 'string') + .slice(0, 5) + : []; + + // Validate importance + const importance = + typeof parsed.importance === 'number' + ? Math.max(0, Math.min(1, parsed.importance)) + : 0.5; + + return { + memory: parsed.memory.trim(), + type, + topics, + importance, + }; + } catch (error) { + logger.error( + { err: error, output: llmOutput.substring(0, 100) }, + 'memory_parse_error' + ); + return null; + } + } + + /** + * Reset the processor (e.g., when starting a new conversation) + */ + reset(): void { + this.turnCount = 0; + } +} diff --git a/backend/src/helpers/multimodal-stream-manager.ts b/backend/src/helpers/multimodal-stream-manager.ts new file mode 100644 index 0000000..95b701e --- /dev/null +++ b/backend/src/helpers/multimodal-stream-manager.ts @@ -0,0 +1,143 @@ +/** + * Manages a stream of multimodal content (audio and/or text) that can be fed + * asynchronously as data arrives from websocket connections. + * + * This unifies audio and text streaming into a single interface that always + * yields MultimodalContent, making it compatible with the 0.9 graph architecture. + */ + +import { GraphTypes } from '@inworld/runtime/graph'; +import { createLogger } from '../utils/logger.js'; + +const logger = createLogger('MultimodalStreamManager'); + +export interface AudioChunkInterface { + data: number[] | Float32Array; + sampleRate: number; +} + +export class MultimodalStreamManager { + private queue: GraphTypes.MultimodalContent[] = []; + private waitingResolvers: Array< + (value: IteratorResult) => void + > = []; + private ended = false; + + /** + * Add an audio chunk to the stream (wrapped in MultimodalContent) + */ + pushAudio(chunk: AudioChunkInterface): void { + if (this.ended) { + return; + } + + // Create GraphTypes.Audio object and wrap in MultimodalContent + // Note: GraphTypes.Audio requires number[] - conversion from Float32Array is unavoidable + // due to SDK constraint. If chunk.data is already number[], we pass it directly. + const audioData = new GraphTypes.Audio({ + data: Array.isArray(chunk.data) ? chunk.data : Array.from(chunk.data), + sampleRate: chunk.sampleRate, + }); + const multimodalContent = new GraphTypes.MultimodalContent({ + audio: audioData, + }); + + this.pushContent(multimodalContent); + } + + /** + * Add text to the stream (wrapped in MultimodalContent) + */ + pushText(text: string): void { + if (this.ended) { + return; + } + + const multimodalContent = new GraphTypes.MultimodalContent({ text }); + this.pushContent(multimodalContent); + } + + /** + * Internal method to push MultimodalContent to the stream + */ + private pushContent(content: GraphTypes.MultimodalContent): void { + // If there are waiting consumers, resolve immediately + if (this.waitingResolvers.length > 0) { + const resolve = this.waitingResolvers.shift()!; + resolve({ value: content, done: false }); + } else { + // Otherwise, queue the content + this.queue.push(content); + } + } + + /** + * Mark the stream as ended + */ + end(): void { + logger.debug('stream_ending'); + this.ended = true; + + // Resolve all waiting consumers with done signal + while (this.waitingResolvers.length > 0) { + const resolve = this.waitingResolvers.shift()!; + resolve({ + value: undefined as unknown as GraphTypes.MultimodalContent, + done: true, + }); + } + } + + /** + * Create an async iterator for the stream + */ + async *createStream(): AsyncIterableIterator { + while (true) { + // If stream ended and queue is empty, we're done + if (this.ended && this.queue.length === 0) { + logger.debug('stream_iteration_complete'); + return; + } + + // If we have queued content, yield it immediately + if (this.queue.length > 0) { + const content = this.queue.shift()!; + yield content; + continue; + } + + // If stream ended but we just exhausted the queue, we're done + if (this.ended) { + logger.debug('stream_iteration_complete'); + return; + } + + // Otherwise, wait for new content + const result = await new Promise< + IteratorResult + >((resolve) => { + this.waitingResolvers.push(resolve); + }); + + if (result.done) { + return; + } + + yield result.value; + } + } + + /** + * Check if the stream has ended + */ + isEnded(): boolean { + return this.ended; + } + + /** + * Get the current queue length (for debugging) + */ + getQueueLength(): number { + return this.queue.length; + } +} diff --git a/backend/src/helpers/prompt-templates.ts b/backend/src/helpers/prompt-templates.ts new file mode 100644 index 0000000..9001b17 --- /dev/null +++ b/backend/src/helpers/prompt-templates.ts @@ -0,0 +1,29 @@ +/** + * Prompt Templates for Multi-Language Support + * + * Templates are loaded from .njk files in the /prompts folder. + * All templates use Jinja2-style variables that are injected at runtime: + * - {{target_language}} - English name of target language (e.g., "Spanish") + * - {{target_language_native}} - Native name (e.g., "Español") + * - {{teacher_name}} - Teacher persona name + * - {{teacher_description}} - Full teacher persona description + * - {{example_topics}} - Comma-separated list of conversation topics + */ + +import { readFile } from 'fs/promises'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const promptsDir = join(__dirname, '..', 'prompts'); + +async function loadTemplate(name: string): Promise { + const content = await readFile(join(promptsDir, `${name}.njk`), 'utf-8'); + return content.trim(); +} + +export const conversationTemplate = await loadTemplate('conversation'); +export const flashcardPromptTemplate = await loadTemplate('flashcard'); +export const responseFeedbackPromptTemplate = + await loadTemplate('response-feedback'); diff --git a/backend/src/prompts/conversation.njk b/backend/src/prompts/conversation.njk new file mode 100644 index 0000000..d797fc9 --- /dev/null +++ b/backend/src/prompts/conversation.njk @@ -0,0 +1,35 @@ +# Context +- You are {{teacher_name}}, {{teacher_description}}. +- You are embedded in a {{target_language}} learning app called 'Inworld Language Tutor', which is a demonstration of the Inworld AI Runtime. +- You can help the user learn {{target_language}} by having natural (verbalized) conversations with them. +- The app generates flashcards for the user during the conversation. They are ANKI formatted and can be exported by the user. + +# Instructions +- Greet the user and introduce yourself in {{target_language}} +- If they don't want anything in particular, lead them in a conversation or lesson about {{example_topics}}, or any other topic which comes to mind +- Don't always ask the user questions, you can talk about yourself as well. Be natural! +- As the user is a learner, you can offer them advise and feedback + +# Communication Style +- Use varied sentence structures +- Generally, be terse, but expound if the user requests it +- As the user's speech is being passed to you via speech-to-text, do your best to understand the user's intent even if there are transcription errors +- Ask open-ended questions to prompt the user to practice their {{target_language}} +{% if relevant_memories and relevant_memories|length > 0 %} + +# What You Remember About This User +{% for memory in relevant_memories %} +- {{ memory.content }}{% if memory.memoryType == 'learning_progress' %} (learning note){% endif %} +{% endfor %} +{% endif %} + +{% if messages and messages|length > 0 %} +Previous conversation: +{% for message in messages %} +{{ message.role }}: {{ message.content }} +{% endfor %} +{% endif %} + +User just said: {{ current_input }} + +Please respond naturally and clearly in 1-2 short sentences (only be more verbose if the user specifically wants it). \ No newline at end of file diff --git a/backend/src/prompts/flashcard.njk b/backend/src/prompts/flashcard.njk new file mode 100644 index 0000000..82c7c27 --- /dev/null +++ b/backend/src/prompts/flashcard.njk @@ -0,0 +1,34 @@ +You are a system that generates flashcards for interesting new vocabulary for a {{target_language}} learning app. + +Based on the ongoing conversation between {{studentName}} and {{teacherName}}, generate one flashcard with the following things: + +- The word in {{target_language}} +- The translation in English +- An example sentence in {{target_language}} +- A mnemonic to help the student remember the word (in English) + +## Conversation + +{% for message in messages %} +{{message.role}}: {{message.content}}{% endfor %} + +## Already Created Flashcards + +{% for flashcard in flashcards %} +- Word: {{flashcard.targetWord}} +{% endfor %} + +## Guidelines + +- The word must be related to the topics used in the conversation +- The word should be useful to the learner so they can continue the conversation with new vocabulary +- Avoid cognates + +Now, return JSON with the following format: + +{ + "targetWord": "string", + "english": "string", + "example": "string", + "mnemonic": "string" +} \ No newline at end of file diff --git a/backend/src/prompts/memory-generation.njk b/backend/src/prompts/memory-generation.njk new file mode 100644 index 0000000..142a964 --- /dev/null +++ b/backend/src/prompts/memory-generation.njk @@ -0,0 +1,30 @@ +You are analyzing a language learning conversation to extract memorable facts about the user. + +Conversation context ({{target_language}} learning session): +{% for message in messages %} +{{ message.role }}: {{ message.content }} +{% endfor %} + +Based on this conversation, create ONE concise memory about the user in English. Focus on: +- Learning progress: vocabulary struggles, grammar issues, topics covered, skill improvements +- Personal context: interests, goals, preferences, life details they shared + +Output format (JSON): +{ + "memory": "The user [specific fact about them]", + "type": "learning_progress" or "personal_context", + "topics": ["topic1", "topic2"], + "importance": 0.5 +} + +Rules: +- Memory must be in English (even if conversation was in another language) +- Keep it factual, specific, and concise (1-2 sentences) +- Topics should be 1-3 relevant keywords +- Importance scale: + - 0.3: Minor detail (casual mention) + - 0.5: Normal (general learning note or preference) + - 0.7: Significant (clear goal, major struggle, important interest) + - 0.9: Critical (explicit request to remember, strong preference) +- If nothing memorable was said, return: {"memory": "", "type": "personal_context", "topics": [], "importance": 0} +- Return ONLY the JSON object, no other text diff --git a/backend/src/prompts/response-feedback.njk b/backend/src/prompts/response-feedback.njk new file mode 100644 index 0000000..96a7378 --- /dev/null +++ b/backend/src/prompts/response-feedback.njk @@ -0,0 +1,18 @@ +You are a {{targetLanguage}} language tutor assistant. Your task is to analyze the student's most recent utterance and provide brief, helpful feedback. + +## Conversation so far: +{% for message in messages %} +{{ message.role }}: {{ message.content }} +{% endfor %} + +## Student's last utterance: +{{ currentTranscript }} + +## Instructions: +- If the student made any grammar, vocabulary, or pronunciation errors in their {{targetLanguage}}, offer a gentle correction +- If the student's response was good, offer a brief word of encouragement or a small tip to improve +- Keep your feedback to exactly ONE sentence in English +- Don't worry about punctuation or spelling as that is from audio transcription. Instead focus on their word choice or grammar +- Be encouraging and constructive + +Your feedback (one sentence in English): \ No newline at end of file diff --git a/backend/src/server.ts b/backend/src/server.ts new file mode 100644 index 0000000..4e970b9 --- /dev/null +++ b/backend/src/server.ts @@ -0,0 +1,82 @@ +/** + * Language Learning Server - Inworld Runtime 0.9 + * + * This server uses a long-running circular graph with AssemblyAI for VAD/STT. + * Key components: + * - ConversationGraphWrapper: The main graph that processes audio → STT → LLM → TTS + * - ConnectionManager: Manages WebSocket connections and feeds audio to the graph + * - FlashcardProcessor: Generates flashcards from conversations + */ + +// Load environment variables FIRST +import dotenv from 'dotenv'; +dotenv.config(); + +import express from 'express'; +import cors from 'cors'; +import { createServer } from 'http'; +import { WebSocketServer } from 'ws'; + +import { serverConfig } from './config/server.js'; +import { serverLogger as logger } from './utils/logger.js'; + +// Import services +import { initTelemetry } from './services/telemetry.js'; +import { + initializeGraph, + exportGraphConfigs, +} from './services/graph-service.js'; +import { setupWebSocketHandlers } from './services/websocket-handler.js'; +import { apiRouter } from './services/api-routes.js'; +import { createGracefulShutdown } from './services/shutdown.js'; + +// Initialize Express and servers +const app = express(); +const server = createServer(app); +const wss = new WebSocketServer({ server }); + +// Middleware +app.use(express.json()); +app.use( + cors({ + origin: process.env.ALLOWED_ORIGINS?.split(',') || '*', + methods: ['GET', 'POST', 'OPTIONS'], + credentials: true, + }) +); + +// Initialize telemetry +initTelemetry(); + +// API routes +app.use('/api', apiRouter); +app.get('/health', (_req, res) => { + res + .status(200) + .json({ status: 'healthy', timestamp: new Date().toISOString() }); +}); + +// WebSocket handlers +setupWebSocketHandlers(wss); + +// Server startup +async function startServer(): Promise { + try { + await initializeGraph(); + await exportGraphConfigs(); + server.listen(serverConfig.port, () => { + logger.info({ port: serverConfig.port }, 'server_started'); + logger.info('using_inworld_runtime_0.9_with_assemblyai_stt'); + }); + } catch (error) { + logger.fatal({ err: error }, 'server_start_failed'); + process.exit(1); + } +} + +startServer(); + +// Graceful shutdown +const gracefulShutdown = createGracefulShutdown(server, wss); +process.on('SIGTERM', gracefulShutdown); +process.on('SIGINT', gracefulShutdown); diff --git a/backend/src/services/api-routes.ts b/backend/src/services/api-routes.ts new file mode 100644 index 0000000..ab1a7a6 --- /dev/null +++ b/backend/src/services/api-routes.ts @@ -0,0 +1,69 @@ +/** + * API Routes + * + * Express router for REST API endpoints. + */ + +import { Router } from 'express'; +import { AnkiExporter } from '../helpers/anki-exporter.js'; +import { + getLanguageOptions, + DEFAULT_LANGUAGE_CODE, +} from '../config/languages.js'; +import { serverLogger as logger } from '../utils/logger.js'; + +export const apiRouter = Router(); + +// ANKI export endpoint +apiRouter.post('/export-anki', async (req, res) => { + try { + const { flashcards, deckName, languageCode } = req.body; + + if (!flashcards || !Array.isArray(flashcards) || flashcards.length === 0) { + res.status(400).json({ error: 'No flashcards provided' }); + return; + } + + const exporter = new AnkiExporter(); + const validCount = exporter.countValidFlashcards(flashcards); + + if (validCount === 0) { + res.status(400).json({ error: 'No valid flashcards to export' }); + return; + } + + const defaultDeckName = `Inworld Language Tutor Spanish Cards`; + const apkgBuffer = await exporter.exportFlashcards( + flashcards, + deckName || defaultDeckName + ); + + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader( + 'Content-Disposition', + `attachment; filename="${(deckName || defaultDeckName).replace(/[^a-zA-Z0-9]/g, '_')}.apkg"` + ); + res.send(apkgBuffer); + } catch (error) { + logger.error({ err: error }, 'anki_export_error'); + res.status(500).json({ error: 'Failed to export Anki deck' }); + } +}); + +// Languages endpoint +apiRouter.get('/languages', (_req, res) => { + try { + const languages = getLanguageOptions(); + res.json({ languages, defaultLanguage: DEFAULT_LANGUAGE_CODE }); + } catch (error) { + logger.error({ err: error }, 'get_languages_error'); + res.status(500).json({ error: 'Failed to get languages' }); + } +}); + +// Health check endpoint for Cloud Run +apiRouter.get('/health', (_req, res) => { + res + .status(200) + .json({ status: 'healthy', timestamp: new Date().toISOString() }); +}); diff --git a/backend/src/services/graph-service.ts b/backend/src/services/graph-service.ts new file mode 100644 index 0000000..1bfd910 --- /dev/null +++ b/backend/src/services/graph-service.ts @@ -0,0 +1,79 @@ +/** + * Graph Service + * + * Manages graph initialization and configuration export. + */ + +import path from 'path'; +import { fileURLToPath } from 'url'; +import { existsSync } from 'fs'; +import { mkdir, writeFile } from 'fs/promises'; + +import { + getConversationGraph, + destroyConversationGraph, + ConversationGraphWrapper, +} from '../graphs/conversation-graph.js'; +import { getFlashcardGraph } from '../graphs/flashcard-graph.js'; +import { getResponseFeedbackGraph } from '../graphs/response-feedback-graph.js'; +import { initializeTTSGraphs } from '../graphs/simple-tts-graph.js'; +import { serverLogger as logger } from '../utils/logger.js'; +import { connections } from './state.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Reference to the initialized graph wrapper +let graphWrapper: ConversationGraphWrapper | null = null; + +export function getGraphWrapper(): ConversationGraphWrapper | null { + return graphWrapper; +} + +export async function initializeGraph(): Promise { + const assemblyAIApiKey = process.env.ASSEMBLY_AI_API_KEY; + if (!assemblyAIApiKey) { + throw new Error('ASSEMBLY_AI_API_KEY environment variable is required'); + } + + logger.info('initializing_conversation_graph'); + graphWrapper = getConversationGraph({ + assemblyAIApiKey, + connections, + defaultLanguageCode: 'es', // Always Spanish + }); + logger.info('conversation_graph_initialized'); + + // Initialize TTS graphs for all supported languages + logger.info('initializing_tts_graphs'); + initializeTTSGraphs(); + logger.info('tts_graphs_initialized'); +} + +export async function exportGraphConfigs(): Promise { + // Navigate from services/ to graphs/configs/ + const configDir = path.join(__dirname, '../graphs/configs'); + + if (!existsSync(configDir)) { + await mkdir(configDir, { recursive: true }); + } + + const graphs = [ + { id: 'flashcard-generation-graph', graph: getFlashcardGraph() }, + { id: 'response-feedback-graph', graph: getResponseFeedbackGraph() }, + ...(graphWrapper + ? [{ id: 'lang-learning-conversation-graph', graph: graphWrapper.graph }] + : []), + ]; + + for (const { id, graph } of graphs) { + const filePath = path.join(configDir, `${id}.json`); + await writeFile(filePath, graph.toJSON(), 'utf-8'); + logger.info({ graphId: id, path: filePath }, 'graph_config_exported'); + } +} + +export async function destroyGraph(): Promise { + await destroyConversationGraph(); + graphWrapper = null; +} diff --git a/backend/src/services/memory-service.ts b/backend/src/services/memory-service.ts new file mode 100644 index 0000000..96e85e9 --- /dev/null +++ b/backend/src/services/memory-service.ts @@ -0,0 +1,254 @@ +/** + * Memory Service + * + * Handles memory storage and retrieval using Supabase with pgvector. + * Memories are stored in English with vector embeddings for semantic search. + */ +import { getSupabaseClient, isSupabaseConfigured } from '../config/supabase.js'; +import { + MemoryRecord, + MemoryMatch, + MemoryType, + SupabaseMemoryRow, + VALID_MEMORY_TYPES, +} from '../types/memory.js'; +import { createLogger } from '../utils/logger.js'; + +/** + * Validate and convert a memory_type string to MemoryType + */ +function validateMemoryType(type: string): MemoryType { + if (VALID_MEMORY_TYPES.includes(type as MemoryType)) { + return type as MemoryType; + } + // Default to personal_context for unknown types + return 'personal_context'; +} + +const logger = createLogger('MemoryService'); + +/** + * Memory Service class for storing and retrieving user memories + */ +export class MemoryService { + /** + * Store a new memory with its embedding + * @param memory - The memory record to store + * @returns The ID of the stored memory, or null if storage failed + */ + async storeMemory(memory: MemoryRecord): Promise { + if (!isSupabaseConfigured()) { + logger.debug('supabase_not_configured_skipping_store'); + return null; + } + + try { + const supabase = getSupabaseClient(); + if (!supabase) { + return null; + } + + // Format embedding for Supabase pgvector + const embeddingStr = memory.embedding + ? `[${memory.embedding.join(',')}]` + : null; + + const { data, error } = await supabase + .from('user_memories') + .insert({ + user_id: memory.userId, + content: memory.content, + memory_type: memory.memoryType, + topics: memory.topics, + importance: memory.importance, + embedding: embeddingStr, + }) + .select('id') + .single(); + + if (error) { + logger.error({ err: error }, 'failed_to_store_memory'); + return null; + } + + logger.info( + { memoryId: data.id, type: memory.memoryType, topics: memory.topics }, + 'memory_stored' + ); + return data.id; + } catch (error) { + logger.error({ err: error }, 'memory_store_exception'); + return null; + } + } + + /** + * Retrieve similar memories using vector similarity search + * @param userId - The user's ID + * @param queryEmbedding - The embedding vector to search with + * @param limit - Maximum number of memories to return (default: 3) + * @param threshold - Minimum similarity threshold (default: 0.7) + * @returns Array of matching memories with similarity scores + */ + async retrieveMemories( + userId: string, + queryEmbedding: number[], + limit: number = 3, + threshold: number = 0.7 + ): Promise { + if (!isSupabaseConfigured()) { + logger.debug('supabase_not_configured_skipping_retrieve'); + return []; + } + + try { + const supabase = getSupabaseClient(); + if (!supabase) { + logger.warn('supabase_client_not_available'); + return []; + } + + // Format embedding for Supabase RPC call + const embeddingStr = `[${queryEmbedding.join(',')}]`; + + const startTime = performance.now(); + + const { data, error } = await supabase.rpc('match_memories', { + query_embedding: embeddingStr, + match_user_id: userId, + match_threshold: threshold, + match_count: limit, + }); + + const elapsed = performance.now() - startTime; + + if (error) { + logger.error( + { err: error, code: error.code, message: error.message }, + 'failed_to_retrieve_memories' + ); + return []; + } + + logger.info( + { + memoriesFound: data?.length || 0, + queryTimeMs: elapsed.toFixed(2), + threshold, + limit, + }, + 'memories_retrieved' + ); + + const memories: MemoryMatch[] = (data || []).map( + (row: SupabaseMemoryRow) => ({ + id: row.id, + content: row.content, + memoryType: validateMemoryType(row.memory_type), + topics: row.topics || [], + importance: row.importance, + similarity: row.similarity ?? 0, + }) + ); + + return memories; + } catch (error) { + logger.error({ err: error }, 'memory_retrieve_exception'); + return []; + } + } + + /** + * Get all memories for a user (for debugging/admin purposes) + * @param userId - The user's ID + * @param limit - Maximum number of memories to return (default: 50) + * @returns Array of memory records + */ + async getUserMemories( + userId: string, + limit: number = 50 + ): Promise { + if (!isSupabaseConfigured()) { + return []; + } + + try { + const supabase = getSupabaseClient(); + if (!supabase) { + return []; + } + + const { data, error } = await supabase + .from('user_memories') + .select('id, content, memory_type, topics, importance, created_at') + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .limit(limit); + + if (error) { + logger.error({ err: error }, 'failed_to_get_user_memories'); + return []; + } + + return (data || []).map((row: SupabaseMemoryRow) => ({ + id: row.id, + userId, + content: row.content, + memoryType: validateMemoryType(row.memory_type), + topics: row.topics || [], + importance: row.importance, + createdAt: row.created_at, + })); + } catch (error) { + logger.error({ err: error }, 'get_user_memories_exception'); + return []; + } + } + + /** + * Delete a specific memory + * @param memoryId - The memory ID to delete + * @returns True if deleted successfully + */ + async deleteMemory(memoryId: string): Promise { + if (!isSupabaseConfigured()) { + return false; + } + + try { + const supabase = getSupabaseClient(); + if (!supabase) { + return false; + } + + const { error } = await supabase + .from('user_memories') + .delete() + .eq('id', memoryId); + + if (error) { + logger.error({ err: error, memoryId }, 'failed_to_delete_memory'); + return false; + } + + logger.info({ memoryId }, 'memory_deleted'); + return true; + } catch (error) { + logger.error({ err: error }, 'delete_memory_exception'); + return false; + } + } +} + +// Singleton instance +let memoryService: MemoryService | null = null; + +/** + * Get the singleton MemoryService instance + */ +export function getMemoryService(): MemoryService { + if (!memoryService) { + memoryService = new MemoryService(); + } + return memoryService; +} diff --git a/backend/src/services/shutdown.ts b/backend/src/services/shutdown.ts new file mode 100644 index 0000000..a18ebd8 --- /dev/null +++ b/backend/src/services/shutdown.ts @@ -0,0 +1,67 @@ +/** + * Graceful Shutdown Service + * + * Handles cleanup of all resources during server shutdown. + */ + +import { Server } from 'http'; +import { WebSocketServer } from 'ws'; +import { stopInworldRuntime } from '@inworld/runtime'; + +import { serverLogger as logger } from '../utils/logger.js'; +import { + connectionManagers, + setShuttingDown, + isShuttingDown, +} from './state.js'; +import { destroyGraph } from './graph-service.js'; + +export function createGracefulShutdown( + server: Server, + wss: WebSocketServer +): () => Promise { + return async function gracefulShutdown(): Promise { + if (isShuttingDown()) return; + setShuttingDown(true); + + logger.info('shutdown_initiated'); + + try { + // Close all WebSocket connections + logger.info( + { connectionCount: wss.clients.size }, + 'closing_websocket_connections' + ); + wss.clients.forEach((ws) => { + if (ws.readyState === ws.OPEN || ws.readyState === ws.CONNECTING) { + ws.close(); + } + }); + + wss.close(); + + // Clean up connection managers + for (const manager of connectionManagers.values()) { + manager.destroy().catch(() => {}); + } + + // Clean up graph wrapper + await destroyGraph(); + + server.close(() => { + logger.info('http_server_closed'); + }); + + stopInworldRuntime() + .then(() => logger.info('inworld_runtime_stopped')) + .catch(() => {}); + + logger.info('shutdown_complete'); + } catch { + // Ignore errors during shutdown + } + + process.exitCode = 0; + process.exit(0); + }; +} diff --git a/backend/src/services/state.ts b/backend/src/services/state.ts new file mode 100644 index 0000000..87fcffe --- /dev/null +++ b/backend/src/services/state.ts @@ -0,0 +1,35 @@ +/** + * Global State Management + * + * Centralized state for managing connections, processors, and shutdown status. + */ + +import { ConnectionManager } from '../helpers/connection-manager.js'; +import { FlashcardProcessor } from '../helpers/flashcard-processor.js'; +import { FeedbackProcessor } from '../helpers/feedback-processor.js'; +import { MemoryProcessor } from '../helpers/memory-processor.js'; +import { ConnectionsMap } from '../types/index.js'; + +// Shared connections map (used by graph nodes) +export const connections: ConnectionsMap = {}; + +// Connection managers per WebSocket +export const connectionManagers = new Map(); +export const flashcardProcessors = new Map(); +export const feedbackProcessors = new Map(); +export const memoryProcessors = new Map(); +export const connectionAttributes = new Map< + string, + { timezone?: string; userId?: string; languageCode?: string } +>(); + +// Shutdown flag +let _isShuttingDown = false; + +export function isShuttingDown(): boolean { + return _isShuttingDown; +} + +export function setShuttingDown(value: boolean): void { + _isShuttingDown = value; +} diff --git a/backend/src/services/telemetry.ts b/backend/src/services/telemetry.ts new file mode 100644 index 0000000..b7f1018 --- /dev/null +++ b/backend/src/services/telemetry.ts @@ -0,0 +1,36 @@ +/** + * Telemetry Service + * + * Initializes and configures Inworld telemetry. + */ + +import { telemetry } from '@inworld/runtime'; +import { MetricType } from '@inworld/runtime/telemetry'; +import { serverConfig } from '../config/server.js'; +import { serverLogger as logger } from '../utils/logger.js'; + +export function initTelemetry(): void { + try { + const telemetryApiKey = process.env.INWORLD_API_KEY; + if (telemetryApiKey) { + telemetry.init({ + apiKey: telemetryApiKey, + appName: serverConfig.telemetry.appName, + appVersion: serverConfig.telemetry.appVersion, + }); + logger.debug('telemetry_initialized'); + logger.debug(`appName: ${serverConfig.telemetry.appName}`); + + telemetry.configureMetric({ + metricType: MetricType.CounterUInt, + name: 'flashcard_clicks_total', + description: 'Total flashcard clicks', + unit: 'clicks', + }); + } else { + logger.warn('telemetry_disabled_no_api_key'); + } + } catch (error) { + logger.error({ err: error }, 'telemetry_init_failed'); + } +} diff --git a/backend/src/services/websocket-handler.ts b/backend/src/services/websocket-handler.ts new file mode 100644 index 0000000..3034a1d --- /dev/null +++ b/backend/src/services/websocket-handler.ts @@ -0,0 +1,676 @@ +/** + * WebSocket Handler + * + * Manages WebSocket connections and message processing. + */ + +import { WebSocketServer, WebSocket } from 'ws'; +import { telemetry } from '@inworld/runtime'; +import { GraphTypes } from '@inworld/runtime/graph'; + +import { ConnectionManager } from '../helpers/connection-manager.js'; +import { FlashcardProcessor } from '../helpers/flashcard-processor.js'; +import { FeedbackProcessor } from '../helpers/feedback-processor.js'; +import { MemoryProcessor } from '../helpers/memory-processor.js'; +import { + getSupportedLanguageCodes, + DEFAULT_LANGUAGE_CODE, +} from '../config/languages.js'; +import { serverLogger as logger } from '../utils/logger.js'; +import { getSimpleTTSGraph } from '../graphs/simple-tts-graph.js'; +import { serverConfig } from '../config/server.js'; + +import { + connections, + connectionManagers, + flashcardProcessors, + feedbackProcessors, + memoryProcessors, + connectionAttributes, + isShuttingDown, +} from './state.js'; +import { getGraphWrapper } from './graph-service.js'; + +export function setupWebSocketHandlers(wss: WebSocketServer): void { + wss.on('connection', async (ws: WebSocket) => { + const graphWrapper = getGraphWrapper(); + if (!graphWrapper) { + logger.error('graph_not_initialized_rejecting_connection'); + ws.close(1011, 'Server not ready'); + return; + } + + const connectionId = `conn_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; + logger.info({ connectionId }, 'websocket_connected'); + + // Default language is Spanish, but can be changed via conversation_switch + const languageCode = DEFAULT_LANGUAGE_CODE; + + // Create connection manager (replaces AudioProcessor) + const connectionManager = new ConnectionManager( + connectionId, + ws, + graphWrapper, + connections, + languageCode + ); + + // Create flashcard processor + const flashcardProcessor = new FlashcardProcessor(languageCode); + + // Create feedback processor + const feedbackProcessor = new FeedbackProcessor(languageCode); + + // Create memory processor + const memoryProcessor = new MemoryProcessor(languageCode); + + // Store processors + connectionManagers.set(connectionId, connectionManager); + flashcardProcessors.set(connectionId, flashcardProcessor); + feedbackProcessors.set(connectionId, feedbackProcessor); + memoryProcessors.set(connectionId, memoryProcessor); + connectionAttributes.set(connectionId, { + languageCode: languageCode, + }); + + // Set up flashcard generation callback + connectionManager.setFlashcardCallback(async (messages) => { + if (isShuttingDown()) { + logger.debug( + { connectionId }, + 'skipping_flashcard_generation_shutting_down' + ); + return; + } + + try { + const attrs = connectionAttributes.get(connectionId) || {}; + const userAttributes: Record = { + timezone: attrs.timezone || '', + }; + + const targetingKey = attrs.userId || connectionId; + const userContext = { + attributes: userAttributes, + targetingKey, + }; + + const flashcards = await flashcardProcessor.generateFlashcards( + messages, + 1, + userContext + ); + if (flashcards.length > 0) { + const conversationId = connectionManager.getConversationId(); + ws.send( + JSON.stringify({ + type: 'flashcards_generated', + flashcards, + conversationId: conversationId || null, + }) + ); + } + } catch (error) { + if (!isShuttingDown()) { + logger.error( + { err: error, connectionId }, + 'flashcard_generation_error' + ); + } + } + }); + + // Set up feedback generation callback + connectionManager.setFeedbackCallback( + async (messages, currentTranscript) => { + if (isShuttingDown()) { + logger.debug( + { connectionId }, + 'skipping_feedback_generation_shutting_down' + ); + return; + } + + try { + const attrs = connectionAttributes.get(connectionId) || {}; + const userAttributes: Record = { + timezone: attrs.timezone || '', + }; + + const targetingKey = attrs.userId || connectionId; + const userContext = { + attributes: userAttributes, + targetingKey, + }; + + const feedback = await feedbackProcessor.generateFeedback( + messages, + currentTranscript, + userContext + ); + + if (feedback) { + const conversationId = connectionManager.getConversationId(); + ws.send( + JSON.stringify({ + type: 'feedback_generated', + messageContent: currentTranscript, + feedback, + conversationId: conversationId || null, + }) + ); + } + } catch (error) { + if (!isShuttingDown()) { + logger.error( + { err: error, connectionId }, + 'feedback_generation_error' + ); + } + } + } + ); + + // Set up memory generation callback + connectionManager.setMemoryCallback(async (messages) => { + if (isShuttingDown()) { + return; + } + + const attrs = connectionAttributes.get(connectionId) || {}; + const userId = attrs.userId; + + if (!userId) { + // Can't create memories without a user ID + return; + } + + // Increment turn and check if we should create a memory + memoryProcessor.incrementTurn(); + + if (memoryProcessor.shouldCreateMemory()) { + // Wait for memory creation to complete + await memoryProcessor.createMemoryAsync(userId, messages); + } + }); + + // Start the graph for this connection + try { + await connectionManager.start(); + logger.info({ connectionId }, 'graph_started'); + } catch (error) { + logger.error({ err: error, connectionId }, 'graph_start_failed'); + ws.close(1011, 'Failed to start audio processing'); + return; + } + + // Handle incoming messages + ws.on('message', (data) => { + handleMessage(connectionId, ws, connectionManager, data); + }); + + ws.on('error', (error) => { + logger.error({ err: error, connectionId }, 'websocket_error'); + }); + + ws.on('close', async () => { + logger.info({ connectionId }, 'websocket_closed'); + + // Clean up connection manager + const manager = connectionManagers.get(connectionId); + if (manager) { + try { + await manager.destroy(); + } catch (error) { + logger.error( + { err: error, connectionId }, + 'connection_manager_destroy_error' + ); + } + connectionManagers.delete(connectionId); + } + + // Clean up other processors + flashcardProcessors.delete(connectionId); + feedbackProcessors.delete(connectionId); + memoryProcessors.delete(connectionId); + connectionAttributes.delete(connectionId); + }); + }); +} + +function handleMessage( + connectionId: string, + ws: WebSocket, + connectionManager: ConnectionManager, + data: Buffer | ArrayBuffer | Buffer[] +): void { + try { + const message = JSON.parse(data.toString()); + + if (message.type === 'audio_chunk' && message.audio_data) { + // Process audio chunk + connectionManager.addAudioChunk(message.audio_data); + } else if (message.type === 'reset_flashcards') { + const processor = flashcardProcessors.get(connectionId); + if (processor) { + processor.reset(); + } + } else if (message.type === 'conversation_context_reset') { + // Reset backend state when switching conversations + connectionManager.reset(); + flashcardProcessors.get(connectionId)?.reset(); + logger.info({ connectionId }, 'conversation_context_reset'); + } else if (message.type === 'conversation_update') { + handleConversationUpdate(connectionId, connectionManager, message); + } else if (message.type === 'conversation_switch') { + handleConversationSwitch(connectionId, ws, connectionManager, message); + } else if (message.type === 'user_context') { + handleUserContext(connectionId, message); + } else if (message.type === 'flashcard_clicked') { + handleFlashcardClicked(connectionId, message); + } else if (message.type === 'text_message') { + handleTextMessage(connectionId, ws, connectionManager, message); + } else if (message.type === 'tts_pronounce_request') { + handleTTSPronounce(connectionId, ws, message); + } else { + logger.debug( + { connectionId, messageType: message.type }, + 'received_message' + ); + } + } catch (error) { + logger.error({ err: error, connectionId }, 'message_processing_error'); + } +} + +function handleConversationUpdate( + connectionId: string, + connectionManager: ConnectionManager, + message: { + data?: { + messages?: Array<{ role: string; content: string; timestamp?: string }>; + }; + messages?: Array<{ role: string; content: string; timestamp?: string }>; + } +): void { + // Handle both formats: { data: { messages: [...] } } and { messages: [...] } + const messages = + message.messages || message.data?.messages || (message.data as any)?.messages; + + if (!messages || !Array.isArray(messages)) { + logger.debug( + { connectionId, hasData: !!message.data, hasMessages: !!message.messages }, + 'conversation_update_missing_or_invalid_messages' + ); + return; + } + + logger.info( + { connectionId, messageCount: messages.length }, + 'loading_conversation_history' + ); + + try { + connectionManager.loadConversationHistory(messages); + logger.info( + { connectionId, messageCount: messages.length }, + 'conversation_history_loaded' + ); + } catch (error) { + logger.error( + { err: error, connectionId }, + 'failed_to_load_conversation_history' + ); + } +} + +async function handleConversationSwitch( + connectionId: string, + ws: WebSocket, + connectionManager: ConnectionManager, + message: { + conversationId?: string; + languageCode?: string; + messages?: Array<{ role: string; content: string; timestamp?: string }>; + data?: { + conversationId?: string; + languageCode?: string; + messages?: Array<{ role: string; content: string; timestamp?: string }>; + }; + } +): Promise { + const conversationId = + message.conversationId || message.data?.conversationId; + const requestedLanguageCode = message.languageCode || message.data?.languageCode; + const messages = message.messages || message.data?.messages; + + if (!conversationId || !requestedLanguageCode) { + logger.warn( + { connectionId, hasConversationId: !!conversationId, hasLanguageCode: !!requestedLanguageCode }, + 'conversation_switch_missing_required_fields' + ); + ws.send( + JSON.stringify({ + type: 'error', + message: 'Missing conversationId or languageCode', + timestamp: Date.now(), + }) + ); + return; + } + + if (!messages || !Array.isArray(messages)) { + logger.warn({ connectionId }, 'conversation_switch_missing_messages'); + ws.send( + JSON.stringify({ + type: 'error', + message: 'Missing messages array', + timestamp: Date.now(), + }) + ); + return; + } + + // Validate language code + const supportedCodes = getSupportedLanguageCodes(); + const languageCode = supportedCodes.includes(requestedLanguageCode) + ? requestedLanguageCode + : DEFAULT_LANGUAGE_CODE; + + if (requestedLanguageCode !== languageCode) { + logger.warn( + { + connectionId, + requestedCode: requestedLanguageCode, + fallback: languageCode, + }, + 'invalid_language_code_using_fallback' + ); + } + + logger.info( + { + connectionId, + conversationId, + languageCode, + messageCount: messages.length, + }, + 'conversation_switch_requested' + ); + + try { + // Switch conversation (waits for pending operations FIRST) + // This ensures any flashcard/feedback generation in progress uses the OLD language + await connectionManager.switchConversation( + conversationId, + languageCode, + messages + ); + + // Update processors with new language AFTER pending operations complete + // This ensures flashcard generation for the old conversation uses the old language + const flashcardProcessor = flashcardProcessors.get(connectionId); + const feedbackProcessor = feedbackProcessors.get(connectionId); + const memoryProcessor = memoryProcessors.get(connectionId); + + if (flashcardProcessor) { + flashcardProcessor.setLanguage(languageCode); + } + if (feedbackProcessor) { + feedbackProcessor.setLanguage(languageCode); + } + if (memoryProcessor) { + memoryProcessor.setLanguage(languageCode); + } + + // Send ready signal + ws.send( + JSON.stringify({ + type: 'conversation_ready', + conversationId, + languageCode, + timestamp: Date.now(), + }) + ); + + logger.info( + { connectionId, conversationId, languageCode }, + 'conversation_switch_complete' + ); + } catch (error) { + logger.error( + { err: error, connectionId, conversationId }, + 'conversation_switch_error' + ); + ws.send( + JSON.stringify({ + type: 'error', + message: 'Failed to switch conversation', + timestamp: Date.now(), + }) + ); + } +} + +function handleUserContext( + connectionId: string, + message: { + timezone?: string; + userId?: string | null; + languageCode?: string; + data?: { timezone?: string; userId?: string | null; languageCode?: string }; + } +): void { + const timezone = message.timezone || message.data?.timezone; + const userId = message.userId || message.data?.userId; + const languageCode = message.languageCode || message.data?.languageCode; + const currentAttrs = connectionAttributes.get(connectionId) || {}; + + // Validate language code + const supportedCodes = getSupportedLanguageCodes(); + const validatedLanguageCode = languageCode && supportedCodes.includes(languageCode) + ? languageCode + : currentAttrs.languageCode || DEFAULT_LANGUAGE_CODE; + + connectionAttributes.set(connectionId, { + ...currentAttrs, + timezone: timezone || currentAttrs.timezone, + userId: userId || currentAttrs.userId, + languageCode: validatedLanguageCode, + }); + + // Update connection manager and processors with the language + const manager = connectionManagers.get(connectionId); + if (manager && validatedLanguageCode !== currentAttrs.languageCode) { + manager.setLanguage(validatedLanguageCode); + + // Update processors with new language + const flashcardProcessor = flashcardProcessors.get(connectionId); + const feedbackProcessor = feedbackProcessors.get(connectionId); + const memoryProcessor = memoryProcessors.get(connectionId); + + if (flashcardProcessor) { + flashcardProcessor.setLanguage(validatedLanguageCode); + } + if (feedbackProcessor) { + feedbackProcessor.setLanguage(validatedLanguageCode); + } + if (memoryProcessor) { + memoryProcessor.setLanguage(validatedLanguageCode); + } + } + + // Set user ID on connection manager for memory retrieval + if (userId && manager) { + manager.setUserId(userId); + } +} + +function handleFlashcardClicked( + connectionId: string, + message: { + card?: { + id?: string; + targetWord?: string; + spanish?: string; + word?: string; + english?: string; + translation?: string; + }; + } +): void { + const card = message.card; + if (!card || typeof card !== 'object') { + logger.debug({ connectionId }, 'flashcard_clicked_missing_card_data'); + return; + } + try { + const attrs = connectionAttributes.get(connectionId) || {}; + telemetry.metric.recordCounterUInt('flashcard_clicks_total', 1, { + connectionId, + cardId: card.id || '', + targetWord: card.targetWord || card.spanish || card.word || '', + english: card.english || card.translation || '', + source: 'ui', + timezone: attrs.timezone || '', + languageCode: 'es', + }); + } catch (error) { + logger.error({ err: error, connectionId }, 'flashcard_click_record_error'); + } +} + +function handleTextMessage( + connectionId: string, + ws: WebSocket, + connectionManager: ConnectionManager, + message: { text?: string } +): void { + const text = message.text; + if (typeof text !== 'string' || text.trim().length === 0) { + logger.debug({ connectionId }, 'empty_or_invalid_text_message_ignored'); + return; + } + if (text.length > 200) { + logger.warn({ connectionId, length: text.length }, 'text_message_too_long'); + ws.send( + JSON.stringify({ + type: 'error', + message: 'Text message too long (max 200 chars)', + }) + ); + return; + } + connectionManager.sendTextMessage(text.trim()); +} + +/** + * Convert audio data to base64 string for WebSocket transmission + * Inworld TTS returns Float32 PCM in [-1.0, 1.0] range + */ +function convertAudioToBase64(audio: { + data?: string | number[] | Float32Array; + sampleRate?: number; +}): { base64: string; format: 'float32' | 'int16' } | null { + if (!audio.data) return null; + + if (typeof audio.data === 'string') { + // Already base64 - assume Int16 format for backwards compatibility + return { base64: audio.data, format: 'int16' }; + } + + // Inworld SDK returns audio.data as an array of raw bytes (0-255) + // These bytes ARE the Float32 PCM data in IEEE 754 format (4 bytes per sample) + const audioBuffer = Array.isArray(audio.data) + ? Buffer.from(audio.data) + : Buffer.from( + audio.data.buffer, + audio.data.byteOffset, + audio.data.byteLength + ); + + return { + base64: audioBuffer.toString('base64'), + format: 'float32', + }; +} + +async function handleTTSPronounce( + connectionId: string, + ws: WebSocket, + message: { text?: string; languageCode?: string } +): Promise { + const text = message.text; + + if (typeof text !== 'string' || text.trim().length === 0) { + ws.send( + JSON.stringify({ type: 'tts_pronounce_error', error: 'Empty text' }) + ); + return; + } + + if (text.length > 100) { + logger.warn( + { connectionId, length: text.length }, + 'tts_pronounce_text_too_long' + ); + ws.send( + JSON.stringify({ type: 'tts_pronounce_error', error: 'Text too long' }) + ); + return; + } + + try { + // Get language from connection manager (current conversation language) + const connectionManager = connectionManagers.get(connectionId); + const languageCode = connectionManager?.getLanguageCode() || message.languageCode || DEFAULT_LANGUAGE_CODE; + + logger.debug( + { connectionId, languageCode, textLength: text.length }, + 'tts_pronounce_starting' + ); + + const graph = getSimpleTTSGraph(languageCode); + const executionResult = await graph.start({ text: text.trim() }); + + for await (const res of executionResult.outputStream) { + if ('processResponse' in res) { + const resultWithProcess = res as { + processResponse: ( + handlers: Record Promise | void> + ) => Promise; + }; + await resultWithProcess.processResponse({ + TTSOutputStream: async (ttsData: unknown) => { + const ttsStream = ttsData as GraphTypes.TTSOutputStream; + for await (const chunk of ttsStream) { + if (chunk.audio?.data) { + const audioResult = convertAudioToBase64(chunk.audio); + if (audioResult) { + ws.send( + JSON.stringify({ + type: 'tts_pronounce_audio', + audio: audioResult.base64, + audioFormat: audioResult.format, + sampleRate: + chunk.audio.sampleRate || + serverConfig.audio.ttsSampleRate, + }) + ); + } + } + } + }, + }); + } + } + + ws.send(JSON.stringify({ type: 'tts_pronounce_complete' })); + } catch (error) { + logger.error({ err: error, connectionId }, 'tts_pronounce_error'); + ws.send( + JSON.stringify({ type: 'tts_pronounce_error', error: 'TTS failed' }) + ); + } +} diff --git a/backend/src/types/index.ts b/backend/src/types/index.ts new file mode 100644 index 0000000..4f43dda --- /dev/null +++ b/backend/src/types/index.ts @@ -0,0 +1,89 @@ +/** + * Types for the 0.9 long-running graph architecture + */ + +import { WebSocket } from 'ws'; +import type { MultimodalStreamManager } from '../helpers/multimodal-stream-manager.js'; +import type { GraphOutputStream } from '@inworld/runtime/graph'; + +/** + * Chat message in conversation history + */ +export interface ChatMessage { + id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + timestamp?: string; +} + +/** + * Connection state for a session + * This is the central state object that graph nodes read from + */ +export interface State { + interactionId: string; + messages: ChatMessage[]; + voiceId?: string; + // Language learning specific + userName: string; + targetLanguage: string; + languageCode: string; + // Output modalities (for graph routing) + output_modalities: ('text' | 'audio')[]; + // User ID for memory retrieval/creation (optional - anonymous users won't have this) + userId?: string; + // Conversation ID for routing responses to the correct conversation + conversationId?: string; +} + +/** + * Connection object for a WebSocket session + */ +export interface Connection { + ws: WebSocket; + state: State; + unloaded?: true; + multimodalStreamManager?: MultimodalStreamManager; + currentAudioGraphExecution?: Promise; + currentAudioExecutionStream?: GraphOutputStream; + onSpeechDetected?: (interactionId: string) => void; + onPartialTranscript?: (text: string, interactionId: string) => void; + // Utterance stitching support + pendingTranscript?: string; // Stores transcript from interrupted turn for stitching + isProcessingInterrupted?: boolean; // Flag to stop current LLM/TTS processing +} + +/** + * Map of session IDs to connections + */ +export type ConnectionsMap = { [sessionId: string]: Connection }; + +/** + * Text input passed between graph nodes + */ +export interface TextInput { + sessionId: string; + text: string; + interactionId: string; + voiceId?: string; +} + +/** + * Interaction info extracted from STT + */ +export interface InteractionInfo { + sessionId: string; + interactionId: string; + text: string; + interactionComplete: boolean; +} + +// Re-export memory types for convenience +export type { + MemoryType, + MemoryRecord, + MemoryMatch, + StateWithMemories, + SupabaseMemoryRow, +} from './memory.js'; +export { VALID_MEMORY_TYPES } from './memory.js'; diff --git a/backend/src/types/memory.ts b/backend/src/types/memory.ts new file mode 100644 index 0000000..382b22f --- /dev/null +++ b/backend/src/types/memory.ts @@ -0,0 +1,106 @@ +/** + * Memory Types for Language Learning App + * + * Defines interfaces for user memories - used for personalized conversations. + */ + +import type { State } from './index.js'; + +/** + * Type of memory content + * - learning_progress: Info about user's language learning (vocabulary, grammar, struggles) + * - personal_context: Personal details shared by user (interests, goals, preferences) + */ +export type MemoryType = 'learning_progress' | 'personal_context'; + +/** Valid memory types for runtime validation */ +export const VALID_MEMORY_TYPES: MemoryType[] = [ + 'learning_progress', + 'personal_context', +]; + +/** + * Memory record for storing in Supabase + */ +export interface MemoryRecord { + id?: string; + userId: string; + + // Memory content (English) + content: string; + + // Metadata + memoryType: MemoryType; + topics: string[]; + importance: number; // 0.0 to 1.0 + + // Embedding (1024 dimensions for BAAI/bge-large-en-v1.5) + embedding?: number[]; + + // Timestamps + createdAt?: string; + updatedAt?: string; +} + +/** + * Memory match returned from similarity search + */ +export interface MemoryMatch { + id: string; + content: string; + memoryType: MemoryType; + topics: string[]; + importance: number; + similarity: number; // Cosine similarity score (0.0 to 1.0) +} + +/** + * Raw LLM output when generating a memory + */ +export interface MemoryGenerationOutput { + memory: string; + type: MemoryType; + topics: string[]; + importance: number; +} + +/** + * Input for memory creation + */ +export interface MemoryCreationInput { + userId: string; + messages: Array<{ role: string; content: string }>; + languageCode: string; +} + +/** + * Input for memory retrieval + */ +export interface MemoryRetrievalInput { + userId: string; + queryText: string; + limit?: number; + threshold?: number; +} + +/** + * Extended state with relevant memories attached + * Used by MemoryRetrievalNode and DialogPromptBuilderNode + */ +export interface StateWithMemories extends State { + relevantMemories?: MemoryMatch[]; +} + +/** + * Raw row format from Supabase user_memories table + * Used for type-safe database row mapping + */ +export interface SupabaseMemoryRow { + id: string; + content: string; + memory_type: string; + topics: string[]; + importance: number; + similarity?: number; + created_at?: string; +} diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts new file mode 100644 index 0000000..3403cb8 --- /dev/null +++ b/backend/src/utils/logger.ts @@ -0,0 +1,58 @@ +/** + * Structured Logging with Pino + * + * Provides a centralized logging system with: + * - Log levels (trace, debug, info, warn, error, fatal) + * - Structured JSON output in production + * - Pretty-printed output in development + * - Child loggers with context (module, sessionId) + */ + +import pino from 'pino'; + +const isDevelopment = process.env.NODE_ENV !== 'production'; + +/** + * Root logger instance + * Configure LOG_LEVEL env var to change verbosity (default: 'info') + */ +export const logger = pino({ + level: process.env.LOG_LEVEL || 'info', + transport: isDevelopment + ? { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'SYS:standard', + ignore: 'pid,hostname', + }, + } + : undefined, +}); + +/** + * Create a child logger for a specific module + * @param module - Module name (e.g., 'Server', 'ConnectionManager') + */ +export function createLogger(module: string): pino.Logger { + return logger.child({ module }); +} + +/** + * Create a session-scoped logger with module and sessionId context + * @param module - Module name + * @param sessionId - Session/connection identifier + */ +export function createSessionLogger( + module: string, + sessionId: string +): pino.Logger { + return logger.child({ module, sessionId }); +} + +// Pre-created module loggers for common components +export const serverLogger = createLogger('Server'); +export const graphLogger = createLogger('Graph'); +export const connectionLogger = createLogger('ConnectionManager'); +export const flashcardLogger = createLogger('FlashcardProcessor'); +export const feedbackLogger = createLogger('FeedbackProcessor'); diff --git a/backend/tsconfig.eslint.json b/backend/tsconfig.eslint.json new file mode 100644 index 0000000..6b5a2b1 --- /dev/null +++ b/backend/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*", "vitest.config.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/tsconfig.json b/backend/tsconfig.json similarity index 78% rename from tsconfig.json rename to backend/tsconfig.json index beab951..ef82c5e 100644 --- a/tsconfig.json +++ b/backend/tsconfig.json @@ -6,7 +6,7 @@ "moduleResolution": "bundler", "lib": ["ES2022"], "outDir": "./dist", - "rootDir": "./", + "rootDir": "./src", "strict": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, @@ -22,13 +22,6 @@ "noFallthroughCasesInSwitch": true, "noEmit": false }, - "include": ["backend/**/*"], - "exclude": [ - "node_modules", - "dist", - "frontend", - "templates/**", - "**/*.test.ts", - "**/*.spec.ts" - ] + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/*.spec.ts"] } diff --git a/backend/vitest.config.ts b/backend/vitest.config.ts new file mode 100644 index 0000000..6b2ae2a --- /dev/null +++ b/backend/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + root: './', + include: ['src/**/*.test.ts'], + coverage: { + reporter: ['text', 'lcov'], + exclude: ['node_modules', '__tests__', 'graphs/nodes/**'], + }, + }, +}); diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..c987b94 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]); +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x'; +import reactDom from 'eslint-plugin-react-dom'; + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]); +``` diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..d89d64e --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,73 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import prettierConfig from 'eslint-config-prettier'; +import prettierPlugin from 'eslint-plugin-prettier'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import globals from 'globals'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export default [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + prettierConfig, + { + files: ['**/*.ts', '**/*.tsx'], + ...tseslint.configs.recommendedTypeChecked[0], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: __dirname, + }, + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + prettier: prettierPlugin, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + 'prettier/prettier': 'error', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, + { + files: ['**/context/**/*.tsx'], + rules: { + 'react-refresh/only-export-components': 'off', + }, + }, + { + files: ['public/**/*.js'], + languageOptions: { + globals: { + AudioWorkletProcessor: 'readonly', + registerProcessor: 'readonly', + }, + }, + }, + { + ignores: ['dist/**', 'node_modules/**', '*.config.js', '**/*.js', '!public'], + }, +]; + diff --git a/frontend/index.html b/frontend/index.html index e84a55b..09d608f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,93 +1,16 @@ - + - - - - Aprendemo - + + + + - - - -
-
-

Aprendemo

-
- - Connecting... -
-
-
- -
-
-
- -
-
-

Conversation

-
- - -
-
- -
-
-
-
-
- - -
-
-

Flashcards

- 0 cards -
- -
-
-
-

-
-
-
-
-
-
-
- - - - - - - - - - \ No newline at end of file + + Inworld Language Tutor + + +
+ + + diff --git a/frontend/js/audio-processor.js b/frontend/js/audio-processor.js deleted file mode 100644 index d4517f6..0000000 --- a/frontend/js/audio-processor.js +++ /dev/null @@ -1,59 +0,0 @@ -class AudioProcessor extends AudioWorkletProcessor { - constructor(options) { - super(); - this.sourceSampleRate = options.processorOptions.sourceSampleRate; - this.targetSampleRate = 16000; - this.resampleRatio = this.sourceSampleRate / this.targetSampleRate; - this.buffer = null; - } - - process(inputs) { - const inputChannel = inputs[0][0]; - if (!inputChannel) return true; - - const currentLength = this.buffer ? this.buffer.length : 0; - const newBuffer = new Float32Array(currentLength + inputChannel.length); - if (this.buffer) { - newBuffer.set(this.buffer, 0); - } - newBuffer.set(inputChannel, currentLength); - this.buffer = newBuffer; - - // Put stuff back into 16k - const numOutputSamples = Math.floor( - this.buffer.length / this.resampleRatio - ); - if (numOutputSamples === 0) return true; - - const resampledData = new Float32Array(numOutputSamples); - for (let i = 0; i < numOutputSamples; i++) { - const correspondingInputIndex = i * this.resampleRatio; - const lowerIndex = Math.floor(correspondingInputIndex); - const upperIndex = Math.ceil(correspondingInputIndex); - const interpolationFactor = correspondingInputIndex - lowerIndex; - - const lowerValue = this.buffer[lowerIndex] || 0; - const upperValue = this.buffer[upperIndex] || 0; - - resampledData[i] = - lowerValue + (upperValue - lowerValue) * interpolationFactor; - } - - const consumedInputSamples = numOutputSamples * this.resampleRatio; - this.buffer = this.buffer.slice(Math.round(consumedInputSamples)); - - // Convert Float32Array to Int16Array - const int16Array = new Int16Array(resampledData.length); - for (let i = 0; i < resampledData.length; i++) { - int16Array[i] = Math.max( - -32768, - Math.min(32767, resampledData[i] * 32768) - ); - } - - this.port.postMessage(int16Array.buffer, [int16Array.buffer]); - return true; - } -} - -registerProcessor('audio-processor', AudioProcessor); diff --git a/frontend/js/chat-ui.js b/frontend/js/chat-ui.js deleted file mode 100644 index 369621b..0000000 --- a/frontend/js/chat-ui.js +++ /dev/null @@ -1,301 +0,0 @@ -export class ChatUI { - constructor() { - this.messagesContainer = document.getElementById('messages'); - this.transcriptContainer = document.getElementById('currentTranscript'); - this.typewriterTimers = new Map(); // Track active typewriter effects - this.typewriterSpeed = 25; // milliseconds per character - this.llmTypewriterCallback = null; // Callback for when LLM typewriter completes - } - - render( - chatHistory, - currentTranscript, - currentLLMResponse, - pendingTranscription, - streamingLLMResponse, - isRecording, - speechDetected - ) { - this.renderMessages( - chatHistory, - pendingTranscription, - streamingLLMResponse, - currentTranscript, - isRecording, - speechDetected - ); - this.renderCurrentTranscript(currentTranscript); - } - - renderMessages( - messages, - pendingTranscription, - streamingLLMResponse, - currentTranscript, - isRecording, - speechDetected - ) { - // Only clear and rebuild if the conversation history changed - const currentHistoryLength = this.messagesContainer.querySelectorAll( - '.message:not(.streaming)' - ).length; - if (currentHistoryLength !== messages.length) { - this.messagesContainer.innerHTML = ''; - - // Render existing conversation history - messages.forEach((message) => { - const messageElement = this.createMessageElement(message); - this.messagesContainer.appendChild(messageElement); - }); - } - - // Handle real-time transcript updates (while recording and speech detected) - // Show immediately when VAD activates, even before transcript text arrives - const existingRealtimeTranscript = document.getElementById( - 'realtime-transcript' - ); - if (speechDetected && isRecording && !pendingTranscription) { - if (!existingRealtimeTranscript) { - const userMessage = this.createRealtimeTranscriptElement(); - userMessage.id = 'realtime-transcript'; - this.messagesContainer.appendChild(userMessage); - } - // Update the transcript in real-time (no typewriter effect) - // Show 3-dot loading animation if no text yet, otherwise show the actual transcript - const transcriptElement = document.getElementById('realtime-transcript'); - if (transcriptElement) { - const textNode = transcriptElement.querySelector('.transcript-text'); - const loadingDots = transcriptElement.querySelector('.loading-dots'); - if (currentTranscript) { - if (textNode) textNode.textContent = currentTranscript; - if (loadingDots) loadingDots.style.display = 'none'; - } else { - if (textNode) textNode.textContent = ''; - if (loadingDots) loadingDots.style.display = 'flex'; - } - this.scrollToBottom(); - } - } else if (existingRealtimeTranscript) { - existingRealtimeTranscript.remove(); - } - - // Handle pending user transcription (final transcription) - const existingUserStreaming = document.getElementById( - 'pending-transcription' - ); - if (pendingTranscription) { - // Remove real-time transcript if it exists - if (existingRealtimeTranscript) { - existingRealtimeTranscript.remove(); - } - if (!existingUserStreaming) { - const userMessage = this.createMessageElement({ - role: 'learner', - content: '', - }); - userMessage.classList.add('streaming'); - userMessage.id = 'pending-transcription'; - this.messagesContainer.appendChild(userMessage); - // Only start typewriter for new transcriptions - this.startTypewriter( - 'pending-transcription', - pendingTranscription, - this.typewriterSpeed * 0.8 - ); - } - // Don't restart typewriter if element already exists - } else if (existingUserStreaming) { - existingUserStreaming.remove(); - this.clearTypewriter('pending-transcription'); - } - - // Handle streaming LLM response - const existingLLMStreaming = document.getElementById( - 'streaming-llm-response' - ); - if (streamingLLMResponse) { - console.log( - '[ChatUI] Handling streaming LLM response:', - streamingLLMResponse - ); - if (!existingLLMStreaming) { - console.log( - '[ChatUI] Creating new streaming LLM element and starting typewriter' - ); - const assistantMessage = this.createStreamingMessageElement(''); - assistantMessage.id = 'streaming-llm-response'; - this.messagesContainer.appendChild(assistantMessage); - // Only start typewriter for new LLM responses - this.startTypewriter( - 'streaming-llm-response', - streamingLLMResponse, - this.typewriterSpeed, - this.llmTypewriterCallback - ); - } - // Don't restart typewriter if element already exists - } else if (existingLLMStreaming) { - console.log('[ChatUI] Removing existing LLM streaming element'); - // Clear typewriter first (this will call the completion callback) - this.clearTypewriter('streaming-llm-response'); - // Then remove the element - existingLLMStreaming.remove(); - } - - this.scrollToBottom(); - } - - createMessageElement(message) { - const div = document.createElement('div'); - div.className = `message ${message.role}`; - div.textContent = message.content; - return div; - } - - createStreamingMessageElement(content) { - const div = document.createElement('div'); - div.className = 'message teacher streaming'; - - // Create text node for content - const textNode = document.createTextNode(content); - div.appendChild(textNode); - - // Add typing indicator - const cursor = document.createElement('span'); - cursor.className = 'streaming-cursor'; - cursor.textContent = '▊'; - div.appendChild(cursor); - - return div; - } - - createRealtimeTranscriptElement() { - const div = document.createElement('div'); - div.className = 'message learner streaming realtime'; - - // Create container for text - const textNode = document.createElement('span'); - textNode.className = 'transcript-text'; - div.appendChild(textNode); - - // Add 3-dot loading animation - const loadingDots = document.createElement('span'); - loadingDots.className = 'loading-dots'; - loadingDots.innerHTML = ''; - div.appendChild(loadingDots); - - return div; - } - - renderCurrentTranscript(transcript) { - if (transcript) { - this.transcriptContainer.textContent = transcript; - } else { - this.transcriptContainer.textContent = ''; - } - } - - startTypewriter(elementId, fullText, speed, onComplete) { - const element = document.getElementById(elementId); - if (!element) { - // Element doesn't exist, call completion callback immediately if provided - if (onComplete) { - console.log( - `[Typewriter] Element ${elementId} not found, calling completion callback immediately` - ); - onComplete(); - } - return; - } - - // Clear any existing timer for this element - if (this.typewriterTimers.has(elementId)) { - const existingTimer = this.typewriterTimers.get(elementId); - clearInterval(existingTimer.timer || existingTimer); - this.typewriterTimers.delete(elementId); - - // If there was a previous timer with a callback, call it now since we're replacing it - if (existingTimer.onComplete) { - existingTimer.onComplete(); - } - } - - // Get the text content node (accounting for streaming cursor) - const textContent = element.querySelector('.streaming-cursor') - ? element.childNodes[0] - : element; - - // Start fresh - clear the content and start typing from beginning - textContent.textContent = ''; - let currentIndex = 0; - - console.log(`[Typewriter] Starting ${elementId}, target: "${fullText}"`); - - const timer = setInterval(() => { - // Check if element still exists - const currentElement = document.getElementById(elementId); - if (!currentElement) { - console.log( - `[Typewriter] Element ${elementId} removed, completing immediately` - ); - clearInterval(timer); - this.typewriterTimers.delete(elementId); - if (onComplete) { - onComplete(); - } - return; - } - - if (currentIndex < fullText.length) { - const newText = fullText.substring(0, currentIndex + 1); - textContent.textContent = newText; - currentIndex++; - this.scrollToBottom(); - } else { - console.log(`[Typewriter] Complete: ${elementId}`); - clearInterval(timer); - this.typewriterTimers.delete(elementId); - - // Call completion callback if provided - if (onComplete) { - onComplete(); - } - } - }, speed); - - this.typewriterTimers.set(elementId, { timer, fullText, onComplete }); - } - - clearTypewriter(elementId) { - if (this.typewriterTimers.has(elementId)) { - const timerData = this.typewriterTimers.get(elementId); - clearInterval(timerData.timer || timerData); - - // If there's a completion callback, call it before clearing - // This ensures the text gets finalized even if typewriter is interrupted - if (timerData.onComplete) { - console.log( - `[Typewriter] Clearing ${elementId}, calling completion callback` - ); - timerData.onComplete(); - } - - this.typewriterTimers.delete(elementId); - } - } - - clearAllTypewriters() { - this.typewriterTimers.forEach((timerData, elementId) => { - clearInterval(timerData.timer || timerData); - }); - this.typewriterTimers.clear(); - } - - setLLMTypewriterCallback(callback) { - this.llmTypewriterCallback = callback; - } - - scrollToBottom() { - this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight; - } -} diff --git a/frontend/js/flashcard-ui.js b/frontend/js/flashcard-ui.js deleted file mode 100644 index fa236a4..0000000 --- a/frontend/js/flashcard-ui.js +++ /dev/null @@ -1,163 +0,0 @@ -export class FlashcardUI { - constructor() { - this.flashcardsGrid = document.getElementById('flashcardsGrid'); - this.cardCount = document.getElementById('cardCount'); - this.flashcards = []; - } - - render(flashcards) { - this.flashcards = flashcards; - this.updateCardCount(flashcards.length); - this.renderFlashcards(flashcards); - } - - addFlashcards(newFlashcards) { - // Add new flashcards to the existing collection - this.flashcards = [...this.flashcards, ...newFlashcards]; - this.render(this.flashcards); - } - - updateCardCount(count) { - if (count >= 1) { - this.cardCount.textContent = `Export ${count} card${count !== 1 ? 's' : ''} to Anki`; - this.cardCount.style.cursor = 'pointer'; - this.cardCount.style.color = '#666'; - this.cardCount.style.textDecoration = 'underline'; - this.cardCount.onclick = () => this.exportToAnki(); - } else { - this.cardCount.textContent = `${count} card${count !== 1 ? 's' : ''}`; - this.cardCount.style.cursor = 'default'; - this.cardCount.style.color = 'inherit'; - this.cardCount.style.textDecoration = 'none'; - this.cardCount.onclick = null; - } - } - - renderFlashcards(flashcards) { - this.flashcardsGrid.innerHTML = ''; - - if (flashcards.length === 0) { - this.renderEmptyState(); - return; - } - - // Show only the latest flashcards, scroll to see older ones - const sortedFlashcards = [...flashcards].sort((a, b) => { - const timeA = new Date(a.timestamp || 0).getTime(); - const timeB = new Date(b.timestamp || 0).getTime(); - return timeB - timeA; // Most recent first - }); - - sortedFlashcards.forEach((flashcard) => { - const cardElement = this.createFlashcardElement(flashcard); - this.flashcardsGrid.appendChild(cardElement); - }); - } - - renderEmptyState() { - const emptyState = document.createElement('div'); - emptyState.className = 'empty-state'; - emptyState.innerHTML = ''; - this.flashcardsGrid.appendChild(emptyState); - } - - createFlashcardElement(flashcard) { - const card = document.createElement('div'); - card.className = 'flashcard'; - - card.innerHTML = ` -
-
-
${this.escapeHtml(flashcard.spanish || flashcard.word || '')}
-
-
-
${this.escapeHtml(flashcard.english || flashcard.translation || '')}
-
${this.escapeHtml(flashcard.example || flashcard.example_sentence || '')}
-
- Remember: - ${this.escapeHtml(flashcard.mnemonic || '')} -
-
-
- `; - - card.addEventListener('click', () => { - this.flipCard(card); - if (typeof this.onCardClick === 'function') { - this.onCardClick(flashcard); - } - }); - - return card; - } - - flipCard(cardElement) { - cardElement.classList.toggle('flipped'); - } - - escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - async exportToAnki() { - try { - // Filter out invalid flashcards - const validFlashcards = this.flashcards.filter( - (flashcard) => - flashcard.spanish && - flashcard.english && - flashcard.spanish.trim() !== '' && - flashcard.english.trim() !== '' - ); - - if (validFlashcards.length === 0) { - alert('No valid flashcards to export'); - return; - } - - // Show loading state - const originalText = this.cardCount.textContent; - this.cardCount.textContent = 'Exporting...'; - this.cardCount.style.cursor = 'wait'; - - const response = await fetch('/api/export-anki', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - flashcards: validFlashcards, - deckName: 'Aprendemo Spanish Cards', - }), - }); - - if (!response.ok) { - throw new Error('Export failed'); - } - - // Create download link - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'aprendemo_cards.apkg'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - - console.log('ANKI deck exported successfully!'); - - // Restore original state - this.updateCardCount(this.flashcards.length); - } catch (error) { - console.error('Error exporting to ANKI:', error); - alert('Failed to export flashcards to ANKI'); - - // Restore original state - this.updateCardCount(this.flashcards.length); - } - } -} diff --git a/frontend/js/ios-audio-workarounds.js b/frontend/js/ios-audio-workarounds.js deleted file mode 100644 index e195b9c..0000000 --- a/frontend/js/ios-audio-workarounds.js +++ /dev/null @@ -1,578 +0,0 @@ -/** - * iOS Audio Workarounds for WebSocket Audio Streaming - * Handles iOS-specific audio limitations and provides fallback solutions - */ - -class IOSAudioHandler { - constructor() { - this.isIOS = this.detectIOS(); - this.audioContext = null; - this.audioUnlocked = false; - this.microphoneStream = null; - this.audioProcessor = null; - this.audioChunks = []; - this.currentAudioUrl = null; - this.audioQueue = []; - this.isPlaying = false; - - // iOS-specific audio constraints - this.audioConstraints = { - audio: { - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - sampleRate: 16000, - channelCount: 1, - // iOS-specific constraints - latency: 0.02, - sampleSize: 16, - volume: 1.0, - }, - }; - - if (this.isIOS) { - console.log('[iOS Audio] iOS device detected, initializing workarounds'); - this.initializeIOSWorkarounds(); - } - } - - detectIOS() { - const userAgent = navigator.userAgent || navigator.vendor || window.opera; - - // Check for iOS devices - if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { - return true; - } - - // Check for iPad on iOS 13+ (reports as Mac) - if (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) { - return true; - } - - // Additional check for iOS Safari - const isIOSSafari = - !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/) && - /iPad|iPhone|iPod/.test(navigator.userAgent); - - return isIOSSafari; - } - - initializeIOSWorkarounds() { - // Add touch event listeners for audio unlock - this.setupTouchHandlers(); - - // Setup visibility change handler to resume audio - this.setupVisibilityHandler(); - - // Add iOS-specific meta tags if not present - this.addIOSMetaTags(); - - // Prevent iOS zoom on double-tap - this.preventDoubleTapZoom(); - } - - setupTouchHandlers() { - // Unlock audio on first user interaction - const unlockAudio = async () => { - if (!this.audioUnlocked) { - await this.unlockAudioContext(); - this.audioUnlocked = true; - console.log('[iOS Audio] Audio unlocked via user interaction'); - } - }; - - // Add multiple event types to catch user interaction - ['touchstart', 'touchend', 'click'].forEach((eventType) => { - document.addEventListener(eventType, unlockAudio, { - once: true, - passive: true, - }); - }); - } - - setupVisibilityHandler() { - // Resume audio context when app becomes visible - document.addEventListener('visibilitychange', async () => { - if (!document.hidden && this.audioContext) { - if (this.audioContext.state === 'suspended') { - await this.audioContext.resume(); - console.log( - '[iOS Audio] Resumed audio context after visibility change' - ); - } - } - }); - } - - addIOSMetaTags() { - // Ensure proper viewport settings for iOS - let viewport = document.querySelector('meta[name="viewport"]'); - if (!viewport) { - viewport = document.createElement('meta'); - viewport.name = 'viewport'; - document.head.appendChild(viewport); - } - viewport.content = - 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'; - - // Add iOS web app capable meta tag - if (!document.querySelector('meta[name="apple-mobile-web-app-capable"]')) { - const webAppMeta = document.createElement('meta'); - webAppMeta.name = 'apple-mobile-web-app-capable'; - webAppMeta.content = 'yes'; - document.head.appendChild(webAppMeta); - } - } - - preventDoubleTapZoom() { - let lastTouchEnd = 0; - document.addEventListener( - 'touchend', - (event) => { - const now = Date.now(); - if (now - lastTouchEnd <= 300) { - event.preventDefault(); - } - lastTouchEnd = now; - }, - false - ); - } - - async unlockAudioContext() { - try { - // Create or resume audio context - if (!this.audioContext) { - this.audioContext = new (window.AudioContext || - window.webkitAudioContext)({ - sampleRate: 16000, - latencyHint: 'interactive', - }); - } - - if (this.audioContext.state === 'suspended') { - await this.audioContext.resume(); - } - - // Play a silent buffer to unlock audio - const buffer = this.audioContext.createBuffer(1, 1, 22050); - const source = this.audioContext.createBufferSource(); - source.buffer = buffer; - source.connect(this.audioContext.destination); - source.start(0); - - console.log( - '[iOS Audio] AudioContext unlocked, state:', - this.audioContext.state - ); - - // Dispatch custom event to notify app - window.dispatchEvent( - new CustomEvent('ios-audio-unlocked', { - detail: { audioContext: this.audioContext }, - }) - ); - - return this.audioContext; - } catch (error) { - console.error('[iOS Audio] Failed to unlock audio context:', error); - throw error; - } - } - - async startMicrophone(onAudioData) { - try { - console.log('[iOS Audio] Starting microphone...'); - - // Ensure audio context is ready - if (!this.audioContext || this.audioContext.state === 'suspended') { - await this.unlockAudioContext(); - } - - // Request microphone permission with iOS-optimized constraints - this.microphoneStream = await navigator.mediaDevices.getUserMedia( - this.audioConstraints - ); - console.log('[iOS Audio] Microphone access granted'); - - // Create audio processing pipeline - const source = this.audioContext.createMediaStreamSource( - this.microphoneStream - ); - - // Use ScriptProcessorNode for iOS compatibility (AudioWorklet may not work) - const bufferSize = 4096; - this.audioProcessor = this.audioContext.createScriptProcessor( - bufferSize, - 1, - 1 - ); - - this.audioProcessor.onaudioprocess = (event) => { - const inputData = event.inputBuffer.getChannelData(0); - - // Convert Float32Array to Int16Array - const int16Array = new Int16Array(inputData.length); - for (let i = 0; i < inputData.length; i++) { - const s = Math.max(-1, Math.min(1, inputData[i])); - int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7fff; - } - - // Convert to base64 - const base64Audio = this.arrayBufferToBase64(int16Array.buffer); - - // Call the callback with audio data - if (onAudioData) { - onAudioData(base64Audio); - } - }; - - // Connect the pipeline - source.connect(this.audioProcessor); - this.audioProcessor.connect(this.audioContext.destination); - - console.log('[iOS Audio] Microphone pipeline connected'); - return true; - } catch (error) { - console.error('[iOS Audio] Failed to start microphone:', error); - - // Provide user-friendly error message - if (error.name === 'NotAllowedError') { - alert( - 'Microphone access denied. Please enable microphone permissions in Settings > Safari > Microphone.' - ); - } else if (error.name === 'NotFoundError') { - alert( - 'No microphone found. Please ensure your device has a working microphone.' - ); - } else { - alert(`Microphone error: ${error.message}`); - } - - throw error; - } - } - - stopMicrophone() { - console.log('[iOS Audio] Stopping microphone...'); - - if (this.audioProcessor) { - this.audioProcessor.disconnect(); - this.audioProcessor = null; - } - - if (this.microphoneStream) { - this.microphoneStream.getTracks().forEach((track) => track.stop()); - this.microphoneStream = null; - } - - console.log('[iOS Audio] Microphone stopped'); - } - - // PCM to WAV conversion for iOS - createWAVFromPCM(pcmData, sampleRate = 16000) { - // PCM data is Int16Array - const numChannels = 1; - const bitsPerSample = 16; - const dataLength = pcmData.byteLength; - - // Create WAV header - const headerLength = 44; - const fileLength = headerLength + dataLength; - const arrayBuffer = new ArrayBuffer(fileLength); - const view = new DataView(arrayBuffer); - - // RIFF header - const writeString = (offset, string) => { - for (let i = 0; i < string.length; i++) { - view.setUint8(offset + i, string.charCodeAt(i)); - } - }; - - writeString(0, 'RIFF'); - view.setUint32(4, fileLength - 8, true); // File size - 8 - writeString(8, 'WAVE'); - - // fmt chunk - writeString(12, 'fmt '); - view.setUint32(16, 16, true); // fmt chunk size - view.setUint16(20, 1, true); // Audio format (1 = PCM) - view.setUint16(22, numChannels, true); - view.setUint32(24, sampleRate, true); - view.setUint32(28, (sampleRate * numChannels * bitsPerSample) / 8, true); // Byte rate - view.setUint16(32, (numChannels * bitsPerSample) / 8, true); // Block align - view.setUint16(34, bitsPerSample, true); - - // data chunk - writeString(36, 'data'); - view.setUint32(40, dataLength, true); - - // Copy PCM data - const dataOffset = 44; - const dataView = new Uint8Array(arrayBuffer, dataOffset); - dataView.set(new Uint8Array(pcmData.buffer || pcmData)); - - return arrayBuffer; - } - - // Audio playback methods for iOS - async playAudioChunk(base64Audio, isLastChunk = false) { - try { - // Add chunk to queue - this.audioChunks.push(base64Audio); - - // If this is the last chunk or we have enough chunks, create and play audio - if (isLastChunk || this.audioChunks.length > 10) { - await this.playAccumulatedAudio(); - } - } catch (error) { - console.error('[iOS Audio] Failed to play audio chunk:', error); - } - } - - async playAccumulatedAudio() { - if (this.audioChunks.length === 0) return; - - try { - console.log( - '[iOS Audio] Processing accumulated audio chunks:', - this.audioChunks.length - ); - - // Decode and combine all base64 chunks into one Int16Array - let totalLength = 0; - const decodedChunks = this.audioChunks.map((base64) => { - const binaryString = atob(base64); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - totalLength += bytes.length; - return bytes; - }); - - // Combine all chunks - const combinedData = new Uint8Array(totalLength); - let offset = 0; - for (const chunk of decodedChunks) { - combinedData.set(chunk, offset); - offset += chunk.length; - } - - // Clear chunks - this.audioChunks = []; - - // Convert PCM to WAV - const wavData = this.createWAVFromPCM(combinedData, 16000); - - // Create blob with WAV data - const audioBlob = new Blob([wavData], { type: 'audio/wav' }); - const audioUrl = URL.createObjectURL(audioBlob); - - // Clean up previous audio URL - if (this.currentAudioUrl) { - URL.revokeObjectURL(this.currentAudioUrl); - } - this.currentAudioUrl = audioUrl; - - // Get audio element - use the one we added to HTML - const audioElement = - document.getElementById('iosAudioElement') || - document.querySelector('audio'); - - if (!audioElement) { - console.error('[iOS Audio] No audio element found!'); - return; - } - - // iOS-specific audio element setup - audioElement.setAttribute('playsinline', ''); - audioElement.setAttribute('webkit-playsinline', ''); - audioElement.preload = 'auto'; - - // Add to queue for sequential playback - this.audioQueue.push(audioUrl); - - // Start playback if not already playing - if (!this.isPlaying) { - await this.playNextInQueue(audioElement); - } - } catch (error) { - console.error('[iOS Audio] Failed to play accumulated audio:', error); - } - } - - async playNextInQueue(audioElement) { - if (this.audioQueue.length === 0) { - this.isPlaying = false; - window.dispatchEvent(new CustomEvent('ios-audio-ended')); - return; - } - - this.isPlaying = true; - const audioUrl = this.audioQueue.shift(); - - return new Promise((resolve) => { - audioElement.src = audioUrl; - - audioElement.onended = async () => { - URL.revokeObjectURL(audioUrl); - await this.playNextInQueue(audioElement); - resolve(); - }; - - audioElement.onerror = (error) => { - console.error('[iOS Audio] Playback error:', error); - URL.revokeObjectURL(audioUrl); - this.playNextInQueue(audioElement); - resolve(); - }; - - // Attempt to play with user gesture handling - const playPromise = audioElement.play(); - - if (playPromise !== undefined) { - playPromise - .then(() => { - console.log('[iOS Audio] Playback started successfully'); - }) - .catch(async (error) => { - console.error('[iOS Audio] Playback failed:', error); - - // If playback fails due to user interaction requirement - if (error.name === 'NotAllowedError') { - // Wait for user interaction - console.log( - '[iOS Audio] Waiting for user interaction to play audio...' - ); - - const playOnInteraction = async () => { - try { - await audioElement.play(); - console.log( - '[iOS Audio] Playback started after user interaction' - ); - document.removeEventListener('touchstart', playOnInteraction); - document.removeEventListener('click', playOnInteraction); - } catch (e) { - console.error('[iOS Audio] Still cannot play:', e); - } - }; - - document.addEventListener('touchstart', playOnInteraction, { - once: true, - }); - document.addEventListener('click', playOnInteraction, { - once: true, - }); - } - }); - } - }); - } - - stopAudioPlayback() { - console.log('[iOS Audio] Stopping audio playback...'); - - // Clear audio queue - this.audioQueue = []; - this.audioChunks = []; - this.isPlaying = false; - - // Stop current audio - const audioElement = document.querySelector('audio'); - if (audioElement) { - audioElement.pause(); - audioElement.src = ''; - } - - // Clean up audio URLs - if (this.currentAudioUrl) { - URL.revokeObjectURL(this.currentAudioUrl); - this.currentAudioUrl = null; - } - } - - // Utility methods - arrayBufferToBase64(buffer) { - const bytes = new Uint8Array(buffer); - let binary = ''; - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); - } - - base64ToArrayBuffer(base64) { - const binaryString = atob(base64); - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes.buffer; - } - - // WebSocket connection helper for iOS - getOptimizedWebSocketURL() { - const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; - const hostname = location.hostname; - const port = location.port || (protocol === 'wss:' ? '443' : '80'); - - // For production - if (hostname !== 'localhost' && hostname !== '127.0.0.1') { - return `${protocol}//${hostname}:${port}`; - } - - // For local development with iOS device - // Try to use the computer's local IP address - if (this.isIOS && hostname === 'localhost') { - // You'll need to replace this with your computer's local IP - // Or implement auto-discovery - console.warn( - '[iOS Audio] Using localhost, consider using computer IP address for iOS testing' - ); - } - - return `${protocol}//${hostname}:8765`; - } - - // Enhanced error handling for iOS - handleIOSError(error, context) { - console.error(`[iOS Audio] Error in ${context}:`, error); - - const errorMessages = { - NotAllowedError: - 'Permission denied. Please allow microphone/audio access.', - NotFoundError: 'Required audio hardware not found.', - NotReadableError: 'Audio hardware is in use by another application.', - OverconstrainedError: 'Audio constraints cannot be satisfied.', - SecurityError: 'Audio access blocked due to security settings.', - TypeError: 'Invalid audio configuration.', - }; - - const message = - errorMessages[error.name] || `Audio error: ${error.message}`; - - // Dispatch error event for app to handle - window.dispatchEvent( - new CustomEvent('ios-audio-error', { - detail: { error, context, message }, - }) - ); - - return message; - } -} - -// Export for use in main application -window.IOSAudioHandler = IOSAudioHandler; - -// Auto-initialize if on iOS -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { - window.iosAudioHandler = new IOSAudioHandler(); - }); -} else { - window.iosAudioHandler = new IOSAudioHandler(); -} diff --git a/frontend/js/main.js b/frontend/js/main.js deleted file mode 100644 index 0b60c4f..0000000 --- a/frontend/js/main.js +++ /dev/null @@ -1,784 +0,0 @@ -import { WebSocketClient } from './websocket-client.js'; -import { AudioHandler } from './audio-handler.js'; -import { AudioPlayer } from './audio-player.js'; -import { ChatUI } from './chat-ui.js'; -import { FlashcardUI } from './flashcard-ui.js'; -import { Storage } from './storage.js'; - -class App { - constructor() { - this.storage = new Storage(); - this.wsClient = new WebSocketClient('ws://localhost:3000'); - this.audioHandler = new AudioHandler(); - this.audioPlayer = new AudioPlayer(); - this.chatUI = new ChatUI(); - this.flashcardUI = new FlashcardUI(); - this.userId = this.getOrCreateUserId(); - this.currentAudioElement = null; // Track current Audio element to prevent simultaneous playback - this.flashcardUI.onCardClick = (card) => { - this.wsClient.send({ type: 'flashcard_clicked', card }); - }; - - this.state = { - chatHistory: [], - flashcards: [], - isRecording: false, - connectionStatus: 'connecting', - currentTranscript: '', - currentLLMResponse: '', - pendingTranscription: null, - pendingLLMResponse: null, - streamingLLMResponse: '', - lastPendingTranscription: null, - speechDetected: false, - llmResponseComplete: false, // Track if LLM response is complete to prevent chunk accumulation - currentResponseId: null, // Track current response to match audio with text - }; - - this.init(); - } - - async init() { - this.loadState(); - this.setupEventListeners(); - await this.connectWebSocket(); - await this.initializeAudioPlayer(); - this.render(); - } - - async initializeAudioPlayer() { - try { - await this.audioPlayer.initialize(); - console.log('Audio player initialized'); - } catch (error) { - console.error('Failed to initialize audio player:', error); - } - } - - loadState() { - const savedState = this.storage.getState(); - if (savedState) { - this.state.chatHistory = savedState.chatHistory || []; - } - - // Load flashcards from storage - this.state.flashcards = this.storage.getFlashcards(); - - // Load existing conversation history - const existingConversation = this.storage.getConversationHistory(); - console.log( - 'Loading existing conversation history:', - existingConversation.messages.length, - 'messages' - ); - console.log( - 'Loading existing flashcards:', - this.state.flashcards.length, - 'flashcards' - ); - } - - saveState() { - this.storage.saveState({ - chatHistory: this.state.chatHistory, - }); - // Flashcards are saved separately through storage.addFlashcards() - } - - setupEventListeners() { - const micButton = document.getElementById('micButton'); - const restartButton = document.getElementById('restartButton'); - - // Check for iOS - const isIOS = - /iPad|iPhone|iPod/.test(navigator.userAgent) || - (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); - - // Add both click and touch events for better iOS support - micButton.addEventListener('click', (e) => { - e.preventDefault(); - this.toggleStreaming(); - }); - - if (isIOS) { - // Add touch event for iOS - micButton.addEventListener( - 'touchend', - (e) => { - e.preventDefault(); - this.toggleStreaming(); - }, - { passive: false } - ); - - // Prevent double-tap zoom on mic button - let lastTouchEnd = 0; - micButton.addEventListener( - 'touchend', - (e) => { - const now = Date.now(); - if (now - lastTouchEnd <= 300) { - e.preventDefault(); - } - lastTouchEnd = now; - }, - false - ); - } - - // Restart button event listener - restartButton.addEventListener('click', (e) => { - e.preventDefault(); - this.restartConversation(); - }); - - if (isIOS) { - restartButton.addEventListener( - 'touchend', - (e) => { - e.preventDefault(); - this.restartConversation(); - }, - { passive: false } - ); - } - - this.wsClient.on('connection', (status) => { - this.state.connectionStatus = status; - - // Send existing conversation history to backend when connected - if (status === 'connected') { - const existingConversation = this.storage.getConversationHistory(); - if (existingConversation.messages.length > 0) { - console.log( - 'Sending existing conversation history to backend:', - existingConversation.messages.length, - 'messages' - ); - this.wsClient.send({ - type: 'conversation_update', - data: existingConversation, - }); - } - } - - this.render(); - }); - - this.wsClient.on('transcript_update', (text) => { - this.state.currentTranscript = text; - this.state.speechDetected = true; // Ensure speech detected is true when we get transcript - this.render(); - }); - - this.wsClient.on('ai_response', (response) => { - this.addMessage('teacher', response.text); - this.state.currentTranscript = ''; - this.state.speechDetected = false; - // Note: ai_response is legacy - audio should come via audio_stream now - // But if it does come, stop any current playback first - if (response.audio) { - this.audioPlayer.stop(); - // Convert WAV base64 to PCM and play through AudioPlayer - // For now, use the legacy playAudio but stop current playback first - this.playAudio(response.audio); - } - }); - - this.wsClient.on('flashcard_generated', (flashcard) => { - this.addFlashcard(flashcard); - }); - - this.wsClient.on('flashcards_generated', (flashcards) => { - console.log('Received new flashcards:', flashcards); - this.addMultipleFlashcards(flashcards); - }); - - this.wsClient.on('speech_detected', (data) => { - // Show listening indicator immediately when VAD detects speech - // Use empty string or placeholder - transcript will update as it arrives - this.state.currentTranscript = data.text || ''; - this.state.speechDetected = true; - - // Trigger interrupt behavior immediately on VAD detection - // This freezes the current response if one is streaming - this.handleInterrupt(); - - this.render(); - }); - - this.wsClient.on('speech_ended', (data) => { - // VAD stopped detecting speech - clear the real-time transcript bubble - console.log( - '[Main] VAD stopped detecting speech, clearing real-time transcript' - ); - - // Clear speech detected state to hide the real-time transcript bubble - // Only clear if we don't have a pending transcription (which means speech was processed) - // If we have a pending transcription, it will be handled by the transcription handler - if (!this.state.pendingTranscription) { - this.state.currentTranscript = ''; - this.state.speechDetected = false; - this.render(); - } - }); - - this.wsClient.on('transcription', (data) => { - // Stop any ongoing audio from previous response to prevent audio/text mismatch - // A new transcription means a new conversation turn is starting - this.audioPlayer.stop(); - if (this.currentAudioElement) { - this.currentAudioElement.pause(); - this.currentAudioElement.src = ''; - this.currentAudioElement = null; - } - - this.state.pendingTranscription = data.text; - this.state.currentTranscript = ''; - this.state.speechDetected = false; // Clear speech detected when transcription is complete - - // If we have a frozen LLM response from an interrupt, finalize it now - if (this.state.pendingLLMResponse && !this.state.streamingLLMResponse) { - // We have a frozen response from an interrupt, finalize it with this transcription - console.log( - '[Main] Finalizing frozen LLM response with new transcription' - ); - this.checkAndUpdateConversation(); - } - - // Before resetting LLM streaming, finalize any pending LLM response - // This ensures the text is added to history even if typewriter was interrupted - if ( - this.state.streamingLLMResponse && - this.state.streamingLLMResponse.trim() && - this.state.llmResponseComplete && - !this.state.pendingLLMResponse - ) { - console.log( - '[Main] Finalizing LLM response before clearing streaming state' - ); - this.state.pendingLLMResponse = this.state.streamingLLMResponse; - this.checkAndUpdateConversation(); - } - - // Reset LLM streaming for new conversation turn - this.state.streamingLLMResponse = ''; // Reset LLM streaming for new conversation - this.state.llmResponseComplete = false; // Reset completion flag for new response - this.state.currentResponseId = null; // Reset response ID - - // Only render if the transcription changed to avoid restarting typewriter - if (this.state.lastPendingTranscription !== data.text) { - this.state.lastPendingTranscription = data.text; - this.render(); - } - - // Check if we can update conversation (will happen if we just finalized a frozen response) - this.checkAndUpdateConversation(); - }); - - this.wsClient.on('llm_response_chunk', (data) => { - // Only accumulate chunks if response is not yet complete - // This prevents chunks from being added after llm_response_complete - if (!this.state.llmResponseComplete) { - this.state.streamingLLMResponse += data.text; - } else { - console.warn( - '[Main] Ignoring llm_response_chunk after response complete' - ); - } - }); - - this.wsClient.on('llm_response_complete', (data) => { - console.log( - '[Main] LLM response complete, starting typewriter with:', - data.text - ); - - // Generate a unique ID for this response to match audio with text - const responseId = `response_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; - this.state.currentResponseId = responseId; - - // Mark response as complete to prevent further chunk accumulation - this.state.llmResponseComplete = true; - - // Use the provided text (which should be the complete response) - // This ensures we use the backend's final text, not accumulated chunks - const finalText = data.text || this.state.streamingLLMResponse; - this.state.streamingLLMResponse = finalText; - - console.log('[Main] About to render for typewriter effect'); - - // Set up callback for when typewriter finishes - this.chatUI.setLLMTypewriterCallback(() => { - console.log('[Main] LLM typewriter finished, updating conversation'); - // Only update if this is still the current response (not interrupted) - if (this.state.currentResponseId === responseId) { - this.state.pendingLLMResponse = finalText; - this.checkAndUpdateConversation(); - } else { - console.log( - '[Main] Response was interrupted, skipping conversation update' - ); - } - }); - - this.render(); // Start typewriter effect - }); - - this.wsClient.on('audio_stream', (data) => { - this.handleAudioStream(data); - }); - - this.wsClient.on('audio_stream_complete', (data) => { - console.log('Audio stream complete signal received'); - this.audioPlayer.markStreamComplete(); - }); - - // Interrupt handling: stop audio playback and freeze current response - // This is triggered both by 'interrupt' message and 'speech_detected' (VAD) - this.wsClient.on('interrupt', (_data) => { - console.log('[Main] Interrupt message received'); - this.handleInterrupt(); - }); - - this.audioHandler.on('audioChunk', (audioData) => { - this.wsClient.sendAudioChunk(audioData); - }); - - this.audioPlayer.on('playback_started', () => { - console.log('Audio playback started'); - }); - - this.audioPlayer.on('playback_finished', () => { - console.log('Audio playback finished'); - }); - } - - async connectWebSocket() { - try { - await this.wsClient.connect(); - // After connection, send lightweight user context (timezone) - try { - const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; - this.wsClient.send({ - type: 'user_context', - timezone: tz, - userId: this.userId, - }); - } catch (e) { - // ignore - } - } catch (error) { - console.error('WebSocket connection failed:', error); - this.state.connectionStatus = 'disconnected'; - this.render(); - } - } - - getOrCreateUserId() { - try { - const key = 'aprende-user-id'; - let id = localStorage.getItem(key); - if (!id) { - id = - typeof crypto !== 'undefined' && crypto.randomUUID - ? crypto.randomUUID() - : `u_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; - localStorage.setItem(key, id); - } - return id; - } catch (_) { - return `u_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; - } - } - - async toggleStreaming() { - if (!this.state.isRecording) { - try { - await this.audioHandler.startStreaming(); - this.state.isRecording = true; - this.state.currentTranscript = ''; - this.state.speechDetected = false; - } catch (error) { - console.error('Failed to start streaming:', error); - alert( - 'Microphone access denied. Please enable microphone permissions.' - ); - return; - } - } else { - this.audioHandler.stopStreaming(); - this.state.isRecording = false; - this.state.currentTranscript = ''; - this.state.speechDetected = false; - } - this.render(); - } - - restartConversation() { - // Stop any ongoing recording - if (this.state.isRecording) { - this.audioHandler.stopStreaming(); - this.state.isRecording = false; - } - - // Stop audio playback - try { - this.audioPlayer.stop(); - } catch (error) { - console.error('Error stopping audio:', error); - } - - // Clear conversation history from storage - this.storage.clearConversation(); - - // Clear chat history from state - this.state.chatHistory = []; - this.state.currentTranscript = ''; - this.state.currentLLMResponse = ''; - this.state.pendingTranscription = null; - this.state.pendingLLMResponse = null; - this.state.streamingLLMResponse = ''; - this.state.lastPendingTranscription = null; - this.state.speechDetected = false; - this.state.llmResponseComplete = false; - this.state.currentResponseId = null; - - // Clear typewriters - this.chatUI.clearAllTypewriters(); - - // Save cleared state - this.saveState(); - - // Send restart message to backend - if (this.wsClient && this.state.connectionStatus === 'connected') { - this.wsClient.send({ - type: 'restart_conversation', - }); - } - - // Re-render UI - this.render(); - } - - checkAndUpdateConversation() { - // Only proceed when we have both transcription and LLM response - console.log( - 'checkAndUpdateConversation called - pending transcription:', - this.state.pendingTranscription, - 'pending LLM:', - this.state.pendingLLMResponse - ); - - if (this.state.pendingTranscription && this.state.pendingLLMResponse) { - // Check if we've already added both messages together to prevent duplicates - const lastUserMessage = this.state.chatHistory - .filter((m) => m.role === 'learner') - .pop(); - const lastTeacherMessage = this.state.chatHistory - .filter((m) => m.role === 'teacher') - .pop(); - - // Check if both messages together are duplicates - const isDuplicate = - lastUserMessage?.content === this.state.pendingTranscription && - lastTeacherMessage?.content === this.state.pendingLLMResponse; - - if (isDuplicate) { - console.log( - '[Main] Duplicate conversation turn detected, skipping update' - ); - // Still clear the pending state - this.state.pendingTranscription = null; - this.state.pendingLLMResponse = null; - this.state.streamingLLMResponse = ''; - this.state.lastPendingTranscription = null; - this.state.llmResponseComplete = false; - return; - } - - // Check if teacher message was already added (from interrupt) but user message wasn't - // In this case, we just need to add the user message - const teacherAlreadyAdded = - lastTeacherMessage?.content === this.state.pendingLLMResponse; - const userAlreadyAdded = - lastUserMessage?.content === this.state.pendingTranscription; - - if (teacherAlreadyAdded && !userAlreadyAdded) { - console.log( - '[Main] Teacher message already added from interrupt, adding user message' - ); - // Just add the user message - this.storage.addMessage('user', this.state.pendingTranscription); - this.addMessageToHistory('learner', this.state.pendingTranscription); - - // Send conversation update - const conversationHistory = this.storage.getConversationHistory(); - this.wsClient.send({ - type: 'conversation_update', - data: conversationHistory, - }); - - // Clear pending state - this.state.pendingTranscription = null; - this.state.pendingLLMResponse = null; - this.state.streamingLLMResponse = ''; - this.state.lastPendingTranscription = null; - this.state.llmResponseComplete = false; - this.state.currentResponseId = null; - - this.render(); - return; - } - - console.log('Adding messages to conversation history...'); - - // Add both messages to conversation history (with automatic truncation) - const userHistory = this.storage.addMessage( - 'user', - this.state.pendingTranscription - ); - const assistantHistory = this.storage.addMessage( - 'assistant', - this.state.pendingLLMResponse - ); - - console.log( - 'User message added, total messages:', - userHistory.messages.length - ); - console.log( - 'Assistant message added, total messages:', - assistantHistory.messages.length - ); - - // Add to chat history for display - this.addMessageToHistory('learner', this.state.pendingTranscription); - this.addMessageToHistory('teacher', this.state.pendingLLMResponse); - - // Get updated conversation history and send to backend - const conversationHistory = this.storage.getConversationHistory(); - console.log( - 'Sending conversation update to backend:', - conversationHistory.messages.length, - 'messages' - ); - - this.wsClient.send({ - type: 'conversation_update', - data: conversationHistory, - }); - - // Clear pending messages and streaming state - this.state.pendingTranscription = null; - this.state.pendingLLMResponse = null; - this.state.streamingLLMResponse = ''; - this.state.lastPendingTranscription = null; - this.state.llmResponseComplete = false; - this.state.currentResponseId = null; - - // Clear any active typewriters before rendering final state - this.chatUI.clearAllTypewriters(); - this.render(); - } - } - - addMessage(role, content) { - // This method is kept for backward compatibility with existing addMessage calls - this.addMessageToHistory(role, content); - } - - addMessageToHistory(role, content) { - const message = { role, content }; - this.state.chatHistory.push(message); - this.saveState(); - this.render(); - } - - addFlashcard(flashcard) { - const exists = this.state.flashcards.some( - (card) => - card.spanish === flashcard.spanish || card.word === flashcard.word - ); - - if (!exists) { - this.state.flashcards.push(flashcard); - this.storage.addFlashcards([flashcard]); - this.saveState(); - this.render(); - } - } - - addMultipleFlashcards(flashcards) { - // Use storage method which handles deduplication and persistence - const updatedFlashcards = this.storage.addFlashcards(flashcards); - this.state.flashcards = updatedFlashcards; - this.saveState(); - this.render(); - } - - getContextForBackend() { - return { - chatHistory: this.state.chatHistory.slice(-10), - flashcards: this.state.flashcards, - }; - } - - handleInterrupt() { - console.log( - '[Main] Handling interrupt - stopping audio and freezing current response' - ); - try { - // Stop audio playback - this.audioPlayer.stop(); - // Also stop any Audio element playback - if (this.currentAudioElement) { - this.currentAudioElement.pause(); - this.currentAudioElement.src = ''; - this.currentAudioElement = null; - } - - // Freeze the current LLM response if there is one - // This means finalizing it and adding it to chat history, but keeping it visible - if ( - this.state.streamingLLMResponse && - this.state.streamingLLMResponse.trim() - ) { - console.log( - '[Main] Freezing current LLM response:', - this.state.streamingLLMResponse - ); - - // Stop typewriter effect immediately - this.chatUI.clearAllTypewriters(); - - // Get the frozen text - const frozenText = this.state.streamingLLMResponse; - - // Save the frozen response - this.state.pendingLLMResponse = frozenText; - - // If we have a pending transcription, finalize the conversation turn now - // Otherwise, we'll finalize it when the transcription arrives - if (this.state.pendingTranscription) { - // We have both, so we can finalize this conversation turn immediately - // This adds both messages to chat history - this.checkAndUpdateConversation(); - } else { - // No transcription yet - add just the LLM response to chat history now - // so it stays visible. We'll add the user message when transcription arrives. - // Check if this message is already in history to avoid duplicates - const lastTeacherMessage = this.state.chatHistory - .filter((m) => m.role === 'teacher') - .pop(); - - if (lastTeacherMessage?.content !== frozenText) { - console.log('[Main] Adding frozen LLM response to chat history'); - this.addMessageToHistory('teacher', frozenText); - } - } - - // Clear streaming state - this will remove the streaming element - // But the message is now in chat history, so it will stay visible - this.state.streamingLLMResponse = ''; - this.state.llmResponseComplete = false; - this.state.currentResponseId = null; - } else { - // No streaming response, just clear state - this.state.streamingLLMResponse = ''; - this.state.llmResponseComplete = false; - this.state.currentResponseId = null; - } - - // Render to update UI - this.render(); - } catch (error) { - console.warn('Error handling interrupt:', error); - } - } - - async handleAudioStream(data) { - try { - if (data.audio && data.audio.length > 0) { - console.log( - `Received audio stream: ${data.audio.length} bytes at ${data.sampleRate}Hz${data.text ? ` with text: "${data.text}"` : ''}` - ); - await this.audioPlayer.addAudioStream(data.audio, data.sampleRate); - } - } catch (error) { - console.error('Error handling audio stream:', error); - } - } - - playAudio(audioData) { - // Stop any current AudioPlayer playback to prevent simultaneous audio - this.audioPlayer.stop(); - - // Stop any existing Audio element playback - if (this.currentAudioElement) { - try { - this.currentAudioElement.pause(); - this.currentAudioElement.src = ''; - this.currentAudioElement = null; - } catch (error) { - console.warn('Error stopping previous audio element:', error); - } - } - - const audio = new Audio(); - this.currentAudioElement = audio; - audio.src = `data:audio/wav;base64,${audioData}`; - audio.play().catch(console.error); - - // Clean up when audio finishes - audio.onended = () => { - if (this.currentAudioElement === audio) { - this.currentAudioElement = null; - } - }; - - audio.onerror = () => { - if (this.currentAudioElement === audio) { - this.currentAudioElement = null; - } - }; - } - - render() { - this.updateConnectionStatus(); - this.chatUI.render( - this.state.chatHistory, - this.state.currentTranscript, - this.state.currentLLMResponse, - this.state.pendingTranscription, - this.state.streamingLLMResponse, - this.state.isRecording, - this.state.speechDetected - ); - this.flashcardUI.render(this.state.flashcards); - - const micButton = document.getElementById('micButton'); - const restartButton = document.getElementById('restartButton'); - micButton.disabled = this.state.connectionStatus !== 'connected'; - micButton.classList.toggle('recording', this.state.isRecording); - restartButton.disabled = this.state.connectionStatus !== 'connected'; - } - - updateConnectionStatus() { - const statusDot = document.getElementById('statusDot'); - const statusText = document.getElementById('statusText'); - - statusDot.className = `status-dot ${this.state.connectionStatus}`; - - const statusMessages = { - connecting: 'Connecting...', - connected: 'Connected', - disconnected: 'Disconnected', - }; - - statusText.textContent = - statusMessages[this.state.connectionStatus] || 'Unknown'; - } -} - -new App(); diff --git a/frontend/js/storage.js b/frontend/js/storage.js deleted file mode 100644 index 8ad4e7a..0000000 --- a/frontend/js/storage.js +++ /dev/null @@ -1,149 +0,0 @@ -export class Storage { - constructor() { - this.storageKey = 'aprende-app-state'; - this.conversationKey = 'aprende-conversation-history'; - this.flashcardsKey = 'aprende-flashcards'; - } - - saveState(state) { - try { - const serializedState = JSON.stringify(state); - localStorage.setItem(this.storageKey, serializedState); - } catch (error) { - console.error('Failed to save state to localStorage:', error); - } - } - - getState() { - try { - const serializedState = localStorage.getItem(this.storageKey); - if (serializedState === null) { - return null; - } - return JSON.parse(serializedState); - } catch (error) { - console.error('Failed to load state from localStorage:', error); - return null; - } - } - - clearState() { - try { - localStorage.removeItem(this.storageKey); - } catch (error) { - console.error('Failed to clear state from localStorage:', error); - } - } - - // Conversation history methods - getConversationHistory() { - try { - const serializedHistory = localStorage.getItem(this.conversationKey); - if (serializedHistory === null) { - return { messages: [] }; - } - return JSON.parse(serializedHistory); - } catch (error) { - console.error( - 'Failed to load conversation history from localStorage:', - error - ); - return { messages: [] }; - } - } - - addMessage(role, content) { - const history = this.getConversationHistory(); - - // Add new message with timestamp - const message = { - role: role, - content: content, - timestamp: new Date().toISOString(), - }; - - history.messages.push(message); - - // Truncate to keep only last 40 turns (80 messages: 40 user + 40 assistant) - if (history.messages.length > 80) { - history.messages = history.messages.slice(-80); - } - - // Save updated history - try { - const serializedHistory = JSON.stringify(history); - localStorage.setItem(this.conversationKey, serializedHistory); - } catch (error) { - console.error( - 'Failed to save conversation history to localStorage:', - error - ); - } - - return history; - } - - clearConversation() { - try { - localStorage.removeItem(this.conversationKey); - } catch (error) { - console.error( - 'Failed to clear conversation history from localStorage:', - error - ); - } - } - - // Flashcard methods - getFlashcards() { - try { - const serializedFlashcards = localStorage.getItem(this.flashcardsKey); - if (serializedFlashcards === null) { - return []; - } - return JSON.parse(serializedFlashcards); - } catch (error) { - console.error('Failed to load flashcards from localStorage:', error); - return []; - } - } - - saveFlashcards(flashcards) { - try { - const serializedFlashcards = JSON.stringify(flashcards); - localStorage.setItem(this.flashcardsKey, serializedFlashcards); - } catch (error) { - console.error('Failed to save flashcards to localStorage:', error); - } - } - - addFlashcards(newFlashcards) { - const existingFlashcards = this.getFlashcards(); - - // Filter out duplicates based on spanish word - const uniqueNewFlashcards = newFlashcards.filter((newCard) => { - return !existingFlashcards.some( - (existing) => - existing.spanish?.toLowerCase() === newCard.spanish?.toLowerCase() - ); - }); - - const updatedFlashcards = [...existingFlashcards, ...uniqueNewFlashcards]; - - // Keep only the last 100 flashcards - if (updatedFlashcards.length > 100) { - updatedFlashcards.splice(0, updatedFlashcards.length - 100); - } - - this.saveFlashcards(updatedFlashcards); - return updatedFlashcards; - } - - clearFlashcards() { - try { - localStorage.removeItem(this.flashcardsKey); - } catch (error) { - console.error('Failed to clear flashcards from localStorage:', error); - } - } -} diff --git a/frontend/js/websocket-client.js b/frontend/js/websocket-client.js deleted file mode 100644 index a07ff1a..0000000 --- a/frontend/js/websocket-client.js +++ /dev/null @@ -1,233 +0,0 @@ -export class WebSocketClient { - constructor(url) { - this.url = url; - this.ws = null; - this.listeners = new Map(); - this.reconnectAttempts = 0; - this.maxReconnectAttempts = 5; - this.reconnectDelay = 1000; - this.pingInterval = null; - - // Check for iOS and use optimized URL if available - this.isIOS = - /iPad|iPhone|iPod/.test(navigator.userAgent) || - (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); - - if (this.isIOS && window.iosAudioHandler) { - const optimizedUrl = window.iosAudioHandler.getOptimizedWebSocketURL(); - if (optimizedUrl) { - console.log( - '[WebSocketClient] Using iOS-optimized WebSocket URL:', - optimizedUrl - ); - this.url = optimizedUrl; - } - } - } - - on(event, callback) { - if (!this.listeners.has(event)) { - this.listeners.set(event, []); - } - this.listeners.get(event).push(callback); - } - - emit(event, data) { - const callbacks = this.listeners.get(event); - if (callbacks) { - callbacks.forEach((callback) => callback(data)); - } - } - - async connect() { - return new Promise((resolve, reject) => { - try { - this.ws = new WebSocket(this.url); - - this.ws.onopen = () => { - console.log('WebSocket connected'); - this.reconnectAttempts = 0; - this.emit('connection', 'connected'); - - // Start ping/pong for iOS to keep connection alive - if (this.isIOS) { - this.startPingPong(); - } - - resolve(); - }; - - this.ws.onmessage = (event) => { - try { - const message = JSON.parse(event.data); - this.handleMessage(message); - } catch (error) { - console.error('Failed to parse WebSocket message:', error); - } - }; - - this.ws.onclose = (event) => { - console.log('WebSocket disconnected:', event.code, event.reason); - this.emit('connection', 'disconnected'); - - // Stop ping/pong - this.stopPingPong(); - - if ( - !event.wasClean && - this.reconnectAttempts < this.maxReconnectAttempts - ) { - this.scheduleReconnect(); - } - }; - - this.ws.onerror = (error) => { - console.error('WebSocket error:', error); - this.emit('connection', 'disconnected'); - reject(error); - }; - } catch (error) { - reject(error); - } - }); - } - - scheduleReconnect() { - this.reconnectAttempts++; - const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); - - console.log( - `Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})` - ); - - setTimeout(() => { - this.emit('connection', 'connecting'); - this.connect().catch(() => { - if (this.reconnectAttempts >= this.maxReconnectAttempts) { - console.error('Max reconnection attempts reached'); - this.emit('connection', 'disconnected'); - } - }); - }, delay); - } - - handleMessage(message) { - switch (message.type) { - case 'transcript_update': - this.emit('transcript_update', message.data.text); - break; - - case 'transcription': - this.emit('transcription', { - text: message.text, - timestamp: message.timestamp, - }); - break; - - case 'ai_response': - this.emit('ai_response', { - text: message.data.text, - audio: message.data.audio, - }); - break; - - case 'flashcard_generated': - this.emit('flashcard_generated', message.data); - break; - - case 'flashcards_generated': - this.emit('flashcards_generated', message.flashcards); - break; - case 'introduction_state_updated': - this.emit('introduction_state_updated', message.introduction_state); - break; - - case 'connection_status': - // Connection status received - break; - - case 'speech_detected': - this.emit('speech_detected', message.data); - break; - - case 'speech_ended': - this.emit('speech_ended', message.data); - break; - - case 'llm_response_chunk': - this.emit('llm_response_chunk', { - text: message.text, - timestamp: message.timestamp, - }); - break; - - case 'llm_response_complete': - this.emit('llm_response_complete', { - text: message.text, - timestamp: message.timestamp, - }); - break; - - case 'audio_stream': - this.emit('audio_stream', { - audio: message.audio, - sampleRate: message.sampleRate, - timestamp: message.timestamp, - }); - break; - - case 'audio_stream_complete': - this.emit('audio_stream_complete', { - timestamp: message.timestamp, - }); - break; - - case 'interrupt': - this.emit('interrupt', { reason: message.reason }); - break; - - default: - console.log('Unknown message type:', message.type); - } - } - - send(message) { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - this.ws.send(JSON.stringify(message)); - } - } - - sendAudioChunk(audioData) { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - const message = { - type: 'audio_chunk', - audio_data: audioData, - }; - this.ws.send(JSON.stringify(message)); - } - } - - disconnect() { - this.stopPingPong(); - if (this.ws) { - this.ws.close(); - } - } - - startPingPong() { - // Send ping every 30 seconds to keep connection alive on iOS - this.pingInterval = setInterval(() => { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - this.send({ type: 'ping' }); - console.log('[WebSocketClient] Ping sent to keep connection alive'); - } - }, 30000); - } - - stopPingPong() { - if (this.pingInterval) { - clearInterval(this.pingInterval); - this.pingInterval = null; - } - } -} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..8b55696 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4553 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@supabase/supabase-js": "^2.89.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "jsdom": "^25.0.0", + "prettier": "^3.2.5", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4", + "vitest": "^4.0.16" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@supabase/auth-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.89.0.tgz", + "integrity": "sha512-wiWZdz8WMad8LQdJMWYDZ2SJtZP5MwMqzQq3ehtW2ngiI3UTgbKiFrvMUUS3KADiVlk4LiGfODB2mrYx7w2f8w==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.89.0.tgz", + "integrity": "sha512-XEueaC5gMe5NufNYfBh9kPwJlP5M2f+Ogr8rvhmRDAZNHgY6mI35RCkYDijd92pMcNM7g8pUUJov93UGUnqfyw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.89.0.tgz", + "integrity": "sha512-/b0fKrxV9i7RNOEXMno/I1862RsYhuUo+Q6m6z3ar1f4ulTMXnDfv0y4YYxK2POcgrOXQOgKYQx1eArybyNvtg==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.89.0.tgz", + "integrity": "sha512-aMOvfDb2a52u6PX6jrrjvACHXGV3zsOlWRzZsTIOAJa0hOVvRp01AwC1+nLTGUzxzezejrYeCX+KnnM1xHdl+w==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.89.0.tgz", + "integrity": "sha512-6zKcXofk/M/4Eato7iqpRh+B+vnxeiTumCIP+Tz26xEqIiywzD9JxHq+udRrDuv6hXE+pmetvJd8n5wcf4MFRQ==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.89.0.tgz", + "integrity": "sha512-KlaRwSfFA0fD73PYVMHj5/iXFtQGCcX7PSx0FdQwYEEw9b2wqM7GxadY+5YwcmuEhalmjFB/YvqaoNVF+sWUlg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.89.0", + "@supabase/functions-js": "2.89.0", + "@supabase/postgrest-js": "2.89.0", + "@supabase/realtime-js": "2.89.0", + "@supabase/storage-js": "2.89.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", + "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", + "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/type-utils": "8.51.0", + "@typescript-eslint/utils": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.51.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz", + "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", + "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.51.0", + "@typescript-eslint/types": "^8.51.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", + "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", + "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", + "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", + "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.51.0", + "@typescript-eslint/tsconfig-utils": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", + "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", + "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.51.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.16", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", + "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.16", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", + "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", + "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.51.0.tgz", + "integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.51.0", + "@typescript-eslint/parser": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", + "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.16", + "@vitest/mocker": "4.0.16", + "@vitest/pretty-format": "4.0.16", + "@vitest/runner": "4.0.16", + "@vitest/snapshot": "4.0.16", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.16", + "@vitest/browser-preview": "4.0.16", + "@vitest/browser-webdriverio": "4.0.16", + "@vitest/ui": "4.0.16", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..0fe7d9c --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,42 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "start": "vite preview", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@supabase/supabase-js": "^2.89.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "jsdom": "^25.0.0", + "prettier": "^3.2.5", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4", + "vitest": "^4.0.16" + } +} diff --git a/frontend/public/audio-processor.js b/frontend/public/audio-processor.js new file mode 100644 index 0000000..b2f19cf --- /dev/null +++ b/frontend/public/audio-processor.js @@ -0,0 +1,75 @@ +/** + * AudioWorklet processor for capturing and resampling microphone audio + * Buffers to 100ms chunks (1600 samples at 16kHz) to meet AssemblyAI requirements + * Outputs Float32 audio (backend handles conversion to PCM16) + */ +class AudioProcessor extends AudioWorkletProcessor { + constructor(options) { + super(); + this.sourceSampleRate = options.processorOptions.sourceSampleRate; + this.targetSampleRate = 16000; + this.resampleRatio = this.sourceSampleRate / this.targetSampleRate; + + // Input buffer for resampling + this.inputBuffer = null; + + // Output buffer to collect 100ms of resampled audio (1600 samples at 16kHz) + // AssemblyAI requires chunks between 50-1000ms + this.outputBuffer = []; + this.outputBufferSize = 1600; // 100ms at 16kHz + } + + process(inputs) { + const inputChannel = inputs[0][0]; + if (!inputChannel) return true; + + // Accumulate input samples + const currentLength = this.inputBuffer ? this.inputBuffer.length : 0; + const newBuffer = new Float32Array(currentLength + inputChannel.length); + if (this.inputBuffer) { + newBuffer.set(this.inputBuffer, 0); + } + newBuffer.set(inputChannel, currentLength); + this.inputBuffer = newBuffer; + + // Resample to 16kHz + const numOutputSamples = Math.floor( + this.inputBuffer.length / this.resampleRatio + ); + if (numOutputSamples === 0) return true; + + const resampledData = new Float32Array(numOutputSamples); + for (let i = 0; i < numOutputSamples; i++) { + const correspondingInputIndex = i * this.resampleRatio; + const lowerIndex = Math.floor(correspondingInputIndex); + const upperIndex = Math.ceil(correspondingInputIndex); + const interpolationFactor = correspondingInputIndex - lowerIndex; + + const lowerValue = this.inputBuffer[lowerIndex] || 0; + const upperValue = this.inputBuffer[upperIndex] || 0; + + resampledData[i] = + lowerValue + (upperValue - lowerValue) * interpolationFactor; + } + + // Keep unconsumed input samples + const consumedInputSamples = numOutputSamples * this.resampleRatio; + this.inputBuffer = this.inputBuffer.slice(Math.round(consumedInputSamples)); + + // Add Float32 samples to output buffer + for (let i = 0; i < resampledData.length; i++) { + this.outputBuffer.push(resampledData[i]); + + // When we have 100ms of audio (1600 samples), send it as Float32 + if (this.outputBuffer.length >= this.outputBufferSize) { + const float32Array = new Float32Array(this.outputBuffer); + this.port.postMessage(float32Array.buffer, [float32Array.buffer]); + this.outputBuffer = []; + } + } + + return true; + } +} + +registerProcessor('audio-processor', AudioProcessor); diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..0de7171 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..34f4f93 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,51 @@ +import { AppProvider, useApp } from './context/AppContext'; +import { Header } from './components/Header'; +import { Sidebar } from './components/Sidebar'; +import { ChatSection } from './components/ChatSection'; +import { FlashcardsSection } from './components/FlashcardsSection'; +import './styles/main.css'; + +function AppContent() { + const { state } = useApp(); + const { switchingConversation } = state; + + return ( +
+
+
+ +
+
+
+
+ {switchingConversation ? ( +
+
+
Switching conversation...
+
+ ) : ( + <> + + + + )} +
+
+
+ {/* Hidden audio element for iOS compatibility */} +
+
+
+ ); +} + +function App() { + return ( + + + + ); +} + +export default App; diff --git a/frontend/src/__tests__/services/Storage.test.ts b/frontend/src/__tests__/services/Storage.test.ts new file mode 100644 index 0000000..c121715 --- /dev/null +++ b/frontend/src/__tests__/services/Storage.test.ts @@ -0,0 +1,380 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { Storage } from '../../services/Storage'; +import type { Flashcard } from '../../types'; + +describe('Storage', () => { + let storage: Storage; + + beforeEach(() => { + storage = new Storage(); + }); + + describe('getLanguage / saveLanguage', () => { + it('returns "es" as default', () => { + expect(storage.getLanguage()).toBe('es'); + }); + + it('returns saved language', () => { + storage.saveLanguage('fr'); + expect(storage.getLanguage()).toBe('fr'); + }); + + it('persists language across instances', () => { + storage.saveLanguage('de'); + const newStorage = new Storage(); + expect(newStorage.getLanguage()).toBe('de'); + }); + }); + + describe('addMessage', () => { + it('adds message to conversation history', () => { + storage.addMessage('user', 'Hello'); + const history = storage.getConversationHistory(); + + expect(history.messages.length).toBe(1); + expect(history.messages[0].role).toBe('user'); + expect(history.messages[0].content).toBe('Hello'); + }); + + it('adds multiple messages in order', () => { + storage.addMessage('user', 'Hello'); + storage.addMessage('assistant', 'Hi there!'); + const history = storage.getConversationHistory(); + + expect(history.messages.length).toBe(2); + expect(history.messages[0].content).toBe('Hello'); + expect(history.messages[1].content).toBe('Hi there!'); + }); + + it('includes timestamp on messages', () => { + storage.addMessage('user', 'Hello'); + const history = storage.getConversationHistory(); + + expect(history.messages[0].timestamp).toBeTruthy(); + // Verify it's a valid ISO date string + expect(() => new Date(history.messages[0].timestamp)).not.toThrow(); + }); + + it('truncates to 80 messages max', () => { + // Add 85 messages + for (let i = 0; i < 85; i++) { + storage.addMessage('user', `Message ${i}`); + } + + const history = storage.getConversationHistory(); + expect(history.messages.length).toBe(80); + // First 5 should be truncated, so first message is "Message 5" + expect(history.messages[0].content).toBe('Message 5'); + // Last message should be "Message 84" + expect(history.messages[79].content).toBe('Message 84'); + }); + }); + + describe('clearConversation', () => { + it('clears all messages', () => { + storage.addMessage('user', 'Hello'); + storage.addMessage('assistant', 'Hi!'); + storage.clearConversation(); + + const history = storage.getConversationHistory(); + expect(history.messages.length).toBe(0); + }); + }); + + describe('createConversation', () => { + it('creates conversation with unique ID', () => { + const conv = storage.createConversation('es'); + expect(conv.id).toBeTruthy(); + expect(conv.languageCode).toBe('es'); + }); + + it('creates conversation with title containing random numbers', () => { + const conv = storage.createConversation('es'); + expect(conv.title).toMatch(/^Chat \d{5}$/); + }); + + it('creates multiple conversations with different IDs', () => { + const conv1 = storage.createConversation('es'); + const conv2 = storage.createConversation('es'); + expect(conv1.id).not.toBe(conv2.id); + }); + + it('adds conversation to list', () => { + storage.createConversation('es'); + const list = storage.getConversationList('es'); + expect(list.length).toBe(1); + }); + + it('adds new conversations at beginning of list', () => { + const conv1 = storage.createConversation('es'); + const conv2 = storage.createConversation('es'); + const list = storage.getConversationList('es'); + + expect(list[0].id).toBe(conv2.id); + expect(list[1].id).toBe(conv1.id); + }); + }); + + describe('deleteConversation', () => { + it('removes conversation from list', () => { + const conv = storage.createConversation('es'); + storage.deleteConversation(conv.id, 'es'); + + const list = storage.getConversationList('es'); + expect(list.length).toBe(0); + }); + + it('removes conversation data', () => { + const conv = storage.createConversation('es'); + storage.deleteConversation(conv.id, 'es'); + + const data = storage.getConversation(conv.id); + expect(data).toBeNull(); + }); + }); + + describe('renameConversation', () => { + it('updates conversation title', () => { + const conv = storage.createConversation('es'); + storage.renameConversation(conv.id, 'My Spanish Chat', 'es'); + + const list = storage.getConversationList('es'); + expect(list[0].title).toBe('My Spanish Chat'); + }); + }); + + describe('saveConversation / getConversation', () => { + it('saves and retrieves conversation messages', () => { + const conv = storage.createConversation('es'); + const messages = [ + { + role: 'user' as const, + content: 'Hola', + timestamp: new Date().toISOString(), + }, + { + role: 'assistant' as const, + content: '¡Hola!', + timestamp: new Date().toISOString(), + }, + ]; + + storage.saveConversation(conv.id, messages, 'es'); + const data = storage.getConversation(conv.id); + + expect(data).not.toBeNull(); + expect(data!.messages.length).toBe(2); + expect(data!.messages[0].content).toBe('Hola'); + }); + }); + + describe('addFlashcards', () => { + it('adds flashcards to storage', () => { + const cards = [ + { targetWord: 'hola', english: 'hello', example: '', mnemonic: '' }, + ]; + const result = storage.addFlashcards(cards as Partial[], 'es'); + + expect(result.length).toBe(1); + expect(result[0].targetWord).toBe('hola'); + }); + + it('deduplicates flashcards by targetWord', () => { + const cards1 = [ + { targetWord: 'hola', english: 'hello', example: '', mnemonic: '' }, + ]; + storage.addFlashcards(cards1 as Partial[], 'es'); + + const cards2 = [ + { targetWord: 'hola', english: 'hello', example: '', mnemonic: '' }, + { targetWord: 'adios', english: 'goodbye', example: '', mnemonic: '' }, + ]; + const result = storage.addFlashcards( + cards2 as Partial[], + 'es' + ); + + // Should only have 2 cards (hola + adios), not 3 + expect(result.length).toBe(2); + }); + + it('is case-insensitive for duplicate detection', () => { + const cards1 = [ + { targetWord: 'Hola', english: 'hello', example: '', mnemonic: '' }, + ]; + storage.addFlashcards(cards1 as Partial[], 'es'); + + const cards2 = [ + { + targetWord: 'hola', + english: 'hello again', + example: '', + mnemonic: '', + }, + ]; + const result = storage.addFlashcards( + cards2 as Partial[], + 'es' + ); + + expect(result.length).toBe(1); + expect(result[0].targetWord).toBe('Hola'); + }); + + it('adds language code to flashcards', () => { + const cards = [ + { targetWord: 'bonjour', english: 'hello', example: '', mnemonic: '' }, + ]; + const result = storage.addFlashcards(cards as Partial[], 'fr'); + + expect(result[0].languageCode).toBe('fr'); + }); + + it('handles legacy spanish field', () => { + const cards = [ + { spanish: 'hola', english: 'hello', example: '', mnemonic: '' }, + ]; + const result = storage.addFlashcards(cards as Partial[], 'es'); + + expect(result[0].targetWord).toBe('hola'); + }); + + it('limits to 100 flashcards per language', () => { + // Add 110 flashcards + for (let i = 0; i < 110; i++) { + storage.addFlashcards( + [ + { + targetWord: `word${i}`, + english: `translation${i}`, + example: '', + mnemonic: '', + }, + ] as Partial[], + 'es' + ); + } + + const flashcards = storage.getFlashcards('es'); + expect(flashcards.length).toBe(100); + }); + }); + + describe('clearFlashcards', () => { + it('clears flashcards for a language', () => { + storage.addFlashcards( + [ + { targetWord: 'hola', english: 'hello', example: '', mnemonic: '' }, + ] as Partial[], + 'es' + ); + storage.clearFlashcards('es'); + + const flashcards = storage.getFlashcards('es'); + expect(flashcards.length).toBe(0); + }); + + it('does not affect flashcards for other languages', () => { + storage.addFlashcards( + [ + { targetWord: 'hola', english: 'hello', example: '', mnemonic: '' }, + ] as Partial[], + 'es' + ); + storage.addFlashcards( + [ + { + targetWord: 'bonjour', + english: 'hello', + example: '', + mnemonic: '', + }, + ] as Partial[], + 'fr' + ); + + storage.clearFlashcards('es'); + + expect(storage.getFlashcards('es').length).toBe(0); + expect(storage.getFlashcards('fr').length).toBe(1); + }); + }); + + describe('per-conversation flashcards', () => { + it('stores flashcards per conversation', () => { + const conv = storage.createConversation('es'); + const cards = [ + { targetWord: 'hola', english: 'hello', example: '', mnemonic: '' }, + ]; + + storage.addFlashcardsForConversation( + conv.id, + cards as Partial[], + 'es' + ); + const result = storage.getFlashcardsForConversation(conv.id); + + expect(result.length).toBe(1); + expect(result[0].conversationId).toBe(conv.id); + }); + + it('isolates flashcards between conversations', () => { + const conv1 = storage.createConversation('es'); + const conv2 = storage.createConversation('es'); + + storage.addFlashcardsForConversation( + conv1.id, + [ + { targetWord: 'hola', english: 'hello', example: '', mnemonic: '' }, + ] as Partial[], + 'es' + ); + storage.addFlashcardsForConversation( + conv2.id, + [ + { + targetWord: 'adios', + english: 'goodbye', + example: '', + mnemonic: '', + }, + ] as Partial[], + 'es' + ); + + expect(storage.getFlashcardsForConversation(conv1.id).length).toBe(1); + expect(storage.getFlashcardsForConversation(conv1.id)[0].targetWord).toBe( + 'hola' + ); + expect(storage.getFlashcardsForConversation(conv2.id).length).toBe(1); + expect(storage.getFlashcardsForConversation(conv2.id)[0].targetWord).toBe( + 'adios' + ); + }); + }); + + describe('getAllConversations', () => { + it('returns conversations across all languages', () => { + storage.createConversation('es'); + storage.createConversation('fr'); + storage.createConversation('de'); + + const all = storage.getAllConversations(); + expect(all.length).toBe(3); + }); + + it('sorts by updatedAt descending', () => { + const conv1 = storage.createConversation('es'); + + // Wait a tiny bit to ensure different timestamps + storage.createConversation('fr'); + + // Update conv1 to make it more recent + storage.saveConversation(conv1.id, [], 'es'); + + const all = storage.getAllConversations(); + // conv1 was updated more recently + expect(all[0].id).toBe(conv1.id); + }); + }); +}); diff --git a/frontend/src/__tests__/setup.ts b/frontend/src/__tests__/setup.ts new file mode 100644 index 0000000..9365ffa --- /dev/null +++ b/frontend/src/__tests__/setup.ts @@ -0,0 +1,36 @@ +/// + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = value; + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + store = {}; + }, + get length() { + return Object.keys(store).length; + }, + key: (i: number) => Object.keys(store)[i] || null, + }; +})(); + +Object.defineProperty(global, 'localStorage', { value: localStorageMock }); + +// Mock crypto.randomUUID +Object.defineProperty(global, 'crypto', { + value: { + randomUUID: () => `test-uuid-${Math.random().toString(36).slice(2, 10)}`, + }, +}); + +// Reset localStorage before each test +beforeEach(() => { + localStorageMock.clear(); +}); diff --git a/frontend/src/components/ChatSection.tsx b/frontend/src/components/ChatSection.tsx new file mode 100644 index 0000000..89f9ee9 --- /dev/null +++ b/frontend/src/components/ChatSection.tsx @@ -0,0 +1,224 @@ +import { + useEffect, + useRef, + useCallback, + useState, + type FormEvent, + type KeyboardEvent, +} from 'react'; +import { useApp } from '../context/AppContext'; +import { Message } from './Message'; +import { StreamingMessage } from './StreamingMessage'; + +export function ChatSection() { + const { state, toggleRecording, sendTextMessage } = useApp(); + const [textInput, setTextInput] = useState(''); + const { + chatHistory, + currentTranscript, + pendingTranscription, + streamingLLMResponse, + isRecording, + speechDetected, + connectionStatus, + currentResponseId, + currentLanguage, + currentConversationId, + conversations, + availableLanguages, + } = state; + + // Get the flag for the current conversation or selected language + const getCurrentFlag = (): string => { + const currentConversation = conversations.find( + (c) => c.id === currentConversationId + ); + const langCode = currentConversation?.languageCode || currentLanguage; + const lang = availableLanguages.find((l) => l.code === langCode); + return lang?.flag || '🌐'; + }; + + const messagesContainerRef = useRef(null); + const responseIdRef = useRef(null); + + useEffect(() => { + responseIdRef.current = currentResponseId; + }, [currentResponseId]); + + // Instant scroll to bottom - used during streaming/typing + const scrollToBottomInstant = useCallback(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = + messagesContainerRef.current.scrollHeight; + } + }, []); + + // Smooth scroll to bottom - used for new messages + const scrollToBottomSmooth = useCallback(() => { + requestAnimationFrame(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTo({ + top: messagesContainerRef.current.scrollHeight, + behavior: 'smooth', + }); + } + }); + }, []); + + // Scroll when chat history changes (new messages added) + useEffect(() => { + scrollToBottomSmooth(); + }, [chatHistory, scrollToBottomSmooth]); + + // Scroll when streaming source content updates + useEffect(() => { + scrollToBottomInstant(); + }, [ + currentTranscript, + pendingTranscription, + streamingLLMResponse, + scrollToBottomInstant, + ]); + + const handleTextSubmit = useCallback( + (e: FormEvent) => { + e.preventDefault(); + if (textInput.trim()) { + sendTextMessage(textInput); + setTextInput(''); + } + }, + [textInput, sendTextMessage] + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + if (textInput.trim()) { + sendTextMessage(textInput); + setTextInput(''); + } + } + }, + [textInput, sendTextMessage] + ); + + const isConnected = connectionStatus === 'connected'; + + return ( +
+
+

+ Conversation + {getCurrentFlag()} +

+ +
+
+
+ {/* Loading overlay when not connected */} + {connectionStatus !== 'connected' && ( +
+
+
+ )} + + {/* Render existing conversation history */} + {chatHistory.map((message, index) => ( + + ))} + + {/* Real-time transcript (while speaking) */} + {speechDetected && isRecording && !pendingTranscription && ( +
+ {currentTranscript} + {!currentTranscript && ( + + + + + + )} +
+ )} + + {/* Pending user transcription (final) - no typewriter, shown immediately */} + {pendingTranscription && ( + + )} + + {/* Streaming LLM response */} + {streamingLLMResponse && ( + + )} +
+
+ {currentTranscript} +
+
+ setTextInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={!isConnected} + maxLength={200} + /> + +
+
+
+ ); +} + +// Simple component to show finalized user transcription (no typewriter effect) +function PendingTranscription({ text }: { text: string }) { + return ( +
+ {text} +
+ ); +} diff --git a/frontend/src/components/FeedbackTooltip.tsx b/frontend/src/components/FeedbackTooltip.tsx new file mode 100644 index 0000000..d259e94 --- /dev/null +++ b/frontend/src/components/FeedbackTooltip.tsx @@ -0,0 +1,58 @@ +import { useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; + +interface FeedbackTooltipProps { + feedback: string | null; + visible: boolean; + position: { x: number; y: number }; + onMouseEnter?: () => void; + onMouseLeave?: () => void; +} + +export function FeedbackTooltip({ + feedback, + visible, + position, + onMouseEnter, + onMouseLeave, +}: FeedbackTooltipProps) { + const tooltipRef = useRef(null); + const isLoading = feedback === null; + + // Adjust position after content loads + useEffect(() => { + if (visible && tooltipRef.current && !isLoading) { + const tooltipRect = tooltipRef.current.getBoundingClientRect(); + tooltipRef.current.style.top = `${position.y - tooltipRect.height}px`; + } + }, [visible, isLoading, position.y, feedback]); + + if (!visible) return null; + + const tooltipContent = ( +
+
+ + {isLoading ? '' : feedback || 'No feedback available'} + +
+
+ + + +
+
+ ); + + return createPortal(tooltipContent, document.body); +} diff --git a/frontend/src/components/Flashcard.tsx b/frontend/src/components/Flashcard.tsx new file mode 100644 index 0000000..31923ee --- /dev/null +++ b/frontend/src/components/Flashcard.tsx @@ -0,0 +1,98 @@ +import { useState, useCallback } from 'react'; +import type { Flashcard as FlashcardType } from '../types'; + +interface FlashcardProps { + flashcard: FlashcardType; + onCardClick?: (flashcard: FlashcardType) => void; + onPronounce?: (flashcard: FlashcardType) => void; + isPronouncing?: boolean; +} + +function capitalizeFirstLetter(text: string): string { + if (!text) return text; + return text.charAt(0).toUpperCase() + text.slice(1); +} + +export function Flashcard({ + flashcard, + onCardClick, + onPronounce, + isPronouncing = false, +}: FlashcardProps) { + const [isFlipped, setIsFlipped] = useState(false); + + const handleClick = useCallback(() => { + setIsFlipped((prev) => !prev); + onCardClick?.(flashcard); + }, [flashcard, onCardClick]); + + const handlePronounce = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + onPronounce?.(flashcard); + }, + [flashcard, onPronounce] + ); + + // Support both new 'targetWord' and legacy 'spanish' field + const targetWord = + flashcard.targetWord || flashcard.spanish || flashcard.word || ''; + const english = flashcard.english || flashcard.translation || ''; + const example = flashcard.example || flashcard.example_sentence || ''; + const mnemonic = flashcard.mnemonic || ''; + + // Capitalize the first letter of the target word for display + const displayTargetWord = capitalizeFirstLetter(targetWord); + + return ( +
+
+
+
+ {displayTargetWord} +
+ +
+
+
+ {english} +
+
+ {example} +
+ {mnemonic && ( +
+ Remember:{' '} + {mnemonic} +
+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/FlashcardsSection.tsx b/frontend/src/components/FlashcardsSection.tsx new file mode 100644 index 0000000..e7c5145 --- /dev/null +++ b/frontend/src/components/FlashcardsSection.tsx @@ -0,0 +1,150 @@ +import { useState, useCallback } from 'react'; +import { useApp } from '../context/AppContext'; +import { Flashcard } from './Flashcard'; +import type { Flashcard as FlashcardType } from '../types'; + +// Helper for API URL for Cloud Run deployment +const getApiUrl = (path: string): string => { + const backendUrl = import.meta.env.VITE_BACKEND_URL; + return backendUrl ? `${backendUrl}${path}` : path; +}; + +export function FlashcardsSection() { + const { state, wsClient, pronounceWord } = useApp(); + const { flashcards, currentLanguage, pronouncingCardId } = state; + const [isExporting, setIsExporting] = useState(false); + + const handleCardClick = useCallback( + (card: FlashcardType) => { + wsClient.send({ type: 'flashcard_clicked', card }); + }, + [wsClient] + ); + + const handlePronounce = useCallback( + (card: FlashcardType) => { + const targetWord = card.targetWord || card.spanish || card.word || ''; + if (!targetWord) return; + + pronounceWord(targetWord); + }, + [pronounceWord] + ); + + const exportToAnki = useCallback(async () => { + const validFlashcards = flashcards.filter((flashcard) => { + const targetWord = flashcard.targetWord || flashcard.spanish; + return ( + targetWord && + flashcard.english && + targetWord.trim() !== '' && + flashcard.english.trim() !== '' + ); + }); + + if (validFlashcards.length === 0) { + alert('No valid flashcards to export'); + return; + } + + setIsExporting(true); + + try { + const languageNames: Record = { + es: 'Spanish', + ja: 'Japanese', + fr: 'French', + }; + const languageName = languageNames[currentLanguage] || 'Language'; + + const response = await fetch(getApiUrl('/api/export-anki'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + flashcards: validFlashcards, + deckName: `Inworld Language Tutor ${languageName} Cards`, + languageCode: currentLanguage, + }), + }); + + if (!response.ok) { + throw new Error('Export failed'); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `inworld_language_tutor_${currentLanguage}_cards.apkg`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + console.log('ANKI deck exported successfully!'); + } catch (error) { + console.error('Error exporting to ANKI:', error); + alert('Failed to export flashcards to ANKI'); + } finally { + setIsExporting(false); + } + }, [flashcards, currentLanguage]); + + // Sort flashcards by timestamp (most recent first) + const sortedFlashcards = [...flashcards].sort((a, b) => { + const timeA = new Date(a.timestamp || 0).getTime(); + const timeB = new Date(b.timestamp || 0).getTime(); + return timeB - timeA; + }); + + const cardCount = flashcards.length; + const canExport = cardCount >= 1; + + return ( +
+
+

Flashcards

+ + {isExporting + ? 'Exporting...' + : canExport + ? `Export ${cardCount} card${cardCount !== 1 ? 's' : ''} to Anki` + : `${cardCount} card${cardCount !== 1 ? 's' : ''}`} + +
+
+
+ {sortedFlashcards.length === 0 ? ( +
+ ) : ( + sortedFlashcards.map((flashcard, index) => { + const cardId = + flashcard.targetWord || + flashcard.spanish || + flashcard.word || + ''; + return ( + + ); + }) + )} +
+
+
+ ); +} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..f9e7970 --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,219 @@ +import { useState, useRef, useEffect } from 'react'; +import { useApp } from '../context/AppContext'; +import { useAuth } from '../context/AuthContext'; + +export function Header() { + const { state, toggleSidebar } = useApp(); + const { connectionStatus } = state; + const { user, isLoading, isConfigured, signUp, signIn, signOut } = useAuth(); + + const [showAuthForm, setShowAuthForm] = useState(false); + const [isSignUp, setIsSignUp] = useState(false); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + const authFormRef = useRef(null); + const signInButtonRef = useRef(null); + + const statusMessages: Record = { + connecting: 'Connecting...', + connected: 'Connected', + disconnected: 'Disconnected', + }; + + // Close auth form when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + const target = event.target as Node; + const clickedInAuthForm = + authFormRef.current && authFormRef.current.contains(target); + const clickedSignInButton = + signInButtonRef.current && signInButtonRef.current.contains(target); + + // Close auth form if clicked outside (but not on the Sign In button itself) + if (showAuthForm && !clickedInAuthForm && !clickedSignInButton) { + setShowAuthForm(false); + } + } + // Use a small delay to prevent immediate closing when opening + const timeoutId = setTimeout(() => { + document.addEventListener('mousedown', handleClickOutside); + }, 100); + + return () => { + clearTimeout(timeoutId); + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showAuthForm]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSubmitting(true); + + const result = isSignUp + ? await signUp(email, password) + : await signIn(email, password); + + setSubmitting(false); + + if (result.error) { + setError(result.error); + } else { + setShowAuthForm(false); + setEmail(''); + setPassword(''); + } + }; + + const toggleAuthMode = () => { + setIsSignUp(!isSignUp); + setError(null); + }; + + const handleSignOut = async () => { + await signOut(); + }; + + return ( +
+
+
+ +

Inworld Language Tutor

+
+ +
+ {/* Connection Status - Always Visible */} +
+ + + {statusMessages[connectionStatus] || 'Unknown'} + +
+ + {/* Auth Section - Always Visible */} + {isConfigured && ( +
+ {isLoading ? ( +
Loading...
+ ) : user ? ( +
+ + {user.email} + + +
+ ) : ( + <> + + {showAuthForm && ( +
+
+
+ {isSignUp ? 'Create Account' : 'Sign In'} + +
+ {error && ( +
{error}
+ )} + setEmail(e.target.value)} + required + autoComplete="email" + className="header-auth-input" + /> + setPassword(e.target.value)} + required + minLength={6} + autoComplete={ + isSignUp ? 'new-password' : 'current-password' + } + className="header-auth-input" + /> + + +
+
+ )} + + )} +
+ )} + + {/* Logo */} +
+ Inworld Language Tutor +
+
+
+
+ ); +} diff --git a/frontend/src/components/Message.tsx b/frontend/src/components/Message.tsx new file mode 100644 index 0000000..7ecf307 --- /dev/null +++ b/frontend/src/components/Message.tsx @@ -0,0 +1,102 @@ +import { useState, useRef, useCallback, useEffect } from 'react'; +import type { ChatMessage } from '../types'; +import { TranslationTooltip } from './TranslationTooltip'; +import { FeedbackTooltip } from './FeedbackTooltip'; +import { useApp } from '../context/AppContext'; + +interface MessageProps { + message: ChatMessage; +} + +export function Message({ message }: MessageProps) { + const { state } = useApp(); + const [showTooltip, setShowTooltip] = useState(false); + const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); + const messageRef = useRef(null); + const hoverTimeoutRef = useRef | null>(null); + const hideTimeoutRef = useRef | null>(null); + + const handleMouseEnter = useCallback(() => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); + hideTimeoutRef.current = null; + } + + hoverTimeoutRef.current = setTimeout(() => { + if (messageRef.current) { + const rect = messageRef.current.getBoundingClientRect(); + setTooltipPosition({ + x: rect.left + window.scrollX, + y: rect.top + window.scrollY - 8, + }); + setShowTooltip(true); + } + }, 300); + }, []); + + const handleMouseLeave = useCallback(() => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + + hideTimeoutRef.current = setTimeout(() => { + setShowTooltip(false); + }, 150); + }, []); + + const handleTooltipMouseEnter = useCallback(() => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); + hideTimeoutRef.current = null; + } + }, []); + + const handleTooltipMouseLeave = useCallback(() => { + setShowTooltip(false); + }, []); + + useEffect(() => { + return () => { + if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); + if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); + }; + }, []); + + // Get feedback for learner messages from the feedbackMap + const feedback = + message.role === 'learner' + ? (state.feedbackMap[message.content] ?? null) + : null; + + return ( + <> +
+ {message.content} +
+ {message.role === 'teacher' && ( + + )} + {message.role === 'learner' && ( + + )} + + ); +} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..cae0f70 --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,275 @@ +import { useState, useRef, useEffect } from 'react'; +import { useApp } from '../context/AppContext'; + +export function Sidebar() { + const { + state, + selectConversation, + createNewConversation, + deleteConversation, + renameConversation, + toggleSidebar, + changeUiLanguage, + } = useApp(); + const { + conversations, + currentConversationId, + sidebarOpen, + availableLanguages, + uiLanguage, + } = state; + + // Helper to get flag for a language code + const getFlag = (languageCode: string): string => { + const lang = availableLanguages.find((l) => l.code === languageCode); + return lang?.flag || ''; + }; + + const [editingId, setEditingId] = useState(null); + const [editValue, setEditValue] = useState(''); + const [showLangMenu, setShowLangMenu] = useState(false); + const inputRef = useRef(null); + const langMenuRef = useRef(null); + + // Use uiLanguage for the button display (doesn't change when switching conversations) + const currentLang = availableLanguages.find( + (l) => l.code === uiLanguage + ); + + // Focus input when editing starts + useEffect(() => { + if (editingId && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [editingId]); + + // Close language menu when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + langMenuRef.current && + !langMenuRef.current.contains(event.target as Node) + ) { + setShowLangMenu(false); + } + } + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleLanguageSelect = (langCode: string) => { + if (langCode !== uiLanguage) { + changeUiLanguage(langCode); + } + setShowLangMenu(false); + }; + + const startEditing = (conversationId: string, currentTitle: string) => { + setEditingId(conversationId); + setEditValue(currentTitle); + }; + + const saveEdit = () => { + if (editingId && editValue.trim()) { + renameConversation(editingId, editValue.trim()); + } + setEditingId(null); + setEditValue(''); + }; + + const cancelEdit = () => { + setEditingId(null); + setEditValue(''); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + saveEdit(); + } else if (e.key === 'Escape') { + cancelEdit(); + } + }; + + return ( + <> + {/* Overlay for mobile */} + {sidebarOpen && ( +
+ )} + + + + ); +} diff --git a/frontend/src/components/StreamingMessage.tsx b/frontend/src/components/StreamingMessage.tsx new file mode 100644 index 0000000..b2615bc --- /dev/null +++ b/frontend/src/components/StreamingMessage.tsx @@ -0,0 +1,81 @@ +import { useState, useRef, useCallback, useEffect } from 'react'; +import { TranslationTooltip } from './TranslationTooltip'; + +interface StreamingMessageProps { + text: string; +} + +export function StreamingMessage({ text }: StreamingMessageProps) { + const [showTooltip, setShowTooltip] = useState(false); + const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); + const messageRef = useRef(null); + const hoverTimeoutRef = useRef | null>(null); + const hideTimeoutRef = useRef | null>(null); + + const handleMouseEnter = useCallback(() => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); + hideTimeoutRef.current = null; + } + + hoverTimeoutRef.current = setTimeout(() => { + if (messageRef.current) { + const rect = messageRef.current.getBoundingClientRect(); + setTooltipPosition({ + x: rect.left + window.scrollX, + y: rect.top + window.scrollY - 8, + }); + setShowTooltip(true); + } + }, 300); + }, []); + + const handleMouseLeave = useCallback(() => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + + hideTimeoutRef.current = setTimeout(() => { + setShowTooltip(false); + }, 150); + }, []); + + const handleTooltipMouseEnter = useCallback(() => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); + hideTimeoutRef.current = null; + } + }, []); + + const handleTooltipMouseLeave = useCallback(() => { + setShowTooltip(false); + }, []); + + useEffect(() => { + return () => { + if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); + if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); + }; + }, []); + + return ( + <> +
+ {text} +
+ + + ); +} diff --git a/frontend/src/components/TranslationTooltip.tsx b/frontend/src/components/TranslationTooltip.tsx new file mode 100644 index 0000000..7270bb5 --- /dev/null +++ b/frontend/src/components/TranslationTooltip.tsx @@ -0,0 +1,71 @@ +import { useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import { useTranslator } from '../hooks/useTranslator'; + +interface TranslationTooltipProps { + text: string; + visible: boolean; + position: { x: number; y: number }; + onMouseEnter?: () => void; + onMouseLeave?: () => void; +} + +export function TranslationTooltip({ + text, + visible, + position, + onMouseEnter, + onMouseLeave, +}: TranslationTooltipProps) { + const { translation, isLoading, translate, clearTranslation } = + useTranslator(); + const tooltipRef = useRef(null); + const lastTextRef = useRef(''); + + useEffect(() => { + if (visible && text && text !== lastTextRef.current) { + lastTextRef.current = text; + translate(text); + } else if (!visible) { + lastTextRef.current = ''; + clearTranslation(); + } + }, [visible, text, translate, clearTranslation]); + + // Adjust position after content loads + useEffect(() => { + if (visible && tooltipRef.current && !isLoading) { + const tooltipRect = tooltipRef.current.getBoundingClientRect(); + tooltipRef.current.style.top = `${position.y - tooltipRect.height}px`; + } + }, [visible, isLoading, position.y, translation]); + + if (!visible) return null; + + const tooltipContent = ( +
+
+ + {isLoading ? '' : translation || 'Translation unavailable'} + +
+
+ + + +
+
+ ); + + return createPortal(tooltipContent, document.body); +} diff --git a/frontend/src/context/AppContext.tsx b/frontend/src/context/AppContext.tsx new file mode 100644 index 0000000..468126e --- /dev/null +++ b/frontend/src/context/AppContext.tsx @@ -0,0 +1,1573 @@ +import React, { + createContext, + useContext, + useReducer, + useEffect, + useRef, + useCallback, + useMemo, + type ReactNode, +} from 'react'; +import type { + AppState, + ChatMessage, + Flashcard, + Language, + ConnectionStatus, + AudioStreamData, + FeedbackGeneratedPayload, + ConversationSummary, +} from '../types'; +import { HybridStorage } from '../services/HybridStorage'; +import { WebSocketClient } from '../services/WebSocketClient'; +import { AudioHandler } from '../services/AudioHandler'; +import { AudioPlayer } from '../services/AudioPlayer'; +import { useAuth } from './AuthContext'; + +// Helper to determine WebSocket URL for Cloud Run deployment +const getWebSocketUrl = (): string => { + const backendUrl = import.meta.env.VITE_BACKEND_URL; + if (backendUrl) { + // Convert https:// to wss:// or http:// to ws:// + return backendUrl.replace(/^http/, 'ws'); + } + // Local development: use same host with port 3000 + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${window.location.hostname}:3000`; +}; + +// Helper for API URL for Cloud Run deployment +const getApiUrl = (path: string): string => { + const backendUrl = import.meta.env.VITE_BACKEND_URL; + return backendUrl ? `${backendUrl}${path}` : path; +}; + +// Action types +type AppAction = + | { type: 'SET_CONNECTION_STATUS'; payload: ConnectionStatus } + | { type: 'SET_LANGUAGE'; payload: string } + | { type: 'SET_UI_LANGUAGE'; payload: string } + | { type: 'SET_AVAILABLE_LANGUAGES'; payload: Language[] } + | { type: 'SET_CHAT_HISTORY'; payload: ChatMessage[] } + | { type: 'ADD_MESSAGE'; payload: ChatMessage } + | { type: 'SET_CURRENT_TRANSCRIPT'; payload: string } + | { type: 'SET_PENDING_TRANSCRIPTION'; payload: string | null } + | { type: 'SET_STREAMING_LLM_RESPONSE'; payload: string } + | { type: 'APPEND_LLM_CHUNK'; payload: string } + | { type: 'SET_LLM_COMPLETE'; payload: boolean } + | { type: 'SET_RESPONSE_ID'; payload: string | null } + | { type: 'SET_RECORDING'; payload: boolean } + | { type: 'SET_SPEECH_DETECTED'; payload: boolean } + | { type: 'SET_FLASHCARDS'; payload: Flashcard[] } + | { type: 'ADD_FLASHCARDS'; payload: Flashcard[] } + | { type: 'SET_PRONOUNCING_CARD_ID'; payload: string | null } + | { + type: 'SET_FEEDBACK'; + payload: { messageContent: string; feedback: string }; + } + | { type: 'RESET_STREAMING_STATE' } + | { type: 'RESET_CONVERSATION' } + | { type: 'SET_CONVERSATIONS'; payload: ConversationSummary[] } + | { type: 'SET_CURRENT_CONVERSATION_ID'; payload: string | null } + | { type: 'SET_SIDEBAR_OPEN'; payload: boolean } + | { type: 'ADD_CONVERSATION'; payload: ConversationSummary } + | { type: 'REMOVE_CONVERSATION'; payload: string } + | { type: 'RENAME_CONVERSATION'; payload: { id: string; title: string } } + | { type: 'SET_USER_ID'; payload: string | null } + | { type: 'SET_SWITCHING_CONVERSATION'; payload: boolean }; + +// Initial state +const createInitialState = (storage: HybridStorage): AppState => { + // Determine the language from the current conversation, not from stored preference + let initialLanguage = storage.getLanguage(); // fallback to stored preference + const currentConversationId = storage.getCurrentConversationId(); + if (currentConversationId) { + const allConversations = storage.getAllConversations(); + const currentConversation = allConversations.find((c) => c.id === currentConversationId); + if (currentConversation) { + initialLanguage = currentConversation.languageCode; + } + } + + return { + connectionStatus: 'connecting', + currentLanguage: initialLanguage, + uiLanguage: storage.getUiLanguage() || 'es', + availableLanguages: [], + chatHistory: [], + currentTranscript: '', + pendingTranscription: null, + streamingLLMResponse: '', + llmResponseComplete: false, + currentResponseId: null, + isRecording: false, + speechDetected: false, + flashcards: [], + pronouncingCardId: null, + feedbackMap: {}, + userId: null, + conversations: [], + currentConversationId: null, + sidebarOpen: false, + switchingConversation: false, + }; +}; + +// Reducer +function appReducer(state: AppState, action: AppAction): AppState { + switch (action.type) { + case 'SET_CONNECTION_STATUS': + return { ...state, connectionStatus: action.payload }; + case 'SET_LANGUAGE': + return { ...state, currentLanguage: action.payload }; + case 'SET_UI_LANGUAGE': + return { ...state, uiLanguage: action.payload }; + case 'SET_AVAILABLE_LANGUAGES': + return { ...state, availableLanguages: action.payload }; + case 'SET_CHAT_HISTORY': + return { ...state, chatHistory: action.payload }; + case 'ADD_MESSAGE': + return { ...state, chatHistory: [...state.chatHistory, action.payload] }; + case 'SET_CURRENT_TRANSCRIPT': + return { ...state, currentTranscript: action.payload }; + case 'SET_PENDING_TRANSCRIPTION': + return { ...state, pendingTranscription: action.payload }; + case 'SET_STREAMING_LLM_RESPONSE': + return { ...state, streamingLLMResponse: action.payload }; + case 'APPEND_LLM_CHUNK': + return { + ...state, + streamingLLMResponse: state.streamingLLMResponse + action.payload, + }; + case 'SET_LLM_COMPLETE': + return { ...state, llmResponseComplete: action.payload }; + case 'SET_RESPONSE_ID': + return { ...state, currentResponseId: action.payload }; + case 'SET_RECORDING': + return { ...state, isRecording: action.payload }; + case 'SET_SPEECH_DETECTED': + return { ...state, speechDetected: action.payload }; + case 'SET_FLASHCARDS': + return { ...state, flashcards: action.payload }; + case 'ADD_FLASHCARDS': { + const existingWords = new Set( + state.flashcards.map((f) => + (f.targetWord || f.spanish || '').toLowerCase() + ) + ); + const newCards = action.payload.filter( + (f) => + !existingWords.has((f.targetWord || f.spanish || '').toLowerCase()) + ); + return { ...state, flashcards: [...state.flashcards, ...newCards] }; + } + case 'SET_PRONOUNCING_CARD_ID': + return { ...state, pronouncingCardId: action.payload }; + case 'SET_FEEDBACK': + return { + ...state, + feedbackMap: { + ...state.feedbackMap, + [action.payload.messageContent]: action.payload.feedback, + }, + }; + case 'RESET_STREAMING_STATE': + return { + ...state, + streamingLLMResponse: '', + llmResponseComplete: false, + currentResponseId: null, + }; + case 'RESET_CONVERSATION': + return { + ...state, + chatHistory: [], + currentTranscript: '', + pendingTranscription: null, + streamingLLMResponse: '', + llmResponseComplete: false, + currentResponseId: null, + speechDetected: false, + }; + case 'SET_CONVERSATIONS': + return { ...state, conversations: action.payload }; + case 'SET_CURRENT_CONVERSATION_ID': + return { ...state, currentConversationId: action.payload }; + case 'SET_SIDEBAR_OPEN': + return { ...state, sidebarOpen: action.payload }; + case 'ADD_CONVERSATION': + return { + ...state, + conversations: [action.payload, ...state.conversations], + }; + case 'REMOVE_CONVERSATION': + return { + ...state, + conversations: state.conversations.filter( + (c) => c.id !== action.payload + ), + }; + case 'RENAME_CONVERSATION': + return { + ...state, + conversations: state.conversations.map((c) => + c.id === action.payload.id + ? { + ...c, + title: action.payload.title, + updatedAt: new Date().toISOString(), + } + : c + ), + }; + case 'SET_USER_ID': + return { ...state, userId: action.payload }; + case 'SET_SWITCHING_CONVERSATION': + return { ...state, switchingConversation: action.payload }; + default: + return state; + } +} + +// Context type +interface AppContextType { + state: AppState; + dispatch: React.Dispatch; + storage: HybridStorage; + wsClient: WebSocketClient; + audioHandler: AudioHandler; + audioPlayer: AudioPlayer; + // Actions + toggleRecording: () => Promise; + changeUiLanguage: (newLanguage: string) => void; + handleInterrupt: () => void; + sendTextMessage: (text: string) => void; + pronounceWord: (text: string) => void; + // Conversation actions + selectConversation: (conversationId: string) => void; + createNewConversation: () => void; + deleteConversation: (conversationId: string) => void; + renameConversation: (conversationId: string, newTitle: string) => void; + toggleSidebar: () => void; +} + +const AppContext = createContext(null); + +// Provider +interface AppProviderProps { + children: ReactNode; +} + +export function AppProvider({ children }: AppProviderProps) { + const { supabase, user } = useAuth(); + // Create instances directly using useMemo - these are stable and don't change + const storageInstance = useMemo(() => new HybridStorage(), []); + const storageRef = useRef(storageInstance); + const wsClientInstance = useMemo( + () => new WebSocketClient(getWebSocketUrl()), + [] + ); + const wsClientRef = useRef(wsClientInstance); + const audioHandlerInstance = useMemo(() => new AudioHandler(), []); + const audioHandlerRef = useRef(audioHandlerInstance); + const audioPlayerInstance = useMemo(() => new AudioPlayer(), []); + const audioPlayerRef = useRef(audioPlayerInstance); + const ttsAudioPlayerInstance = useMemo(() => new AudioPlayer(), []); + const ttsAudioPlayerRef = useRef(ttsAudioPlayerInstance); + const hasMigratedRef = useRef(false); + const conversationsLoadedRef = useRef(false); + + const [state, dispatch] = useReducer( + appReducer, + storageInstance, + createInitialState + ); + + // Refs for tracking state in callbacks + const stateRef = useRef(state); + + // Update stateRef in effect to avoid updating ref during render + useEffect(() => { + stateRef.current = state; + }, [state]); + + // Connect/disconnect Supabase based on auth state + useEffect(() => { + const storage = storageRef.current; + + if (supabase && user) { + // Immediately update userId from auth + dispatch({ type: 'SET_USER_ID', payload: user.id }); + + storage.setSupabaseClient(supabase, user.id); + + // Sync data on login + if (!hasMigratedRef.current) { + hasMigratedRef.current = true; + const languages = stateRef.current.availableLanguages.map( + (l) => l.code + ); + const langsToSync = + languages.length > 0 ? languages : [stateRef.current.currentLanguage]; + + // First try to sync ALL conversations FROM Supabase (existing user on new device) + // Then migrate any local data TO Supabase + storage + .syncAllConversationsFromSupabase() + .then((allConversations) => { + // If user has conversations in Supabase, reload the UI state with ALL of them + if (allConversations.length > 0) { + dispatch({ + type: 'SET_CONVERSATIONS', + payload: allConversations, + }); + + // Get the stored current conversation ID + let currentId = storage.getCurrentConversationId(); + + // If we have a stored conversation ID, verify it exists + if (currentId) { + const conversationExists = allConversations.find((c) => c.id === currentId); + if (!conversationExists) { + // Stored conversation ID doesn't exist anymore, clear it + currentId = null; + } + } + + // If no stored conversation found, use the most recent one + if (!currentId && allConversations.length > 0) { + currentId = allConversations[0].id; + } + } + // Also migrate any local data that isn't in Supabase yet + return storage.migrateToSupabase(langsToSync); + }) + .catch(console.error); + } + } else { + // Clear userId on logout + dispatch({ type: 'SET_USER_ID', payload: null }); + + storage.clearSupabaseClient(); + hasMigratedRef.current = false; + conversationsLoadedRef.current = false; + } + }, [supabase, user]); + + const pendingLLMResponseRef = useRef(null); + // Track if the last message was sent via text input (vs audio) + // This allows us to ignore transcription events for text messages + const lastMessageWasTextRef = useRef(false); + // Queue flashcards when conversation doesn't exist yet (race condition fix) + const pendingFlashcardsRef = useRef([]); + + // Refs for callbacks to avoid effect dependency issues + const handleInterruptRef = useRef<() => void>(() => {}); + const checkAndUpdateConversationRef = useRef<() => void>(() => {}); + const processPendingFlashcardsRef = useRef<(conversationId: string) => void>( + () => {} + ); + const selectConversationRef = useRef<(conversationId: string) => void>(() => {}); + + // Initialize audio players + useEffect(() => { + const audioPlayer = audioPlayerRef.current; + const ttsAudioPlayer = ttsAudioPlayerRef.current; + audioPlayer.initialize().catch(console.error); + ttsAudioPlayer.initialize().catch(console.error); + return () => { + audioPlayer.destroy(); + ttsAudioPlayer.destroy(); + }; + }, []); + + // Load initial state (conversations across all languages) + // Only run if Supabase sync hasn't already loaded conversations + useEffect(() => { + if (conversationsLoadedRef.current) { + return; // Supabase sync already loaded conversations + } + + const storage = storageRef.current; + + // Load ALL conversations across all languages + const allConversations = storage.getAllConversations(); + dispatch({ type: 'SET_CONVERSATIONS', payload: allConversations }); + + // If no conversations, clear flashcards + if (allConversations.length === 0) { + dispatch({ type: 'SET_FLASHCARDS', payload: [] }); + conversationsLoadedRef.current = true; + } + }, []); // Run once on mount + + // Save chat history to current conversation when it changes + useEffect(() => { + const storage = storageRef.current; + const currentId = stateRef.current.currentConversationId; + const currentLang = stateRef.current.currentLanguage; + + if (currentId && state.chatHistory.length > 0) { + const messages = state.chatHistory.map((m) => ({ + role: m.role === 'learner' ? 'user' : 'assistant', + content: m.content, + timestamp: new Date().toISOString(), + })) as import('../types').ConversationMessage[]; + storage.saveConversation(currentId, messages, currentLang); + } + }, [state.chatHistory]); + + // Fetch available languages + useEffect(() => { + const fetchLanguages = async () => { + try { + const response = await fetch(getApiUrl('/api/languages')); + if (response.ok) { + const data = await response.json(); + dispatch({ + type: 'SET_AVAILABLE_LANGUAGES', + payload: data.languages, + }); + + // Don't reset language if we have a current conversation - let selectConversation handle it + // Only validate if we don't have a conversation loaded yet + const currentLang = stateRef.current.currentLanguage; + const hasCurrentConversation = !!stateRef.current.currentConversationId; + const isValidLanguage = data.languages.some( + (lang: Language) => lang.code === currentLang + ); + // Only reset language if it's invalid AND we don't have a current conversation + // (if we have a conversation, selectConversation will set the correct language) + if (!isValidLanguage && !hasCurrentConversation) { + dispatch({ + type: 'SET_LANGUAGE', + payload: data.defaultLanguage || 'es', + }); + } + } + } catch (error) { + console.error('Failed to fetch languages:', error); + dispatch({ + type: 'SET_AVAILABLE_LANGUAGES', + payload: [ + { code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇲🇽' }, + ], + }); + } + }; + fetchLanguages(); + }, []); + + // Check and update conversation + const checkAndUpdateConversation = useCallback(() => { + const currentState = stateRef.current; + const pendingTranscription = currentState.pendingTranscription; + const pendingLLMResponse = pendingLLMResponseRef.current; + + const storage = storageRef.current; + const wsClient = wsClientRef.current; + + // Track conversation info (may be newly created or from state) + let conversationId = currentState.currentConversationId; + let conversationTitle: string | null = null; + + // Auto-create conversation if none exists and we're about to add messages + if (!conversationId && (pendingTranscription || pendingLLMResponse)) { + const newConversation = storage.createConversation( + currentState.currentLanguage + ); + conversationId = newConversation.id; + conversationTitle = newConversation.title; + dispatch({ type: 'ADD_CONVERSATION', payload: newConversation }); + dispatch({ + type: 'SET_CURRENT_CONVERSATION_ID', + payload: newConversation.id, + }); + storage.setCurrentConversationId(newConversation.id); + // Process any flashcards that arrived before conversation was created + processPendingFlashcardsRef.current(newConversation.id); + } else if (conversationId) { + // Get title from existing conversation in state + const currentConvo = currentState.conversations.find( + (c) => c.id === conversationId + ); + conversationTitle = currentConvo?.title || null; + } + + // Case 1: We have a pending LLM response but user message was already added (text input case) + if (pendingLLMResponse && !pendingTranscription) { + // Add only the teacher response + storage.addMessage('assistant', pendingLLMResponse); + dispatch({ + type: 'ADD_MESSAGE', + payload: { role: 'teacher', content: pendingLLMResponse }, + }); + + const conversationHistory = storage.getConversationHistory(); + wsClient.send({ type: 'conversation_update', data: conversationHistory }); + + pendingLLMResponseRef.current = null; + dispatch({ type: 'RESET_STREAMING_STATE' }); + return; + } + + // Case 2: We have both pending transcription and LLM response (audio input case) + if (pendingTranscription && pendingLLMResponse) { + // Auto-rename conversation on first user message (if still has default name) + if ( + conversationId && + conversationTitle && + /^Chat \d{5}$/.test(conversationTitle) + ) { + const newTitle = + pendingTranscription.length > 10 + ? pendingTranscription.slice(0, 10) + '...' + : pendingTranscription; + storage.renameConversation( + conversationId, + newTitle, + currentState.currentLanguage + ); + dispatch({ + type: 'RENAME_CONVERSATION', + payload: { id: conversationId, title: newTitle }, + }); + } + + storage.addMessage('user', pendingTranscription); + dispatch({ + type: 'ADD_MESSAGE', + payload: { role: 'learner', content: pendingTranscription }, + }); + + storage.addMessage('assistant', pendingLLMResponse); + dispatch({ + type: 'ADD_MESSAGE', + payload: { role: 'teacher', content: pendingLLMResponse }, + }); + + const conversationHistory = storage.getConversationHistory(); + wsClient.send({ type: 'conversation_update', data: conversationHistory }); + + dispatch({ type: 'SET_PENDING_TRANSCRIPTION', payload: null }); + pendingLLMResponseRef.current = null; + dispatch({ type: 'RESET_STREAMING_STATE' }); + } + }, []); + + // Update refs in effect to avoid updating during render + useEffect(() => { + checkAndUpdateConversationRef.current = checkAndUpdateConversation; + }, [checkAndUpdateConversation]); + + // Process any pending flashcards that were queued before conversation existed + const processPendingFlashcards = useCallback((conversationId: string) => { + const storage = storageRef.current; + const pending = pendingFlashcardsRef.current; + + if (pending.length > 0) { + console.log( + `[AppContext] Processing ${pending.length} pending flashcards for conversation ${conversationId}` + ); + const updatedFlashcards = storage.addFlashcardsForConversation( + conversationId, + pending, + stateRef.current.currentLanguage + ); + dispatch({ type: 'SET_FLASHCARDS', payload: updatedFlashcards }); + pendingFlashcardsRef.current = []; + } + }, []); + + // Update refs in effect to avoid updating during render + useEffect(() => { + processPendingFlashcardsRef.current = processPendingFlashcards; + }, [processPendingFlashcards]); + + // Process pending flashcards when conversation ID becomes available + // This handles the race condition where flashcards arrive before the conversation is created + useEffect(() => { + if ( + state.currentConversationId && + pendingFlashcardsRef.current.length > 0 + ) { + processPendingFlashcards(state.currentConversationId); + } + }, [state.currentConversationId, processPendingFlashcards]); + + // Handle interrupt + const handleInterrupt = useCallback(() => { + console.log('[AppContext] Handling interrupt'); + const audioPlayer = audioPlayerRef.current; + audioPlayer.stop(); + + const currentState = stateRef.current; + if (currentState.streamingLLMResponse?.trim()) { + const frozenText = currentState.streamingLLMResponse; + pendingLLMResponseRef.current = frozenText; + + if (currentState.pendingTranscription) { + checkAndUpdateConversationRef.current(); + } else { + const lastTeacherMessage = currentState.chatHistory + .filter((m) => m.role === 'teacher') + .pop(); + + if (lastTeacherMessage?.content !== frozenText) { + dispatch({ + type: 'ADD_MESSAGE', + payload: { role: 'teacher', content: frozenText }, + }); + } + } + + dispatch({ type: 'RESET_STREAMING_STATE' }); + } + }, []); + + // Update refs in effect to avoid updating during render + useEffect(() => { + handleInterruptRef.current = handleInterrupt; + }, [handleInterrupt]); + + // Setup WebSocket event listeners - runs once on mount + useEffect(() => { + const wsClient = wsClientRef.current; + const storage = storageRef.current; + const audioPlayer = audioPlayerRef.current; + + // Clear any existing listeners to prevent duplicates + wsClient.clearAllListeners(); + + wsClient.on('connection', (status) => { + dispatch({ + type: 'SET_CONNECTION_STATUS', + payload: status as ConnectionStatus, + }); + + if (status === 'connected') { + const existingConversation = storage.getConversationHistory(); + if (existingConversation.messages.length > 0) { + wsClient.send({ + type: 'conversation_update', + data: existingConversation, + }); + } + } + }); + + wsClient.on('speech_detected', (data) => { + const payload = data as { text?: string; conversationId?: string }; + + // Only process if it's for the current conversation + if ( + !payload.conversationId || + payload.conversationId === stateRef.current.currentConversationId + ) { + dispatch({ + type: 'SET_CURRENT_TRANSCRIPT', + payload: payload.text || '', + }); + dispatch({ type: 'SET_SPEECH_DETECTED', payload: true }); + handleInterruptRef.current(); + } + }); + + wsClient.on('partial_transcript', (data) => { + const payload = data as { text?: string; conversationId?: string }; + const text = payload.text; + + // Only process if it's for the current conversation + if ( + text && + (!payload.conversationId || + payload.conversationId === stateRef.current.currentConversationId) + ) { + dispatch({ type: 'SET_CURRENT_TRANSCRIPT', payload: text }); + dispatch({ type: 'SET_SPEECH_DETECTED', payload: true }); + } + }); + + wsClient.on('speech_ended', () => { + if (!stateRef.current.pendingTranscription) { + dispatch({ type: 'SET_CURRENT_TRANSCRIPT', payload: '' }); + dispatch({ type: 'SET_SPEECH_DETECTED', payload: false }); + } + }); + + wsClient.on('transcription', (data) => { + const payload = data as { text: string; conversationId?: string }; + const text = payload.text; + + // Only process if it's for the current conversation + if ( + !payload.conversationId || + payload.conversationId === stateRef.current.currentConversationId + ) { + audioPlayer.stop(); + + dispatch({ type: 'SET_CURRENT_TRANSCRIPT', payload: '' }); + dispatch({ type: 'SET_SPEECH_DETECTED', payload: false }); + + // If the last message was sent via text input, ignore this transcription event + // because the user message was already added in sendTextMessage + if (lastMessageWasTextRef.current) { + lastMessageWasTextRef.current = false; + // Still need to check for LLM response and update conversation + if ( + pendingLLMResponseRef.current && + !stateRef.current.streamingLLMResponse + ) { + checkAndUpdateConversationRef.current(); + } + + if ( + stateRef.current.streamingLLMResponse?.trim() && + stateRef.current.llmResponseComplete && + !pendingLLMResponseRef.current + ) { + pendingLLMResponseRef.current = stateRef.current.streamingLLMResponse; + checkAndUpdateConversationRef.current(); + } + + dispatch({ type: 'RESET_STREAMING_STATE' }); + checkAndUpdateConversationRef.current(); + return; + } + + // This is an audio-based transcription - set pending transcription + dispatch({ type: 'SET_PENDING_TRANSCRIPTION', payload: text }); + + if ( + pendingLLMResponseRef.current && + !stateRef.current.streamingLLMResponse + ) { + checkAndUpdateConversationRef.current(); + } + + if ( + stateRef.current.streamingLLMResponse?.trim() && + stateRef.current.llmResponseComplete && + !pendingLLMResponseRef.current + ) { + pendingLLMResponseRef.current = stateRef.current.streamingLLMResponse; + checkAndUpdateConversationRef.current(); + } + + dispatch({ type: 'RESET_STREAMING_STATE' }); + checkAndUpdateConversationRef.current(); + } + }); + + wsClient.on('llm_response_chunk', (data) => { + const payload = data as { text: string; conversationId?: string }; + + // Only process if it's for the current conversation + if ( + !payload.conversationId || + payload.conversationId === stateRef.current.currentConversationId + ) { + if (!stateRef.current.llmResponseComplete) { + dispatch({ + type: 'APPEND_LLM_CHUNK', + payload: payload.text, + }); + } + } + }); + + wsClient.on('llm_response_complete', (data) => { + const payload = data as { text?: string; conversationId?: string }; + + // Only process if it's for the current conversation + if ( + !payload.conversationId || + payload.conversationId === stateRef.current.currentConversationId + ) { + const responseId = `response_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; + dispatch({ type: 'SET_RESPONSE_ID', payload: responseId }); + dispatch({ type: 'SET_LLM_COMPLETE', payload: true }); + + const finalText = + payload.text || stateRef.current.streamingLLMResponse; + dispatch({ type: 'SET_STREAMING_LLM_RESPONSE', payload: finalText }); + + // Store the LLM response and trigger conversation update + pendingLLMResponseRef.current = finalText; + checkAndUpdateConversationRef.current(); + } + }); + + wsClient.on('audio_stream', (data) => { + const audioData = data as AudioStreamData & { conversationId?: string }; + + // Block all audio during conversation switch + if (stateRef.current.switchingConversation) { + return; + } + + // Only process if it's for the current conversation + if ( + !audioData.conversationId || + audioData.conversationId === stateRef.current.currentConversationId + ) { + audioPlayer.addAudioStream( + audioData.audio, + audioData.sampleRate, + false, + audioData.audioFormat + ); + } + }); + + wsClient.on('audio_stream_complete', (data) => { + // Block all audio during conversation switch + if (stateRef.current.switchingConversation) { + return; + } + + const payload = data as { conversationId?: string }; + + // Only process if it's for the current conversation + if ( + !payload.conversationId || + payload.conversationId === stateRef.current.currentConversationId + ) { + audioPlayer.markStreamComplete(); + } + }); + + // TTS pronunciation handlers (for flashcard pronunciation) + const ttsAudioPlayer = ttsAudioPlayerRef.current; + wsClient.on('tts_pronounce_audio', (data) => { + // Block all audio during conversation switch + if (stateRef.current.switchingConversation) { + return; + } + + const audioData = data as { + audio: string; + audioFormat: string; + sampleRate: number; + }; + ttsAudioPlayer.addAudioStream( + audioData.audio, + audioData.sampleRate, + false, + audioData.audioFormat as 'int16' | 'float32' + ); + }); + + wsClient.on('tts_pronounce_complete', () => { + // Block all audio during conversation switch + if (stateRef.current.switchingConversation) { + return; + } + dispatch({ type: 'SET_PRONOUNCING_CARD_ID', payload: null }); + }); + + wsClient.on('tts_pronounce_error', () => { + // Block all audio during conversation switch + if (stateRef.current.switchingConversation) { + return; + } + dispatch({ type: 'SET_PRONOUNCING_CARD_ID', payload: null }); + }); + + wsClient.on('interrupt', (data) => { + const payload = data as { reason?: string; conversationId?: string }; + const reason = payload.reason; + + // Only process if it's for the current conversation + if ( + !payload.conversationId || + payload.conversationId === stateRef.current.currentConversationId + ) { + if (reason === 'continuation_detected') { + // User is continuing their utterance - discard partial response silently + console.log( + '[AppContext] Continuation detected - discarding partial response' + ); + audioPlayer.stop(); + // Don't save the partial response - just reset streaming state + dispatch({ type: 'RESET_STREAMING_STATE' }); + pendingLLMResponseRef.current = null; + } else { + // Normal interrupt (speech_start) - use regular interrupt handling + handleInterruptRef.current(); + } + } + }); + + wsClient.on('conversation_rollback', (data) => { + // Server removed messages due to utterance continuation - sync frontend state + const payload = data as { + messages: Array<{ role: string; content: string }>; + removedCount: number; + conversationId?: string; + }; + const { messages, removedCount, conversationId } = payload; + + // Only process if it's for the current conversation + if ( + !conversationId || + conversationId === stateRef.current.currentConversationId + ) { + console.log( + `[AppContext] Conversation rollback - removed ${removedCount} messages` + ); + + // Convert backend format to frontend format + const chatHistory = messages.map((m) => ({ + role: m.role === 'user' ? 'learner' : 'teacher', + content: m.content, + })) as ChatMessage[]; + + // Update chat history to match server state + dispatch({ type: 'SET_CHAT_HISTORY', payload: chatHistory }); + + // Also update storage to stay in sync + storage.clearConversation(); + messages.forEach((m) => { + storage.addMessage(m.role === 'user' ? 'user' : 'assistant', m.content); + }); + + // Clear any pending state + dispatch({ type: 'SET_PENDING_TRANSCRIPTION', payload: null }); + pendingLLMResponseRef.current = null; + } + }); + + wsClient.on('flashcards_generated', (data) => { + const payload = data as { flashcards: Flashcard[]; conversationId?: string }; + const cards = payload.flashcards || (data as Flashcard[]); + const conversationId = payload.conversationId; + + // Use conversationId from payload if provided, otherwise use current + const targetConversationId = conversationId || stateRef.current.currentConversationId; + + if (targetConversationId) { + // Conversation exists - store flashcards immediately + const updatedFlashcards = storage.addFlashcardsForConversation( + targetConversationId, + Array.isArray(cards) ? cards : [], + stateRef.current.currentLanguage + ); + + // Only update UI if this is for the current conversation + if (targetConversationId === stateRef.current.currentConversationId) { + dispatch({ type: 'SET_FLASHCARDS', payload: updatedFlashcards }); + } + } else { + // No conversation yet - queue flashcards for later processing + console.log( + `[AppContext] Queuing ${Array.isArray(cards) ? cards.length : 0} flashcards (no conversation yet)` + ); + pendingFlashcardsRef.current = [ + ...pendingFlashcardsRef.current, + ...(Array.isArray(cards) ? cards : []), + ]; + } + }); + + wsClient.on('feedback_generated', (data) => { + const payload = data as FeedbackGeneratedPayload & { conversationId?: string }; + const { messageContent, feedback, conversationId } = payload; + + // Only process feedback if it's for the current conversation + if (!conversationId || conversationId === stateRef.current.currentConversationId) { + dispatch({ type: 'SET_FEEDBACK', payload: { messageContent, feedback } }); + } + }); + + wsClient.on('conversation_ready', (data) => { + const { conversationId, languageCode } = data as { + conversationId: string; + languageCode: string; + }; + + // Ensure all audio is stopped + const audioHandler = audioHandlerRef.current; + const audioPlayer = audioPlayerRef.current; + const ttsAudioPlayer = ttsAudioPlayerRef.current; + audioHandler.stopStreaming(); + audioPlayer.stop(); + ttsAudioPlayer.stop(); + + const storage = storageRef.current; + + // Update language if needed + if (languageCode && languageCode !== stateRef.current.currentLanguage) { + dispatch({ type: 'SET_LANGUAGE', payload: languageCode }); + storage.saveLanguage(languageCode); + } + + // Load the conversation data + const conversationData = storage.getConversation(conversationId); + if (conversationData) { + const chatHistory = conversationData.messages.map((m) => ({ + role: m.role === 'user' ? 'learner' : 'teacher', + content: m.content, + })) as ChatMessage[]; + dispatch({ type: 'SET_CHAT_HISTORY', payload: chatHistory }); + } else { + dispatch({ type: 'SET_CHAT_HISTORY', payload: [] }); + } + + dispatch({ + type: 'SET_CURRENT_CONVERSATION_ID', + payload: conversationId, + }); + storage.setCurrentConversationId(conversationId); + + // Load flashcards for this specific conversation + const flashcards = storage.getFlashcardsForConversation(conversationId); + dispatch({ type: 'SET_FLASHCARDS', payload: flashcards }); + + // Reset streaming state + dispatch({ type: 'RESET_STREAMING_STATE' }); + pendingLLMResponseRef.current = null; + lastMessageWasTextRef.current = false; + pendingFlashcardsRef.current = []; + + // Hide loading screen + dispatch({ type: 'SET_SWITCHING_CONVERSATION', payload: false }); + + console.log(`Conversation ${conversationId} ready with language ${languageCode}`); + }); + + // Connect + wsClient.connect().catch((error) => { + console.error('WebSocket connection failed:', error); + dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'disconnected' }); + }); + + // Cleanup - only runs on unmount + return () => { + wsClient.clearAllListeners(); + wsClient.disconnect(); + }; + }, []); // Empty dependency array - only run on mount/unmount + + // Audio chunk handler + useEffect(() => { + const audioHandler = audioHandlerRef.current; + const wsClient = wsClientRef.current; + + const handleAudioChunk = (audioData: string) => { + wsClient.sendAudioChunk(audioData); + }; + + audioHandler.on('audioChunk', handleAudioChunk); + + return () => { + audioHandler.off('audioChunk', handleAudioChunk); + }; + }, []); + + // User context on connect + useEffect(() => { + const wsClient = wsClientRef.current; + + if (state.connectionStatus === 'connected') { + try { + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; + wsClient.send({ + type: 'user_context', + timezone: tz, + // Use auth user ID - will be null if not authenticated + userId: user?.id || null, + // Send current language so backend initializes with correct language + languageCode: state.currentLanguage, + }); + } catch { + // ignore + } + } + }, [state.connectionStatus, user, state.currentLanguage]); + + // Toggle recording + const toggleRecording = useCallback(async () => { + const audioHandler = audioHandlerRef.current; + + if (!state.isRecording) { + try { + await audioHandler.startStreaming(); + dispatch({ type: 'SET_RECORDING', payload: true }); + dispatch({ type: 'SET_CURRENT_TRANSCRIPT', payload: '' }); + dispatch({ type: 'SET_SPEECH_DETECTED', payload: false }); + } catch (error) { + console.error('Failed to start streaming:', error); + alert( + 'Microphone access denied. Please enable microphone permissions.' + ); + } + } else { + audioHandler.stopStreaming(); + dispatch({ type: 'SET_RECORDING', payload: false }); + dispatch({ type: 'SET_CURRENT_TRANSCRIPT', payload: '' }); + dispatch({ type: 'SET_SPEECH_DETECTED', payload: false }); + } + }, [state.isRecording]); + + // Change UI language preference (for the button and new conversations) + const changeUiLanguage = useCallback((newLanguage: string) => { + const storage = storageRef.current; + + // Only update UI language (for the button and new conversations) + // currentLanguage should only change when switching/creating conversations + dispatch({ type: 'SET_UI_LANGUAGE', payload: newLanguage }); + storage.saveUiLanguage(newLanguage); + }, []); + + // Send text message (bypasses audio/STT) + const sendTextMessage = useCallback( + (text: string) => { + const wsClient = wsClientRef.current; + const storage = storageRef.current; + const trimmedText = text.trim(); + + if (!trimmedText || state.connectionStatus !== 'connected') return; + + let conversationId = stateRef.current.currentConversationId; + let conversationTitle: string | null = null; + + // Auto-create conversation if none exists + if (!conversationId) { + const newConversation = storage.createConversation( + stateRef.current.currentLanguage + ); + conversationId = newConversation.id; + conversationTitle = newConversation.title; + dispatch({ type: 'ADD_CONVERSATION', payload: newConversation }); + dispatch({ + type: 'SET_CURRENT_CONVERSATION_ID', + payload: newConversation.id, + }); + storage.setCurrentConversationId(newConversation.id); + // Process any flashcards that arrived before conversation was created + processPendingFlashcardsRef.current(newConversation.id); + } else { + // Get title from existing conversation in state + const currentConvo = stateRef.current.conversations.find( + (c) => c.id === conversationId + ); + conversationTitle = currentConvo?.title || null; + } + + // Auto-rename conversation on first user message (if still has default name) + if ( + conversationId && + conversationTitle && + /^Chat \d{5}$/.test(conversationTitle) + ) { + const newTitle = + trimmedText.length > 10 + ? trimmedText.slice(0, 10) + '...' + : trimmedText; + storage.renameConversation( + conversationId, + newTitle, + stateRef.current.currentLanguage + ); + dispatch({ + type: 'RENAME_CONVERSATION', + payload: { id: conversationId, title: newTitle }, + }); + } + + // Add user message to chat history immediately (unlike audio where we wait for transcription) + storage.addMessage('user', trimmedText); + dispatch({ + type: 'ADD_MESSAGE', + payload: { role: 'learner', content: trimmedText }, + }); + + // Flag that this was a text message so we can ignore the transcription event + lastMessageWasTextRef.current = true; + + // Send to backend + wsClient.send({ type: 'text_message', text: trimmedText }); + }, + [state.connectionStatus] + ); + + // Pronounce a word using TTS (for flashcard pronunciation) + const pronounceWord = useCallback( + (text: string) => { + const wsClient = wsClientRef.current; + const ttsAudioPlayer = ttsAudioPlayerRef.current; + const trimmedText = text.trim(); + + if (state.connectionStatus !== 'connected' || !trimmedText) return; + + // Stop any currently playing TTS audio + ttsAudioPlayer.stop(); + + // Use the text itself as the card ID for tracking + dispatch({ type: 'SET_PRONOUNCING_CARD_ID', payload: trimmedText }); + wsClient.send({ + type: 'tts_pronounce_request', + text: trimmedText, + }); + }, + [state.connectionStatus, state.currentLanguage] + ); + + // Select a conversation from the sidebar + const selectConversation = useCallback( + (conversationId: string) => { + const storage = storageRef.current; + const audioHandler = audioHandlerRef.current; + const audioPlayer = audioPlayerRef.current; + const ttsAudioPlayer = ttsAudioPlayerRef.current; + const wsClient = wsClientRef.current; + + // Stop any ongoing recording/playback and audio + if (state.isRecording) { + audioHandler.stopStreaming(); + dispatch({ type: 'SET_RECORDING', payload: false }); + } + // Stop main audio playback (TTS responses) + audioPlayer.stop(); + // Stop TTS audio playback (flashcard pronunciation) + ttsAudioPlayer.stop(); + + // Save current conversation first if it exists + if ( + state.currentConversationId && + stateRef.current.chatHistory.length > 0 + ) { + const messages = stateRef.current.chatHistory.map((m) => ({ + role: m.role === 'learner' ? 'user' : 'assistant', + content: m.content, + timestamp: new Date().toISOString(), + })) as import('../types').ConversationMessage[]; + storage.saveConversation( + state.currentConversationId, + messages, + state.currentLanguage + ); + } + + // Find the conversation to get its language + const conversation = stateRef.current.conversations.find( + (c) => c.id === conversationId + ); + const targetLanguage = + conversation?.languageCode || state.currentLanguage; + + // Set language immediately (before waiting for backend response) + // This ensures the language is correct right away, especially on page refresh + if (targetLanguage !== state.currentLanguage) { + dispatch({ type: 'SET_LANGUAGE', payload: targetLanguage }); + storage.saveLanguage(targetLanguage); + } + + // Load the selected conversation data + const conversationData = storage.getConversation(conversationId); + + // Send conversation_switch message to backend + if (state.connectionStatus === 'connected') { + // Stop all audio immediately when switching starts - do this FIRST + audioHandler.stopStreaming(); + audioPlayer.stop(); + ttsAudioPlayer.stop(); + + // Clear any pending audio streams by stopping again after a brief delay + // This ensures any queued audio is also stopped + setTimeout(() => { + audioPlayer.stop(); + ttsAudioPlayer.stop(); + }, 50); + + // Show loading screen + dispatch({ type: 'SET_SWITCHING_CONVERSATION', payload: true }); + + // Don't update UI yet - wait for conversation_ready message + // Send switch request to backend + wsClient.send({ + type: 'conversation_switch', + conversationId: conversationId, + languageCode: targetLanguage, + messages: conversationData?.messages || [], + }); + + // UI will be updated when conversation_ready is received + } else { + + if (conversationData) { + const chatHistory = conversationData.messages.map((m) => ({ + role: m.role === 'user' ? 'learner' : 'teacher', + content: m.content, + })) as ChatMessage[]; + dispatch({ type: 'SET_CHAT_HISTORY', payload: chatHistory }); + } else { + dispatch({ type: 'SET_CHAT_HISTORY', payload: [] }); + } + + dispatch({ + type: 'SET_CURRENT_CONVERSATION_ID', + payload: conversationId, + }); + storage.setCurrentConversationId(conversationId); + + const flashcards = storage.getFlashcardsForConversation(conversationId); + dispatch({ type: 'SET_FLASHCARDS', payload: flashcards }); + + dispatch({ type: 'RESET_STREAMING_STATE' }); + pendingLLMResponseRef.current = null; + lastMessageWasTextRef.current = false; + pendingFlashcardsRef.current = []; + dispatch({ type: 'SET_SWITCHING_CONVERSATION', payload: false }); + } + + // Close sidebar on mobile + dispatch({ type: 'SET_SIDEBAR_OPEN', payload: false }); + }, + [ + state.isRecording, + state.currentConversationId, + state.currentLanguage, + state.connectionStatus, + ] + ); + + // Update ref so initialization can call selectConversation + useEffect(() => { + selectConversationRef.current = selectConversation; + + // If conversations haven't been loaded yet and we have conversations in state, load the current one + if (!conversationsLoadedRef.current && state.conversations.length > 0 && selectConversationRef.current) { + const storage = storageRef.current; + + // Get the stored current conversation ID + let currentId = storage.getCurrentConversationId(); + + // If we have a stored conversation ID, verify it exists + if (currentId) { + const conversationExists = state.conversations.find((c) => c.id === currentId); + if (!conversationExists) { + currentId = null; + } + } + + // If no stored conversation found, use the most recent one + if (!currentId && state.conversations.length > 0) { + currentId = state.conversations[0].id; + } + + // Use selectConversation to load the conversation + if (currentId) { + selectConversationRef.current(currentId); + conversationsLoadedRef.current = true; + } + } + }, [selectConversation, state.conversations]); + + // Create a new conversation + const createNewConversation = useCallback(() => { + const storage = storageRef.current; + const audioHandler = audioHandlerRef.current; + const audioPlayer = audioPlayerRef.current; + const ttsAudioPlayer = ttsAudioPlayerRef.current; + const wsClient = wsClientRef.current; + + // Stop any ongoing recording/playback and audio + if (state.isRecording) { + audioHandler.stopStreaming(); + dispatch({ type: 'SET_RECORDING', payload: false }); + } + // Stop main audio playback (TTS responses) + audioPlayer.stop(); + // Stop TTS audio playback (flashcard pronunciation) + ttsAudioPlayer.stop(); + + // Save current conversation first if it exists + if ( + state.currentConversationId && + stateRef.current.chatHistory.length > 0 + ) { + const messages = stateRef.current.chatHistory.map((m) => ({ + role: m.role === 'learner' ? 'user' : 'assistant', + content: m.content, + timestamp: new Date().toISOString(), + })) as import('../types').ConversationMessage[]; + storage.saveConversation( + state.currentConversationId, + messages, + state.currentLanguage + ); + } + + // Create new conversation with the language shown on the button (uiLanguage) + // This ensures the new conversation uses the language the user selected/expects + const languageForNewConversation = state.uiLanguage; + const newConversation = storage.createConversation( + languageForNewConversation + ); + dispatch({ type: 'ADD_CONVERSATION', payload: newConversation }); + dispatch({ + type: 'SET_CURRENT_CONVERSATION_ID', + payload: newConversation.id, + }); + storage.setCurrentConversationId(newConversation.id); + + // Clear chat and flashcards (new conversation has no flashcards) + dispatch({ type: 'RESET_CONVERSATION' }); + dispatch({ type: 'SET_FLASHCARDS', payload: [] }); + pendingLLMResponseRef.current = null; + lastMessageWasTextRef.current = false; + pendingFlashcardsRef.current = []; + + // For new conversations, send conversation_switch with empty messages + if (state.connectionStatus === 'connected') { + // Stop all audio immediately when switching starts - do this FIRST + audioHandler.stopStreaming(); + audioPlayer.stop(); + ttsAudioPlayer.stop(); + + // Clear any pending audio streams by stopping again after a brief delay + setTimeout(() => { + audioPlayer.stop(); + ttsAudioPlayer.stop(); + }, 50); + + dispatch({ type: 'SET_SWITCHING_CONVERSATION', payload: true }); + + wsClient.send({ + type: 'conversation_switch', + conversationId: newConversation.id, + languageCode: languageForNewConversation, + messages: [], + }); + } else { + dispatch({ type: 'SET_SWITCHING_CONVERSATION', payload: false }); + } + + // Close sidebar on mobile + dispatch({ type: 'SET_SIDEBAR_OPEN', payload: false }); + }, [ + state.isRecording, + state.currentConversationId, + state.uiLanguage, + state.connectionStatus, + ]); + + // Delete a conversation + const deleteConversation = useCallback( + (conversationId: string) => { + const storage = storageRef.current; + + // Find the conversation to get its language code + const conversation = stateRef.current.conversations.find( + (c) => c.id === conversationId + ); + const languageCode = conversation?.languageCode || state.currentLanguage; + + storage.deleteConversation(conversationId, languageCode); + storage.clearFlashcardsForConversation(conversationId); + dispatch({ type: 'REMOVE_CONVERSATION', payload: conversationId }); + + // If we deleted the current conversation, switch to another or create new + if (state.currentConversationId === conversationId) { + const remainingConversations = stateRef.current.conversations.filter( + (c) => c.id !== conversationId + ); + + if (remainingConversations.length > 0) { + selectConversation(remainingConversations[0].id); + } else { + createNewConversation(); + } + } + }, + [ + state.currentLanguage, + state.currentConversationId, + selectConversation, + createNewConversation, + ] + ); + + // Rename a conversation + const renameConversation = useCallback( + (conversationId: string, newTitle: string) => { + const storage = storageRef.current; + const trimmedTitle = newTitle.trim(); + if (!trimmedTitle) return; + + // Find the conversation to get its language code + const conversation = stateRef.current.conversations.find( + (c) => c.id === conversationId + ); + const languageCode = conversation?.languageCode || state.currentLanguage; + + storage.renameConversation(conversationId, trimmedTitle, languageCode); + dispatch({ + type: 'RENAME_CONVERSATION', + payload: { id: conversationId, title: trimmedTitle }, + }); + }, + [state.currentLanguage] + ); + + // Toggle sidebar + const toggleSidebar = useCallback(() => { + dispatch({ + type: 'SET_SIDEBAR_OPEN', + payload: !stateRef.current.sidebarOpen, + }); + }, []); + + // Use direct instances instead of refs for context value + // These instances are stable and don't change, so accessing them during render is safe + const value: AppContextType = useMemo( + () => ({ + state, + dispatch, + storage: storageInstance, + wsClient: wsClientInstance, + audioHandler: audioHandlerInstance, + audioPlayer: audioPlayerInstance, + toggleRecording, + changeUiLanguage, + handleInterrupt, + sendTextMessage, + pronounceWord, + selectConversation, + createNewConversation, + deleteConversation, + renameConversation, + toggleSidebar, + }), + [ + state, + dispatch, + storageInstance, + wsClientInstance, + audioHandlerInstance, + audioPlayerInstance, + toggleRecording, + changeUiLanguage, + handleInterrupt, + sendTextMessage, + pronounceWord, + selectConversation, + createNewConversation, + deleteConversation, + renameConversation, + toggleSidebar, + ] + ); + + return {children}; +} + +export function useApp() { + const context = useContext(AppContext); + if (!context) { + throw new Error('useApp must be used within an AppProvider'); + } + return context; +} diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx new file mode 100644 index 0000000..8334a93 --- /dev/null +++ b/frontend/src/context/AuthContext.tsx @@ -0,0 +1,148 @@ +import { + createContext, + useContext, + useEffect, + useState, + useMemo, + type ReactNode, +} from 'react'; +import { createClient } from '@supabase/supabase-js'; +import type { SupabaseClient, User, Session } from '@supabase/supabase-js'; + +interface AuthContextType { + supabase: SupabaseClient | null; + user: User | null; + session: Session | null; + isLoading: boolean; + isConfigured: boolean; + signUp: ( + email: string, + password: string + ) => Promise<{ error: string | null }>; + signIn: ( + email: string, + password: string + ) => Promise<{ error: string | null }>; + signOut: () => Promise; +} + +const AuthContext = createContext(null); + +// Singleton Supabase client to prevent multiple instances +let supabaseClientInstance: SupabaseClient | null = null; + +function getSupabaseClient(): SupabaseClient | null { + if (supabaseClientInstance) { + return supabaseClientInstance; + } + + const url = import.meta.env.VITE_SUPABASE_URL; + const publishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY; + + if (!url || !publishableKey) { + console.log('Supabase not configured - running in anonymous mode'); + return null; + } + + supabaseClientInstance = createClient(url, publishableKey, { + auth: { + storageKey: 'aprende-auth-token', + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: true, + }, + }); + + return supabaseClientInstance; +} + +export function AuthProvider({ children }: { children: ReactNode }) { + // Use useMemo to ensure client is only created once + const supabase = useMemo(() => getSupabaseClient(), []); + + const [user, setUser] = useState(null); + const [session, setSession] = useState(null); + const isConfigured = supabase !== null; + const [isLoading, setIsLoading] = useState(isConfigured); + + useEffect(() => { + if (!supabase) { + return; + } + + // Get initial session + supabase.auth.getSession().then(({ data: { session } }) => { + setSession(session); + setUser(session?.user ?? null); + setIsLoading(false); + }); + + // Listen for auth changes + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + setSession(session); + setUser(session?.user ?? null); + }); + + return () => subscription.unsubscribe(); + }, [supabase]); + + const signUp = async ( + email: string, + password: string + ): Promise<{ error: string | null }> => { + if (!supabase) return { error: 'Supabase not configured' }; + + const { error } = await supabase.auth.signUp({ + email, + password, + }); + + return { error: error?.message ?? null }; + }; + + const signIn = async ( + email: string, + password: string + ): Promise<{ error: string | null }> => { + if (!supabase) return { error: 'Supabase not configured' }; + + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + + return { error: error?.message ?? null }; + }; + + const signOut = async () => { + if (!supabase) return; + await supabase.auth.signOut(); + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within AuthProvider'); + } + return context; +} diff --git a/frontend/src/hooks/useTranslator.ts b/frontend/src/hooks/useTranslator.ts new file mode 100644 index 0000000..5429d80 --- /dev/null +++ b/frontend/src/hooks/useTranslator.ts @@ -0,0 +1,26 @@ +import { useState, useCallback, useRef } from 'react'; +import { Translator } from '../services/Translator'; + +export function useTranslator() { + const translatorRef = useRef(new Translator()); + const [translation, setTranslation] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const translate = useCallback(async (text: string) => { + setIsLoading(true); + try { + const result = await translatorRef.current.translate(text, 'en', 'auto'); + setTranslation(result); + } catch { + setTranslation(null); + } finally { + setIsLoading(false); + } + }, []); + + const clearTranslation = useCallback(() => { + setTranslation(null); + }, []); + + return { translation, isLoading, translate, clearTranslation }; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..26af896 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,12 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { AuthProvider } from './context/AuthContext'; +import App from './App.tsx'; + +createRoot(document.getElementById('root')!).render( + + + + + +); diff --git a/frontend/js/audio-handler.js b/frontend/src/services/AudioHandler.ts similarity index 73% rename from frontend/js/audio-handler.js rename to frontend/src/services/AudioHandler.ts index 8c51b90..1c0a51f 100644 --- a/frontend/js/audio-handler.js +++ b/frontend/src/services/AudioHandler.ts @@ -1,14 +1,19 @@ +import type { IOSAudioHandler } from '../types'; + +type EventCallback = (data: string) => void; + export class AudioHandler { - constructor() { - this.audioContext = null; - this.workletNode = null; - this.scriptProcessor = null; - this.stream = null; - this.microphone = null; - this.isStreaming = false; - this.listeners = new Map(); + private audioContext: AudioContext | null = null; + private workletNode: AudioWorkletNode | null = null; + private scriptProcessor: ScriptProcessorNode | null = null; + private stream: MediaStream | null = null; + private microphone: MediaStreamAudioSourceNode | null = null; + private isStreaming = false; + private listeners = new Map(); + private isIOS: boolean; + private iosHandler: IOSAudioHandler | null; - // Check for iOS and use iOS handler if available + constructor() { this.isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); @@ -20,41 +25,52 @@ export class AudioHandler { } } - setupIOSEventListeners() { - // Listen for iOS audio unlock event - window.addEventListener('ios-audio-unlocked', (event) => { + private setupIOSEventListeners(): void { + window.addEventListener('ios-audio-unlocked', ((event: CustomEvent) => { console.log('[AudioHandler] iOS audio unlocked'); this.audioContext = event.detail.audioContext; - }); + }) as EventListener); - // Listen for iOS audio errors - window.addEventListener('ios-audio-error', (event) => { + window.addEventListener('ios-audio-error', ((event: CustomEvent) => { console.error('[AudioHandler] iOS audio error:', event.detail.message); this.emit('error', event.detail); - }); + }) as EventListener); - // Listen for iOS audio ended window.addEventListener('ios-audio-ended', () => { console.log('[AudioHandler] iOS audio playback ended'); - this.emit('playback_finished'); + this.emit('playback_finished', ''); }); } - on(event, callback) { + on(event: string, callback: EventCallback): void { if (!this.listeners.has(event)) { this.listeners.set(event, []); } - this.listeners.get(event).push(callback); + this.listeners.get(event)!.push(callback); } - emit(event, data) { + off(event: string, callback: EventCallback): void { + const callbacks = this.listeners.get(event); + if (callbacks) { + const index = callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } + } + } + + clearAllListeners(): void { + this.listeners.clear(); + } + + private emit(event: string, data: string): void { const callbacks = this.listeners.get(event); if (callbacks) { callbacks.forEach((callback) => callback(data)); } } - async startStreaming() { + async startStreaming(): Promise { try { console.log('Starting continuous audio streaming...'); @@ -62,11 +78,9 @@ export class AudioHandler { if (this.isIOS && this.iosHandler) { console.log('[AudioHandler] Using iOS audio handler for microphone'); - // First unlock audio context if needed - await this.iosHandler.unlockAudioContext(); + await this.iosHandler.unlockAudioContext?.(); - // Start microphone with iOS workarounds - const success = await this.iosHandler.startMicrophone((audioData) => { + const success = await this.iosHandler.startMicrophone?.((audioData) => { if (this.isStreaming) { this.emit('audioChunk', audioData); } @@ -92,11 +106,12 @@ export class AudioHandler { this.stream = await navigator.mediaDevices.getUserMedia(constraints); console.log('Microphone access granted for continuous streaming'); - // Create AudioContext for real-time processing - this.audioContext = new (window.AudioContext || - window.webkitAudioContext)(); + this.audioContext = new ( + window.AudioContext || + (window as unknown as { webkitAudioContext: typeof AudioContext }) + .webkitAudioContext + )(); - // Resume AudioContext if suspended (required on many browsers) if (this.audioContext.state === 'suspended') { console.log('Audio context suspended, resuming...'); await this.audioContext.resume(); @@ -104,7 +119,7 @@ export class AudioHandler { this.microphone = this.audioContext.createMediaStreamSource(this.stream); - // Try AudioWorklet first, fallback to ScriptProcessorNode for iOS + // Try AudioWorklet first, fallback to ScriptProcessorNode if (this.audioContext.audioWorklet) { console.log('Setting up AudioWorklet processor...'); await this.setupAudioWorklet(); @@ -121,9 +136,11 @@ export class AudioHandler { } } - async setupAudioWorklet() { + private async setupAudioWorklet(): Promise { + if (!this.audioContext || !this.microphone) return; + try { - await this.audioContext.audioWorklet.addModule('js/audio-processor.js'); + await this.audioContext.audioWorklet.addModule('/audio-processor.js'); console.log('AudioWorklet processor loaded'); this.workletNode = new AudioWorkletNode( @@ -136,9 +153,9 @@ export class AudioHandler { } ); - this.workletNode.port.onmessage = (event) => { + this.workletNode.port.onmessage = (event: MessageEvent) => { if (this.isStreaming) { - const int16Buffer = event.data; + const int16Buffer = event.data as ArrayBuffer; const base64Audio = btoa( String.fromCharCode(...new Uint8Array(int16Buffer)) ); @@ -146,7 +163,6 @@ export class AudioHandler { } }; - // Connect the audio pipeline this.microphone.connect(this.workletNode); this.workletNode.connect(this.audioContext.destination); } catch (error) { @@ -155,7 +171,9 @@ export class AudioHandler { } } - setupScriptProcessorNode() { + private setupScriptProcessorNode(): void { + if (!this.audioContext || !this.microphone) return; + console.log('Setting up ScriptProcessorNode for compatibility...'); const bufferSize = 4096; @@ -167,11 +185,10 @@ export class AudioHandler { const targetSampleRate = 16000; const sourceSampleRate = this.audioContext.sampleRate; - // Maybe we should check if this equals 1 to not do the whole resampling const resampleRatio = sourceSampleRate / targetSampleRate; - let buffer = null; + let buffer: Float32Array | null = null; - this.scriptProcessor.onaudioprocess = (event) => { + this.scriptProcessor.onaudioprocess = (event: AudioProcessingEvent) => { if (this.isStreaming) { const inputData = event.inputBuffer.getChannelData(0); @@ -184,7 +201,7 @@ export class AudioHandler { newBuffer.set(inputData, currentLength); buffer = newBuffer; - // Make it into 16k + // Resample to 16kHz const numOutputSamples = Math.floor(buffer.length / resampleRatio); if (numOutputSamples === 0) return; @@ -202,7 +219,7 @@ export class AudioHandler { lowerValue + (upperValue - lowerValue) * interpolationFactor; } - // Save the remainder of the buffer for the next process call + // Save remainder for next process call const consumedInputSamples = numOutputSamples * resampleRatio; buffer = buffer.slice(Math.round(consumedInputSamples)); @@ -222,18 +239,17 @@ export class AudioHandler { } }; - // Connect the audio pipeline this.microphone.connect(this.scriptProcessor); this.scriptProcessor.connect(this.audioContext.destination); } - stopStreaming() { + stopStreaming(): void { console.log('Stopping continuous audio streaming...'); this.isStreaming = false; // Use iOS handler if available if (this.isIOS && this.iosHandler) { - this.iosHandler.stopMicrophone(); + this.iosHandler.stopMicrophone?.(); console.log('[AudioHandler] iOS microphone stopped'); return; } @@ -268,4 +284,8 @@ export class AudioHandler { console.log('Continuous audio streaming stopped'); } + + getIsStreaming(): boolean { + return this.isStreaming; + } } diff --git a/frontend/js/audio-player.js b/frontend/src/services/AudioPlayer.ts similarity index 60% rename from frontend/js/audio-player.js rename to frontend/src/services/AudioPlayer.ts index 8ee152e..a36a341 100644 --- a/frontend/js/audio-player.js +++ b/frontend/src/services/AudioPlayer.ts @@ -1,16 +1,19 @@ +import type { IOSAudioHandler } from '../types'; + +type EventCallback = () => void; + export class AudioPlayer { - constructor() { - this.audioContext = null; - this.audioQueue = []; - this.isPlaying = false; - this.isStartingPlayback = false; - this.currentSource = null; - this.sampleRate = 16000; // Default sample rate to match backend - this.listeners = new Map(); - this.isStreamingActive = false; - this.streamTimeout = null; + private audioContext: AudioContext | null = null; + private audioQueue: AudioBuffer[] = []; + private isPlaying = false; + private isStartingPlayback = false; + private currentSource: AudioBufferSourceNode | null = null; + private listeners = new Map(); + private streamTimeout: ReturnType | null = null; + private isIOS: boolean; + private iosHandler: IOSAudioHandler | null; - // Check for iOS and use iOS handler if available + constructor() { this.isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); @@ -21,26 +24,28 @@ export class AudioPlayer { } } - on(event, callback) { + on(event: string, callback: EventCallback): void { if (!this.listeners.has(event)) { this.listeners.set(event, []); } - this.listeners.get(event).push(callback); + this.listeners.get(event)!.push(callback); } - emit(event, data) { + private emit(event: string): void { const callbacks = this.listeners.get(event); if (callbacks) { - callbacks.forEach((callback) => callback(data)); + callbacks.forEach((callback) => callback()); } } - async initialize() { + async initialize(): Promise { try { - this.audioContext = new (window.AudioContext || - window.webkitAudioContext)(); + this.audioContext = new ( + window.AudioContext || + (window as unknown as { webkitAudioContext: typeof AudioContext }) + .webkitAudioContext + )(); - // Resume AudioContext if suspended if (this.audioContext.state === 'suspended') { await this.audioContext.resume(); } @@ -55,29 +60,29 @@ export class AudioPlayer { } } - async addAudioStream(base64Audio, sampleRate = 16000, isLastChunk = false) { + async addAudioStream( + base64Audio: string, + sampleRate: number = 16000, + isLastChunk: boolean = false, + audioFormat: 'int16' | 'float32' = 'int16' + ): Promise { if (!base64Audio || base64Audio.length === 0) { console.warn('Empty audio data received'); return; } - // Mark streaming as active - this.isStreamingActive = true; - - // Clear any existing timeout if (this.streamTimeout) { clearTimeout(this.streamTimeout); } - // Set a timeout to detect end of streaming (if no new chunks for 1 second) this.streamTimeout = setTimeout(() => { this.endStreaming(); }, 1000); - // Use iOS handler for audio playback if available + // Use iOS handler if available if (this.isIOS && this.iosHandler) { try { - await this.iosHandler.playAudioChunk(base64Audio, isLastChunk); + await this.iosHandler.playAudioChunk?.(base64Audio, isLastChunk); if (!this.isPlaying) { this.isPlaying = true; this.emit('playback_started'); @@ -97,8 +102,6 @@ export class AudioPlayer { } try { - this.sampleRate = sampleRate; - // Decode base64 to binary const binaryString = atob(base64Audio); const bytes = new Uint8Array(binaryString.length); @@ -107,20 +110,18 @@ export class AudioPlayer { bytes[i] = binaryString.charCodeAt(i); } - // Create audio buffer from the decoded data + // Create audio buffer const audioBuffer = await this.createAudioBuffer( bytes.buffer, - sampleRate + sampleRate, + audioFormat ); - // Queue the audio buffer for playback this.audioQueue.push(audioBuffer); // Start playback immediately if not already playing - // Use a flag to prevent race conditions if (!this.isPlaying && !this.isStartingPlayback) { this.isStartingPlayback = true; - // Use requestAnimationFrame for better timing requestAnimationFrame(() => { this.isStartingPlayback = false; this.playNextBuffer(); @@ -131,32 +132,66 @@ export class AudioPlayer { } } - async createAudioBuffer(arrayBuffer, sampleRate) { - try { - // We know the backend sends raw PCM Int16 data, so skip decode attempt - // and directly process as PCM for faster playback + private async createAudioBuffer( + arrayBuffer: ArrayBuffer, + sampleRate: number, + audioFormat: 'int16' | 'float32' = 'int16' + ): Promise { + if (!this.audioContext) { + throw new Error('Audio context not initialized'); + } + + let numSamples: number; + + console.log( + `[AudioPlayer] createAudioBuffer: format=${audioFormat}, byteLength=${arrayBuffer.byteLength}, sampleRate=${sampleRate}` + ); + + if (audioFormat === 'float32') { + const float32Array = new Float32Array(arrayBuffer); + numSamples = float32Array.length; + console.log( + `[AudioPlayer] Float32 samples: ${numSamples}, first 3 values: [${Array.from( + float32Array.slice(0, 3) + ) + .map((v) => v.toFixed(4)) + .join(', ')}]` + ); + + const audioBuffer = this.audioContext.createBuffer( + 1, + numSamples, + sampleRate + ); + const channelData = audioBuffer.getChannelData(0); + + for (let i = 0; i < numSamples; i++) { + channelData[i] = float32Array[i]; + } + + return audioBuffer; + } else { + // Int16 PCM format const int16Array = new Int16Array(arrayBuffer); + numSamples = int16Array.length; + console.log(`[AudioPlayer] Int16 samples: ${numSamples}`); + const audioBuffer = this.audioContext.createBuffer( 1, - int16Array.length, + numSamples, sampleRate ); const channelData = audioBuffer.getChannelData(0); - // Convert Int16 to Float32 and normalize - for (let i = 0; i < int16Array.length; i++) { + for (let i = 0; i < numSamples; i++) { channelData[i] = int16Array[i] / 32768.0; } return audioBuffer; - } catch (error) { - console.error('Error creating audio buffer:', error); - throw error; } } - playNextBuffer() { - // Prevent multiple simultaneous calls + private playNextBuffer(): void { if (this.isPlaying && this.currentSource) { return; } @@ -167,24 +202,21 @@ export class AudioPlayer { return; } - const audioBuffer = this.audioQueue.shift(); + if (!this.audioContext) return; + + const audioBuffer = this.audioQueue.shift()!; this.isPlaying = true; - // Create buffer source this.currentSource = this.audioContext.createBufferSource(); this.currentSource.buffer = audioBuffer; - // Connect to destination this.currentSource.connect(this.audioContext.destination); - // Set up event handlers this.currentSource.onended = () => { this.currentSource = null; - // Play next buffer in queue this.playNextBuffer(); }; - // Start playback try { this.currentSource.start(0); this.emit('playback_started'); @@ -193,15 +225,20 @@ export class AudioPlayer { console.error('Error starting audio playback:', error); this.currentSource = null; this.isPlaying = false; - // Try next buffer this.playNextBuffer(); } } - stop() { + stop(): void { + // Clear stream timeout + if (this.streamTimeout) { + clearTimeout(this.streamTimeout); + this.streamTimeout = null; + } + // Use iOS handler if available if (this.isIOS && this.iosHandler) { - this.iosHandler.stopAudioPlayback(); + this.iosHandler.stopAudioPlayback?.(); this.isPlaying = false; this.isStartingPlayback = false; this.emit('playback_stopped'); @@ -209,26 +246,24 @@ export class AudioPlayer { } // Standard implementation - // Stop current source first if (this.currentSource) { try { this.currentSource.stop(); this.currentSource.disconnect(); this.currentSource = null; } catch (error) { - // Source might already be stopped, ignore console.warn('Error stopping audio source:', error); } } - // Clear the queue and reset flags + // Clear audio queue to prevent any queued audio from playing this.audioQueue = []; this.isPlaying = false; this.isStartingPlayback = false; this.emit('playback_stopped'); } - destroy() { + destroy(): void { this.stop(); if (this.audioContext) { @@ -239,33 +274,29 @@ export class AudioPlayer { this.listeners.clear(); } - getQueueLength() { - return this.audioQueue.length; - } - - isPlaybackActive() { - return this.isPlaying; - } - - endStreaming() { + private endStreaming(): void { console.log('[AudioPlayer] Stream ended, finalizing audio playback'); - this.isStreamingActive = false; - // Clear timeout if (this.streamTimeout) { clearTimeout(this.streamTimeout); this.streamTimeout = null; } - // For iOS, signal that streaming is complete if (this.isIOS && this.iosHandler) { - this.iosHandler.playAudioChunk('', true); // Send empty chunk with isLastChunk=true + this.iosHandler.playAudioChunk?.('', true); } } - // Method to be called when backend signals stream complete - markStreamComplete() { + markStreamComplete(): void { console.log('[AudioPlayer] Stream marked as complete by backend'); this.endStreaming(); } + + getQueueLength(): number { + return this.audioQueue.length; + } + + isPlaybackActive(): boolean { + return this.isPlaying; + } } diff --git a/frontend/src/services/HybridStorage.ts b/frontend/src/services/HybridStorage.ts new file mode 100644 index 0000000..e33091d --- /dev/null +++ b/frontend/src/services/HybridStorage.ts @@ -0,0 +1,349 @@ +import type { SupabaseClient } from '@supabase/supabase-js'; +import { Storage } from './Storage'; +import { SupabaseStorage } from './SupabaseStorage'; +import type { + Flashcard, + ConversationSummary, + ConversationMessage, +} from '../types'; + +export class HybridStorage extends Storage { + private supabaseStorage: SupabaseStorage | null = null; + private syncInProgress = false; + + setSupabaseClient(supabase: SupabaseClient, userId: string): void { + this.supabaseStorage = new SupabaseStorage(supabase, userId); + } + + clearSupabaseClient(): void { + this.supabaseStorage = null; + } + + isSupabaseConnected(): boolean { + return this.supabaseStorage !== null; + } + + // Override methods to sync with Supabase + + override saveLanguage(languageCode: string): void { + super.saveLanguage(languageCode); + this.supabaseStorage?.saveLanguage(languageCode).catch(console.error); + } + + override saveConversation( + conversationId: string, + messages: ConversationMessage[], + languageCode: string + ): void { + super.saveConversation(conversationId, messages, languageCode); + this.supabaseStorage + ?.saveConversation(conversationId, messages, languageCode) + .catch(console.error); + } + + override createConversation(languageCode: string): ConversationSummary { + const summary = super.createConversation(languageCode); + // Sync to Supabase using the same ID as localStorage + this.supabaseStorage + ?.createConversation(languageCode, summary.title, summary.id) + .catch(console.error); + return summary; + } + + override deleteConversation( + conversationId: string, + languageCode: string + ): void { + super.deleteConversation(conversationId, languageCode); + this.supabaseStorage + ?.deleteConversation(conversationId) + .catch(console.error); + } + + override renameConversation( + conversationId: string, + newTitle: string, + languageCode: string + ): void { + super.renameConversation(conversationId, newTitle, languageCode); + this.supabaseStorage + ?.renameConversation(conversationId, newTitle) + .catch(console.error); + } + + override addFlashcards( + newFlashcards: Flashcard[], + languageCode: string + ): Flashcard[] { + const result = super.addFlashcards(newFlashcards, languageCode); + this.supabaseStorage + ?.addFlashcards(newFlashcards, languageCode) + .catch(console.error); + return result; + } + + override clearFlashcards(languageCode: string): void { + super.clearFlashcards(languageCode); + this.supabaseStorage?.clearFlashcards(languageCode).catch(console.error); + } + + // Per-conversation flashcard methods + override addFlashcardsForConversation( + conversationId: string, + newFlashcards: Flashcard[], + languageCode: string + ): Flashcard[] { + const result = super.addFlashcardsForConversation( + conversationId, + newFlashcards, + languageCode + ); + this.supabaseStorage + ?.addFlashcardsForConversation( + conversationId, + newFlashcards, + languageCode + ) + .catch(console.error); + return result; + } + + override clearFlashcardsForConversation(conversationId: string): void { + super.clearFlashcardsForConversation(conversationId); + this.supabaseStorage + ?.clearFlashcardsForConversation(conversationId) + .catch(console.error); + } + + // Migration: upload localStorage data to Supabase + async migrateToSupabase(languages: string[]): Promise { + if (!this.supabaseStorage || this.syncInProgress) return; + + this.syncInProgress = true; + try { + // Migrate language preference + const language = this.getLanguage(); + await this.supabaseStorage.saveLanguage(language); + + // Migrate conversations and flashcards for each language + for (const lang of languages) { + // Migrate conversations + const conversations = super.getConversationList(lang); + for (const conv of conversations) { + try { + // Create conversation in Supabase using the SAME ID as localStorage + await this.supabaseStorage.createConversation( + lang, + conv.title, + conv.id // Pass the local ID to use in Supabase + ); + + // Get messages and save to Supabase + const data = super.getConversation(conv.id); + if (data && data.messages.length > 0) { + await this.supabaseStorage.saveConversation( + conv.id, + data.messages, + lang + ); + } + } catch (e) { + // Conversation might already exist, that's OK + console.log(`Conversation ${conv.id} may already exist:`, e); + } + } + + // Migrate flashcards + const flashcards = super.getFlashcards(lang); + if (flashcards.length > 0) { + await this.supabaseStorage.addFlashcards(flashcards, lang); + } + } + + console.log('Migration to Supabase complete'); + } finally { + this.syncInProgress = false; + } + } + + // Sync: merge Supabase data with localStorage (on login) + // This preserves any anonymous conversations while also loading cloud data + async syncFromSupabase(languages: string[]): Promise<{ + conversations: Map; + flashcards: Map; + }> { + if (!this.supabaseStorage) { + return { conversations: new Map(), flashcards: new Map() }; + } + + const conversations = new Map(); + const flashcards = new Map(); + + try { + // Sync conversations and flashcards for each language + for (const lang of languages) { + // Get both remote and local conversations + const remoteConversations = + await this.supabaseStorage.getConversationList(lang); + const localConversations = super.getConversationList(lang); + + // Merge: remote conversations + local conversations not in remote + const remoteIds = new Set(remoteConversations.map((c) => c.id)); + const localOnly = localConversations.filter( + (c) => !remoteIds.has(c.id) + ); + const merged = [...remoteConversations, ...localOnly]; + + // Sort by updatedAt descending + merged.sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + ); + + conversations.set(lang, merged); + + // Save merged list to localStorage + try { + localStorage.setItem( + `aprende-conversations-${lang}`, + JSON.stringify(merged) + ); + + // Load remote conversation messages to localStorage + for (const conv of remoteConversations) { + const data = await this.supabaseStorage.getConversation(conv.id); + if (data) { + localStorage.setItem( + `aprende-conversation-${conv.id}`, + JSON.stringify(data) + ); + } + } + } catch (e) { + console.error('Failed to sync conversations to localStorage:', e); + } + + // Merge flashcards: combine remote + local, deduplicate by targetWord + const remoteFlashcards = await this.supabaseStorage.getFlashcards(lang); + const localFlashcards = super.getFlashcards(lang); + + const seenWords = new Set(); + const mergedFlashcards: Flashcard[] = []; + + // Remote flashcards take priority + for (const f of remoteFlashcards) { + const word = (f.targetWord || '').toLowerCase(); + if (!seenWords.has(word)) { + seenWords.add(word); + mergedFlashcards.push(f); + } + } + // Add local flashcards not in remote + for (const f of localFlashcards) { + const word = (f.targetWord || '').toLowerCase(); + if (!seenWords.has(word)) { + seenWords.add(word); + mergedFlashcards.push(f); + } + } + + flashcards.set(lang, mergedFlashcards); + super.saveFlashcards(mergedFlashcards, lang); + } + + console.log('Sync from Supabase complete (merged with local data)'); + } catch (e) { + console.error('Failed to sync from Supabase:', e); + } + + return { conversations, flashcards }; + } + + // Sync ALL conversations from Supabase (regardless of language) + // Returns a flat list merged with local conversations + async syncAllConversationsFromSupabase(): Promise { + if (!this.supabaseStorage) { + return super.getAllConversations(); + } + + try { + // Get all remote conversations (no language filter) + const remoteConversations = + await this.supabaseStorage.getAllConversations(); + const localConversations = super.getAllConversations(); + + // Merge: remote conversations + local conversations not in remote + const remoteIds = new Set(remoteConversations.map((c) => c.id)); + const localOnly = localConversations.filter((c) => !remoteIds.has(c.id)); + const merged = [...remoteConversations, ...localOnly]; + + // Sort by updatedAt descending + merged.sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + ); + + // Save merged conversations to localStorage (grouped by language) + const byLanguage = new Map(); + for (const conv of merged) { + const lang = conv.languageCode; + if (!byLanguage.has(lang)) { + byLanguage.set(lang, []); + } + byLanguage.get(lang)!.push(conv); + } + for (const [lang, convos] of byLanguage) { + localStorage.setItem( + `aprende-conversations-${lang}`, + JSON.stringify(convos) + ); + } + + // Load remote conversation messages and flashcards to localStorage + for (const conv of remoteConversations) { + const data = await this.supabaseStorage.getConversation(conv.id); + if (data) { + localStorage.setItem( + `aprende-conversation-${conv.id}`, + JSON.stringify(data) + ); + } + + // Also sync flashcards for this conversation + const remoteFlashcards = + await this.supabaseStorage.getFlashcardsForConversation(conv.id); + if (remoteFlashcards.length > 0) { + // Merge with any local flashcards (remote takes priority) + const localFlashcards = super.getFlashcardsForConversation(conv.id); + const seenWords = new Set(); + const mergedFlashcards: Flashcard[] = []; + + // Remote flashcards take priority + for (const f of remoteFlashcards) { + const word = (f.targetWord || '').toLowerCase(); + if (!seenWords.has(word)) { + seenWords.add(word); + mergedFlashcards.push(f); + } + } + // Add local flashcards not in remote + for (const f of localFlashcards) { + const word = (f.targetWord || '').toLowerCase(); + if (!seenWords.has(word)) { + seenWords.add(word); + mergedFlashcards.push(f); + } + } + + super.saveFlashcardsForConversation(conv.id, mergedFlashcards); + } + } + + console.log(`Synced ${merged.length} conversations from Supabase`); + return merged; + } catch (e) { + console.error('Failed to sync all conversations from Supabase:', e); + return super.getAllConversations(); + } + } +} diff --git a/frontend/src/services/Storage.ts b/frontend/src/services/Storage.ts new file mode 100644 index 0000000..1fe1c36 --- /dev/null +++ b/frontend/src/services/Storage.ts @@ -0,0 +1,533 @@ +import type { + Flashcard, + ConversationHistory, + ConversationSummary, + ConversationData, + ConversationMessage, +} from '../types'; + +export class Storage { + private storageKey = 'aprende-app-state'; + private conversationKey = 'aprende-conversation-history'; // Legacy single conversation + private conversationsListKeyPrefix = 'aprende-conversations-'; // + languageCode + private conversationDataKeyPrefix = 'aprende-conversation-'; // + conversationId + private flashcardsKey = 'aprende-flashcards'; // Legacy per-language flashcards + private flashcardsConversationKeyPrefix = 'aprende-flashcards-conv-'; // + conversationId + private languageKey = 'aprende-language'; + private uiLanguageKey = 'aprende-ui-language'; + private currentConversationKey = 'aprende-current-conversation'; + + // Language preference methods + getLanguage(): string { + try { + return localStorage.getItem(this.languageKey) || 'es'; + } catch (error) { + console.error('Failed to load language from localStorage:', error); + return 'es'; + } + } + + saveLanguage(languageCode: string): void { + try { + localStorage.setItem(this.languageKey, languageCode); + } catch (error) { + console.error('Failed to save language to localStorage:', error); + } + } + + getUiLanguage(): string { + try { + return localStorage.getItem(this.uiLanguageKey) || 'es'; + } catch (error) { + console.error('Failed to load UI language from localStorage:', error); + return 'es'; + } + } + + saveUiLanguage(languageCode: string): void { + try { + localStorage.setItem(this.uiLanguageKey, languageCode); + } catch (error) { + console.error('Failed to save UI language to localStorage:', error); + } + } + + // State methods + saveState(state: { chatHistory: unknown[] }): void { + try { + const serializedState = JSON.stringify(state); + localStorage.setItem(this.storageKey, serializedState); + } catch (error) { + console.error('Failed to save state to localStorage:', error); + } + } + + getState(): { chatHistory: unknown[] } | null { + try { + const serializedState = localStorage.getItem(this.storageKey); + if (serializedState === null) { + return null; + } + return JSON.parse(serializedState); + } catch (error) { + console.error('Failed to load state from localStorage:', error); + return null; + } + } + + // Conversation history methods + getConversationHistory(): ConversationHistory { + try { + const serializedHistory = localStorage.getItem(this.conversationKey); + if (serializedHistory === null) { + return { messages: [] }; + } + return JSON.parse(serializedHistory); + } catch (error) { + console.error( + 'Failed to load conversation history from localStorage:', + error + ); + return { messages: [] }; + } + } + + addMessage(role: 'user' | 'assistant', content: string): ConversationHistory { + const history = this.getConversationHistory(); + + const message = { + role, + content, + timestamp: new Date().toISOString(), + }; + + history.messages.push(message); + + // Truncate to keep only last 40 turns (80 messages) + if (history.messages.length > 80) { + history.messages = history.messages.slice(-80); + } + + try { + const serializedHistory = JSON.stringify(history); + localStorage.setItem(this.conversationKey, serializedHistory); + } catch (error) { + console.error( + 'Failed to save conversation history to localStorage:', + error + ); + } + + return history; + } + + clearConversation(): void { + try { + localStorage.removeItem(this.conversationKey); + } catch (error) { + console.error( + 'Failed to clear conversation history from localStorage:', + error + ); + } + } + + // Multi-conversation methods + + private generateId(): string { + return typeof crypto !== 'undefined' && crypto.randomUUID + ? crypto.randomUUID() + : `c_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; + } + + getConversationList(languageCode: string): ConversationSummary[] { + try { + const key = this.conversationsListKeyPrefix + languageCode; + const data = localStorage.getItem(key); + if (!data) { + // Check for legacy data to migrate + return this.migrateToMultiConversation(languageCode); + } + return JSON.parse(data); + } catch (error) { + console.error('Failed to load conversation list:', error); + return []; + } + } + + private migrateToMultiConversation( + languageCode: string + ): ConversationSummary[] { + // Check if there's legacy conversation data + const legacyData = localStorage.getItem(this.conversationKey); + const currentLanguage = this.getLanguage(); + + // Only migrate if we have legacy data AND we're loading for the same language + if (legacyData && currentLanguage === languageCode) { + try { + const legacy = JSON.parse(legacyData) as ConversationHistory; + if (legacy.messages && legacy.messages.length > 0) { + // Create first conversation from legacy data + const conversationId = this.generateId(); + const now = new Date().toISOString(); + const randomNumbers = Math.floor(10000 + Math.random() * 90000); + const summary: ConversationSummary = { + id: conversationId, + title: `Chat ${randomNumbers}`, + languageCode, + createdAt: now, + updatedAt: now, + }; + + const conversationData: ConversationData = { + id: conversationId, + messages: legacy.messages, + }; + + // Save the migrated data + localStorage.setItem( + this.conversationsListKeyPrefix + languageCode, + JSON.stringify([summary]) + ); + localStorage.setItem( + this.conversationDataKeyPrefix + conversationId, + JSON.stringify(conversationData) + ); + localStorage.setItem( + this.currentConversationKey, + conversationId + ); + + // Remove legacy data + localStorage.removeItem(this.conversationKey); + + return [summary]; + } + } catch (error) { + console.error('Failed to migrate legacy conversation:', error); + } + } + return []; + } + + getConversation(conversationId: string): ConversationData | null { + try { + const key = this.conversationDataKeyPrefix + conversationId; + const data = localStorage.getItem(key); + if (!data) return null; + return JSON.parse(data); + } catch (error) { + console.error('Failed to load conversation:', error); + return null; + } + } + + saveConversation( + conversationId: string, + messages: ConversationMessage[], + languageCode: string + ): void { + try { + const conversationData: ConversationData = { + id: conversationId, + messages, + }; + localStorage.setItem( + this.conversationDataKeyPrefix + conversationId, + JSON.stringify(conversationData) + ); + + // Update the summary's updatedAt timestamp + const list = this.getConversationList(languageCode); + const index = list.findIndex((c) => c.id === conversationId); + if (index !== -1) { + list[index].updatedAt = new Date().toISOString(); + localStorage.setItem( + this.conversationsListKeyPrefix + languageCode, + JSON.stringify(list) + ); + } + } catch (error) { + console.error('Failed to save conversation:', error); + } + } + + createConversation(languageCode: string): ConversationSummary { + const list = this.getConversationList(languageCode); + const now = new Date().toISOString(); + const conversationId = this.generateId(); + const randomNumbers = Math.floor(10000 + Math.random() * 90000); + + const summary: ConversationSummary = { + id: conversationId, + title: `Chat ${randomNumbers}`, + languageCode, + createdAt: now, + updatedAt: now, + }; + + // Add to beginning of list (most recent first) + list.unshift(summary); + + localStorage.setItem( + this.conversationsListKeyPrefix + languageCode, + JSON.stringify(list) + ); + + // Create empty conversation data + const conversationData: ConversationData = { + id: conversationId, + messages: [], + }; + localStorage.setItem( + this.conversationDataKeyPrefix + conversationId, + JSON.stringify(conversationData) + ); + + return summary; + } + + deleteConversation(conversationId: string, languageCode: string): void { + try { + // Remove conversation data + localStorage.removeItem(this.conversationDataKeyPrefix + conversationId); + + // Remove from list + const list = this.getConversationList(languageCode); + const filtered = list.filter((c) => c.id !== conversationId); + localStorage.setItem( + this.conversationsListKeyPrefix + languageCode, + JSON.stringify(filtered) + ); + + // If this was the current conversation, clear current + const currentId = this.getCurrentConversationId(); + if (currentId === conversationId) { + localStorage.removeItem(this.currentConversationKey); + } + } catch (error) { + console.error('Failed to delete conversation:', error); + } + } + + renameConversation( + conversationId: string, + newTitle: string, + languageCode: string + ): void { + try { + const list = this.getConversationList(languageCode); + const index = list.findIndex((c) => c.id === conversationId); + if (index !== -1) { + list[index].title = newTitle; + list[index].updatedAt = new Date().toISOString(); + localStorage.setItem( + this.conversationsListKeyPrefix + languageCode, + JSON.stringify(list) + ); + } + } catch (error) { + console.error('Failed to rename conversation:', error); + } + } + + getCurrentConversationId(): string | null { + try { + return localStorage.getItem(this.currentConversationKey); + } catch { + return null; + } + } + + setCurrentConversationId(conversationId: string): void { + try { + localStorage.setItem(this.currentConversationKey, conversationId); + } catch (error) { + console.error('Failed to set current conversation:', error); + } + } + + getAllConversations(): ConversationSummary[] { + try { + const allConversations: ConversationSummary[] = []; + // Iterate through all localStorage keys to find conversation lists + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(this.conversationsListKeyPrefix)) { + const data = localStorage.getItem(key); + if (data) { + const conversations = JSON.parse(data) as ConversationSummary[]; + allConversations.push(...conversations); + } + } + } + // Sort by updatedAt descending (most recent first) + allConversations.sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + ); + return allConversations; + } catch (error) { + console.error('Failed to get all conversations:', error); + return []; + } + } + + // Flashcard methods + private getFlashcardsKey(languageCode?: string): string { + if (!languageCode) { + return this.flashcardsKey; + } + return `${this.flashcardsKey}-${languageCode}`; + } + + getFlashcards(languageCode: string): Flashcard[] { + try { + const key = this.getFlashcardsKey(languageCode); + const serializedFlashcards = localStorage.getItem(key); + if (serializedFlashcards === null) { + // Try to migrate from old format if no language-specific data exists + if (languageCode === 'es') { + const oldFlashcards = localStorage.getItem(this.flashcardsKey); + if (oldFlashcards) { + const parsed = JSON.parse(oldFlashcards) as Flashcard[]; + const migrated = parsed.map((card) => ({ + ...card, + targetWord: card.targetWord || card.spanish || '', + languageCode: 'es', + })); + this.saveFlashcards(migrated, 'es'); + return migrated; + } + } + return []; + } + return JSON.parse(serializedFlashcards); + } catch (error) { + console.error('Failed to load flashcards from localStorage:', error); + return []; + } + } + + saveFlashcards(flashcards: Flashcard[], languageCode: string): void { + try { + const key = this.getFlashcardsKey(languageCode); + const serializedFlashcards = JSON.stringify(flashcards); + localStorage.setItem(key, serializedFlashcards); + } catch (error) { + console.error('Failed to save flashcards to localStorage:', error); + } + } + + addFlashcards(newFlashcards: Flashcard[], languageCode: string): Flashcard[] { + const existingFlashcards = this.getFlashcards(languageCode); + + // Filter out duplicates + const uniqueNewFlashcards = newFlashcards.filter((newCard) => { + const newWord = newCard.targetWord || newCard.spanish || ''; + return !existingFlashcards.some((existing) => { + const existingWord = existing.targetWord || existing.spanish || ''; + return existingWord.toLowerCase() === newWord.toLowerCase(); + }); + }); + + // Add language code to new flashcards + const flashcardsWithLanguage = uniqueNewFlashcards.map((card) => ({ + ...card, + targetWord: card.targetWord || card.spanish || '', + languageCode, + })); + + const updatedFlashcards = [ + ...existingFlashcards, + ...flashcardsWithLanguage, + ]; + + // Keep only the last 100 flashcards per language + if (updatedFlashcards.length > 100) { + updatedFlashcards.splice(0, updatedFlashcards.length - 100); + } + + this.saveFlashcards(updatedFlashcards, languageCode); + return updatedFlashcards; + } + + clearFlashcards(languageCode: string): void { + try { + const key = this.getFlashcardsKey(languageCode); + localStorage.removeItem(key); + } catch (error) { + console.error('Failed to clear flashcards from localStorage:', error); + } + } + + // Per-conversation flashcard methods + getFlashcardsForConversation(conversationId: string): Flashcard[] { + try { + const key = this.flashcardsConversationKeyPrefix + conversationId; + const data = localStorage.getItem(key); + if (!data) return []; + return JSON.parse(data); + } catch (error) { + console.error('Failed to load flashcards for conversation:', error); + return []; + } + } + + saveFlashcardsForConversation( + conversationId: string, + flashcards: Flashcard[] + ): void { + try { + const key = this.flashcardsConversationKeyPrefix + conversationId; + localStorage.setItem(key, JSON.stringify(flashcards)); + } catch (error) { + console.error('Failed to save flashcards for conversation:', error); + } + } + + addFlashcardsForConversation( + conversationId: string, + newFlashcards: Flashcard[], + languageCode: string + ): Flashcard[] { + const existingFlashcards = + this.getFlashcardsForConversation(conversationId); + + // Filter out duplicates + const uniqueNewFlashcards = newFlashcards.filter((newCard) => { + const newWord = newCard.targetWord || newCard.spanish || ''; + return !existingFlashcards.some((existing) => { + const existingWord = existing.targetWord || existing.spanish || ''; + return existingWord.toLowerCase() === newWord.toLowerCase(); + }); + }); + + // Add conversation ID and language code to new flashcards + const flashcardsWithIds = uniqueNewFlashcards.map((card) => ({ + ...card, + targetWord: card.targetWord || card.spanish || '', + conversationId, + languageCode, + })); + + const updatedFlashcards = [...existingFlashcards, ...flashcardsWithIds]; + + // Keep only the last 100 flashcards per conversation + if (updatedFlashcards.length > 100) { + updatedFlashcards.splice(0, updatedFlashcards.length - 100); + } + + this.saveFlashcardsForConversation(conversationId, updatedFlashcards); + return updatedFlashcards; + } + + clearFlashcardsForConversation(conversationId: string): void { + try { + const key = this.flashcardsConversationKeyPrefix + conversationId; + localStorage.removeItem(key); + } catch (error) { + console.error('Failed to clear flashcards for conversation:', error); + } + } +} diff --git a/frontend/src/services/SupabaseStorage.ts b/frontend/src/services/SupabaseStorage.ts new file mode 100644 index 0000000..29959bd --- /dev/null +++ b/frontend/src/services/SupabaseStorage.ts @@ -0,0 +1,297 @@ +import type { SupabaseClient } from '@supabase/supabase-js'; +import type { + Flashcard, + ConversationSummary, + ConversationData, + ConversationMessage, +} from '../types'; + +export class SupabaseStorage { + private supabase: SupabaseClient; + private userId: string; + + constructor(supabase: SupabaseClient, userId: string) { + this.supabase = supabase; + this.userId = userId; + } + + // User preferences + + async getLanguage(): Promise { + const { data } = await this.supabase + .from('user_preferences') + .select('language_code') + .eq('user_id', this.userId) + .maybeSingle(); + return data?.language_code ?? 'es'; + } + + async saveLanguage(languageCode: string): Promise { + await this.supabase.from('user_preferences').upsert( + { + user_id: this.userId, + language_code: languageCode, + updated_at: new Date().toISOString(), + }, + { onConflict: 'user_id' } + ); + } + + // Conversations + + async getConversationList( + languageCode: string + ): Promise { + const { data } = await this.supabase + .from('conversations') + .select('id, title, language_code, created_at, updated_at') + .eq('user_id', this.userId) + .eq('language_code', languageCode) + .order('updated_at', { ascending: false }); + + return (data ?? []).map((c) => ({ + id: c.id, + title: c.title, + languageCode: c.language_code, + createdAt: c.created_at, + updatedAt: c.updated_at, + })); + } + + async getAllConversations(): Promise { + const { data } = await this.supabase + .from('conversations') + .select('id, title, language_code, created_at, updated_at') + .eq('user_id', this.userId) + .order('updated_at', { ascending: false }); + + return (data ?? []).map((c) => ({ + id: c.id, + title: c.title, + languageCode: c.language_code, + createdAt: c.created_at, + updatedAt: c.updated_at, + })); + } + + async getConversation( + conversationId: string + ): Promise { + const { data: messages } = await this.supabase + .from('conversation_messages') + .select('role, content, created_at') + .eq('conversation_id', conversationId) + .order('created_at', { ascending: true }); + + if (!messages) return null; + + return { + id: conversationId, + messages: messages.map((m) => ({ + role: m.role as 'user' | 'assistant', + content: m.content, + timestamp: m.created_at, + })), + }; + } + + async saveConversation( + conversationId: string, + messages: ConversationMessage[], + _languageCode: string + ): Promise { + // Delete existing messages for this conversation + await this.supabase + .from('conversation_messages') + .delete() + .eq('conversation_id', conversationId); + + // Insert new messages + if (messages.length > 0) { + await this.supabase.from('conversation_messages').insert( + messages.map((m) => ({ + conversation_id: conversationId, + role: m.role, + content: m.content, + created_at: m.timestamp || new Date().toISOString(), + })) + ); + } + + // Update conversation's updated_at + await this.supabase + .from('conversations') + .update({ updated_at: new Date().toISOString() }) + .eq('id', conversationId); + } + + async createConversation( + languageCode: string, + title: string, + id?: string + ): Promise { + const now = new Date().toISOString(); + const insertData: Record = { + user_id: this.userId, + language_code: languageCode, + title, + created_at: now, + updated_at: now, + }; + + // Use provided ID if given (to match localStorage ID) + if (id) { + insertData.id = id; + + // Check if conversation already exists + const { data: existing } = await this.supabase + .from('conversations') + .select('id, title, language_code, created_at, updated_at') + .eq('id', id) + .maybeSingle(); + + if (existing) { + return { + id: existing.id, + title: existing.title, + languageCode: existing.language_code, + createdAt: existing.created_at, + updatedAt: existing.updated_at, + }; + } + } + + const { data, error } = await this.supabase + .from('conversations') + .insert(insertData) + .select('id, title, language_code, created_at, updated_at') + .single(); + + if (error || !data) { + throw new Error(`Failed to create conversation: ${error?.message}`); + } + + return { + id: data.id, + title: data.title, + languageCode: data.language_code, + createdAt: data.created_at, + updatedAt: data.updated_at, + }; + } + + async deleteConversation(conversationId: string): Promise { + // Messages will be cascade deleted + await this.supabase.from('conversations').delete().eq('id', conversationId); + } + + async renameConversation( + conversationId: string, + newTitle: string + ): Promise { + await this.supabase + .from('conversations') + .update({ title: newTitle, updated_at: new Date().toISOString() }) + .eq('id', conversationId); + } + + // Flashcards + + async getFlashcards(languageCode: string): Promise { + const { data } = await this.supabase + .from('flashcards') + .select('*') + .eq('user_id', this.userId) + .eq('language_code', languageCode) + .order('created_at', { ascending: false }) + .limit(100); + + return (data ?? []).map((f) => ({ + targetWord: f.target_word, + english: f.english, + example: f.example, + mnemonic: f.mnemonic, + timestamp: f.created_at, + languageCode: f.language_code, + })); + } + + async addFlashcards( + flashcards: Flashcard[], + languageCode: string + ): Promise { + const toInsert = flashcards.map((f) => ({ + user_id: this.userId, + language_code: languageCode, + target_word: f.targetWord || f.spanish || '', + english: f.english, + example: f.example, + mnemonic: f.mnemonic, + })); + + // Use upsert to handle duplicates gracefully + await this.supabase + .from('flashcards') + .upsert(toInsert, { onConflict: 'user_id,language_code,target_word' }); + } + + async clearFlashcards(languageCode: string): Promise { + await this.supabase + .from('flashcards') + .delete() + .eq('user_id', this.userId) + .eq('language_code', languageCode); + } + + // Per-conversation flashcard methods + async getFlashcardsForConversation( + conversationId: string + ): Promise { + const { data } = await this.supabase + .from('flashcards') + .select('*') + .eq('user_id', this.userId) + .eq('conversation_id', conversationId) + .order('created_at', { ascending: false }) + .limit(100); + + return (data ?? []).map((f) => ({ + targetWord: f.target_word, + english: f.english, + example: f.example, + mnemonic: f.mnemonic, + timestamp: f.created_at, + languageCode: f.language_code, + conversationId: f.conversation_id, + })); + } + + async addFlashcardsForConversation( + conversationId: string, + flashcards: Flashcard[], + languageCode: string + ): Promise { + const toInsert = flashcards.map((f) => ({ + user_id: this.userId, + conversation_id: conversationId, + language_code: languageCode, + target_word: f.targetWord || f.spanish || '', + english: f.english, + example: f.example, + mnemonic: f.mnemonic, + })); + + // Use upsert to handle duplicates gracefully + await this.supabase + .from('flashcards') + .upsert(toInsert, { onConflict: 'user_id,conversation_id,target_word' }); + } + + async clearFlashcardsForConversation(conversationId: string): Promise { + await this.supabase + .from('flashcards') + .delete() + .eq('user_id', this.userId) + .eq('conversation_id', conversationId); + } +} diff --git a/frontend/src/services/Translator.ts b/frontend/src/services/Translator.ts new file mode 100644 index 0000000..9cc9fe6 --- /dev/null +++ b/frontend/src/services/Translator.ts @@ -0,0 +1,77 @@ +export class Translator { + private cache = new Map(); + private pendingRequests = new Map>(); + + async translate( + text: string, + targetLang: string = 'en', + sourceLang: string = 'auto' + ): Promise { + if (!text || !text.trim()) { + return ''; + } + + const cacheKey = `${sourceLang}:${targetLang}:${text}`; + + // Check cache first + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey)!; + } + + // Check if there's already a pending request + if (this.pendingRequests.has(cacheKey)) { + return this.pendingRequests.get(cacheKey)!; + } + + // Create the translation request + const requestPromise = this.fetchTranslation(text, targetLang, sourceLang) + .then((translation) => { + this.cache.set(cacheKey, translation); + this.pendingRequests.delete(cacheKey); + return translation; + }) + .catch((error) => { + this.pendingRequests.delete(cacheKey); + throw error; + }); + + this.pendingRequests.set(cacheKey, requestPromise); + return requestPromise; + } + + private async fetchTranslation( + text: string, + targetLang: string, + sourceLang: string + ): Promise { + const encodedText = encodeURIComponent(text); + const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodedText}`; + + try { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Translation failed: ${response.status}`); + } + + const data = await response.json(); + + // Google's response format: [[["translated text", "original text", ...], ...], ...] + if (data && data[0]) { + const translatedParts = data[0] + .filter((part: unknown[]) => part && part[0]) + .map((part: unknown[]) => part[0]); + return translatedParts.join(''); + } + + throw new Error('Invalid translation response format'); + } catch (error) { + console.error('[Translator] Translation error:', error); + throw error; + } + } + + clearCache(): void { + this.cache.clear(); + } +} diff --git a/frontend/src/services/WebSocketClient.ts b/frontend/src/services/WebSocketClient.ts new file mode 100644 index 0000000..69c5869 --- /dev/null +++ b/frontend/src/services/WebSocketClient.ts @@ -0,0 +1,374 @@ +import type { Flashcard } from '../types'; + +type EventCallback = (data: unknown) => void; + +export class WebSocketClient { + private url: string; + private ws: WebSocket | null = null; + private listeners = new Map(); + private reconnectAttempts = 0; + private maxReconnectAttempts = 5; + private reconnectDelay = 1000; + private pingInterval: ReturnType | null = null; + private isIOS: boolean; + private isConnecting = false; + private isIntentionalDisconnect = false; + private connectionId = 0; // Track current connection to ignore stale handlers + + constructor(url: string) { + this.url = url; + + // Check for iOS + this.isIOS = + /iPad|iPhone|iPod/.test(navigator.userAgent) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + + if (this.isIOS && window.iosAudioHandler) { + const optimizedUrl = window.iosAudioHandler.getOptimizedWebSocketURL?.(); + if (optimizedUrl) { + console.log( + '[WebSocketClient] Using iOS-optimized WebSocket URL:', + optimizedUrl + ); + this.url = optimizedUrl; + } + } + } + + on(event: string, callback: EventCallback): void { + if (!this.listeners.has(event)) { + this.listeners.set(event, []); + } + this.listeners.get(event)!.push(callback); + } + + off(event: string, callback: EventCallback): void { + const callbacks = this.listeners.get(event); + if (callbacks) { + const index = callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } + } + } + + clearAllListeners(): void { + this.listeners.clear(); + } + + isConnected(): boolean { + return this.ws !== null && this.ws.readyState === WebSocket.OPEN; + } + + private emit(event: string, data?: unknown): void { + const callbacks = this.listeners.get(event); + console.log( + `[WebSocketClient] emit('${event}'), listeners:`, + callbacks?.length ?? 0 + ); + if (callbacks) { + callbacks.forEach((callback) => callback(data)); + } + } + + async connect(): Promise { + // Prevent duplicate connections + if (this.isConnecting) { + console.log('[WebSocketClient] Connection already in progress, skipping'); + return Promise.resolve(); + } + if (this.isConnected()) { + console.log('[WebSocketClient] Already connected, skipping'); + return Promise.resolve(); + } + + this.isConnecting = true; + this.isIntentionalDisconnect = false; + + // Increment connection ID - this allows us to ignore handlers from stale connections + const thisConnectionId = ++this.connectionId; + + return new Promise((resolve, reject) => { + try { + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + // Ignore if this is a stale connection + if (thisConnectionId !== this.connectionId) { + console.log( + '[WebSocketClient] Ignoring onopen from stale connection' + ); + return; + } + + console.log('WebSocket connected'); + this.isConnecting = false; + this.reconnectAttempts = 0; + this.emit('connection', 'connected'); + + // Start ping/pong for iOS + if (this.isIOS) { + this.startPingPong(); + } + + resolve(); + }; + + this.ws.onmessage = (event: MessageEvent) => { + // Ignore messages from stale connections + if (thisConnectionId !== this.connectionId) { + return; + } + + try { + const message = JSON.parse(event.data); + this.handleMessage(message); + } catch (error) { + console.error('Failed to parse WebSocket message:', error); + } + }; + + this.ws.onclose = (event: CloseEvent) => { + // Ignore if this is a stale connection + if (thisConnectionId !== this.connectionId) { + console.log( + '[WebSocketClient] Ignoring onclose from stale connection' + ); + return; + } + + console.log('WebSocket disconnected:', event.code, event.reason); + this.isConnecting = false; + this.emit('connection', 'disconnected'); + + this.stopPingPong(); + + // Only attempt reconnect if not intentionally disconnected + if ( + !this.isIntentionalDisconnect && + !event.wasClean && + this.reconnectAttempts < this.maxReconnectAttempts + ) { + this.scheduleReconnect(); + } + }; + + this.ws.onerror = (error) => { + // Ignore if this is a stale connection + if (thisConnectionId !== this.connectionId) { + console.log( + '[WebSocketClient] Ignoring onerror from stale connection' + ); + return; + } + + console.error('WebSocket error:', error); + this.isConnecting = false; + this.emit('connection', 'disconnected'); + reject(error); + }; + } catch (error) { + this.isConnecting = false; + reject(error); + } + }); + } + + private scheduleReconnect(): void { + this.reconnectAttempts++; + const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); + + console.log( + `Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})` + ); + + setTimeout(() => { + this.emit('connection', 'connecting'); + this.connect().catch(() => { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error('Max reconnection attempts reached'); + this.emit('connection', 'disconnected'); + } + }); + }, delay); + } + + private handleMessage(message: { + type: string; + [key: string]: unknown; + }): void { + switch (message.type) { + + case 'transcription': + this.emit('transcription', { + text: message.text, + timestamp: message.timestamp, + }); + break; + + case 'ai_response': + this.emit('ai_response', { + text: (message.data as { text: string }).text, + audio: (message.data as { audio?: string }).audio, + }); + break; + + case 'flashcard_generated': + this.emit('flashcard_generated', message.data); + break; + + case 'flashcards_generated': + this.emit('flashcards_generated', message.flashcards as Flashcard[]); + break; + + case 'feedback_generated': + this.emit('feedback_generated', { + messageContent: message.messageContent, + feedback: message.feedback, + }); + break; + + case 'introduction_state_updated': + this.emit('introduction_state_updated', message.introduction_state); + break; + + case 'connection_status': + // Connection status received + break; + + case 'speech_detected': + this.emit('speech_detected', message.data); + break; + + case 'speech_ended': + this.emit('speech_ended', message.data); + break; + + case 'partial_transcript': + this.emit('partial_transcript', { + text: message.text, + interactionId: message.interactionId, + timestamp: message.timestamp, + }); + break; + + case 'llm_response_chunk': + this.emit('llm_response_chunk', { + text: message.text, + timestamp: message.timestamp, + }); + break; + + case 'llm_response_complete': + this.emit('llm_response_complete', { + text: message.text, + timestamp: message.timestamp, + }); + break; + + case 'audio_stream': + this.emit('audio_stream', { + audio: message.audio, + audioFormat: message.audioFormat || 'int16', + sampleRate: message.sampleRate, + timestamp: message.timestamp, + }); + break; + + case 'audio_stream_complete': + this.emit('audio_stream_complete', { + timestamp: message.timestamp, + }); + break; + + case 'interrupt': + this.emit('interrupt', { reason: message.reason }); + break; + + case 'conversation_rollback': + this.emit('conversation_rollback', { + messages: message.messages, + removedCount: message.removedCount, + timestamp: message.timestamp, + }); + break; + + case 'language_changed': + this.emit('language_changed', { + languageCode: message.languageCode, + languageName: message.languageName, + }); + break; + + case 'tts_pronounce_audio': + this.emit('tts_pronounce_audio', { + audio: message.audio, + audioFormat: message.audioFormat || 'float32', + sampleRate: message.sampleRate, + }); + break; + + case 'tts_pronounce_complete': + this.emit('tts_pronounce_complete', {}); + break; + + case 'tts_pronounce_error': + this.emit('tts_pronounce_error', { + error: message.error, + }); + break; + + case 'conversation_ready': + this.emit('conversation_ready', { + conversationId: message.conversationId, + languageCode: message.languageCode, + }); + break; + + default: + console.log('Unknown message type:', message.type); + } + } + + send(message: { type: string; [key: string]: unknown }): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(message)); + } + } + + sendAudioChunk(audioData: string): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + const message = { + type: 'audio_chunk', + audio_data: audioData, + }; + this.ws.send(JSON.stringify(message)); + } + } + + disconnect(): void { + this.isIntentionalDisconnect = true; + this.isConnecting = false; + this.stopPingPong(); + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } + + private startPingPong(): void { + this.pingInterval = setInterval(() => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.send({ type: 'ping' }); + console.log('[WebSocketClient] Ping sent to keep connection alive'); + } + }, 30000); + } + + private stopPingPong(): void { + if (this.pingInterval) { + clearInterval(this.pingInterval); + this.pingInterval = null; + } + } +} diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css new file mode 100644 index 0000000..ed19f15 --- /dev/null +++ b/frontend/src/styles/main.css @@ -0,0 +1,1858 @@ +/* Reset and Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Roboto Serif', Georgia, Cambria, 'Times New Roman', serif; + background: #fafafa; + color: #1a1a1a; + line-height: 1.5; + font-feature-settings: "liga" 1, "onum" 0, "pnum" 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; +} + +/* App Layout with Sidebar */ +.app-wrapper { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} + +.app-layout { + display: flex; + flex: 1; + min-height: 0; +} + +.app-main { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; +} + +/* Sidebar */ +.sidebar { + width: 260px; + height: 100%; + background: #f9fafb; + border-right: 1px solid #e5e5e5; + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +/* Sidebar Brand */ +.sidebar-brand { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + min-height: 57px; + background: #ffffff; +} + +.sidebar-logo { + font-size: 18px; + font-weight: 600; + color: #1a1a1a; + margin: 0; +} + +.sidebar-header { + padding: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +.new-chat-wrapper { + display: flex; + flex: 1; + position: relative; +} + +.new-chat-button { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + padding: 10px 16px; + background: #1a1a1a; + color: #ffffff; + border: none; + border-radius: 8px 0 0 8px; + font-family: inherit; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.new-chat-button:hover { + background: #333333; +} + +.new-chat-lang-button { + display: flex; + align-items: center; + gap: 4px; + padding: 10px 12px; + background: #1a1a1a; + color: #ffffff; + border: none; + border-left: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 0 8px 8px 0; + cursor: pointer; + transition: all 0.2s ease; +} + +.new-chat-lang-button:hover { + background: #333333; +} + +.new-chat-lang-button .lang-flag { + font-size: 18px; + line-height: 1; +} + +.new-chat-lang-dropdown { + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + background: #ffffff; + border: 1px solid #e5e5e5; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + z-index: 100; + overflow: hidden; +} + +.sidebar-close-button { + display: none; + width: 36px; + height: 36px; + border: none; + background: transparent; + color: #6b7280; + cursor: pointer; + border-radius: 8px; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.sidebar-close-button:hover { + background: #e5e5e5; + color: #1a1a1a; +} + +.sidebar-conversations { + flex: 1; + overflow-y: auto; + padding: 12px; +} + +.sidebar-empty { + padding: 24px 12px; + text-align: center; + color: #6b7280; +} + +.sidebar-empty p { + margin: 0; +} + +.sidebar-empty-hint { + font-size: 13px; + margin-top: 8px !important; + opacity: 0.8; +} + +.conversation-list { + list-style: none; + margin: 0; + padding: 0; +} + +.conversation-item { + display: flex; + align-items: center; + border-radius: 8px; + margin-bottom: 4px; + transition: background-color 0.15s ease; +} + +.conversation-item:hover { + background: #e5e5e5; +} + +.conversation-item.active { + background: #e5e5e5; +} + +.conversation-button { + flex: 1; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + background: none; + border: none; + font-family: inherit; + font-size: 14px; + color: #1a1a1a; + cursor: pointer; + text-align: left; + min-width: 0; +} + +.conversation-flag-left { + font-size: 18px; + flex-shrink: 0; + line-height: 1; +} + +.conversation-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} + +.conversation-actions { + display: none; + align-items: center; + gap: 2px; + margin-right: 8px; +} + +.conversation-item:hover .conversation-actions { + display: flex; +} + +.conversation-action-btn { + width: 28px; + height: 28px; + border: none; + background: transparent; + color: #9ca3af; + cursor: pointer; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.conversation-action-btn:hover { + background: #d1d5db; + color: #1a1a1a; +} + +.conversation-action-btn.conversation-delete:hover { + color: #ef4444; +} + +.conversation-edit { + flex: 1; + padding: 4px 8px; +} + +.conversation-edit-input { + width: 100%; + font-family: inherit; + font-size: 14px; + padding: 6px 10px; + border: 1px solid #1a1a1a; + border-radius: 6px; + background: #ffffff; + color: #1a1a1a; + outline: none; +} + +/* Sidebar Overlay (mobile) */ +.sidebar-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 40; +} + +/* Menu toggle button in header */ +.menu-toggle { + display: none; + width: 40px; + height: 40px; + border: none; + background: transparent; + color: #1a1a1a; + cursor: pointer; + border-radius: 8px; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; +} + +.menu-toggle:hover { + background: #f3f4f6; +} + +.header-left { + display: flex; + align-items: center; + gap: 12px; +} + +.header-logo { + font-size: 18px; + font-weight: 600; + color: #1a1a1a; + margin: 0; +} + +/* Header */ +.header { + background: #ffffff; + border-bottom: 1px solid #e5e5e5; + padding: 0 16px; + height: 57px; + flex-shrink: 0; +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; + height: 100%; +} + +.header-right { + display: flex; + align-items: center; + gap: 8px; +} + +/* Language Selector */ +.lang-selector { + position: relative; +} + +.lang-button { + width: 40px; + height: 40px; + border: none; + background: transparent; + cursor: pointer; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; +} + +.lang-button:hover:not(:disabled) { + background: #f3f4f6; +} + +.lang-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.lang-flag { + font-size: 22px; + line-height: 1; +} + +.lang-dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + background: #ffffff; + border: 1px solid #e5e5e5; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + min-width: 160px; + z-index: 100; + overflow: hidden; +} + +.lang-option { + width: 100%; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border: none; + background: transparent; + cursor: pointer; + font-family: inherit; + font-size: 14px; + color: #1a1a1a; + text-align: left; + transition: background-color 0.15s ease; +} + +.lang-option:hover { + background: #f3f4f6; +} + +.lang-option.active { + background: #f3f4f6; + font-weight: 500; +} + +.lang-name { + flex: 1; +} + +/* Logo Menu Button */ +.logo-menu-button { + width: 40px; + height: 40px; + border: none; + background: transparent; + cursor: pointer; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; +} + +.logo-menu-button:hover { + background: #f3f4f6; +} + +.logo-icon { + width: 28px; + height: 28px; +} + +/* Connection Status in Header */ +.header-connection-status { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-radius: 8px; + background: #f9fafb; + font-size: 13px; + color: #1a1a1a; +} + +.connection-status-text { + font-weight: 500; +} + +/* Auth Section in Header */ +.header-auth-section { + position: relative; +} + +.header-auth-loading { + padding: 6px 12px; + font-size: 13px; + color: #6b7280; +} + +.header-auth-user { + display: flex; + align-items: center; + gap: 8px; +} + +.header-auth-email { + padding: 6px 12px; + font-size: 13px; + color: #1a1a1a; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + background: #f9fafb; + border-radius: 8px; +} + +.header-sign-in-button, +.header-sign-out-button { + padding: 6px 12px; + font-size: 13px; + font-weight: 500; + color: #1a1a1a; + background: #f9fafb; + border: 1px solid #e5e5e5; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + font-family: inherit; +} + +.header-sign-in-button:hover, +.header-sign-out-button:hover { + background: #f3f4f6; + border-color: #d1d5db; +} + +.header-auth-form-wrapper { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 100; +} + +.header-auth-form { + background: #ffffff; + border: 1px solid #e5e5e5; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + padding: 16px; + min-width: 280px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.header-auth-form-header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 16px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 4px; +} + +.header-auth-form-close { + background: none; + border: none; + font-size: 24px; + line-height: 1; + color: #6b7280; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +.header-auth-form-close:hover { + background: #f3f4f6; + color: #1a1a1a; +} + +.header-auth-error { + padding: 8px 12px; + background: #fee2e2; + color: #dc2626; + border-radius: 6px; + font-size: 13px; +} + +.header-auth-input { + padding: 10px 12px; + border: 1px solid #e5e5e5; + border-radius: 8px; + font-size: 14px; + font-family: inherit; + color: #1a1a1a; + background: #ffffff; + transition: border-color 0.2s ease; +} + +.header-auth-input:focus { + outline: none; + border-color: #1a1a1a; +} + +.header-auth-submit { + padding: 10px 16px; + background: #1a1a1a; + color: #ffffff; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.header-auth-submit:hover:not(:disabled) { + background: #000000; +} + +.header-auth-submit:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.header-auth-toggle { + background: none; + border: none; + color: #6b7280; + font-size: 13px; + font-family: inherit; + cursor: pointer; + padding: 4px; + text-align: center; + transition: color 0.2s ease; +} + +.header-auth-toggle:hover { + color: #1a1a1a; +} + +/* Header Dropdown */ +.header-dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + background: #ffffff; + border: 1px solid #e5e5e5; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + min-width: 220px; + z-index: 100; + overflow: hidden; +} + +.dropdown-item { + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.dropdown-status { + flex-direction: row; + align-items: center; + gap: 10px; + font-size: 14px; + color: #1a1a1a; +} + +.dropdown-divider { + height: 1px; + background: #e5e5e5; +} + +.dropdown-label { + font-size: 12px; + color: #6b7280; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.dropdown-select { + font-family: 'Roboto Serif', Georgia, Cambria, 'Times New Roman', serif; + font-size: 14px; + padding: 8px 12px; + border: 1px solid #e5e5e5; + border-radius: 8px; + background: #ffffff; + color: #1a1a1a; + cursor: pointer; + width: 100%; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + padding-right: 32px; +} + +.dropdown-select:hover:not(:disabled) { + border-color: #1a1a1a; +} + +.dropdown-select:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.dropdown-user { + gap: 4px; +} + +.dropdown-email { + font-size: 14px; + color: #1a1a1a; + word-break: break-all; +} + +.dropdown-button { + cursor: pointer; + background: none; + border: none; + font-family: inherit; + font-size: 14px; + color: #1a1a1a; + text-align: left; + transition: background-color 0.15s ease; + width: 100%; +} + +.dropdown-button:hover { + background: #f3f4f6; +} + +.dropdown-loading { + font-size: 14px; + color: #6b7280; +} + +/* Dropdown Auth Form */ +.dropdown-auth-form { + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.dropdown-auth-header { + font-size: 14px; + font-weight: 600; + color: #1a1a1a; +} + +.dropdown-auth-error { + font-size: 13px; + color: #ef4444; + background: #fef2f2; + padding: 8px 10px; + border-radius: 6px; +} + +.dropdown-input { + font-family: 'Roboto Serif', Georgia, Cambria, 'Times New Roman', serif; + font-size: 14px; + padding: 10px 12px; + border: 1px solid #e5e5e5; + border-radius: 8px; + outline: none; + transition: border-color 0.2s ease; +} + +.dropdown-input:focus { + border-color: #1a1a1a; +} + +.dropdown-submit { + font-family: 'Roboto Serif', Georgia, Cambria, 'Times New Roman', serif; + font-size: 14px; + padding: 10px 16px; + border: none; + border-radius: 8px; + background: #1a1a1a; + color: #ffffff; + cursor: pointer; + font-weight: 500; + transition: background 0.2s ease; +} + +.dropdown-submit:hover:not(:disabled) { + background: #333333; +} + +.dropdown-submit:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.dropdown-toggle-auth { + font-family: 'Roboto Serif', Georgia, Cambria, 'Times New Roman', serif; + font-size: 13px; + padding: 0; + border: none; + background: none; + color: #6b7280; + cursor: pointer; + text-align: center; +} + +.dropdown-toggle-auth:hover { + color: #1a1a1a; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #fbbf24; + transition: background-color 0.3s ease; +} + +.status-dot.connected { + background: #10b981; +} + +.status-dot.disconnected { + background: #ef4444; +} + +.status-text { + font-size: 14px; + color: #6b7280; + font-weight: 500; +} + +/* Auth Controls */ +.auth-controls { + display: flex; + align-items: center; +} + +.auth-loading { + font-size: 14px; + color: #6b7280; +} + +.user-menu { + display: flex; + align-items: center; + gap: 8px; +} + +.user-email { + font-size: 13px; + color: #6b7280; + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sign-in-btn, +.sign-out-btn { + font-family: 'Roboto Serif', Georgia, Cambria, 'Times New Roman', serif; + font-size: 14px; + padding: 8px 16px; + border: 1px solid #e5e5e5; + border-radius: 8px; + background: #ffffff; + color: #1a1a1a; + cursor: pointer; + transition: all 0.2s ease; + font-weight: 500; +} + +.sign-in-btn:hover, +.sign-out-btn:hover { + border-color: #1a1a1a; + background: #f9fafb; +} + +.sign-out-btn { + padding: 6px 12px; + font-size: 13px; +} + +/* Auth Dropdown */ +.auth-dropdown-container { + position: relative; +} + +.auth-dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + background: #ffffff; + border: 1px solid #e5e5e5; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + padding: 20px; + width: 280px; + z-index: 100; +} + +.auth-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.auth-form h3 { + font-size: 16px; + font-weight: 600; + margin: 0 0 4px 0; + color: #1a1a1a; +} + +.auth-form input { + font-family: 'Roboto Serif', Georgia, Cambria, 'Times New Roman', serif; + font-size: 14px; + padding: 10px 12px; + border: 1px solid #e5e5e5; + border-radius: 8px; + outline: none; + transition: border-color 0.2s ease; +} + +.auth-form input:focus { + border-color: #1a1a1a; +} + +.auth-error { + font-size: 13px; + color: #ef4444; + background: #fef2f2; + padding: 8px 12px; + border-radius: 6px; +} + +.auth-submit-btn { + font-family: 'Roboto Serif', Georgia, Cambria, 'Times New Roman', serif; + font-size: 14px; + padding: 10px 16px; + border: none; + border-radius: 8px; + background: #1a1a1a; + color: #ffffff; + cursor: pointer; + font-weight: 500; + transition: background 0.2s ease; +} + +.auth-submit-btn:hover:not(:disabled) { + background: #333333; +} + +.auth-submit-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.auth-toggle-btn { + font-family: 'Roboto Serif', Georgia, Cambria, 'Times New Roman', serif; + font-size: 13px; + padding: 0; + border: none; + background: none; + color: #6b7280; + cursor: pointer; + text-align: center; +} + +.auth-toggle-btn:hover { + color: #1a1a1a; +} + +/* Main Layout */ +.main { + padding: 32px 0; + min-height: calc(100vh - 80px); +} + +.app-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 32px; + height: calc(100vh - 144px); + overflow-x: hidden; + position: relative; +} + +.conversation-switch-loading { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: #ffffff; + border-radius: 12px; + border: 1px solid #e5e5e5; + height: 100%; + min-height: 400px; +} + +/* Section Headers */ +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.section-header h2 { + font-size: 20px; + font-weight: 600; + color: #1a1a1a; + display: flex; + align-items: center; + gap: 8px; +} + +.section-header-flag { + font-size: 20px; + line-height: 1; +} + +.card-count { + font-size: 14px; + color: #6b7280; + font-weight: 500; +} + +/* Chat Section */ +.chat-section { + background: #ffffff; + border-radius: 12px; + border: 1px solid #e5e5e5; + padding: 24px; + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + overflow: hidden; +} + +.button-group { + display: flex; + align-items: center; + gap: 8px; +} + +.restart-button { + width: 40px; + height: 40px; + border-radius: 20px; + border: 2px solid #e5e5e5; + background: #ffffff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + color: #6b7280; +} + +.restart-button:hover:not(:disabled) { + border-color: #1a1a1a; + color: #1a1a1a; +} + +.restart-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.mic-button { + width: 48px; + height: 48px; + border-radius: 24px; + border: 2px solid #e5e5e5; + background: #ffffff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + color: #6b7280; +} + +.mic-button:hover:not(:disabled) { + border-color: #1a1a1a; + color: #1a1a1a; +} + +.mic-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.mic-button.recording { + background: #ef4444; + border-color: #ef4444; + color: #ffffff; + animation: pulse-mic 2s ease-in-out infinite; +} + +@keyframes pulse-mic { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); + } + 50% { + box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); + } +} + +.chat-container { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.messages { + flex: 1; + overflow-y: auto; + padding: 16px 0; + display: flex; + flex-direction: column; + position: relative; + gap: 16px; + min-height: 0; +} + +/* Chat loading state */ +.chat-loading { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: #ffffff; + z-index: 10; + width: 100%; + height: 100%; +} + +.chat-loading-spinner { + width: 32px; + height: 32px; + border: 3px solid #e5e5e5; + border-top-color: #1a1a1a; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.chat-loading-text { + margin-top: 12px; + font-size: 14px; + color: #6b7280; + text-align: center; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.message { + max-width: 80%; + padding: 12px 16px; + border-radius: 12px; + font-size: 15px; + line-height: 1.4; +} + +.message.teacher { + background: #f3f4f6; + color: #1a1a1a; + align-self: flex-start; + border-bottom-left-radius: 4px; +} + +.message.learner { + background: #1a1a1a; + color: #ffffff; + align-self: flex-end; + border-bottom-right-radius: 4px; + margin-right: 16px; +} + +.message.streaming { + opacity: 0.8; + position: relative; +} + +.message.realtime { + opacity: 0.9; + border: 1px dashed rgba(26, 26, 26, 0.3); +} + +.loading-dots { + display: flex; + align-items: center; + gap: 4px; + margin-left: 4px; +} + +.loading-dots span { + width: 6px; + height: 6px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.7); + animation: loading-dot 1.4s ease-in-out infinite; +} + +.loading-dots span:nth-child(1) { + animation-delay: 0s; +} + +.loading-dots span:nth-child(2) { + animation-delay: 0.2s; +} + +.loading-dots span:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes loading-dot { + 0%, 60%, 100% { + opacity: 0.3; + transform: scale(0.8); + } + 30% { + opacity: 1; + transform: scale(1); + } +} + +.current-transcript { + display: none; +} + +/* Text Input Form */ +.text-input-form { + display: flex; + gap: 8px; + padding-top: 16px; + margin-top: 16px; +} + +.text-input { + flex: 1; + font-family: 'Roboto Serif', Georgia, Cambria, 'Times New Roman', serif; + font-size: 15px; + padding: 12px 16px; + border: 1px solid #e5e5e5; + border-radius: 24px; + background: #f9fafb; + color: #1a1a1a; + transition: all 0.2s ease; +} + +.text-input:hover:not(:disabled) { + border-color: #d1d5db; +} + +.text-input:focus { + outline: none; + border-color: #1a1a1a; + background: #ffffff; + box-shadow: 0 0 0 2px rgba(26, 26, 26, 0.1); +} + +.text-input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.text-input::placeholder { + color: #9ca3af; +} + +.send-button { + width: 44px; + height: 44px; + border-radius: 22px; + border: none; + background: #1a1a1a; + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.send-button:hover:not(:disabled) { + background: #374151; + transform: scale(1.05); +} + +.send-button:active:not(:disabled) { + transform: scale(0.95); +} + +.send-button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* Flashcards Section */ +.flashcards-section { + background: #ffffff; + border-radius: 12px; + border: 1px solid #e5e5e5; + padding: 24px; + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + overflow-x: hidden; + overflow-y: hidden; +} + +.flashcards-container { + flex: 1; + min-height: 0; + position: relative; +} + +.flashcards-grid { + height: 100%; + overflow-x: hidden; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 16px; +} + +.flashcard { + position: relative; + min-height: 200px; + cursor: pointer; + perspective: 1000px; + flex: 0 0 auto; +} + +.flashcard-inner { + position: relative; + width: 100%; + text-align: center; + transition: transform 0.6s; + transform-style: preserve-3d; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto; +} + +.flashcard-inner > * { + grid-column: 1; + grid-row: 1; +} + +.flashcard.flipped .flashcard-inner { + transform: rotateY(180deg); +} + +.flashcard-front, +.flashcard-back { + width: 100%; + min-height: 200px; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + border-radius: 12px; + padding: 24px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + box-sizing: border-box; +} + +.flashcard-front { + background: #f3f4f6; + color: #1a1a1a; + overflow: hidden; +} + +/* Pronounce button on flashcard */ +.pronounce-button { + position: absolute; + bottom: 12px; + right: 12px; + width: 36px; + height: 36px; + border-radius: 50%; + border: 2px solid #d1d5db; + background: #ffffff; + color: #6b7280; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + z-index: 10; + padding: 0; +} + +.pronounce-button:hover:not(:disabled) { + border-color: #1a1a1a; + color: #1a1a1a; + transform: scale(1.08); +} + +.pronounce-button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.pronounce-button.loading { + border-color: #6b7280; +} + +.pronounce-button svg { + width: 18px; + height: 18px; +} + +.pronounce-spinner { + animation: spin-pronounce 1s linear infinite; +} + +@keyframes spin-pronounce { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.flashcard-back { + background: #ffffff; + border: 2px solid #e5e5e5; + transform: rotateY(180deg); + justify-content: flex-start; + align-items: stretch; + overflow: hidden; +} + +.flashcard-back > *:first-child { + margin-top: 0; +} + +.flashcard-spanish, +.flashcard-target-word { + font-size: 32px; + font-weight: 700; + text-align: center; + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + padding: 0 8px; +} + +.flashcard-english { + font-size: 24px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 16px; + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + flex-shrink: 0; +} + +.flashcard-example { + font-size: 16px; + font-style: italic; + color: #6b7280; + margin-bottom: 16px; + text-align: center; + line-height: 1.4; + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + flex-shrink: 0; +} + +.flashcard-mnemonic { + font-size: 14px; + color: #6b7280; + padding: 12px; + background: rgba(107, 114, 128, 0.08); + border-radius: 8px; + width: 100%; + text-align: left; + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + flex-shrink: 0; + box-sizing: border-box; + line-height: 1.5; + margin-top: auto; +} + +.mnemonic-label { + font-weight: 600; + color: #1a1a1a; + margin-right: 4px; +} + +/* Mobile-specific flashcard layout */ +@media (max-width: 768px) { + .flashcards-grid { + flex-direction: row; + overflow-x: auto; + overflow-y: hidden; + gap: 12px; + padding-bottom: 8px; + scroll-snap-type: x mandatory; + height: 100%; + } + + .flashcard { + min-width: 45%; + width: 45%; + height: calc(100% - 16px); + min-height: 280px; + scroll-snap-align: start; + } + + .flashcard-spanish, + .flashcard-target-word { + font-size: 24px; + } + + .flashcard-english { + font-size: 18px; + } + + .flashcard-example { + font-size: 13px; + padding: 0 8px; + } + + .flashcard-mnemonic { + font-size: 12px; + padding: 8px; + } + + .flashcard-front, + .flashcard-back { + padding: 16px; + } + + .pronounce-button { + width: 32px; + height: 32px; + bottom: 8px; + right: 8px; + } + + .pronounce-button svg { + width: 16px; + height: 16px; + } +} + +.empty-state { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: #9ca3af; + font-size: 15px; + text-align: center; +} + +/* Responsive */ +@media (max-width: 768px) { + /* Sidebar mobile - slide-out drawer */ + .sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + z-index: 50; + transform: translateX(-100%); + transition: transform 0.3s ease; + box-shadow: none; + } + + .sidebar.open { + transform: translateX(0); + box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15); + } + + .sidebar-overlay { + display: block; + } + + .sidebar-close-button { + display: flex; + } + + .menu-toggle { + display: flex; + } + + .main { + padding: 16px 0; + height: calc(100vh - 80px); + overflow: hidden; + } + + .app-grid { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + gap: 16px; + height: 100%; + overflow: hidden; + } + + .chat-section, + .flashcards-section { + height: 100%; + min-height: 0; + overflow: hidden; + padding: 16px; + } + + .container { + padding: 0 16px; + height: 100%; + } + + .messages, + .flashcards-grid { + max-height: none; + } +} + +/* Scrollbar Styling */ +.messages::-webkit-scrollbar, +.flashcards-grid::-webkit-scrollbar { + width: 6px; +} + +.messages::-webkit-scrollbar-track, +.flashcards-grid::-webkit-scrollbar-track { + background: transparent; +} + +.messages::-webkit-scrollbar-thumb, +.flashcards-grid::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 3px; +} + +.messages::-webkit-scrollbar-thumb:hover, +.flashcards-grid::-webkit-scrollbar-thumb:hover { + background: #9ca3af; +} + +/* Translation Tooltip */ +.translation-tooltip { + position: absolute; + z-index: 1000; + background: #1a1a1a; + color: #ffffff; + padding: 10px 14px; + border-radius: 8px; + font-size: 14px; + line-height: 1.4; + max-width: 400px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); + opacity: 0; + visibility: hidden; + transform: translateY(4px); + transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s; + pointer-events: none; +} + +.translation-tooltip.visible { + opacity: 1; + visibility: visible; + transform: translateY(0); + pointer-events: auto; +} + +.translation-tooltip::after { + content: ''; + position: absolute; + bottom: -6px; + left: 20px; + width: 12px; + height: 12px; + background: #1a1a1a; + transform: rotate(45deg); + border-radius: 0 0 2px 0; +} + +.translation-content { + display: block; +} + +.translation-text { + display: block; + word-wrap: break-word; +} + +.translation-loading { + display: none; + align-items: center; + justify-content: center; + gap: 4px; + padding: 4px 0; +} + +.translation-tooltip.loading .translation-loading { + display: flex; +} + +.translation-tooltip.loading .translation-content { + display: none; +} + +.translation-loading span { + width: 6px; + height: 6px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.7); + animation: translation-dot 1.2s ease-in-out infinite; +} + +.translation-loading span:nth-child(1) { + animation-delay: 0s; +} + +.translation-loading span:nth-child(2) { + animation-delay: 0.15s; +} + +.translation-loading span:nth-child(3) { + animation-delay: 0.3s; +} + +@keyframes translation-dot { + 0%, 60%, 100% { + opacity: 0.4; + transform: scale(0.8); + } + 30% { + opacity: 1; + transform: scale(1); + } +} + +/* Make teacher messages show a subtle cursor hint for translation */ +.message.teacher { + cursor: help; + transition: background-color 0.15s ease; +} + +.message.teacher:hover { + background: #e9eaec; +} + +/* Feedback Tooltip for learner messages */ +.feedback-tooltip { + position: absolute; + z-index: 1000; + background: #2d4a3e; + color: #ffffff; + padding: 10px 14px; + border-radius: 8px; + font-size: 14px; + line-height: 1.4; + max-width: 400px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); + opacity: 0; + visibility: hidden; + transform: translateY(4px); + transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s; + pointer-events: none; +} + +.feedback-tooltip.visible { + opacity: 1; + visibility: visible; + transform: translateY(0); + pointer-events: auto; +} + +.feedback-tooltip::after { + content: ''; + position: absolute; + bottom: -6px; + left: 20px; + width: 12px; + height: 12px; + background: #2d4a3e; + transform: rotate(45deg); + border-radius: 0 0 2px 0; +} + +.feedback-content { + display: block; +} + +.feedback-text { + display: block; + word-wrap: break-word; +} + +.feedback-loading { + display: none; + align-items: center; + justify-content: center; + gap: 4px; + padding: 4px 0; +} + +.feedback-tooltip.loading .feedback-loading { + display: flex; +} + +.feedback-tooltip.loading .feedback-content { + display: none; +} + +.feedback-loading span { + width: 6px; + height: 6px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.7); + animation: feedback-dot 1.2s ease-in-out infinite; +} + +.feedback-loading span:nth-child(1) { + animation-delay: 0s; +} + +.feedback-loading span:nth-child(2) { + animation-delay: 0.15s; +} + +.feedback-loading span:nth-child(3) { + animation-delay: 0.3s; +} + +@keyframes feedback-dot { + 0%, 60%, 100% { + opacity: 0.4; + transform: scale(0.8); + } + 30% { + opacity: 1; + transform: scale(1); + } +} + +/* Make learner messages show a subtle cursor hint for feedback */ +.message.learner { + cursor: help; + transition: background-color 0.15s ease; +} + +.message.learner:hover { + background: #333333; +} \ No newline at end of file diff --git a/frontend/src/types/global.d.ts b/frontend/src/types/global.d.ts new file mode 100644 index 0000000..e5a328f --- /dev/null +++ b/frontend/src/types/global.d.ts @@ -0,0 +1,11 @@ +// Global type declarations for Window interface + +import type { IOSAudioHandler } from './index'; + +declare global { + interface Window { + iosAudioHandler?: IOSAudioHandler; + } +} + +export {}; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..4d9b327 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,170 @@ +// iOS Audio Handler interface +export interface IOSAudioHandler { + unlockAudioContext?: () => Promise; + startMicrophone?: (callback: (data: string) => void) => Promise; + stopMicrophone?: () => void; + playAudioChunk?: (data: string, isLastChunk: boolean) => Promise; + stopAudioPlayback?: () => void; + getOptimizedWebSocketURL?: () => string | null; +} + +// Connection status +export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected'; + +// Message role +export type MessageRole = 'learner' | 'teacher'; + +// Chat message for UI display +export interface ChatMessage { + role: MessageRole; + content: string; + timestamp?: string; +} + +// Conversation message for backend +export interface ConversationMessage { + role: 'user' | 'assistant'; + content: string; + timestamp: string; +} + +export interface ConversationHistory { + messages: ConversationMessage[]; +} + +// Multi-conversation support +export interface ConversationSummary { + id: string; + title: string; + languageCode: string; + createdAt: string; + updatedAt: string; +} + +export interface ConversationData { + id: string; + messages: ConversationMessage[]; +} + +// Flashcard +export interface Flashcard { + targetWord: string; + english: string; + example: string; + mnemonic: string; + timestamp?: string; + languageCode?: string; + conversationId?: string; + // Legacy fields for backward compatibility + spanish?: string; + word?: string; + translation?: string; + example_sentence?: string; +} + +// Language +export interface Language { + code: string; + name: string; + nativeName: string; + flag: string; +} + +// Audio stream data +export interface AudioStreamData { + audio: string; + audioFormat: 'int16' | 'float32'; + sampleRate: number; + timestamp?: string; + text?: string; +} + +// WebSocket event payloads +export interface TranscriptUpdatePayload { + text: string; +} + +export interface TranscriptionPayload { + text: string; + timestamp?: string; +} + +export interface LLMResponseChunkPayload { + text: string; + timestamp?: string; +} + +export interface LLMResponseCompletePayload { + text: string; + timestamp?: string; +} + +export interface SpeechDetectedPayload { + text?: string; + interactionId?: string; +} + +export interface PartialTranscriptPayload { + text: string; + interactionId?: string; + timestamp?: string; +} + +export interface InterruptPayload { + reason?: string; +} + +export interface LanguageChangedPayload { + languageCode: string; + languageName: string; +} + +export interface FeedbackGeneratedPayload { + messageContent: string; + feedback: string; +} + +// App state +export interface AppState { + // Connection + connectionStatus: ConnectionStatus; + + // Language + currentLanguage: string; + availableLanguages: Language[]; + uiLanguage: string; // Language displayed in the UI selector (doesn't change when switching conversations) + + // Chat + chatHistory: ChatMessage[]; + currentTranscript: string; + pendingTranscription: string | null; + streamingLLMResponse: string; + llmResponseComplete: boolean; + currentResponseId: string | null; + + // Recording + isRecording: boolean; + speechDetected: boolean; + + // Flashcards + flashcards: Flashcard[]; + pronouncingCardId: string | null; + + // Feedback (keyed by user message content) + feedbackMap: Record; + + // User + userId: string | null; + + // Multi-conversation support + conversations: ConversationSummary[]; + currentConversationId: string | null; + sidebarOpen: boolean; + switchingConversation: boolean; +} + +// Outgoing WebSocket message +export interface OutgoingMessage { + type: string; + [key: string]: unknown; +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/styles/main.css b/frontend/styles/main.css deleted file mode 100644 index 4f2e0b3..0000000 --- a/frontend/styles/main.css +++ /dev/null @@ -1,533 +0,0 @@ -/* Reset and Base */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Roboto Serif', Georgia, Cambria, 'Times New Roman', serif; - background: #fafafa; - color: #1a1a1a; - line-height: 1.5; - font-feature-settings: "liga" 1, "onum" 0, "pnum" 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 24px; -} - -/* Header */ -.header { - background: #ffffff; - border-bottom: 1px solid #e5e5e5; - padding: 16px 0; -} - -.header .container { - display: flex; - justify-content: space-between; - align-items: center; - /* Make header content span full viewport width so items sit in the corners */ - max-width: none; - width: 100%; -} - -.logo { - font-size: 24px; - font-weight: 600; - color: #1a1a1a; -} - -.status-indicator { - display: flex; - align-items: center; - gap: 8px; -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: #fbbf24; - transition: background-color 0.3s ease; -} - -.status-dot.connected { - background: #10b981; -} - -.status-dot.disconnected { - background: #ef4444; -} - -.status-text { - font-size: 14px; - color: #6b7280; - font-weight: 500; -} - -/* Main Layout */ -.main { - padding: 32px 0; - min-height: calc(100vh - 80px); -} - -.app-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 32px; - height: calc(100vh - 144px); - overflow-x: hidden; - overflow-y: visible; -} - -/* Section Headers */ -.section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; -} - -.section-header h2 { - font-size: 20px; - font-weight: 600; - color: #1a1a1a; -} - -.card-count { - font-size: 14px; - color: #6b7280; - font-weight: 500; -} - -/* Chat Section */ -.chat-section { - background: #ffffff; - border-radius: 12px; - border: 1px solid #e5e5e5; - padding: 24px; - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; - overflow: hidden; -} - -.button-group { - display: flex; - align-items: center; - gap: 8px; -} - -.restart-button { - width: 40px; - height: 40px; - border-radius: 20px; - border: 2px solid #e5e5e5; - background: #ffffff; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s ease; - color: #6b7280; -} - -.restart-button:hover:not(:disabled) { - border-color: #1a1a1a; - color: #1a1a1a; -} - -.restart-button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.mic-button { - width: 48px; - height: 48px; - border-radius: 24px; - border: 2px solid #e5e5e5; - background: #ffffff; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s ease; - color: #6b7280; -} - -.mic-button:hover:not(:disabled) { - border-color: #1a1a1a; - color: #1a1a1a; -} - -.mic-button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.mic-button.recording { - background: #ef4444; - border-color: #ef4444; - color: #ffffff; - animation: pulse-mic 2s ease-in-out infinite; -} - -@keyframes pulse-mic { - 0%, 100% { - box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); - } - 50% { - box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); - } -} - -.chat-container { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; - overflow: hidden; -} - -.messages { - flex: 1; - overflow-y: auto; - padding: 16px 0; - display: flex; - flex-direction: column; - gap: 16px; - min-height: 0; -} - -.message { - max-width: 80%; - padding: 12px 16px; - border-radius: 12px; - font-size: 15px; - line-height: 1.4; -} - -.message.teacher { - background: #f3f4f6; - color: #1a1a1a; - align-self: flex-start; - border-bottom-left-radius: 4px; -} - -.message.learner { - background: #1a1a1a; - color: #ffffff; - align-self: flex-end; - border-bottom-right-radius: 4px; - margin-right: 16px; -} - -.message.streaming { - opacity: 0.8; - position: relative; -} - -.message.realtime { - opacity: 0.9; - border: 1px dashed rgba(26, 26, 26, 0.3); -} - -.loading-dots { - display: flex; - align-items: center; - gap: 4px; - margin-left: 4px; -} - -.loading-dots span { - width: 6px; - height: 6px; - border-radius: 50%; - background: rgba(255, 255, 255, 0.7); - animation: loading-dot 1.4s ease-in-out infinite; -} - -.loading-dots span:nth-child(1) { - animation-delay: 0s; -} - -.loading-dots span:nth-child(2) { - animation-delay: 0.2s; -} - -.loading-dots span:nth-child(3) { - animation-delay: 0.4s; -} - -@keyframes loading-dot { - 0%, 60%, 100% { - opacity: 0.3; - transform: scale(0.8); - } - 30% { - opacity: 1; - transform: scale(1); - } -} - -.streaming-cursor { - animation: blink 1s infinite; - color: #6b7280; - margin-left: 2px; -} - -@keyframes blink { - 0%, 50% { opacity: 1; } - 51%, 100% { opacity: 0; } -} - -.current-transcript { - display: none; - /* padding: 12px 16px; - background: #f9fafb; - border: 1px dashed #d1d5db; - border-radius: 8px; - font-size: 14px; - color: #6b7280; - font-style: italic; - min-height: 44px; - display: flex; - align-items: center; */ -} - -/* .current-transcript:empty::before { - content: "Live transcription will appear here..."; -} */ - -/* Flashcards Section */ -.flashcards-section { - background: #ffffff; - border-radius: 12px; - border: 1px solid #e5e5e5; - padding: 24px; - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; - overflow-x: hidden; - overflow-y: hidden; -} - -.flashcards-container { - flex: 1; - min-height: 0; -} - -.flashcards-grid { - height: 100%; - overflow-x: hidden; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 16px; -} - -.flashcard { - position: relative; - height: 200px; - min-height: 200px; - cursor: pointer; - perspective: 1000px; - flex: 0 0 auto; -} - -.flashcard-inner { - position: relative; - width: 100%; - height: 100%; - text-align: center; - transition: transform 0.6s; - transform-style: preserve-3d; -} - -.flashcard.flipped .flashcard-inner { - transform: rotateY(180deg); -} - -.flashcard-front, -.flashcard-back { - position: absolute; - width: 100%; - height: 100%; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - border-radius: 12px; - padding: 24px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.flashcard-front { - background: #f3f4f6; - color: #1a1a1a; -} - -.flashcard-back { - background: #ffffff; - border: 2px solid #e5e5e5; - transform: rotateY(180deg); -} - -.flashcard-spanish { - font-size: 32px; - font-weight: 700; - text-align: center; -} - -.flashcard-english { - font-size: 24px; - font-weight: 600; - color: #1a1a1a; - margin-bottom: 16px; -} - -.flashcard-example { - font-size: 16px; - font-style: italic; - color: #6b7280; - margin-bottom: 16px; - text-align: center; - line-height: 1.4; -} - -.flashcard-mnemonic { - font-size: 14px; - color: #6b7280; - padding: 12px; - background: rgba(107, 114, 128, 0.08); - border-radius: 8px; - width: 100%; - text-align: center; -} - -.mnemonic-label { - font-weight: 600; - color: #1a1a1a; - margin-right: 4px; -} - -/* Mobile-specific flashcard layout */ -@media (max-width: 768px) { - .flashcards-grid { - flex-direction: row; - overflow-x: auto; - overflow-y: hidden; - gap: 12px; - padding-bottom: 8px; - scroll-snap-type: x mandatory; - height: 100%; - } - - .flashcard { - min-width: 45%; - width: 45%; - height: calc(100% - 16px); - min-height: 280px; - scroll-snap-align: start; - } - - .flashcard-spanish { - font-size: 24px; - } - - .flashcard-english { - font-size: 18px; - } - - .flashcard-example { - font-size: 13px; - padding: 0 8px; - } - - .flashcard-mnemonic { - font-size: 12px; - padding: 8px; - } - - .flashcard-front, - .flashcard-back { - padding: 16px; - } -} - -.empty-state { - height: 100%; - display: flex; - align-items: center; - justify-content: center; - color: #9ca3af; - font-size: 15px; - text-align: center; -} - -/* Responsive */ -@media (max-width: 768px) { - .main { - padding: 16px 0; - height: calc(100vh - 80px); - overflow: hidden; - } - - .app-grid { - grid-template-columns: 1fr; - grid-template-rows: 1fr 1fr; - gap: 16px; - height: 100%; - overflow: hidden; - } - - .chat-section, - .flashcards-section { - height: 100%; - min-height: 0; - overflow: hidden; - padding: 16px; - } - - .container { - padding: 0 16px; - height: 100%; - } - - .messages, - .flashcards-grid { - max-height: none; - } -} - -/* Scrollbar Styling */ -.messages::-webkit-scrollbar, -.flashcards-grid::-webkit-scrollbar { - width: 6px; -} - -.messages::-webkit-scrollbar-track, -.flashcards-grid::-webkit-scrollbar-track { - background: transparent; -} - -.messages::-webkit-scrollbar-thumb, -.flashcards-grid::-webkit-scrollbar-thumb { - background: #d1d5db; - border-radius: 3px; -} - -.messages::-webkit-scrollbar-thumb:hover, -.flashcards-grid::-webkit-scrollbar-thumb:hover { - background: #9ca3af; -} \ No newline at end of file diff --git a/frontend/tsconfig.eslint.json b/frontend/tsconfig.eslint.json new file mode 100644 index 0000000..6bdef84 --- /dev/null +++ b/frontend/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..842b6f8 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client", "node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"], + "exclude": [ + "node_modules", + "dist", + "src/__tests__", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.spec.ts", + "**/*.spec.tsx" + ] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..55584f1 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, +}); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..5b2ae34 --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + include: ['src/__tests__/**/*.test.ts', 'src/__tests__/**/*.test.tsx'], + setupFiles: ['./src/__tests__/setup.ts'], + }, +}); diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..3cc2184 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,10 @@ +[api] +enabled = true +port = 54321 + +[db] +port = 54322 + +[studio] +enabled = true +port = 54323 diff --git a/supabase/migrations/20240108000000_initial_schema.sql b/supabase/migrations/20240108000000_initial_schema.sql new file mode 100644 index 0000000..090a89d --- /dev/null +++ b/supabase/migrations/20240108000000_initial_schema.sql @@ -0,0 +1,157 @@ +-- Inworld Language Tutor - Complete Database Schema +-- Run with: npx supabase db push +-- This creates all tables, indexes, RLS policies, and functions + +-------------------------------------------------------------------------------- +-- Extensions +-------------------------------------------------------------------------------- + +-- Enable pgvector for semantic memory search +create extension if not exists vector; + +-------------------------------------------------------------------------------- +-- Core Tables +-------------------------------------------------------------------------------- + +-- User preferences (language preference) +create table public.user_preferences ( + id uuid primary key default gen_random_uuid(), + user_id uuid references auth.users(id) on delete cascade not null unique, + language_code text default 'es' not null, + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null +); + +-- Conversations (multi-conversation support, per language) +create table public.conversations ( + id uuid primary key default gen_random_uuid(), + user_id uuid references auth.users(id) on delete cascade not null, + language_code text not null, + title text not null, + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null +); + +-- Conversation messages (belong to a conversation) +create table public.conversation_messages ( + id uuid primary key default gen_random_uuid(), + conversation_id uuid references public.conversations(id) on delete cascade not null, + role text check (role in ('user', 'assistant')) not null, + content text not null, + created_at timestamptz default now() not null +); + +-- Flashcards (per user per conversation) +create table public.flashcards ( + id uuid primary key default gen_random_uuid(), + user_id uuid references auth.users(id) on delete cascade not null, + language_code text not null, + conversation_id uuid references public.conversations(id) on delete cascade, + target_word text not null, + english text not null, + example text, + mnemonic text, + created_at timestamptz default now() not null, + unique(user_id, conversation_id, target_word) +); + +-------------------------------------------------------------------------------- +-- Memory System (pgvector) +-------------------------------------------------------------------------------- + +-- User memories with embeddings for semantic search +create table public.user_memories ( + id uuid primary key default gen_random_uuid(), + user_id uuid references auth.users(id) on delete cascade not null, + content text not null, + memory_type text check (memory_type in ('learning_progress', 'personal_context')) not null, + topics text[] default '{}', + importance float default 0.5, + embedding vector(1024), -- BAAI/bge-large-en-v1.5 model + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null +); + +-------------------------------------------------------------------------------- +-- Indexes +-------------------------------------------------------------------------------- + +-- Core table indexes +create index idx_conversations_user_lang on public.conversations(user_id, language_code, updated_at desc); +create index idx_messages_conversation on public.conversation_messages(conversation_id, created_at); +create index idx_flashcards_conversation on public.flashcards(user_id, conversation_id); +create index idx_flashcards_conversation_id on public.flashcards(conversation_id); + +-- Memory indexes +create index idx_memories_embedding on public.user_memories + using ivfflat (embedding vector_cosine_ops) with (lists = 100); +create index idx_memories_user on public.user_memories(user_id, created_at desc); + +-------------------------------------------------------------------------------- +-- Row Level Security +-------------------------------------------------------------------------------- + +alter table public.user_preferences enable row level security; +alter table public.conversations enable row level security; +alter table public.conversation_messages enable row level security; +alter table public.flashcards enable row level security; +alter table public.user_memories enable row level security; + +-- RLS Policies (users can only access their own data) +-- Note: Service role key bypasses RLS, which is what the backend uses +create policy "Users manage own preferences" on public.user_preferences for all using ((select auth.uid()) = user_id); +create policy "Users manage own conversations" on public.conversations for all using ((select auth.uid()) = user_id); +create policy "Users manage own messages" on public.conversation_messages for all + using (conversation_id in (select id from public.conversations where user_id = (select auth.uid()))); +create policy "Users manage own flashcards" on public.flashcards for all using ((select auth.uid()) = user_id); +create policy "Users manage own memories" on public.user_memories for all using ((select auth.uid()) = user_id); + +-------------------------------------------------------------------------------- +-- Functions +-------------------------------------------------------------------------------- + +-- Search memories by cosine similarity (optimized for index usage) +create or replace function match_memories( + query_embedding vector(1024), + match_user_id uuid, + match_threshold float default 0.7, + match_count int default 3 +) +returns table ( + id uuid, + content text, + memory_type text, + topics text[], + importance float, + similarity float +) +language sql stable +as $$ + select + user_memories.id, + user_memories.content, + user_memories.memory_type, + user_memories.topics, + user_memories.importance, + 1 - (user_memories.embedding <=> query_embedding) as similarity + from user_memories + where user_memories.user_id = match_user_id + and user_memories.embedding is not null + and user_memories.embedding <=> query_embedding < (1 - match_threshold) + order by user_memories.embedding <=> query_embedding + limit match_count; +$$; + +-- Trigger function to auto-update timestamps +create or replace function update_updated_at_column() +returns trigger as $$ +begin + new.updated_at = now(); + return new; +end; +$$ language plpgsql; + +create trigger update_user_memories_updated_at + before update on public.user_memories + for each row + execute function update_updated_at_column();