diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..637bab7
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,82 @@
+# MeetVault — Agent Guide
+
+## Repo layout
+
+```text
+MeetVault/
+├── Overview/ # Architecture docs, schema, data flow
+│ └── README.md
+├── Wireframes/ # Desktop client design notes
+│ └── Desktop/
+│ ├── Desktop_README.md
+│ └── Desktop-Demo/ # Runnable Tauri + React demo
+│ └── speech-engine/ # Local Whisper transcription engine
+```
+
+## Core architecture (remember this)
+
+- **User machine**: records audio/video, runs local Whisper for STT, sends transcript to backend. Keeps compute and cost on the device.
+- **Backend API**: auth, meeting metadata, presigned upload URLs, transcript ingestion, summarization jobs, task extraction, reminder scheduling.
+- **PostgreSQL**: structured app data + `processing_jobs` table used as a job queue (no external broker needed for MVP).
+- **SeaweedFS**: S3-compatible object storage for large media files.
+
+## PostgreSQL gotchas
+
+### Enable UUID generation first
+
+```sql
+CREATE EXTENSION IF NOT EXISTS pgcrypto;
+```
+
+### Always set `updated_at` manually
+
+PostgreSQL does not auto-update it. Create a shared trigger function and apply to every table that tracks changes:
+
+```sql
+CREATE OR REPLACE FUNCTION set_updated_at()
+ RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = NOW();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- then attach per-table triggers, e.g.:
+CREATE TRIGGER trg_meetings_updated_at
+ BEFORE UPDATE ON meetings FOR EACH ROW EXECUTE FUNCTION set_updated_at();
+```
+
+### Job queue pattern (no external broker)
+
+Workers pick jobs with this exact query:
+
+```sql
+SELECT *
+FROM processing_jobs
+WHERE status = 'queued'
+ AND available_at <= NOW()
+ORDER BY priority DESC, created_at
+FOR UPDATE SKIP LOCKED
+LIMIT 1;
+```
+
+Key indexes for workers:
+
+```sql
+CREATE INDEX idx_schedules_due ON meeting_schedules(status, scheduled_at);
+CREATE INDEX idx_processing_jobs_queue ON processing_jobs(status, available_at, priority DESC);
+```
+
+### SeaweedFS object paths
+
+Files live under: `meeting-storage/users/{user_id}/meetings/{meeting_id}/original/`
+
+## Testing flow
+
+1. Run the desktop demo locally (Tauri + React) to verify UI and local transcription without backend dependencies.
+2. Spin up PostgreSQL, run the schema SQL from `Overview/README.md`, then start the backend API.
+3. End-to-end: record a short clip on the desktop client → upload via presigned URL → ingest transcript → trigger summarization job → verify summary appears in DB.
+
+## What is intentionally out of scope for MVP
+
+- Kafka, Elasticsearch, Kubernetes, vector DBs, Redis (unless job volume grows), microservices. PostgreSQL + SeaweedFS + workers is sufficient.
diff --git a/Readme.md b/Readme.md
index e69de29..1b84428 100644
--- a/Readme.md
+++ b/Readme.md
@@ -0,0 +1,44 @@
+# MeetVault
+
+Self-hosted meeting recording and intelligence platform. The user machine handles local transcription; the backend API manages auth, metadata, uploads, summarization jobs, tasks, and reminders. Large media files are stored in SeaweedFS via its S3-compatible API.
+
+## Architecture
+
+```text
+User machine:
+ - Record audio/video
+ - Local Whisper STT → transcript
+ - Upload to SeaweedFS (presigned URLs)
+ - Send transcript to backend
+
+Backend API:
+ - Auth, users, subscriptions
+ - Meeting metadata
+ - Transcript ingestion
+ - Summarization jobs
+ - Task extraction
+ - Reminder scheduling
+ - PostgreSQL for structured data
+```
+
+## Quick start
+
+### Backend
+
+1. Start PostgreSQL and run the schema SQL from `Overview/README.md` (includes `pgcrypto` extension, all tables, indexes, triggers).
+2. Run the backend API server.
+3. Spin up SeaweedFS and configure presigned URL endpoints on the backend.
+
+### Desktop client
+
+Run the Tauri + React demo in `Wireframes/Desktop/MeetVault-Desktop-Demo/`. It supports local Whisper transcription, speaker diarization via diart, live overlay window, recording capture, and mock speech-engine mode for UI testing without ML models.
+
+## Stack (MVP)
+
+Frontend: React / Next.js
+Backend: FastAPI or Node.js
+Database: PostgreSQL + `processing_jobs` table as job queue
+Media storage: SeaweedFS
+Local STT: Whisper / whisper.cpp
+
+No Kafka, Elasticsearch, Kubernetes, vector DBs, or Redis are needed for the MVP.
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/.gitignore b/Wireframes/Desktop/MeetVault-Desktop-Demo/.gitignore
new file mode 100644
index 0000000..e4fb597
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+dist/
+src-tauri/target/
+speech-engine/.venv/
+speech-engine/__pycache__/
+.env
+*.pyc
+.DS_Store
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/PROJECT_SCOPE.md b/Wireframes/Desktop/MeetVault-Desktop-Demo/PROJECT_SCOPE.md
new file mode 100644
index 0000000..ab2e235
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/PROJECT_SCOPE.md
@@ -0,0 +1,14 @@
+# Demo scope lock
+
+This file records the requested scope for the current MeetVault desktop demo.
+
+- No login UI or authentication flow.
+- No subscription UI or billing logic.
+- Recordings and transcript files remain local.
+- Meeting Details must support local video/audio playback.
+- Live transcription is local Whisper.
+- Speaker identifier means session speaker diarization (`Speaker 1`, `Speaker 2`, etc.), not biometric identity recognition.
+- Speaker labels can be renamed live.
+- Realtime overlay is included.
+- Settings are included and locally persisted.
+- Backend actions are placeholders and display exactly: `Processing (The job). Please wait a moment`.
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/README.md b/Wireframes/Desktop/MeetVault-Desktop-Demo/README.md
new file mode 100644
index 0000000..c052f11
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/README.md
@@ -0,0 +1,421 @@
+# MeetVault Desktop Demo
+
+This repository is a runnable desktop-demo scaffold based on the supplied MeetVault wireframes and desktop architecture notes.
+
+## Demo scope
+
+Included now:
+
+- Tauri + React + TypeScript desktop UI
+- Dashboard
+- Meetings list
+- Meeting details
+- Local video/audio playback
+- Live meeting screen
+- Real-time Whisper transcription through a local Python speech engine
+- Real-time speaker diarization through diart
+- `Speaker 1`, `Speaker 2`, ... labels
+- Live speaker rename
+- Transparent always-on-top overlay window
+- Microphone capture
+- Optional system-audio/screen capture through the WebView capture API
+- Local recording files
+- Local transcript data
+- Functional Settings screen
+- Mock speech-engine mode for UI testing without ML models
+
+Not included in this demo:
+
+- User login
+- User identification/account verification
+- Subscription/payment
+- PostgreSQL
+- SeaweedFS
+- Remote cloud storage
+- Real summary/task/reminder backend
+
+The backend-facing demo state is intentionally:
+
+```text
+Processing (The job). Please wait a moment
+```
+
+Summary/task/reminder controls show this processing state but do not call a production backend yet.
+
+---
+
+## Architecture
+
+```text
+Desktop UI (Tauri + React)
+ │
+ ├── microphone / optional system audio + screen
+ │
+ ├── MediaRecorder ───────────► local recording.webm
+ │
+ └── 16 kHz PCM
+ │
+ ▼
+ Local Speech Engine
+ Python WebSocket server
+ │
+ ┌──────┴───────┐
+ ▼ ▼
+ faster-whisper diart
+ transcription diarization
+ │ │
+ └──────┬───────┘
+ ▼
+ speaker-aware segments
+ │
+ ┌──────┴──────────┐
+ ▼ ▼
+ Live transcript Overlay window
+```
+
+The local speech engine is **not** the future MeetVault backend. It is a local ML side process used by the desktop demo.
+
+---
+
+## Project structure
+
+```text
+MeetVault-Desktop-Demo/
+├── src/ React desktop UI
+│ ├── components/
+│ ├── lib/
+│ │ ├── capture.ts microphone/system capture + MediaRecorder
+│ │ ├── speechClient.ts local WebSocket speech client
+│ │ ├── liveBus.ts main-window ↔ overlay events
+│ │ └── localFiles.ts local recording/transcript persistence
+│ ├── pages/
+│ │ ├── Dashboard.tsx
+│ │ ├── LiveMeeting.tsx
+│ │ ├── Meetings.tsx
+│ │ ├── MeetingDetails.tsx
+│ │ ├── Settings.tsx
+│ │ └── Overlay.tsx
+│ └── store/
+├── src-tauri/ Tauri 2 desktop shell
+├── speech-engine/ local Whisper + diart process
+├── docs/ supplied source wireframes/README
+└── scripts/
+```
+
+---
+
+## Prerequisites
+
+### Desktop UI
+
+- Node.js 20+
+- Rust toolchain compatible with your installed Tauri 2 release
+- Platform-specific Tauri prerequisites
+
+### Real speech engine
+
+Recommended:
+
+- Python 3.10 or 3.11
+- ffmpeg
+- PortAudio
+- libsndfile
+- Python packages in `speech-engine/requirements.txt`
+- Hugging Face/pyannote model access required by your diart setup
+
+For UI-only testing, use mock speech mode instead.
+
+---
+
+## 1. Install frontend dependencies
+
+```bash
+npm install
+```
+
+---
+
+## 2. Test the UI in a browser
+
+```bash
+npm run dev
+```
+
+Open:
+
+```text
+http://127.0.0.1:1420
+```
+
+Browser mode is useful for UI development. Tauri-only functions such as the native overlay window and persistent AppLocalData storage use reduced fallbacks in browser mode.
+
+---
+
+## 3. Install the local speech engine
+
+Create a Python 3.10/3.11 virtual environment inside `speech-engine`:
+
+```bash
+cd speech-engine
+python -m venv .venv
+```
+
+Windows:
+
+```powershell
+.venv\Scripts\activate
+pip install -r requirements.txt
+```
+
+Linux/macOS:
+
+```bash
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+### Real mode
+
+```bash
+python server.py
+```
+
+### Mock mode
+
+```bash
+python server.py --mock
+```
+
+The application expects the local engine at:
+
+```text
+ws://127.0.0.1:8765/ws/live
+```
+
+This can be changed from Settings.
+
+---
+
+## 4. Run as a desktop application
+
+From the repository root:
+
+```bash
+npm run desktop
+```
+
+On Windows, convenience scripts are also provided:
+
+```powershell
+scripts\start-demo.ps1
+```
+
+or for UI testing with mock speech:
+
+```powershell
+scripts\start-demo-mock.ps1
+```
+
+---
+
+# Live meeting behavior
+
+When a meeting starts:
+
+1. MeetVault requests microphone access.
+2. If **Capture system audio / screen** is enabled, it also opens the system screen/audio picker.
+3. Audio is mixed locally.
+4. The mixed audio is recorded through `MediaRecorder`.
+5. The same audio is converted to mono 16 kHz signed PCM.
+6. PCM is streamed to `speech-engine/server.py`.
+7. Whisper produces timestamped text.
+8. diart produces speaker time ranges.
+9. The local engine merges the two timelines.
+10. The UI receives segments such as:
+
+```json
+{
+ "speakerId": "speaker_1",
+ "start": 18.2,
+ "end": 21.7,
+ "text": "We should finish the API before Friday.",
+ "final": true
+}
+```
+
+11. The user can rename `speaker_1` to `Alice` without changing the underlying diarization ID.
+12. The main transcript and overlay immediately display the renamed speaker.
+
+---
+
+# Live overlay
+
+The Tauri build creates a second transparent window.
+
+Properties:
+
+- Always on top
+- Transparent
+- Resizable
+- No decorations
+- Hidden from the taskbar
+- Shows the latest transcript lines
+- Speaker name can be clicked to rename
+- Receives the same live transcript event stream as the main window
+
+The overlay is opened from **Show overlay** on the live meeting screen.
+
+---
+
+# Local files
+
+There is no cloud storage in this demo.
+
+In Tauri mode, media is stored below the application's local-data directory:
+
+```text
+meetings/{meeting_uuid}/recording.webm
+meetings/{meeting_uuid}/meeting.json
+meetings/{meeting_uuid}/transcript.json
+```
+
+The Meeting Details page can play the stored video/audio file.
+
+If screen capture is enabled, the recording normally contains video + mixed audio.
+
+If only microphone capture is enabled, the recording is audio-only.
+
+---
+
+# Video playback
+
+The Meeting Details screen includes a real HTML5 video/audio player.
+
+The Tauri asset protocol is scoped only to MeetVault's local `meetings` directory.
+
+This allows locally recorded media to be played without uploading it anywhere.
+
+---
+
+# Settings
+
+The Settings screen is functional and persisted locally.
+
+Current settings include:
+
+### General
+
+- Launch at startup preference
+- Keep in tray preference
+- Global hotkey value
+- Theme preference
+
+The startup/tray/hotkey preferences are stored now; OS registration is intentionally left for the packaging phase.
+
+### Audio
+
+- Microphone device ID
+- System audio/screen capture toggle
+- Microphone level monitoring
+
+### Transcription
+
+- Whisper model: `base`, `small`, `medium`
+- Compute device: auto/CPU/CUDA
+- Language
+- Speaker diarization toggle
+
+### Overlay
+
+- Enabled
+- Opacity
+- Font size
+- Visible line count
+
+### Speech engine
+
+- Real Whisper + diart mode
+- Mock mode
+- Local WebSocket URL
+
+---
+
+# Backend placeholder
+
+This demo deliberately has no remote backend implementation.
+
+Any backend-only action returns/displays:
+
+```text
+Processing (The job). Please wait a moment
+```
+
+This currently applies to example actions such as:
+
+- Generate/regenerate summary
+- Create reminder
+- Action-item processing
+
+The UI is structured so a real API can replace `src/lib/backend.ts` later.
+
+---
+
+# Current limitations
+
+## System audio
+
+System-audio capture depends on the operating system, WebView implementation, selected source, and whether that source permits audio capture.
+
+Microphone capture is the reliable baseline for the demo.
+
+## diart model setup
+
+Real diart execution can require pyannote model terms/authentication. The exact dependency combinations can also be sensitive to Python, PyTorch, torchaudio, and platform versions.
+
+Use the recommended Python version and validate the speech-engine environment on the target development machine before packaging.
+
+## Realtime accuracy
+
+Speaker diarization can temporarily swap speaker labels, especially with:
+
+- overlapping speakers
+- short utterances
+- background noise
+- similar voices
+
+The rename function changes the display name but does not perform biometric identity recognition.
+
+## Long recordings
+
+The media recorder writes the final Blob when the meeting ends in this initial demo. Before production use, change this to incremental/chunked file writing so multi-hour meetings do not accumulate the entire media Blob in memory.
+
+---
+
+# Recommended next development steps
+
+1. Validate real Whisper + diart on the target Windows machine.
+2. Replace final-Blob recording with incremental local file writes.
+3. Add proper microphone device enumeration.
+4. Add native Windows loopback audio capture if system-audio reliability is required.
+5. Add session crash recovery.
+6. Add transcript segment editing/reassignment.
+7. Package the Python speech engine as a sidecar or replace it with native whisper.cpp + a native/ONNX diarization runtime.
+8. Implement the real backend jobs/API later.
+9. Replace local-only media with SeaweedFS when backend/storage work begins.
+
+---
+
+# Demo design decisions from the supplied specification
+
+```text
+Login / subscription REMOVED
+Remote backend PLACEHOLDER ONLY
+Backend message "Processing (The job). Please wait a moment"
+Media storage LOCAL
+Video playback INCLUDED
+Live transcription INCLUDED
+Speaker diarization INCLUDED
+Live speaker rename INCLUDED
+Realtime overlay INCLUDED
+Settings INCLUDED
+```
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/docs/MeetVault_Desktop_README_original.md b/Wireframes/Desktop/MeetVault-Desktop-Demo/docs/MeetVault_Desktop_README_original.md
new file mode 100644
index 0000000..9711789
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/docs/MeetVault_Desktop_README_original.md
@@ -0,0 +1,1049 @@
+# MeetVault Desktop
+
+MeetVault Desktop is the local desktop client for MeetVault.
+
+Its main purpose is to:
+
+- Record meeting audio/video
+- Capture microphone and system audio
+- Transcribe speech locally using Whisper
+- Detect and separate speakers in real time using diart
+- Show a real-time transcription overlay on top of other applications
+- Allow users to rename detected speakers during the meeting
+- Upload recordings to SeaweedFS
+- Send the final transcript to the MeetVault backend
+- Receive generated summaries, action items, and reminders from the backend
+
+The desktop application should continue to work locally during a meeting even if the backend is temporarily unavailable.
+
+---
+
+# Core Desktop Architecture
+
+```text
+ MeetVault Desktop
+
+ ┌─────────────────┐
+ │ Desktop UI │
+ │ React + Tauri │
+ └────────┬────────┘
+ │
+ ▼
+ ┌─────────────────────┐
+ │ Audio Engine │
+ │ │
+ │ Microphone capture │
+ │ System audio capture│
+ │ Audio mixing │
+ └─────────┬───────────┘
+ │
+ ┌───────────────┴───────────────┐
+ │ │
+ ▼ ▼
+ ┌───────────────┐ ┌────────────────┐
+ │ Whisper │ │ diart │
+ │ │ │ │
+ │ transcription │ │ diarization │
+ │ + timestamps │ │ speaker labels │
+ └───────┬───────┘ └───────┬────────┘
+ │ │
+ └──────────────┬───────────────┘
+ ▼
+ ┌────────────────────┐
+ │ Segment Merger │
+ │ │
+ │ text + timestamps │
+ │ speaker timeline │
+ └─────────┬──────────┘
+ │
+ ┌─────────────┴──────────────┐
+ │ │
+ ▼ ▼
+ ┌─────────────────┐ ┌──────────────────┐
+ │ Live Transcript │ │ Realtime Overlay │
+ │ Window │ │ Always-on-top │
+ └─────────────────┘ └──────────────────┘
+```
+
+---
+
+# Recommended Desktop Stack
+
+```text
+Desktop shell
+Tauri
+
+UI
+React + TypeScript
+
+Native/local processing
+Rust where required
+
+Transcription
+Whisper / whisper.cpp
+
+Speaker diarization
+diart
+
+Backend API
+MeetVault backend
+
+Structured data
+PostgreSQL
+
+Media storage
+SeaweedFS
+```
+
+Tauri is recommended because the application needs:
+
+- Multiple windows
+- Transparent overlay windows
+- Always-on-top behavior
+- Native audio access
+- Local file access
+- Background processes
+- System tray support
+- Global shortcuts
+- Lower memory usage than a full Electron application
+
+---
+
+# Real-Time Audio Pipeline
+
+The application processes meeting audio locally.
+
+```text
+Microphone
+ │
+ ├────────────────┐
+ │ │
+System Audio │
+ │ │
+ └──────┬─────────┘
+ ▼
+ Audio Mixer
+ │
+ ▼
+ Audio Stream
+ │
+ ┌─────┴─────┐
+ │ │
+ ▼ ▼
+ Whisper diart
+ │ │
+ │ │
+ ▼ ▼
+ text + speaker
+ timestamps segments
+ │ │
+ └─────┬─────┘
+ ▼
+ Segment Merger
+ │
+ ▼
+ Live Transcript Event
+ │
+ ┌─────┴────────┐
+ │ │
+ ▼ ▼
+ Main Window Overlay Window
+```
+
+---
+
+# Whisper
+
+Whisper is responsible for speech-to-text.
+
+For the first version, a lightweight local runtime such as `whisper.cpp` is recommended.
+
+The transcription engine should provide:
+
+```text
+text
+start timestamp
+end timestamp
+confidence if available
+partial/final state
+```
+
+Example:
+
+```json
+{
+ "start": 18.2,
+ "end": 21.7,
+ "text": "We should finish the API before Friday.",
+ "final": true
+}
+```
+
+## Suggested Model Strategy
+
+Default:
+
+```text
+Whisper small
+```
+
+Possible fallback for lower-end hardware:
+
+```text
+Whisper base
+```
+
+Possible higher-accuracy option:
+
+```text
+Whisper medium
+```
+
+The selected model should be configurable in Settings.
+
+The goal is to balance:
+
+```text
+accuracy
+latency
+CPU/GPU usage
+memory usage
+```
+
+---
+
+# diart
+
+diart is responsible for real-time speaker diarization.
+
+It does not need to know a person's real identity.
+
+It only needs to produce stable speaker labels such as:
+
+```text
+Speaker 1
+Speaker 2
+Speaker 3
+```
+
+Example diarization result:
+
+```json
+{
+ "speaker": "speaker_1",
+ "start": 18.0,
+ "end": 22.0
+}
+```
+
+The application combines the diart result with the Whisper result using timestamps.
+
+---
+
+# Transcript + Speaker Merge
+
+Whisper may produce:
+
+```text
+18.2 - 21.7
+
+"We should finish the API before Friday."
+```
+
+diart may produce:
+
+```text
+18.0 - 22.0
+
+speaker_1
+```
+
+The merger produces:
+
+```json
+{
+ "speaker_id": "speaker_1",
+ "speaker_name": "Speaker 1",
+ "start": 18.2,
+ "end": 21.7,
+ "text": "We should finish the API before Friday."
+}
+```
+
+This becomes the core live transcript segment.
+
+---
+
+# Speaker Rename
+
+Detected speakers initially appear as:
+
+```text
+Speaker 1
+Speaker 2
+Speaker 3
+```
+
+The user can rename them at any time.
+
+Example:
+
+```text
+Speaker 1 → Alice
+Speaker 2 → Bob
+```
+
+The underlying diarization ID does not change.
+
+```text
+speaker_1
+ ↓
+display name
+ ↓
+Alice
+```
+
+Recommended local mapping:
+
+```json
+{
+ "speaker_1": "Alice",
+ "speaker_2": "Bob"
+}
+```
+
+Existing transcript lines and future overlay lines should immediately use the new display name.
+
+---
+
+# Real-Time Overlay
+
+The overlay is a dedicated transparent Tauri window.
+
+It should be:
+
+- Always on top
+- Transparent or semi-transparent
+- Resizable
+- Draggable
+- Lockable
+- Optionally click-through
+- Hotkey controlled
+- Able to show only the latest few transcript lines
+
+Example:
+
+```text
+┌──────────────────────────────────────────────┐
+│ Alice │
+│ We should finish the API before Friday. │
+│ │
+│ Bob │
+│ I'll prepare the deployment plan tonight. │
+└──────────────────────────────────────────────┘
+```
+
+The overlay should update continuously while the meeting is running.
+
+---
+
+# Overlay Update Model
+
+The desktop app should distinguish between:
+
+```text
+partial transcript
+final transcript
+```
+
+Example:
+
+```text
+Speaker 1:
+We should finish the...
+```
+
+updates to:
+
+```text
+Speaker 1:
+We should finish the API before Friday.
+```
+
+A partial line may be visually distinguished from finalized text.
+
+Recommended behavior:
+
+```text
+partial
+→ replace current live line
+
+final
+→ commit transcript segment
+→ append to transcript history
+```
+
+---
+
+# Main UI Screens
+
+## Dashboard
+
+Purpose:
+
+- Start a new meeting
+- View recent meetings
+- Check local transcription state
+- Check SeaweedFS/backend connectivity
+- Open Settings
+
+Main actions:
+
+```text
+Start Meeting
+Open Meeting
+Preview Overlay
+Settings
+```
+
+---
+
+# Live Meeting Screen
+
+The Live Meeting screen is the main control surface while recording.
+
+It should show:
+
+- Recording state
+- Meeting timer
+- Live transcript
+- Speaker labels
+- Rename controls
+- Microphone state
+- System audio state
+- Whisper model status
+- Diarization status
+- Overlay toggle
+- Pause
+- Finish meeting
+
+Example:
+
+```text
+Weekly Product Sync REC 00:18:42
+
+Live transcript
+
+Alice
+We should finalize the API contract.
+
+Bob
+I'll update the database migration.
+
+Alice
+Great. I'll prepare the release notes.
+
+
+Speakers
+
+Alice Rename
+Bob Rename
+
+
+Microphone ON
+System Audio ON
+Whisper Healthy
+diart Healthy
+
+[Pause] [Show Overlay] [Finish Meeting]
+```
+
+---
+
+# Speaker Management
+
+The speaker panel should display the diarization speakers detected in the current meeting.
+
+Example:
+
+```text
+speaker_1 Alice
+speaker_2 Bob
+speaker_3 Speaker 3
+```
+
+The user may rename a speaker at any time.
+
+The rename should affect:
+
+- Current live transcript
+- Overlay
+- Final transcript
+- Meeting review
+
+---
+
+# Meeting Details Screen
+
+After a meeting ends, the Meeting Details screen provides:
+
+- Video/audio playback
+- Meeting information
+- Transcript
+- Speaker names
+- Summary
+- Action items
+- Reminders
+- Export options
+
+Example layout:
+
+```text
+Weekly Product Sync
+Sep 27, 2026 • 48 min
+
+┌──────────────────────────┐
+│ Video Player │
+└──────────────────────────┘
+
+Transcript | Summary | Action Items | Files
+
+Alice 00:18:41
+We should finalize the API contract.
+
+Bob 00:18:49
+I'll update the database migration.
+```
+
+The transcript timestamp should be clickable so playback can jump to that position.
+
+---
+
+# Settings
+
+Settings should include the following sections.
+
+## General
+
+```text
+Launch at startup
+Keep running in tray
+Theme
+Global hotkey
+```
+
+## Audio
+
+```text
+Microphone device
+System audio device
+Input level
+System audio level
+Audio monitoring
+```
+
+## Transcription
+
+```text
+Whisper model
+Compute device
+Language
+Chunk size
+Voice activity detection
+```
+
+Suggested model options:
+
+```text
+base
+small
+medium
+```
+
+## Diarization
+
+```text
+Enable speaker diarization
+Maximum expected speakers
+Speaker clustering sensitivity
+```
+
+## Overlay
+
+```text
+Enable overlay
+Always on top
+Click-through mode
+Opacity
+Font size
+Maximum visible lines
+Show speaker labels
+Screen position
+```
+
+## Storage & Sync
+
+```text
+Local recording folder
+Automatic upload
+Upload after meeting
+Delete local media after successful upload
+Backend connection
+SeaweedFS upload status
+```
+
+## Account
+
+```text
+Signed-in user
+Subscription
+Sign out
+```
+
+---
+
+# Local Meeting Session Model
+
+During a live meeting, the desktop application should maintain a local session object.
+
+Example:
+
+```json
+{
+ "meeting_id": "uuid",
+ "started_at": "2026-09-27T09:00:00Z",
+ "status": "recording",
+
+ "speakers": {
+ "speaker_1": "Alice",
+ "speaker_2": "Bob"
+ },
+
+ "segments": [
+ {
+ "id": "segment-1",
+ "speaker_id": "speaker_1",
+ "start": 18.2,
+ "end": 21.7,
+ "text": "We should finish the API before Friday."
+ }
+ ]
+}
+```
+
+This state should be stored locally during recording so a temporary network failure does not lose the transcript.
+
+---
+
+# Transcript Segment Model
+
+A transcript should internally be represented as segments rather than only one large text string.
+
+Recommended structure:
+
+```json
+{
+ "id": "segment-uuid",
+ "speaker_id": "speaker_1",
+ "speaker_name": "Alice",
+ "start": 18.2,
+ "end": 21.7,
+ "text": "We should finish the API before Friday.",
+ "final": true
+}
+```
+
+Benefits:
+
+- Real-time rendering
+- Speaker rename
+- Timestamp navigation
+- Transcript editing
+- Summary generation
+- Export
+- Search
+
+---
+
+# Recording Flow
+
+```text
+User clicks Start Meeting
+ │
+ ▼
+Create local meeting session
+ │
+ ▼
+Start microphone capture
+ │
+ ├──── Start system audio capture
+ │
+ ▼
+Start recording
+ │
+ ├──── Start Whisper
+ │
+ └──── Start diart
+ │
+ ▼
+ Live transcript events
+ │
+ ┌──────┴──────┐
+ ▼ ▼
+ Main UI Overlay
+```
+
+---
+
+# Meeting Finish Flow
+
+```text
+User clicks Finish Meeting
+ │
+ ▼
+Stop audio capture
+ │
+ ▼
+Finalize Whisper segments
+ │
+ ▼
+Finalize speaker mapping
+ │
+ ▼
+Save local transcript
+ │
+ ├──── upload media ─────► SeaweedFS
+ │
+ └──── send transcript ──► Backend API
+ │
+ ▼
+ PostgreSQL
+ │
+ ▼
+ processing_jobs
+ │
+ ┌───────┴────────┐
+ ▼ ▼
+ Summarizer Action Items
+ │
+ ▼
+ Reminders
+```
+
+---
+
+# Upload Strategy
+
+Large media should not pass through the backend API.
+
+Recommended:
+
+```text
+Desktop
+ │
+ │ request presigned URL
+ ▼
+Backend
+ │
+ │ returns temporary upload URL
+ ▼
+Desktop ───────────────────────► SeaweedFS
+ media file
+```
+
+The transcript is small and can be sent directly through the backend API.
+
+```text
+Desktop
+ │
+ │ transcript JSON
+ ▼
+Backend API
+ │
+ ▼
+PostgreSQL
+```
+
+---
+
+# Offline / Network Failure Behavior
+
+The desktop application should not require continuous backend connectivity during a meeting.
+
+If the backend becomes unavailable:
+
+```text
+recording continues
+transcription continues
+diarization continues
+overlay continues
+local saving continues
+```
+
+After the connection returns:
+
+```text
+upload recording
+send transcript
+sync meeting metadata
+```
+
+This is an important design requirement for MeetVault Desktop.
+
+---
+
+# Background Components
+
+The desktop application can be separated into these logical modules:
+
+```text
+MeetVault Desktop
+
+├── UI
+│ ├── Dashboard
+│ ├── Live Meeting
+│ ├── Meeting Details
+│ └── Settings
+│
+├── Overlay
+│ └── Realtime subtitle window
+│
+├── Audio Engine
+│ ├── Microphone
+│ ├── System audio
+│ └── Mixer
+│
+├── Speech Engine
+│ ├── Whisper
+│ └── diart
+│
+├── Transcript Engine
+│ ├── Segment merger
+│ ├── Speaker mapping
+│ └── Transcript state
+│
+├── Recorder
+│ └── Media recording
+│
+├── Local Storage
+│ └── Session recovery/cache
+│
+└── Sync Client
+ ├── Backend API
+ └── SeaweedFS upload
+```
+
+---
+
+# Realtime Event Flow
+
+Internally, the UI should consume events rather than directly controlling the models.
+
+Example events:
+
+```text
+audio.started
+audio.stopped
+
+transcript.partial
+transcript.final
+
+speaker.detected
+speaker.changed
+
+speaker.renamed
+
+recording.started
+recording.stopped
+
+upload.started
+upload.progress
+upload.completed
+upload.failed
+```
+
+Example event:
+
+```json
+{
+ "type": "transcript.final",
+ "payload": {
+ "speaker_id": "speaker_1",
+ "start": 18.2,
+ "end": 21.7,
+ "text": "We should finish the API before Friday."
+ }
+}
+```
+
+Both the main transcript screen and overlay can subscribe to the same event stream.
+
+---
+
+# Realtime Performance Goals
+
+Initial targets:
+
+```text
+Overlay update latency
+< 2 seconds
+
+Partial transcript refresh
+~ 0.5–1.5 seconds
+
+Final segment delay
+~ 1–3 seconds
+
+Overlay frame impact
+Minimal
+
+Meeting duration
+Several hours without restart
+```
+
+Actual latency depends on:
+
+- Whisper model
+- CPU/GPU
+- Audio chunk size
+- Number of speakers
+- diart processing speed
+
+---
+
+# Speaker Diarization Limitations
+
+Speaker diarization is not perfect.
+
+Expected difficult cases include:
+
+- Two people speaking simultaneously
+- Very similar voices
+- Poor microphone quality
+- Strong background noise
+- Echo
+- Speakers far from the microphone
+- Very short speech segments
+
+The application should allow users to correct speaker names and, later, reassign individual transcript segments if necessary.
+
+No biometric speaker identification is required.
+
+MeetVault only needs:
+
+```text
+who spoke when
+```
+
+not:
+
+```text
+who is this person in real life
+```
+
+---
+
+# Security
+
+Permanent SeaweedFS credentials must never be stored in the renderer/frontend.
+
+The desktop app should use:
+
+```text
+Backend authentication
++
+temporary presigned upload URLs
+```
+
+Sensitive local files should be stored only in the configured MeetVault application data directory.
+
+Authentication tokens should use the operating system's secure credential storage when possible.
+
+---
+
+# MVP Scope
+
+The first desktop release should focus on:
+
+- Start/stop meeting recording
+- Microphone capture
+- System audio capture
+- Local Whisper transcription
+- Real-time diart speaker separation
+- Speaker 1 / Speaker 2 labels
+- Rename speakers
+- Real-time overlay
+- Local transcript storage
+- Meeting playback
+- Transcript review
+- Upload recording to SeaweedFS
+- Send transcript to backend
+- Receive generated summary
+- View action items
+- View reminders
+
+Not required for the first release:
+
+- Speaker biometric identification
+- Automatic real-name recognition
+- Cloud transcription
+- Kafka
+- Elasticsearch
+- Kubernetes
+- Vector database
+- Complex microservice architecture
+
+---
+
+# Final Desktop Architecture
+
+```text
+ MeetVault Desktop
+
+ Audio Sources
+ / \
+ Microphone System Audio
+ \ /
+ \ /
+ Audio Mixer
+ │
+ ┌─────────┴─────────┐
+ │ │
+ ▼ ▼
+ Whisper diart
+ │ │
+ transcript speakers
+ │ │
+ └─────────┬─────────┘
+ ▼
+ Segment Merger
+ │
+ ┌─────────────┴──────────────┐
+ │ │
+ ▼ ▼
+ Live Meeting UI Realtime Overlay
+ │
+ ▼
+ Local Session Store
+ │
+ meeting completes
+ │
+ ┌───────┴────────┐
+ │ │
+ ▼ ▼
+ SeaweedFS Backend API
+ media transcript
+ │
+ ▼
+ PostgreSQL
+ │
+ ▼
+ processing_jobs
+ │
+ ┌────────┴─────────┐
+ ▼ ▼
+ Summary Action Items
+ │
+ ▼
+ Reminders
+```
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/docs/MeetVault_UI_Wireframes_v2.drawio b/Wireframes/Desktop/MeetVault-Desktop-Demo/docs/MeetVault_UI_Wireframes_v2.drawio
new file mode 100644
index 0000000..d59fe52
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/docs/MeetVault_UI_Wireframes_v2.drawio
@@ -0,0 +1,577 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/index.html b/Wireframes/Desktop/MeetVault-Desktop-Demo/index.html
new file mode 100644
index 0000000..5fab724
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ MeetVault Demo
+
+
+
+
+
+
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/package-lock.json b/Wireframes/Desktop/MeetVault-Desktop-Demo/package-lock.json
new file mode 100644
index 0000000..8cab182
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/package-lock.json
@@ -0,0 +1,2271 @@
+{
+ "name": "meetvault-desktop-demo",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "meetvault-desktop-demo",
+ "version": "0.1.0",
+ "dependencies": {
+ "@tauri-apps/api": "^2.0.0",
+ "@tauri-apps/plugin-dialog": "^2.0.0",
+ "@tauri-apps/plugin-fs": "^2.0.0",
+ "lucide-react": "^0.468.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^7.1.1",
+ "zustand": "^5.0.2"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": "^2.0.0",
+ "@types/react": "^18.3.18",
+ "@types/react-dom": "^18.3.5",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.5"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@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.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@babel/types": "^7.29.8",
+ "@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.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "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.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.9",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
+ "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+ "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.8",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.8",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.8",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "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/@napi-rs/lzma-linux-x64-gnu": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^22.20 || ^24.12 || >=25"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.5.tgz",
+ "integrity": "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.5.tgz",
+ "integrity": "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.5.tgz",
+ "integrity": "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.5.tgz",
+ "integrity": "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.5.tgz",
+ "integrity": "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.5.tgz",
+ "integrity": "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.5.tgz",
+ "integrity": "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.5.tgz",
+ "integrity": "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.5.tgz",
+ "integrity": "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.5.tgz",
+ "integrity": "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.5.tgz",
+ "integrity": "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.5.tgz",
+ "integrity": "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.5.tgz",
+ "integrity": "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.5.tgz",
+ "integrity": "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.5.tgz",
+ "integrity": "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.5.tgz",
+ "integrity": "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.5.tgz",
+ "integrity": "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.5.tgz",
+ "integrity": "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.5.tgz",
+ "integrity": "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.5.tgz",
+ "integrity": "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.5.tgz",
+ "integrity": "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.5.tgz",
+ "integrity": "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.5.tgz",
+ "integrity": "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.5.tgz",
+ "integrity": "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.5.tgz",
+ "integrity": "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tauri-apps/api": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.12.0.tgz",
+ "integrity": "sha512-fUSxFX8VABYe9W9K5ROdEaKVkNUFRcMSRofjruvaxztXA+I0XnQHcYHF8m/a6iExRUxsUf0HsFWhPBuH9IgzJw==",
+ "license": "Apache-2.0 OR MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/tauri"
+ }
+ },
+ "node_modules/@tauri-apps/cli": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.0.tgz",
+ "integrity": "sha512-GIShnAR4C16kSTA8NO02fo8fOw/I5z5sAUEB5S2HlPHFnBkbihL4c9a/MpkxOm1+q5S+tFd3X4QUjHWxfyGzpQ==",
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "bin": {
+ "tauri": "tauri.js"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/tauri"
+ },
+ "optionalDependencies": {
+ "@tauri-apps/cli-darwin-arm64": "2.12.0",
+ "@tauri-apps/cli-darwin-x64": "2.12.0",
+ "@tauri-apps/cli-linux-arm-gnueabihf": "2.12.0",
+ "@tauri-apps/cli-linux-arm64-gnu": "2.12.0",
+ "@tauri-apps/cli-linux-arm64-musl": "2.12.0",
+ "@tauri-apps/cli-linux-riscv64-gnu": "2.12.0",
+ "@tauri-apps/cli-linux-x64-gnu": "2.12.0",
+ "@tauri-apps/cli-linux-x64-musl": "2.12.0",
+ "@tauri-apps/cli-win32-arm64-msvc": "2.12.0",
+ "@tauri-apps/cli-win32-ia32-msvc": "2.12.0",
+ "@tauri-apps/cli-win32-x64-msvc": "2.12.0"
+ }
+ },
+ "node_modules/@tauri-apps/cli-darwin-arm64": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.0.tgz",
+ "integrity": "sha512-v5J+jISihLGtih1+3yWagbsL+5dbLZORu4vaSI0vhY0Heb+MAW+T/1s1RyKxrQrwEPGBG5kqwfDDTnSbmVkuqA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-darwin-x64": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.0.tgz",
+ "integrity": "sha512-g27I07pfW/dMS8vcNqB9lamgKF0TqLlcy8e7t9qDGsB1iaeyjJWb2TgBF4tM9ZwLqPAZUXbsz28oXljO12WC4g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.0.tgz",
+ "integrity": "sha512-gXyQ710fNc99YuxFxNyeX9PLSrIpbtgBBj7GQo9tSeRT6fIK40q3/gm7X5jqZo7mWLR2m0R/2ALd3vAWo0Z32Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.0.tgz",
+ "integrity": "sha512-H9cyXuq9WgxoHh2NZ+R+oN+W78x5UwaBDEp0/fmqUUkaBoioCuOZErM9xKiIV6bpNAhmw1RoWZ49u37q/vaiAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-arm64-musl": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.0.tgz",
+ "integrity": "sha512-5tFuk3JKMBa1M/iN4gN32OYlSfhY3aqb5PO3zCugxaT+sfecyMKpc3EM9VDbhmwWBE9QD4Tfj7wGhlqx2yy2pQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.0.tgz",
+ "integrity": "sha512-LOrMyV+nb7pKq8P0BZtTBQuyzm1ORjn16R9MlupmxuT5LqxtuYJlw3VMwbaeXCToJ8jbP5u9JnkfmbqDlvETXw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-x64-gnu": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.0.tgz",
+ "integrity": "sha512-aMIkI+X9qvD2+k80QZB3tuOYT41/cEheikC510CwAIZ31KLXORsRASYYzM4mi1/58ICmtzSLFhCC08keir7COw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-x64-musl": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.0.tgz",
+ "integrity": "sha512-9gl3NhUbfuGhSKjeGCypKSdtoezOQRcDUrkhTWtLQEjnajets6peO6NadgI2DF0OzbiiFOzrnnIGMwBS+Ap6Iw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-win32-arm64-msvc": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.0.tgz",
+ "integrity": "sha512-1W+v86wsYLuyBDN0iDessD4CD9o+yNyO0m8Dp3+/vfjiZrJ/v7R7/aTRBeikaq2tLGgxeag0gxnFggBNBGw91A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.0.tgz",
+ "integrity": "sha512-SsBjvq4f7YaBGqcWoE3cycoItPf0SrQeqFqWWgfxoeZWAIz01E31LDSeZ2p/etJiIY0R9oi045chkFfb0O7WGw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-win32-x64-msvc": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.0.tgz",
+ "integrity": "sha512-9zzDKYUH4TKKBgRdDdVQX8fiRO5gJ8hD1nGsHU9DOHG5Jpm3arjbu9Xg1frR20EusG3Sl8X0dppoO/4EoUXUGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/plugin-dialog": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.8.0.tgz",
+ "integrity": "sha512-HSSD5C8UFE9ijD1wue84F7jjaIReFmj8AAPtS+xyjYRltJafIc4BiWHY7SJ6vbbew6u+YljEJHfyNfy5VwywXg==",
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@tauri-apps/api": "^2.12.0"
+ }
+ },
+ "node_modules/@tauri-apps/plugin-fs": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.6.0.tgz",
+ "integrity": "sha512-MhBAZ4rryd+h9fijd/GWDfArqiAeGI0ZNduV4wuRyUVI6wDZ+oDBY7atqtoXKDwlTUVSvpvmWJAY0ehQd/v6Uw==",
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@tauri-apps/api": "^2.12.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/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.31",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.26",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz",
+ "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.29.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.1.tgz",
+ "integrity": "sha512-AUdjuRyCNGUYtqpqfTmWyM4fXay8yIQhmLnvYe/THMGfT9B/34X7xQd3ifKxwyNPPpowVBjLb+64BN9Rn1mizw==",
+ "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.11.25",
+ "caniuse-lite": "^1.0.30001810",
+ "electron-to-chromium": "^1.5.438",
+ "node-releases": "^2.0.57",
+ "update-browserslist-db": "^1.3.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001812",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001812.tgz",
+ "integrity": "sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng==",
+ "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/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/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "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/electron-to-chromium": {
+ "version": "1.5.439",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.439.tgz",
+ "integrity": "sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "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",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "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/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/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/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/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "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/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/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "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/lucide-react": {
+ "version": "0.468.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
+ "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
+ }
+ },
+ "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.19",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
+ "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
+ "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/node-releases": {
+ "version": "2.0.57",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz",
+ "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.28",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
+ "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.18",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.18.4",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz",
+ "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.18.4",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz",
+ "integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.18.4"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.63.5",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz",
+ "integrity": "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+ "@rollup/rollup-android-arm-eabi": "4.63.5",
+ "@rollup/rollup-android-arm64": "4.63.5",
+ "@rollup/rollup-darwin-arm64": "4.63.5",
+ "@rollup/rollup-darwin-x64": "4.63.5",
+ "@rollup/rollup-freebsd-arm64": "4.63.5",
+ "@rollup/rollup-freebsd-x64": "4.63.5",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.63.5",
+ "@rollup/rollup-linux-arm-musleabihf": "4.63.5",
+ "@rollup/rollup-linux-arm64-gnu": "4.63.5",
+ "@rollup/rollup-linux-arm64-musl": "4.63.5",
+ "@rollup/rollup-linux-loong64-gnu": "4.63.5",
+ "@rollup/rollup-linux-loong64-musl": "4.63.5",
+ "@rollup/rollup-linux-ppc64-gnu": "4.63.5",
+ "@rollup/rollup-linux-ppc64-musl": "4.63.5",
+ "@rollup/rollup-linux-riscv64-gnu": "4.63.5",
+ "@rollup/rollup-linux-riscv64-musl": "4.63.5",
+ "@rollup/rollup-linux-s390x-gnu": "4.63.5",
+ "@rollup/rollup-linux-x64-gnu": "4.63.5",
+ "@rollup/rollup-linux-x64-musl": "4.63.5",
+ "@rollup/rollup-openbsd-x64": "4.63.5",
+ "@rollup/rollup-openharmony-arm64": "4.63.5",
+ "@rollup/rollup-win32-arm64-msvc": "4.63.5",
+ "@rollup/rollup-win32-ia32-msvc": "4.63.5",
+ "@rollup/rollup-win32-x64-gnu": "4.63.5",
+ "@rollup/rollup-win32-x64-msvc": "4.63.5",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "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/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/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/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",
+ "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==",
+ "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/vite": {
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "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/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/zustand": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz",
+ "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/package.json b/Wireframes/Desktop/MeetVault-Desktop-Demo/package.json
new file mode 100644
index 0000000..3103b3d
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "meetvault-desktop-demo",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 127.0.0.1 --port 1420",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview --host 127.0.0.1 --port 1420",
+ "tauri": "tauri",
+ "desktop": "tauri dev",
+ "speech": "python speech-engine/server.py",
+ "speech:mock": "python speech-engine/server.py --mock"
+ },
+ "dependencies": {
+ "@tauri-apps/api": "^2.0.0",
+ "@tauri-apps/plugin-dialog": "^2.0.0",
+ "@tauri-apps/plugin-fs": "^2.0.0",
+ "lucide-react": "^0.468.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^7.1.1",
+ "zustand": "^5.0.2"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": "^2.0.0",
+ "@types/react": "^18.3.18",
+ "@types/react-dom": "^18.3.5",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.5"
+ }
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/scripts/start-demo-mock.ps1 b/Wireframes/Desktop/MeetVault-Desktop-Demo/scripts/start-demo-mock.ps1
new file mode 100644
index 0000000..6641208
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/scripts/start-demo-mock.ps1
@@ -0,0 +1,5 @@
+$ErrorActionPreference = "Stop"
+Write-Host "Starting MeetVault mock speech engine..."
+Start-Process powershell -ArgumentList "-NoExit", "-Command", "cd '$PSScriptRoot\..\speech-engine'; python server.py --mock"
+Set-Location "$PSScriptRoot\.."
+npm run desktop
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/scripts/start-demo.ps1 b/Wireframes/Desktop/MeetVault-Desktop-Demo/scripts/start-demo.ps1
new file mode 100644
index 0000000..9d573c8
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/scripts/start-demo.ps1
@@ -0,0 +1,6 @@
+$ErrorActionPreference = "Stop"
+Write-Host "Starting MeetVault speech engine..."
+Start-Process powershell -ArgumentList "-NoExit", "-Command", "cd '$PSScriptRoot\..\speech-engine'; python server.py"
+Set-Location "$PSScriptRoot\.."
+Write-Host "Starting Tauri desktop app..."
+npm run desktop
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/scripts/start-demo.sh b/Wireframes/Desktop/MeetVault-Desktop-Demo/scripts/start-demo.sh
new file mode 100644
index 0000000..38f630c
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/scripts/start-demo.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(dirname "$0")/.."
+python speech-engine/server.py &
+ENGINE_PID=$!
+trap 'kill $ENGINE_PID 2>/dev/null || true' EXIT
+npm run desktop
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/.env.example b/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/.env.example
new file mode 100644
index 0000000..e233a54
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/.env.example
@@ -0,0 +1,3 @@
+# diart uses pyannote models. Authenticate with Hugging Face before real mode.
+# You can either run `huggingface-cli login` or provide a token here if your setup consumes it.
+HF_TOKEN=
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/README.md b/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/README.md
new file mode 100644
index 0000000..ad9a46f
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/README.md
@@ -0,0 +1,66 @@
+# MeetVault Local Speech Engine
+
+This process is **local**, not the future MeetVault backend. It provides the demo's real-time Whisper + diart functionality.
+
+## Recommended environment
+
+Use Python **3.10 or 3.11**. `diart`/PyTorch/pyannote compatibility is much better there than on very new Python releases.
+
+```bash
+python -m venv .venv
+# Windows
+.venv\Scripts\activate
+# Linux/macOS
+source .venv/bin/activate
+
+pip install -r requirements.txt
+```
+
+For diart, install the native audio dependencies required by your OS and authenticate with Hugging Face if the pyannote models are gated in your environment. Follow the current diart/pyannote model instructions and accept their model terms before starting real mode.
+
+## Run real mode
+
+```bash
+python server.py
+```
+
+The desktop application connects to:
+
+```text
+ws://127.0.0.1:8765/ws/live
+```
+
+## Run mock mode
+
+Mock mode requires only the FastAPI stack and is intended to validate UI behavior:
+
+```bash
+python server.py --mock
+```
+
+You can also choose **Mock demo** under MeetVault Settings → Local speech engine.
+
+## Audio protocol
+
+- mono
+- signed int16 little-endian PCM
+- 16 kHz
+- binary WebSocket frames
+
+The first WebSocket message is JSON configuration. The engine replies with JSON events such as:
+
+```json
+{
+ "type": "final",
+ "segment": {
+ "id": "uuid",
+ "speakerId": "speaker_1",
+ "start": 12.1,
+ "end": 15.4,
+ "text": "We should deploy tomorrow.",
+ "final": true
+ }
+}
+```
+
+Speaker names such as `Alice` are **not** generated by the engine. The engine produces stable session labels (`speaker_1`, `speaker_2`, ...), and the MeetVault UI lets the user rename those labels live.
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/requirements.txt b/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/requirements.txt
new file mode 100644
index 0000000..8741fa3
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/requirements.txt
@@ -0,0 +1,13 @@
+# Recommended Python: 3.12
+
+fastapi>=0.115,<1
+uvicorn[standard]>=0.30,<1
+
+# diart 0.9.2 requires NumPy < 2
+numpy>=1.26.4,<2
+
+faster-whisper>=1.1,<2
+python-dotenv>=1,<2
+
+# diart 0.9.2 supports Python 3.10–3.12
+diart==0.9.2
\ No newline at end of file
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/server.py b/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/server.py
new file mode 100644
index 0000000..86a4c37
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/speech-engine/server.py
@@ -0,0 +1,415 @@
+"""MeetVault local live speech engine.
+
+Protocol
+--------
+WebSocket: ws://127.0.0.1:8765/ws/live
+1. Client sends a JSON config message.
+2. Client streams signed 16-bit little-endian mono PCM at 16 kHz.
+3. Server returns JSON events: ready, partial, final, health, error.
+
+Real mode combines faster-whisper with diart speaker diarization. Mock mode is
+available so the desktop UI can be exercised without ML dependencies/models.
+"""
+
+from __future__ import annotations
+
+import argparse
+import asyncio
+import json
+import os
+import queue
+import threading
+import time
+import uuid
+from dataclasses import dataclass
+from typing import Optional
+
+import numpy as np
+from fastapi import FastAPI, WebSocket, WebSocketDisconnect
+from fastapi.middleware.cors import CORSMiddleware
+import uvicorn
+
+SAMPLE_RATE = 16000
+
+
+@dataclass
+class EngineConfig:
+ model: str = "small"
+ compute: str = "auto"
+ language: str = "auto"
+ diarization: bool = True
+ sample_rate: int = SAMPLE_RATE
+
+
+class SpeakerTimeline:
+ """Thread-safe speaker timeline built from diart streaming predictions."""
+
+ def __init__(self) -> None:
+ self._segments: list[tuple[float, float, str]] = []
+ self._label_map: dict[str, str] = {}
+ self._lock = threading.Lock()
+
+ def _stable_label(self, raw: str) -> str:
+ with self._lock:
+ if raw not in self._label_map:
+ self._label_map[raw] = f"speaker_{len(self._label_map) + 1}"
+ return self._label_map[raw]
+
+ def update_from_annotation(self, annotation) -> None:
+ fresh: list[tuple[float, float, str]] = []
+ try:
+ for segment, _, label in annotation.itertracks(yield_label=True):
+ fresh.append((float(segment.start), float(segment.end), self._stable_label(str(label))))
+ except Exception:
+ return
+ if not fresh:
+ return
+ latest_end = max(end for _, end, _ in fresh)
+ with self._lock:
+ # Keep a rolling two-minute history, then merge the latest prediction.
+ self._segments = [s for s in self._segments if s[1] >= latest_end - 120]
+ self._segments.extend(fresh)
+
+ def resolve(self, start: float, end: float) -> str:
+ with self._lock:
+ candidates = list(self._segments)
+ best_label = "speaker_1"
+ best_overlap = 0.0
+ for s, e, label in candidates:
+ overlap = max(0.0, min(end, e) - max(start, s))
+ if overlap > best_overlap:
+ best_overlap = overlap
+ best_label = label
+ return best_label
+
+
+class QueueAudioSource:
+ """diart AudioSource fed by PCM pushed from the WebSocket thread."""
+
+ def __init__(self, sample_rate: int):
+ from diart.sources import AudioSource
+
+ class _Source(AudioSource):
+ def __init__(self, sr: int):
+ super().__init__("meetvault-live", sr)
+ self.q: queue.Queue[Optional[np.ndarray]] = queue.Queue()
+ self.cursor = 0
+
+ @property
+ def is_regular(self):
+ return False
+
+ @property
+ def duration(self):
+ return None
+
+ def push(self, samples: np.ndarray):
+ self.q.put(samples.astype(np.float32, copy=False))
+
+ def close(self):
+ self.q.put(None)
+
+ def read(self):
+ from pyannote.core import SlidingWindow, SlidingWindowFeature
+
+ while True:
+ item = self.q.get()
+ if item is None:
+ self.stream.on_completed()
+ return
+ start = self.cursor / self.sample_rate
+ window = SlidingWindow(start=start, duration=1 / self.sample_rate, step=1 / self.sample_rate)
+ feature = SlidingWindowFeature(item.reshape(-1, 1), window)
+ self.cursor += len(item)
+ self.stream.on_next(feature)
+
+ self.source = _Source(sample_rate)
+
+ def push(self, samples: np.ndarray) -> None:
+ self.source.push(samples)
+
+ def close(self) -> None:
+ self.source.close()
+
+
+class DiartWorker:
+ def __init__(self, enabled: bool, sample_rate: int = SAMPLE_RATE):
+ self.enabled = enabled
+ self.timeline = SpeakerTimeline()
+ self.audio_source: Optional[QueueAudioSource] = None
+ self.thread: Optional[threading.Thread] = None
+ self.error: Optional[str] = None
+ if enabled:
+ self._start(sample_rate)
+
+ def _start(self, sample_rate: int) -> None:
+ try:
+ from diart import SpeakerDiarization
+ from diart.inference import StreamingInference
+
+ pipeline = SpeakerDiarization()
+ expected_rate = int(getattr(pipeline.config, "sample_rate", sample_rate))
+ if expected_rate != sample_rate:
+ raise RuntimeError(f"diart expects {expected_rate} Hz but MeetVault sends {sample_rate} Hz")
+ self.audio_source = QueueAudioSource(sample_rate)
+ inference = StreamingInference(pipeline, self.audio_source.source, do_plot=False)
+
+ def hook(annotation_and_waveform):
+ try:
+ self.timeline.update_from_annotation(annotation_and_waveform[0])
+ except Exception:
+ pass
+
+ inference.attach_hooks(hook)
+ self.thread = threading.Thread(target=inference, name="meetvault-diart", daemon=True)
+ self.thread.start()
+ except Exception as exc:
+ self.error = str(exc)
+ self.enabled = False
+
+ def push(self, samples: np.ndarray) -> None:
+ if self.enabled and self.audio_source:
+ self.audio_source.push(samples)
+
+ def close(self) -> None:
+ if self.audio_source:
+ self.audio_source.close()
+
+ def resolve(self, start: float, end: float) -> str:
+ return self.timeline.resolve(start, end) if self.enabled else "speaker_1"
+
+
+class LiveSpeechSession:
+ def __init__(self, config: EngineConfig):
+ from faster_whisper import WhisperModel
+
+ self.config = config
+ device = config.compute
+ if device == "auto":
+ device = "cpu"
+ compute_type = "float16" if device == "cuda" else "int8"
+ try:
+ self.model = WhisperModel(config.model, device=device, compute_type=compute_type)
+ except Exception:
+ # Useful fallback when CUDA was selected but is not available.
+ self.model = WhisperModel(config.model, device="cpu", compute_type="int8")
+
+ self.diarizer = DiartWorker(config.diarization, config.sample_rate)
+ self.samples = np.empty(0, dtype=np.float32)
+ self.lock = threading.Lock()
+ self.emitted_until = 0.0
+ self.last_process_samples = 0
+ self.closed = False
+
+ def push_pcm(self, raw: bytes) -> None:
+ pcm = np.frombuffer(raw, dtype=" max_samples:
+ dropped = self.samples.size - max_samples
+ self.samples = self.samples[dropped:]
+ self.emitted_until = max(0.0, self.emitted_until - dropped / self.config.sample_rate)
+
+ def total_seconds(self) -> float:
+ with self.lock:
+ return self.samples.size / self.config.sample_rate
+
+ def should_process(self) -> bool:
+ with self.lock:
+ new_samples = self.samples.size - self.last_process_samples
+ return new_samples >= int(self.config.sample_rate * 1.4)
+
+ def transcribe_once(self) -> tuple[list[dict], Optional[str]]:
+ with self.lock:
+ total = self.samples.size
+ if total < self.config.sample_rate:
+ return [], None
+ window_size = min(total, self.config.sample_rate * 10)
+ audio = self.samples[-window_size:].copy()
+ self.last_process_samples = total
+ window_start = max(0.0, total / self.config.sample_rate - len(audio) / self.config.sample_rate)
+
+ segments_iter, _ = self.model.transcribe(
+ audio,
+ language=None if self.config.language in ("", "auto") else self.config.language,
+ beam_size=1,
+ best_of=1,
+ vad_filter=True,
+ word_timestamps=True,
+ condition_on_previous_text=False,
+ temperature=0.0,
+ )
+ segments = list(segments_iter)
+ results: list[dict] = []
+ partial: Optional[str] = None
+ safe_cutoff = total / self.config.sample_rate - 0.65
+
+ for seg in segments:
+ text = seg.text.strip()
+ if not text:
+ continue
+ abs_start = window_start + float(seg.start)
+ abs_end = window_start + float(seg.end)
+ if abs_end <= self.emitted_until + 0.08:
+ continue
+ if abs_end > safe_cutoff:
+ partial = text
+ continue
+ speaker_id = self.diarizer.resolve(abs_start, abs_end)
+ results.append({
+ "id": str(uuid.uuid4()),
+ "speakerId": speaker_id,
+ "start": abs_start,
+ "end": abs_end,
+ "text": text,
+ "final": True,
+ })
+ self.emitted_until = max(self.emitted_until, abs_end)
+
+ return results, partial
+
+ def close(self) -> None:
+ self.closed = True
+ self.diarizer.close()
+
+
+class MockSession:
+ def __init__(self, config: EngineConfig):
+ self.config = config
+ self.sample_count = 0
+ self.index = 0
+ self.lines = [
+ ("speaker_1", "We should finalize the upload flow before Friday."),
+ ("speaker_2", "I can take the local recording and video playback work."),
+ ("speaker_1", "Great. Keep the backend in processing state for this demo."),
+ ("speaker_3", "The overlay should keep updating while another application is open."),
+ ]
+
+ def push_pcm(self, raw: bytes) -> Optional[dict]:
+ self.sample_count += len(raw) // 2
+ threshold = self.config.sample_rate * 2.4
+ if self.sample_count < threshold:
+ return None
+ self.sample_count = 0
+ speaker, text = self.lines[self.index % len(self.lines)]
+ self.index += 1
+ end = self.index * 2.4
+ return {
+ "id": str(uuid.uuid4()), "speakerId": speaker,
+ "start": end - 2.1, "end": end, "text": text, "final": True,
+ }
+
+ def close(self) -> None:
+ pass
+
+
+app = FastAPI(title="MeetVault Local Speech Engine")
+app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
+MOCK_ONLY = False
+
+
+@app.get("/health")
+async def health():
+ return {"ok": True, "mode": "mock" if MOCK_ONLY else "real-capable", "sample_rate": SAMPLE_RATE}
+
+
+@app.websocket("/ws/live")
+async def live(websocket: WebSocket):
+ await websocket.accept()
+ session = None
+ process_task = None
+ try:
+ first = await websocket.receive_text()
+ payload = json.loads(first)
+ config = EngineConfig(
+ model=str(payload.get("model", "small")),
+ compute=str(payload.get("compute", "auto")),
+ language=str(payload.get("language", "auto")),
+ diarization=bool(payload.get("diarization", True)),
+ sample_rate=int(payload.get("sample_rate", SAMPLE_RATE)),
+ )
+ if config.sample_rate != SAMPLE_RATE:
+ await websocket.send_json({"type": "error", "message": "MeetVault speech engine currently requires 16 kHz mono PCM."})
+ return
+
+ if MOCK_ONLY:
+ session = MockSession(config)
+ await websocket.send_json({"type": "ready", "engine": "mock-whisper", "diarization": "mock-diart"})
+ else:
+ try:
+ session = await asyncio.to_thread(LiveSpeechSession, config)
+ except Exception as exc:
+ await websocket.send_json({
+ "type": "error",
+ "message": (
+ f"Could not initialize Whisper/diart: {exc}. "
+ "Use --mock for UI testing. Real diarization may require Hugging Face authentication and accepted pyannote model terms."
+ ),
+ })
+ return
+ diarization_state = "diart ready" if session.diarizer.enabled else f"diart unavailable: {session.diarizer.error}"
+ await websocket.send_json({"type": "ready", "engine": f"Whisper {config.model}", "diarization": diarization_state})
+
+ async def processor():
+ while not session.closed:
+ await asyncio.sleep(0.35)
+ if not session.should_process():
+ continue
+ try:
+ finals, partial = await asyncio.to_thread(session.transcribe_once)
+ for segment in finals:
+ await websocket.send_json({"type": "final", "segment": segment})
+ if partial:
+ await websocket.send_json({"type": "partial", "text": partial})
+ except Exception as exc:
+ await websocket.send_json({"type": "error", "message": f"Live transcription error: {exc}"})
+ await asyncio.sleep(1)
+
+ process_task = asyncio.create_task(processor())
+
+ while True:
+ message = await websocket.receive()
+ if message.get("bytes") is not None:
+ raw = message["bytes"]
+ if isinstance(session, MockSession):
+ segment = session.push_pcm(raw)
+ if segment:
+ await websocket.send_json({"type": "final", "segment": segment})
+ else:
+ session.push_pcm(raw)
+ elif message.get("text"):
+ command = json.loads(message["text"])
+ if command.get("type") == "stop":
+ break
+ except WebSocketDisconnect:
+ pass
+ except Exception as exc:
+ try:
+ await websocket.send_json({"type": "error", "message": str(exc)})
+ except Exception:
+ pass
+ finally:
+ if session:
+ session.close()
+ if process_task:
+ process_task.cancel()
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--host", default="127.0.0.1")
+ parser.add_argument("--port", type=int, default=8765)
+ parser.add_argument("--mock", action="store_true", help="Run without Whisper/diart models")
+ args = parser.parse_args()
+ global MOCK_ONLY
+ MOCK_ONLY = args.mock
+ uvicorn.run(app, host=args.host, port=args.port, log_level="info")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/Cargo.toml b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/Cargo.toml
new file mode 100644
index 0000000..202d547
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "meetvault_demo"
+version = "0.1.0"
+description = "MeetVault desktop demo"
+authors = ["MeetVault"]
+edition = "2021"
+
+[lib]
+name = "meetvault_demo_lib"
+crate-type = ["staticlib", "cdylib", "rlib"]
+
+[build-dependencies]
+tauri-build = { version = "2", features = [] }
+
+[dependencies]
+tauri = { version = "2", features = [] }
+tauri-plugin-fs = "2"
+tauri-plugin-dialog = "2"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/build.rs b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/build.rs
new file mode 100644
index 0000000..d860e1e
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ tauri_build::build()
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/capabilities/default.json b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/capabilities/default.json
new file mode 100644
index 0000000..70c1691
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/capabilities/default.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "default",
+ "description": "MeetVault demo desktop permissions",
+ "windows": ["main", "overlay"],
+ "permissions": [
+ "core:default",
+ "core:webview:allow-create-webview-window",
+ "core:window:allow-show",
+ "core:window:allow-hide",
+ "core:window:allow-set-focus",
+ "fs:allow-mkdir",
+ "fs:allow-write-file",
+ "fs:allow-write-text-file",
+ "fs:allow-read-file",
+ {
+ "identifier": "fs:scope",
+ "allow": [
+ { "path": "$APPLOCALDATA/meetings/**/*" },
+ { "path": "$APPLOCALDATA/meetings/*" }
+ ]
+ },
+ "dialog:default"
+ ]
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/src/lib.rs b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/src/lib.rs
new file mode 100644
index 0000000..b3e9f71
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/src/lib.rs
@@ -0,0 +1,8 @@
+#[cfg_attr(mobile, tauri::mobile_entry_point)]
+pub fn run() {
+ tauri::Builder::default()
+ .plugin(tauri_plugin_fs::init())
+ .plugin(tauri_plugin_dialog::init())
+ .run(tauri::generate_context!())
+ .expect("error while running MeetVault");
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/src/main.rs b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/src/main.rs
new file mode 100644
index 0000000..2ddcf46
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+ meetvault_demo_lib::run();
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/tauri.conf.json b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/tauri.conf.json
new file mode 100644
index 0000000..b6fc504
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src-tauri/tauri.conf.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "https://schema.tauri.app/config/2",
+ "productName": "MeetVault Demo",
+ "version": "0.1.0",
+ "identifier": "com.meetvault.demo",
+ "build": {
+ "beforeDevCommand": "npm run dev",
+ "devUrl": "http://127.0.0.1:1420",
+ "beforeBuildCommand": "npm run build",
+ "frontendDist": "../dist"
+ },
+ "app": {
+ "windows": [
+ {
+ "label": "main",
+ "title": "MeetVault Demo",
+ "width": 1440,
+ "height": 900,
+ "minWidth": 1080,
+ "minHeight": 720,
+ "resizable": true,
+ "center": true
+ }
+ ],
+ "security": {
+ "csp": null,
+ "assetProtocol": {
+ "enable": true,
+ "scope": ["$APPLOCALDATA/meetings/**/*"]
+ }
+ }
+ },
+ "bundle": {
+ "active": true,
+ "targets": "all"
+ }
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/App.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/App.tsx
new file mode 100644
index 0000000..f5a8ea4
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/App.tsx
@@ -0,0 +1,25 @@
+import { HashRouter, Route, Routes } from 'react-router-dom';
+import { Layout } from './components/Layout';
+import { Dashboard } from './pages/Dashboard';
+import { LiveMeeting } from './pages/LiveMeeting';
+import { MeetingDetails } from './pages/MeetingDetails';
+import { Meetings } from './pages/Meetings';
+import { Overlay } from './pages/Overlay';
+import { SettingsPage } from './pages/Settings';
+
+export default function App() {
+ return (
+
+
+ } />
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ );
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/Layout.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/Layout.tsx
new file mode 100644
index 0000000..f690ba5
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/Layout.tsx
@@ -0,0 +1,22 @@
+import { Gauge, History, Settings } from 'lucide-react';
+import { NavLink, Outlet } from 'react-router-dom';
+
+export function Layout() {
+ return (
+
+ );
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/ProcessingCard.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/ProcessingCard.tsx
new file mode 100644
index 0000000..d7bb83f
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/ProcessingCard.tsx
@@ -0,0 +1,13 @@
+import { LoaderCircle } from 'lucide-react';
+
+export function ProcessingCard() {
+ return (
+
+
+
+
Processing
+
Processing (The job). Please wait a moment
+
+
+ );
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/StatusBadge.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/StatusBadge.tsx
new file mode 100644
index 0000000..4fe9383
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/StatusBadge.tsx
@@ -0,0 +1,3 @@
+export function StatusBadge({ status }: { status: string }) {
+ return {status[0].toUpperCase() + status.slice(1)};
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/VideoPlayer.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/VideoPlayer.tsx
new file mode 100644
index 0000000..c0ec9cc
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/components/VideoPlayer.tsx
@@ -0,0 +1,19 @@
+import { Film } from 'lucide-react';
+import { mediaUrl } from '../lib/localFiles';
+
+export function VideoPlayer({ path, mime }: { path?: string; mime?: string }) {
+ const url = mediaUrl(path);
+ if (!url) {
+ return (
+
+
+ No local recording yet
+ Start a meeting with system audio/screen capture to create a video, or microphone-only for audio.
+
+ );
+ }
+ if (mime?.startsWith('audio/')) {
+ return ;
+ }
+ return ;
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/backend.ts b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/backend.ts
new file mode 100644
index 0000000..c957e54
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/backend.ts
@@ -0,0 +1,11 @@
+export interface DemoJobResult {
+ status: 'processing';
+ message: string;
+}
+
+export async function requestDemoBackendJob(): Promise {
+ return {
+ status: 'processing',
+ message: 'Processing (The job). Please wait a moment',
+ };
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/capture.ts b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/capture.ts
new file mode 100644
index 0000000..30707e5
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/capture.ts
@@ -0,0 +1,124 @@
+export interface CaptureOptions {
+ captureSystemAudio: boolean;
+ microphoneDeviceId?: string;
+ onPcm: (pcm: Int16Array) => void;
+ onLevel?: (level: number) => void;
+}
+
+export interface CaptureController {
+ hasVideo: boolean;
+ stop: () => Promise;
+ pause: () => void;
+ resume: () => void;
+}
+
+function downsampleFloat32(input: Float32Array, sourceRate: number, targetRate = 16000): Float32Array {
+ if (sourceRate === targetRate) return input;
+ const ratio = sourceRate / targetRate;
+ const length = Math.round(input.length / ratio);
+ const output = new Float32Array(length);
+ let offset = 0;
+ for (let i = 0; i < length; i++) {
+ const next = Math.round((i + 1) * ratio);
+ let sum = 0;
+ let count = 0;
+ for (; offset < next && offset < input.length; offset++) {
+ sum += input[offset];
+ count++;
+ }
+ output[i] = count ? sum / count : 0;
+ }
+ return output;
+}
+
+function floatToInt16(input: Float32Array): Int16Array {
+ const output = new Int16Array(input.length);
+ for (let i = 0; i < input.length; i++) {
+ const sample = Math.max(-1, Math.min(1, input[i]));
+ output[i] = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
+ }
+ return output;
+}
+
+function pickMime(hasVideo: boolean): string {
+ const candidates = hasVideo
+ ? ['video/webm;codecs=vp8,opus', 'video/webm;codecs=vp9,opus', 'video/webm']
+ : ['audio/webm;codecs=opus', 'audio/webm'];
+ return candidates.find((candidate) => MediaRecorder.isTypeSupported(candidate)) ?? '';
+}
+
+export async function startCapture(options: CaptureOptions): Promise {
+ const mic = await navigator.mediaDevices.getUserMedia({
+ audio: options.microphoneDeviceId && options.microphoneDeviceId !== 'default'
+ ? { deviceId: { exact: options.microphoneDeviceId }, noiseSuppression: true, autoGainControl: true }
+ : { noiseSuppression: true, autoGainControl: true },
+ video: false,
+ });
+
+ let display: MediaStream | undefined;
+ if (options.captureSystemAudio) {
+ display = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
+ }
+
+ const context = new AudioContext();
+ await context.resume();
+ const mix = context.createGain();
+ const destination = context.createMediaStreamDestination();
+ const processor = context.createScriptProcessor(4096, 1, 1);
+ const silent = context.createGain();
+ silent.gain.value = 0;
+
+ context.createMediaStreamSource(mic).connect(mix);
+ if (display && display.getAudioTracks().length) {
+ context.createMediaStreamSource(new MediaStream(display.getAudioTracks())).connect(mix);
+ }
+
+ mix.connect(destination);
+ mix.connect(processor);
+ processor.connect(silent);
+ silent.connect(context.destination);
+
+ processor.onaudioprocess = (event) => {
+ const channel = event.inputBuffer.getChannelData(0);
+ let rms = 0;
+ for (let i = 0; i < channel.length; i++) rms += channel[i] * channel[i];
+ options.onLevel?.(Math.sqrt(rms / channel.length));
+ const downsampled = downsampleFloat32(channel, context.sampleRate, 16000);
+ options.onPcm(floatToInt16(downsampled));
+ };
+
+ const recordingStream = new MediaStream();
+ destination.stream.getAudioTracks().forEach((track) => recordingStream.addTrack(track));
+ const videoTrack = display?.getVideoTracks()[0];
+ if (videoTrack) recordingStream.addTrack(videoTrack);
+
+ const hasVideo = Boolean(videoTrack);
+ const mimeType = pickMime(hasVideo);
+ const recorder = new MediaRecorder(recordingStream, mimeType ? { mimeType } : undefined);
+ const chunks: BlobPart[] = [];
+ recorder.ondataavailable = (event) => {
+ if (event.data.size) chunks.push(event.data);
+ };
+ recorder.start(1000);
+
+ return {
+ hasVideo,
+ pause: () => {
+ if (recorder.state === 'recording') recorder.pause();
+ },
+ resume: () => {
+ if (recorder.state === 'paused') recorder.resume();
+ },
+ stop: () => new Promise((resolve) => {
+ recorder.onstop = async () => {
+ processor.disconnect();
+ silent.disconnect();
+ mic.getTracks().forEach((track) => track.stop());
+ display?.getTracks().forEach((track) => track.stop());
+ await context.close();
+ resolve(new Blob(chunks, { type: recorder.mimeType || (hasVideo ? 'video/webm' : 'audio/webm') }));
+ };
+ recorder.stop();
+ }),
+ };
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/liveBus.ts b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/liveBus.ts
new file mode 100644
index 0000000..9246af7
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/liveBus.ts
@@ -0,0 +1,45 @@
+import { emit } from '@tauri-apps/api/event';
+import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
+import type { LiveState } from '../types';
+import { isTauriRuntime } from './tauri';
+
+const KEY = 'meetvault-live-state-v1';
+
+export async function broadcastLiveState(state: LiveState): Promise {
+ localStorage.setItem(KEY, JSON.stringify(state));
+ if (isTauriRuntime()) {
+ try { await emit('meetvault:live-state', state); } catch { /* ignored in web preview */ }
+ }
+}
+
+export function getCachedLiveState(): LiveState | undefined {
+ const raw = localStorage.getItem(KEY);
+ if (!raw) return undefined;
+ try { return JSON.parse(raw) as LiveState; } catch { return undefined; }
+}
+
+export async function openOverlayWindow(): Promise {
+ if (!isTauriRuntime()) return false;
+ const existing = await WebviewWindow.getByLabel('overlay');
+ if (existing) {
+ await existing.show();
+ await existing.setFocus();
+ return true;
+ }
+ const overlay = new WebviewWindow('overlay', {
+ url: 'index.html#/overlay',
+ title: 'MeetVault Live Overlay',
+ width: 900,
+ height: 250,
+ decorations: false,
+ transparent: true,
+ alwaysOnTop: true,
+ resizable: true,
+ skipTaskbar: true,
+ shadow: false,
+ });
+ return new Promise((resolve) => {
+ overlay.once('tauri://created', () => resolve(true));
+ overlay.once('tauri://error', () => resolve(false));
+ });
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/localFiles.ts b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/localFiles.ts
new file mode 100644
index 0000000..99fc47e
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/localFiles.ts
@@ -0,0 +1,30 @@
+import { convertFileSrc } from '@tauri-apps/api/core';
+import { appLocalDataDir, join } from '@tauri-apps/api/path';
+import { BaseDirectory, mkdir, writeFile, writeTextFile } from '@tauri-apps/plugin-fs';
+import type { Meeting } from '../types';
+import { isTauriRuntime } from './tauri';
+
+export async function saveMeetingLocally(meeting: Meeting, media: Blob): Promise<{ mediaPath: string; mediaMime: string }> {
+ if (!isTauriRuntime()) {
+ return { mediaPath: URL.createObjectURL(media), mediaMime: media.type || 'video/webm' };
+ }
+
+ const dir = `meetings/${meeting.id}`;
+ await mkdir(dir, { baseDir: BaseDirectory.AppLocalData, recursive: true });
+ const extension = media.type.startsWith('audio/') ? 'webm' : 'webm';
+ const relativeMediaPath = `${dir}/recording.${extension}`;
+ const bytes = new Uint8Array(await media.arrayBuffer());
+ await writeFile(relativeMediaPath, bytes, { baseDir: BaseDirectory.AppLocalData });
+ await writeTextFile(`${dir}/meeting.json`, JSON.stringify(meeting, null, 2), { baseDir: BaseDirectory.AppLocalData });
+ await writeTextFile(`${dir}/transcript.json`, JSON.stringify(meeting.transcript, null, 2), { baseDir: BaseDirectory.AppLocalData });
+
+ const root = await appLocalDataDir();
+ const mediaPath = await join(root, relativeMediaPath);
+ return { mediaPath, mediaMime: media.type || 'video/webm' };
+}
+
+export function mediaUrl(path?: string): string | undefined {
+ if (!path) return undefined;
+ if (path.startsWith('blob:') || path.startsWith('http:') || path.startsWith('https:')) return path;
+ return isTauriRuntime() ? convertFileSrc(path) : path;
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/speechClient.ts b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/speechClient.ts
new file mode 100644
index 0000000..d8d7e31
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/speechClient.ts
@@ -0,0 +1,92 @@
+import type { AppSettings, SpeechEvent } from '../types';
+
+export class SpeechClient {
+ private ws?: WebSocket;
+ private mockTimer = 0;
+ private mockSamples = 0;
+ private mockIndex = 0;
+ private onEvent: (event: SpeechEvent) => void;
+ private settings: AppSettings;
+
+ constructor(settings: AppSettings, onEvent: (event: SpeechEvent) => void) {
+ this.settings = settings;
+ this.onEvent = onEvent;
+ }
+
+ async connect(): Promise {
+ if (this.settings.speechMode === 'mock') {
+ this.onEvent({ type: 'ready', engine: 'mock-whisper', diarization: 'mock-diart' });
+ return;
+ }
+
+ await new Promise((resolve, reject) => {
+ const ws = new WebSocket(this.settings.speechEngineUrl);
+ this.ws = ws;
+ ws.binaryType = 'arraybuffer';
+ ws.onopen = () => {
+ ws.send(JSON.stringify({
+ type: 'config',
+ model: this.settings.whisperModel,
+ compute: this.settings.computeDevice,
+ language: this.settings.language,
+ diarization: this.settings.diarization,
+ sample_rate: 16000,
+ }));
+ resolve();
+ };
+ ws.onerror = () => reject(new Error('Could not connect to the local speech engine. Start speech-engine/server.py or switch Settings → Speech engine mode to Mock.'));
+ ws.onmessage = (message) => {
+ try {
+ const event = JSON.parse(String(message.data)) as SpeechEvent;
+ this.onEvent(event);
+ } catch {
+ // Ignore malformed diagnostics.
+ }
+ };
+ ws.onclose = () => this.onEvent({ type: 'health', message: 'Speech engine disconnected' });
+ });
+ }
+
+ sendPcm(pcm: Int16Array): void {
+ if (this.settings.speechMode === 'mock') {
+ this.mockSamples += pcm.length;
+ if (this.mockSamples > 16000 * 2.6) {
+ this.mockSamples = 0;
+ this.emitMockLine();
+ }
+ return;
+ }
+ if (this.ws?.readyState === WebSocket.OPEN) {
+ this.ws.send(pcm.buffer.slice(pcm.byteOffset, pcm.byteOffset + pcm.byteLength));
+ }
+ }
+
+ stop(): void {
+ if (this.mockTimer) window.clearTimeout(this.mockTimer);
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify({ type: 'stop' }));
+ this.ws?.close();
+ }
+
+ private emitMockLine(): void {
+ const lines = [
+ ['speaker_1', 'We should finalize the upload flow before Friday.'],
+ ['speaker_2', 'I can take the local storage and video playback work.'],
+ ['speaker_1', 'Great. The backend can remain in processing state for the demo.'],
+ ['speaker_3', 'Can we also keep the live overlay visible while another app is open?'],
+ ];
+ const [speakerId, text] = lines[this.mockIndex % lines.length];
+ this.mockIndex += 1;
+ const now = this.mockIndex * 2.6;
+ this.onEvent({
+ type: 'final',
+ segment: {
+ id: crypto.randomUUID(),
+ speakerId,
+ start: Math.max(0, now - 2.4),
+ end: now,
+ text,
+ final: true,
+ },
+ });
+ }
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/tauri.ts b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/tauri.ts
new file mode 100644
index 0000000..acf1b78
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/lib/tauri.ts
@@ -0,0 +1,3 @@
+export function isTauriRuntime(): boolean {
+ return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/main.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/main.tsx
new file mode 100644
index 0000000..5a89047
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/main.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './styles.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+ ,
+);
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Dashboard.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Dashboard.tsx
new file mode 100644
index 0000000..71de812
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Dashboard.tsx
@@ -0,0 +1,64 @@
+import { HardDrive, MonitorUp, Mic2, Plus } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+import { StatusBadge } from '../components/StatusBadge';
+import { useAppStore } from '../store/useAppStore';
+
+function fmt(seconds: number) {
+ if (!seconds) return '—';
+ return `${Math.round(seconds / 60)} min`;
+}
+
+export function Dashboard() {
+ const navigate = useNavigate();
+ const meetings = useAppStore((s) => s.meetings);
+ const settings = useAppStore((s) => s.settings);
+
+ return (
+
+
+
+
Good morning
+
Record, transcribe, identify speakers, and review meetings locally.
+
+
+
+
+
+
+ Quick start
+ Microphone: {settings.microphoneDeviceId === 'default' ? 'Default system microphone' : settings.microphoneDeviceId}
+ Local STT: Whisper {settings.whisperModel}
+
+
+
+ Live overlay
+ Overlay: {settings.overlayEnabled ? 'Enabled' : 'Disabled'}
+ Speaker labels: On · renameable live
+
+
+
+ Local storage
+ Files are stored locally for this demo.
+ No login, subscription, PostgreSQL, or SeaweedFS is used yet.
+ Local first
+
+
+
+
+ Recent meetings
+
+
TitleDateDurationStatusActions
+ {meetings.slice(0, 6).map((meeting) => (
+
+ {meeting.title}
+ {new Date(meeting.startedAt).toLocaleDateString()}
+ {fmt(meeting.durationSeconds)}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/LiveMeeting.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/LiveMeeting.tsx
new file mode 100644
index 0000000..d7e0120
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/LiveMeeting.tsx
@@ -0,0 +1,178 @@
+import { listen } from '@tauri-apps/api/event';
+import { Mic, MonitorUp, Pause, Play, Square } from 'lucide-react';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+import type { SpeechEvent } from '../types';
+import { startCapture, type CaptureController } from '../lib/capture';
+import { broadcastLiveState, openOverlayWindow } from '../lib/liveBus';
+import { saveMeetingLocally } from '../lib/localFiles';
+import { SpeechClient } from '../lib/speechClient';
+import { isTauriRuntime } from '../lib/tauri';
+import { useAppStore } from '../store/useAppStore';
+
+function secondsFmt(total: number) {
+ const m = Math.floor(total / 60).toString().padStart(2, '0');
+ const s = Math.floor(total % 60).toString().padStart(2, '0');
+ return `${m}:${s}`;
+}
+
+export function LiveMeeting() {
+ const navigate = useNavigate();
+ const [params] = useSearchParams();
+ const meetings = useAppStore((s) => s.meetings);
+ const activeMeetingId = useAppStore((s) => s.activeMeetingId);
+ const createMeeting = useAppStore((s) => s.createMeeting);
+ const appendSegment = useAppStore((s) => s.appendSegment);
+ const renameSpeaker = useAppStore((s) => s.renameSpeaker);
+ const finishMeeting = useAppStore((s) => s.finishMeeting);
+ const settings = useAppStore((s) => s.settings);
+
+ const [meetingId, setMeetingId] = useState(activeMeetingId);
+ const [engineState, setEngineState] = useState('Idle');
+ const [partial, setPartial] = useState('');
+ const [elapsed, setElapsed] = useState(0);
+ const [micLevel, setMicLevel] = useState(0);
+ const [running, setRunning] = useState(false);
+ const [paused, setPaused] = useState(false);
+ const [error, setError] = useState();
+ const captureRef = useRef();
+ const speechRef = useRef();
+ const timerRef = useRef();
+ const hasAutoStarted = useRef(false);
+
+ const meeting = useMemo(() => meetings.find((m) => m.id === meetingId), [meetings, meetingId]);
+
+ async function pushLiveState(id = meetingId) {
+ const current = useAppStore.getState().meetings.find((m) => m.id === id);
+ if (!current) return;
+ await broadcastLiveState({ meetingId: current.id, title: current.title, elapsedSeconds: elapsed, transcript: current.transcript, speakerNames: current.speakerNames, recording: running });
+ }
+
+ useEffect(() => { if (meetingId) void pushLiveState(); }, [meeting?.transcript.length, meeting?.speakerNames, elapsed, running]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ useEffect(() => {
+ if (!isTauriRuntime()) return;
+ let unlisten: (() => void) | undefined;
+ listen<{ meetingId?: string; speakerId: string; name: string }>('meetvault:rename-speaker', (event) => {
+ if (!event.payload.meetingId || event.payload.meetingId === meetingId) renameSpeaker(meetingId!, event.payload.speakerId, event.payload.name);
+ }).then((fn) => { unlisten = fn; });
+ return () => unlisten?.();
+ }, [meetingId, renameSpeaker]);
+
+ useEffect(() => {
+ if (params.get('start') === '1' && !hasAutoStarted.current) {
+ hasAutoStarted.current = true;
+ void startLive();
+ }
+ return () => {
+ if (timerRef.current) window.clearInterval(timerRef.current);
+ };
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ async function startLive() {
+ try {
+ setError(undefined);
+ let id = meetingId;
+ if (!id) {
+ id = createMeeting('Weekly Product Sync');
+ setMeetingId(id);
+ }
+ const client = new SpeechClient(settings, (event: SpeechEvent) => {
+ if (event.type === 'final') {
+ appendSegment(id!, event.segment);
+ setPartial('');
+ } else if (event.type === 'partial') {
+ setPartial(event.text);
+ } else if (event.type === 'ready') {
+ setEngineState('Whisper + diart ready');
+ } else if (event.type === 'health') {
+ setEngineState(event.message || 'Healthy');
+ } else if (event.type === 'error') {
+ setError(event.message);
+ }
+ });
+ await client.connect();
+ speechRef.current = client;
+ const capture = await startCapture({
+ captureSystemAudio: settings.captureSystemAudio,
+ microphoneDeviceId: settings.microphoneDeviceId,
+ onPcm: (pcm) => client.sendPcm(pcm),
+ onLevel: setMicLevel,
+ });
+ captureRef.current = capture;
+ setRunning(true);
+ setEngineState('Healthy');
+ const started = Date.now();
+ timerRef.current = window.setInterval(() => setElapsed(Math.floor((Date.now() - started) / 1000)), 1000);
+ if (settings.overlayEnabled) await openOverlayWindow();
+ } catch (e) {
+ setError(e instanceof Error ? e.message : String(e));
+ setEngineState('Unavailable');
+ }
+ }
+
+ async function togglePause() {
+ if (!captureRef.current) return;
+ if (paused) captureRef.current.resume(); else captureRef.current.pause();
+ setPaused(!paused);
+ }
+
+ async function finish() {
+ if (!meetingId || !captureRef.current) return;
+ if (timerRef.current) window.clearInterval(timerRef.current);
+ speechRef.current?.stop();
+ const media = await captureRef.current.stop();
+ const current = useAppStore.getState().meetings.find((m) => m.id === meetingId)!;
+ const saved = await saveMeetingLocally(current, media);
+ finishMeeting(meetingId, saved.mediaPath, saved.mediaMime);
+ setRunning(false);
+ await pushLiveState(meetingId);
+ navigate(`/meetings/${meetingId}`);
+ }
+
+ function rename(speakerId: string) {
+ if (!meetingId) return;
+ const next = window.prompt('Speaker name', meeting?.speakerNames[speakerId] || speakerId);
+ if (next) renameSpeaker(meetingId, speakerId, next);
+ }
+
+ const speakers = Array.from(new Set(meeting?.transcript.map((s) => s.speakerId) ?? []));
+
+ return (
+
+
+
{meeting?.title || 'Live Meeting'}
Real-time local transcription and speaker diarization.
+
● REC {secondsFmt(elapsed)}
+
+ {error &&
{error}
}
+ {!running &&
Ready to start
Live transcription uses the local speech engine. Switch Settings → Speech engine mode to Mock if you only want to test the UI.
}
+
+
+
+ Live transcription
+
+ {meeting?.transcript.map((segment) => (
+
+
+ {segment.text}
+
+ ))}
+ {partial &&
Listening… {partial}
}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/MeetingDetails.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/MeetingDetails.tsx
new file mode 100644
index 0000000..c1db160
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/MeetingDetails.tsx
@@ -0,0 +1,80 @@
+import { ArrowLeft, Copy, RefreshCw, BellRing } from 'lucide-react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { ProcessingCard } from '../components/ProcessingCard';
+import { StatusBadge } from '../components/StatusBadge';
+import { VideoPlayer } from '../components/VideoPlayer';
+import { requestDemoBackendJob } from '../lib/backend';
+import { useAppStore } from '../store/useAppStore';
+
+function ts(value: number) {
+ const m = Math.floor(value / 60).toString().padStart(2, '0');
+ const s = Math.floor(value % 60).toString().padStart(2, '0');
+ return `${m}:${s}`;
+}
+
+export function MeetingDetails() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const meeting = useAppStore((s) => s.meetings.find((m) => m.id === id));
+ const renameSpeaker = useAppStore((s) => s.renameSpeaker);
+ const setProcessing = useAppStore((s) => s.setProcessing);
+ if (!meeting) return Meeting not found
;
+
+ async function backendAction() {
+ await requestDemoBackendJob();
+ setProcessing(meeting!.id);
+ }
+
+ async function copyTranscript() {
+ const text = meeting!.transcript.map((s) => `${meeting!.speakerNames[s.speakerId] || s.speakerId}: ${s.text}`).join('\n');
+ await navigator.clipboard.writeText(text);
+ }
+
+ function rename(speakerId: string) {
+ const next = window.prompt('Speaker name', meeting!.speakerNames[speakerId] || speakerId);
+ if (next) renameSpeaker(meeting!.id, speakerId, next);
+ }
+
+ return (
+
+
+
+
{meeting.title}
{new Date(meeting.startedAt).toLocaleString()} · {Math.round(meeting.durationSeconds / 60)} min · {Object.keys(meeting.speakerNames).length || 'Detected'} speakers
+
+
+
+
+
+
+
Meeting information
+
- Storage
- Local
- Status
- {meeting.status}
- Started
- {new Date(meeting.startedAt).toLocaleString()}
- Ended
- {meeting.endedAt ? new Date(meeting.endedAt).toLocaleString() : '—'}
+
+
+
+
Transcript Summary Action items Files
+
+
+
+ Transcript
+
+ {meeting.transcript.length === 0 &&
No transcript stored for this demo meeting.
}
+ {meeting.transcript.map((segment) => (
+
+
+
{segment.text}
+
+ ))}
+
+
+
+ Backend demo
+
+
+
Summary, tasks and reminders are intentionally not implemented in this demo backend.
+
+
+
+
+
+ );
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Meetings.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Meetings.tsx
new file mode 100644
index 0000000..da48e4f
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Meetings.tsx
@@ -0,0 +1,28 @@
+import { Search } from 'lucide-react';
+import { useMemo, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { StatusBadge } from '../components/StatusBadge';
+import { useAppStore } from '../store/useAppStore';
+
+export function Meetings() {
+ const navigate = useNavigate();
+ const meetings = useAppStore((s) => s.meetings);
+ const [query, setQuery] = useState('');
+ const filtered = useMemo(() => meetings.filter((m) => m.title.toLowerCase().includes(query.toLowerCase())), [meetings, query]);
+
+ return (
+
+
Meetings
Local recordings and transcripts.
+
setQuery(e.target.value)} placeholder="Search meetings" />
+
+ {filtered.map((meeting) => (
+
+ ))}
+
+
+ );
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Overlay.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Overlay.tsx
new file mode 100644
index 0000000..bf92bee
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Overlay.tsx
@@ -0,0 +1,39 @@
+import { listen, emit } from '@tauri-apps/api/event';
+import { getCurrentWindow } from '@tauri-apps/api/window';
+import { useEffect, useMemo, useState } from 'react';
+import type { LiveState } from '../types';
+import { getCachedLiveState } from '../lib/liveBus';
+import { useAppStore } from '../store/useAppStore';
+import { isTauriRuntime } from '../lib/tauri';
+
+export function Overlay() {
+ const settings = useAppStore((s) => s.settings);
+ const [state, setState] = useState(() => getCachedLiveState());
+ useEffect(() => {
+ if (!isTauriRuntime()) return;
+ let unlisten: (() => void) | undefined;
+ listen('meetvault:live-state', (event) => setState(event.payload)).then((fn) => { unlisten = fn; });
+ return () => unlisten?.();
+ }, []);
+
+ const visible = useMemo(() => state?.transcript.slice(-settings.overlayLines) ?? [], [state, settings.overlayLines]);
+
+ async function rename(speakerId: string) {
+ const current = state?.speakerNames[speakerId] || speakerId;
+ const next = window.prompt('Rename speaker', current);
+ if (!next) return;
+ if (isTauriRuntime()) await emit('meetvault:rename-speaker', { meetingId: state?.meetingId, speakerId, name: next });
+ }
+
+ return (
+
+
MeetVault live captions{isTauriRuntime() && }
+ {visible.length === 0 ?
Waiting for speech…
: visible.map((segment) => (
+
+
+ {segment.text}
+
+ ))}
+
+ );
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Settings.tsx b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Settings.tsx
new file mode 100644
index 0000000..6951286
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/pages/Settings.tsx
@@ -0,0 +1,54 @@
+import { useEffect, useState } from 'react';
+import { Save } from 'lucide-react';
+import { useAppStore } from '../store/useAppStore';
+
+export function SettingsPage() {
+ const settings = useAppStore((s) => s.settings);
+ const setSettings = useAppStore((s) => s.setSettings);
+ const [draft, setDraft] = useState(settings);
+ const [saved, setSaved] = useState(false);
+ useEffect(() => setDraft(settings), [settings]);
+
+ const set = (key: K, value: (typeof draft)[K]) => setDraft((d) => ({ ...d, [key]: value }));
+
+ return (
+
+
Settings
Configure MeetVault desktop behavior and local transcription.
+
+
+
+
+
+
+
+
+
+
Local speech engine
+
+
+
Real mode requires speech-engine/server.py. Mock mode lets the UI run without ML dependencies.
+
+
+
{saved ? 'Saved.' : 'Local settings only.'}
+
+ );
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/store/useAppStore.ts b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/store/useAppStore.ts
new file mode 100644
index 0000000..8ab98d7
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/store/useAppStore.ts
@@ -0,0 +1,144 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+import type { AppSettings, Meeting, TranscriptSegment } from '../types';
+
+const defaultSettings: AppSettings = {
+ launchAtStartup: false,
+ keepInTray: true,
+ globalHotkey: 'Ctrl + Shift + M',
+ theme: 'system',
+ microphoneDeviceId: 'default',
+ captureSystemAudio: false,
+ monitorMicLevel: true,
+ whisperModel: 'small',
+ computeDevice: 'auto',
+ language: 'auto',
+ diarization: true,
+ overlayEnabled: true,
+ overlayOpacity: 92,
+ overlayFontSize: 20,
+ overlayLines: 4,
+ overlayClickThrough: false,
+ speechEngineUrl: 'ws://127.0.0.1:8765/ws/live',
+ speechMode: 'real',
+};
+
+const demoMeetings: Meeting[] = [
+ {
+ id: 'demo-weekly-product-sync',
+ title: 'Weekly Product Sync',
+ startedAt: '2026-09-27T09:00:00.000Z',
+ endedAt: '2026-09-27T09:48:05.000Z',
+ durationSeconds: 2885,
+ status: 'processing',
+ transcript: [
+ { id: 'd1', speakerId: 'speaker_1', start: 1121, end: 1126, text: 'We should finalize the API contract before tomorrow.', final: true },
+ { id: 'd2', speakerId: 'speaker_2', start: 1129, end: 1135, text: 'I will update the database migration tonight.', final: true },
+ { id: 'd3', speakerId: 'speaker_1', start: 1142, end: 1148, text: "Perfect. I'll prepare the release notes after that.", final: true },
+ ],
+ speakerNames: { speaker_1: 'Alice', speaker_2: 'Bob' },
+ localOnly: true,
+ },
+ {
+ id: 'demo-backend-review',
+ title: 'Backend Architecture Review',
+ startedAt: '2026-09-26T08:30:00.000Z',
+ endedAt: '2026-09-26T09:31:00.000Z',
+ durationSeconds: 3660,
+ status: 'ready',
+ transcript: [],
+ speakerNames: {},
+ localOnly: true,
+ },
+];
+
+interface AppState {
+ settings: AppSettings;
+ meetings: Meeting[];
+ activeMeetingId?: string;
+ processingMessage?: string;
+ setSettings: (patch: Partial) => void;
+ createMeeting: (title?: string) => string;
+ appendSegment: (meetingId: string, segment: TranscriptSegment) => void;
+ renameSpeaker: (meetingId: string, speakerId: string, name: string) => void;
+ finishMeeting: (meetingId: string, mediaPath?: string, mediaMime?: string) => void;
+ setMeetingMedia: (meetingId: string, mediaPath: string, mediaMime: string) => void;
+ setProcessing: (meetingId: string) => void;
+ clearProcessingMessage: () => void;
+}
+
+export const useAppStore = create()(
+ persist(
+ (set, get) => ({
+ settings: defaultSettings,
+ meetings: demoMeetings,
+ activeMeetingId: undefined,
+ processingMessage: undefined,
+ setSettings: (patch) => set((state) => ({ settings: { ...state.settings, ...patch } })),
+ createMeeting: (title = 'Untitled Meeting') => {
+ const id = crypto.randomUUID();
+ const meeting: Meeting = {
+ id,
+ title,
+ startedAt: new Date().toISOString(),
+ durationSeconds: 0,
+ status: 'recording',
+ transcript: [],
+ speakerNames: {},
+ localOnly: true,
+ };
+ set((state) => ({ meetings: [meeting, ...state.meetings], activeMeetingId: id }));
+ return id;
+ },
+ appendSegment: (meetingId, segment) => {
+ set((state) => ({
+ meetings: state.meetings.map((m) => {
+ if (m.id !== meetingId) return m;
+ const exists = m.transcript.some((s) => s.id === segment.id);
+ return {
+ ...m,
+ transcript: exists ? m.transcript.map((s) => (s.id === segment.id ? segment : s)) : [...m.transcript, segment],
+ };
+ }),
+ }));
+ },
+ renameSpeaker: (meetingId, speakerId, name) => {
+ set((state) => ({
+ meetings: state.meetings.map((m) => m.id === meetingId
+ ? { ...m, speakerNames: { ...m.speakerNames, [speakerId]: name.trim() || speakerId } }
+ : m),
+ }));
+ },
+ finishMeeting: (meetingId, mediaPath, mediaMime) => {
+ const meeting = get().meetings.find((m) => m.id === meetingId);
+ if (!meeting) return;
+ const endedAt = new Date().toISOString();
+ const durationSeconds = Math.max(1, Math.round((Date.parse(endedAt) - Date.parse(meeting.startedAt)) / 1000));
+ set((state) => ({
+ meetings: state.meetings.map((m) => m.id === meetingId ? {
+ ...m,
+ endedAt,
+ durationSeconds,
+ status: 'processing',
+ mediaPath: mediaPath ?? m.mediaPath,
+ mediaMime: mediaMime ?? m.mediaMime,
+ } : m),
+ activeMeetingId: undefined,
+ processingMessage: 'Processing (The job). Please wait a moment',
+ }));
+ },
+ setMeetingMedia: (meetingId, mediaPath, mediaMime) => set((state) => ({
+ meetings: state.meetings.map((m) => m.id === meetingId ? { ...m, mediaPath, mediaMime } : m),
+ })),
+ setProcessing: (meetingId) => set((state) => ({
+ meetings: state.meetings.map((m) => m.id === meetingId ? { ...m, status: 'processing' } : m),
+ processingMessage: 'Processing (The job). Please wait a moment',
+ })),
+ clearProcessingMessage: () => set({ processingMessage: undefined }),
+ }),
+ {
+ name: 'meetvault-demo-store-v1',
+ partialize: (state) => ({ settings: state.settings, meetings: state.meetings }),
+ },
+ ),
+);
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/styles.css b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/styles.css
new file mode 100644
index 0000000..d86a483
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/styles.css
@@ -0,0 +1,138 @@
+:root {
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ color: #13233f;
+ background: #f5f7fb;
+ font-synthesis: none;
+}
+* { box-sizing: border-box; }
+html, body, #root { margin: 0; min-width: 100%; min-height: 100%; }
+body { min-height: 100vh; background: #f5f7fb; }
+button, input, select { font: inherit; }
+button { cursor: pointer; }
+code { background: #eef2f8; padding: 2px 6px; border-radius: 6px; }
+
+.app-shell { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; }
+.sidebar { background: #20252d; color: white; padding: 28px 22px; display: flex; flex-direction: column; gap: 36px; }
+.brand { font-size: 22px; font-weight: 800; letter-spacing: -0.02em; }
+.sidebar nav { display: grid; gap: 10px; }
+.sidebar nav a { color: #d8dee8; text-decoration: none; display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-radius: 10px; font-weight: 650; }
+.sidebar nav a.active { background: #2d66ec; color: white; }
+.sidebar-note { margin-top: auto; background: #f4f6fa; color: #27354d; padding: 14px; border-radius: 12px; display: grid; gap: 4px; font-size: 12px; }
+.main-area { min-width: 0; }
+.page { padding: 42px 48px 56px; max-width: 1500px; margin: 0 auto; }
+.page h1 { font-size: 28px; margin: 0 0 8px; letter-spacing: -0.02em; }
+.page h2 { font-size: 18px; margin: 0; }
+.page h3 { font-size: 15px; margin: 24px 0 10px; }
+.page p { color: #5b6780; }
+.page-heading-row { display: flex; justify-content: space-between; gap: 24px; align-items: flex-start; margin-bottom: 36px; }
+.page-heading-row p { margin: 0; }
+
+.btn { border: 0; border-radius: 9px; padding: 11px 15px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-weight: 700; }
+.btn.primary { background: #2d66ec; color: white; }
+.btn.secondary { background: #edf2ff; color: #2453c3; border: 1px solid #cad7ff; }
+.btn.danger { background: #fff0ef; color: #c3382d; border: 1px solid #ffd0cb; }
+.btn.ghost { background: transparent; color: #2f4b7c; padding-left: 0; }
+.btn.small { padding: 9px 13px; }
+.btn.tiny { padding: 7px 11px; font-size: 12px; }
+.btn:disabled { opacity: .45; cursor: default; }
+.card { background: white; border: 1px solid #dfe5ef; border-radius: 18px; box-shadow: 0 1px 0 rgba(20, 40, 80, .02); }
+.badge { display: inline-flex; align-items: center; justify-content: center; min-width: 82px; padding: 6px 10px; border-radius: 7px; font-size: 12px; font-weight: 750; }
+.badge-ready { background: #eafff5; color: #137b58; border: 1px solid #bdebd9; }
+.badge-processing { background: #eef3ff; color: #3b5fc5; border: 1px solid #ccd8ff; }
+.badge-recording { background: #fff1ef; color: #c43b30; border: 1px solid #ffd1cb; }
+
+.dashboard-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }
+.dashboard-card { padding: 24px; min-height: 190px; }
+.dashboard-card p { margin: 8px 0; font-size: 14px; }
+.card-title { display: flex; align-items: center; gap: 9px; font-weight: 800; margin-bottom: 22px; }
+.section-block { margin-top: 46px; }
+.section-block h2 { margin-bottom: 18px; }
+.table-card { background: white; border: 1px solid #dfe5ef; border-radius: 18px; padding: 10px 20px; }
+.table-row { display: grid; grid-template-columns: 2.3fr 1fr .8fr .8fr .7fr; align-items: center; min-height: 64px; gap: 20px; border-bottom: 1px solid #eef1f6; }
+.table-row:last-child { border-bottom: 0; }
+.table-head { min-height: 46px; color: #6b7891; font-size: 12px; }
+
+.search-box { display: flex; gap: 10px; align-items: center; background: white; border: 1px solid #dfe5ef; border-radius: 12px; padding: 0 14px; max-width: 540px; margin-bottom: 24px; }
+.search-box input { border: 0; outline: 0; width: 100%; padding: 13px 0; background: transparent; }
+.meeting-grid { display: grid; gap: 12px; }
+.meeting-card { border: 1px solid #dfe5ef; border-radius: 15px; background: white; padding: 18px 20px; display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 22px; text-align: left; color: inherit; }
+.meeting-card div { display: grid; gap: 4px; }
+.meeting-card div span { color: #78849a; font-size: 12px; }
+
+.live-grid { display: grid; grid-template-columns: minmax(0, 1.75fr) minmax(310px, .85fr); gap: 28px; }
+.transcript-panel { min-height: 620px; padding: 28px; display: flex; flex-direction: column; }
+.transcript-live-scroll { flex: 1; overflow: auto; padding: 18px 0; display: grid; align-content: start; gap: 18px; }
+.live-line { display: grid; grid-template-columns: 118px 1fr; gap: 16px; align-items: start; }
+.speaker-pill { background: #eefbf5; border: 1px solid #b8ebd6; color: #147056; border-radius: 7px; padding: 7px 10px; font-weight: 750; font-size: 12px; }
+.live-line:nth-child(even) .speaker-pill, .transcript-line:nth-child(even) .speaker-pill { background: #eef2ff; border-color: #ccd7ff; color: #3556b2; }
+.partial-line { background: #f2f4f8; border: 1px solid #e1e5ec; border-radius: 10px; padding: 14px; text-align: center; color: #66738a; }
+.level-row { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px; color: #6e7a91; font-size: 12px; }
+.level-track { height: 7px; border-radius: 999px; background: #edf0f5; overflow: hidden; }
+.level-track i { display: block; height: 100%; background: #2d66ec; }
+.live-actions { display: flex; align-items: center; gap: 10px; margin-top: 22px; }
+.live-actions .danger { margin-left: auto; }
+.live-side { padding: 28px; min-height: 620px; }
+.speaker-editor-list { display: grid; gap: 12px; }
+.speaker-editor { display: grid; grid-template-columns: 1fr auto; gap: 10px; }
+.speaker-editor input, .settings-card input, .settings-card select { width: 100%; border: 1px solid #cad4e4; border-radius: 9px; background: white; padding: 10px 12px; color: #30415f; }
+.source-chips { display: flex; gap: 10px; }
+.source-chips span { background: #f2f4f8; padding: 10px 13px; border-radius: 9px; font-size: 12px; }
+.engine-summary { line-height: 1.6; color: #58657b; margin-bottom: 14px; }
+.rec-badge { background: #fff0ef; color: #c4392e; border: 1px solid #ffccc6; border-radius: 8px; padding: 10px 18px; }
+.start-live-panel { padding: 22px; margin-bottom: 24px; }
+.error-banner { background: #fff0ef; color: #9e2f27; border: 1px solid #ffd0ca; border-radius: 10px; padding: 12px 14px; margin-bottom: 20px; }
+.muted { color: #8792a7; font-size: 13px; }
+
+.details-top-grid { display: grid; grid-template-columns: 1.35fr .85fr; gap: 26px; }
+.video-card { min-height: 330px; overflow: hidden; display: grid; place-items: center; }
+.video-player { width: 100%; height: 100%; min-height: 330px; background: #10141d; object-fit: contain; }
+.audio-wrap { width: 100%; padding: 40px; }
+.audio-wrap audio { width: 100%; }
+.video-empty { min-height: 330px; display: grid; place-items: center; align-content: center; gap: 10px; color: #7b879c; text-align: center; padding: 24px; }
+.video-empty span { max-width: 470px; font-size: 13px; }
+.info-card { padding: 24px; }
+dl { display: grid; grid-template-columns: 110px 1fr; gap: 12px 16px; font-size: 13px; }
+dt { color: #8390a6; } dd { margin: 0; }
+.tabs-bar { margin: 30px 0 20px; background: #eaf0ff; border: 1px solid #cbd8ff; color: #2d55bd; border-radius: 10px; padding: 12px 18px; font-weight: 800; display: flex; gap: 32px; }
+.tabs-bar span { font-weight: 600; color: #6680bd; }
+.details-bottom-grid { display: grid; grid-template-columns: 1.2fr .8fr; gap: 28px; }
+.section-title-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
+.transcript-list { padding: 20px; min-height: 280px; }
+.transcript-line { display: grid; grid-template-columns: 140px 1fr; gap: 18px; align-items: start; padding: 12px 0; border-bottom: 1px solid #edf0f5; }
+.transcript-line p { margin: 4px 0 0; color: #23334e; }
+.empty-state { color: #8490a5; padding: 30px; text-align: center; }
+.processing-card { display: flex; gap: 14px; align-items: center; background: #fff9e8; border: 1px solid #f3dea0; color: #765614; border-radius: 14px; padding: 18px; margin: 12px 0; }
+.processing-card p { margin: 4px 0 0; color: #876a27; }
+.spin { animation: spin 1.2s linear infinite; }
+@keyframes spin { to { transform: rotate(360deg); } }
+.backend-actions { padding: 20px; }
+.backend-actions div { display: flex; gap: 10px; margin-top: 18px; flex-wrap: wrap; }
+
+.settings-section { margin-top: 28px; }
+.settings-section h2 { margin-bottom: 12px; }
+.settings-card { padding: 22px; display: grid; gap: 18px; }
+.settings-card.three-col { grid-template-columns: repeat(3, 1fr); }
+.settings-card.four-col { grid-template-columns: repeat(4, 1fr); }
+.settings-card label { display: grid; gap: 7px; color: #53617a; font-size: 12px; }
+.settings-card .toggle-row { display: flex; align-items: center; gap: 10px; background: #f2f4f8; padding: 11px 13px; border-radius: 9px; align-self: end; min-height: 42px; }
+.settings-card .toggle-row input { width: auto; }
+.settings-card .wide { grid-column: span 2; }
+.settings-help { color: #6e7b90; font-size: 12px; align-self: end; }
+.save-bar { position: sticky; bottom: 0; margin-top: 30px; background: rgba(245,247,251,.92); backdrop-filter: blur(8px); display: flex; justify-content: flex-end; align-items: center; gap: 16px; padding: 12px 0; }
+.save-bar span { color: #6f7b90; font-size: 12px; }
+
+.overlay-root { min-height: 100vh; background: rgba(21, 28, 42, .96); color: white; padding: 18px 26px; border-radius: 20px; overflow: hidden; }
+.overlay-header { display: flex; justify-content: space-between; color: #a9b4c7; font-size: 11px; margin-bottom: 8px; }
+.overlay-header button { border: 0; background: rgba(255,255,255,.1); color: white; border-radius: 7px; padding: 5px 9px; }
+.overlay-line { display: grid; grid-template-columns: 110px 1fr; gap: 16px; align-items: baseline; padding: 5px 0; }
+.overlay-line button { color: #85afff; font-weight: 800; background: none; border: 0; text-align: left; padding: 0; font-size: .72em; }
+.overlay-line:nth-child(even) button { color: #79e7b7; }
+.overlay-empty { color: #d7dde8; padding: 28px 0; text-align: center; }
+
+@media (max-width: 1050px) {
+ .app-shell { grid-template-columns: 190px 1fr; }
+ .page { padding: 28px; }
+ .dashboard-cards { grid-template-columns: 1fr; }
+ .live-grid, .details-top-grid, .details-bottom-grid { grid-template-columns: 1fr; }
+ .settings-card.three-col, .settings-card.four-col { grid-template-columns: 1fr 1fr; }
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/src/types.ts b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/types.ts
new file mode 100644
index 0000000..4999d30
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/src/types.ts
@@ -0,0 +1,82 @@
+export type MeetingStatus = 'recording' | 'processing' | 'ready';
+
+export interface TranscriptSegment {
+ id: string;
+ speakerId: string;
+ start: number;
+ end: number;
+ text: string;
+ final: boolean;
+}
+
+export interface Meeting {
+ id: string;
+ title: string;
+ description?: string;
+ startedAt: string;
+ endedAt?: string;
+ durationSeconds: number;
+ status: MeetingStatus;
+ mediaPath?: string;
+ mediaMime?: string;
+ transcript: TranscriptSegment[];
+ speakerNames: Record;
+ localOnly: boolean;
+}
+
+export interface LiveState {
+ meetingId: string;
+ title: string;
+ elapsedSeconds: number;
+ transcript: TranscriptSegment[];
+ speakerNames: Record;
+ recording: boolean;
+}
+
+export interface AppSettings {
+ launchAtStartup: boolean;
+ keepInTray: boolean;
+ globalHotkey: string;
+ theme: 'system' | 'light' | 'dark';
+ microphoneDeviceId: string;
+ captureSystemAudio: boolean;
+ monitorMicLevel: boolean;
+ whisperModel: 'base' | 'small' | 'medium';
+ computeDevice: 'auto' | 'cpu' | 'cuda';
+ language: string;
+ diarization: boolean;
+ overlayEnabled: boolean;
+ overlayOpacity: number;
+ overlayFontSize: number;
+ overlayLines: number;
+ overlayClickThrough: boolean;
+ speechEngineUrl: string;
+ speechMode: 'real' | 'mock';
+}
+
+export interface SpeechFinalEvent {
+ type: 'final';
+ segment: TranscriptSegment;
+}
+
+export interface SpeechPartialEvent {
+ type: 'partial';
+ text: string;
+ speaker_id?: string;
+ start?: number;
+ end?: number;
+}
+
+export interface SpeechHealthEvent {
+ type: 'ready' | 'health';
+ engine?: string;
+ diarization?: string;
+ message?: string;
+}
+
+export interface SpeechErrorEvent {
+ type: 'error';
+ message: string;
+}
+
+export type SpeechEvent = SpeechFinalEvent | SpeechPartialEvent | SpeechHealthEvent | SpeechErrorEvent;
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/tsconfig.app.json b/Wireframes/Desktop/MeetVault-Desktop-Demo/tsconfig.app.json
new file mode 100644
index 0000000..d7ec975
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/tsconfig.app.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": ["src"]
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/tsconfig.json b/Wireframes/Desktop/MeetVault-Desktop-Demo/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/tsconfig.node.json b/Wireframes/Desktop/MeetVault-Desktop-Demo/tsconfig.node.json
new file mode 100644
index 0000000..5eef90e
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/Wireframes/Desktop/MeetVault-Desktop-Demo/vite.config.ts b/Wireframes/Desktop/MeetVault-Desktop-Demo/vite.config.ts
new file mode 100644
index 0000000..88bba8b
--- /dev/null
+++ b/Wireframes/Desktop/MeetVault-Desktop-Demo/vite.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ clearScreen: false,
+ server: {
+ strictPort: true,
+ host: '127.0.0.1',
+ port: 1420,
+ },
+});