feat(ui): redesign voice cockpit with clinical companion drawer, Svelte 5 migration, and Gemini 3.8 voice
- Re-architect VoiceSession into full-bleed studio cockpit with overlay slide-over companion drawer - Eliminate width squishing defect on LiveInsightPanel and add native dark/light theme support - Integrate authentic Google Gemini 3.8 Flash TTS with Fenrir persona and fallback audio pipeline - Implement comprehensive Playwright E2E visual regression suite across Desktop, iPad, and iPhone (21 tests, 45 snapshots) - Migrate core application to Svelte 5 runes (, , ) with reactive stores - Maintain .NET 8 Azure Functions serverless endpoints and clean build hygiene
No files matched your search
@@ -0,0 +1,300 @@
|
||||
# Architectural Specification & Migration Plan: Svelte 5 Rewrite
|
||||
|
||||
**Project:** Cognitive Clarity (`clarity-helper`)
|
||||
**Target Architecture:** Svelte 5 (Runes) + Vite 6 + Tailwind CSS + `bits-ui` + `lucide-svelte`
|
||||
**Author:** Antigravity Autonomous Architecture Swarm (`[Svelte_Fullstack_Specialist]`, `[React_Frontend_Specialist]`, `[Adversarial_Auditor]`)
|
||||
**Status:** Approved Blueprint
|
||||
**Date:** 2026-09-29
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary & Architectural Rationale
|
||||
|
||||
Cognitive Clarity is currently built on React 19.2, Vite 6, Tailwind CSS 3.4, and Radix UI. While functional, the application suffers from React-specific architectural overhead:
|
||||
1. **Audio DSP Re-render Friction:** WebAudio FFT frequency visualizers (`AudioVisualizer.tsx`) and mic/synthesis volume levels trigger frequent React state updates (`setAudioLevel`, `setAiAudioLevel`) at 60 FPS, causing unnecessary virtual DOM diffing cycles across parent components.
|
||||
2. **Context & Prop Drilling Drag:** Settings and scenario parameters required explicit prop drilling down through `App.tsx` -> `ChatSession.tsx` / `VoiceSession.tsx`, leading to silent parameter dropping bugs.
|
||||
3. **Bundle Weight:** React 19 + `react-dom` + `@radix-ui/*` primitives account for over 250 kB of the runtime JavaScript bundle.
|
||||
|
||||
### Why Svelte 5 (Runes)?
|
||||
- **Fine-Grained Reactivity (Zero Virtual DOM):** Svelte 5 runes (`$state`, `$derived`, `$effect`) update the exact DOM elements bound to audio frequency bars without re-evaluating the component function or neighboring elements.
|
||||
- **Vanishing Runtime & PWA Performance:** Eliminating `react` and `react-dom` reduces the vendor chunk footprint by over 60%, resulting in faster cold starts on mobile PWA standalone installs.
|
||||
- **Universal Reactive Classes:** Complex state machines like WebAudio DSP capture and turn-by-turn clinical supervision can be modeled as pure TypeScript reactive classes (`AudioEngine.svelte.ts`, `SessionStore.svelte.ts`), cleanly decoupling audio hardware from UI presentation.
|
||||
- **Bits UI Parity:** `bits-ui` provides accessible, unstyled headless primitives with 1:1 API parity to Radix UI.
|
||||
|
||||
---
|
||||
|
||||
## 2. Component-by-Component Mapping & Recomposition Matrix
|
||||
|
||||
Every single React component and hook is systematically evaluated, documented, and mapped to its Svelte 5 counterpart:
|
||||
|
||||
### 2.1 Application Shell & State Coordinators
|
||||
|
||||
| React Component | Svelte 5 Component | React Mechanics (`useState`, `useEffect`, etc.) | Svelte 5 Recomposition (`$state`, `$derived`, `$effect`, etc.) |
|
||||
|:---|:---|:---|:---|
|
||||
| **`App.tsx`** | **`src/App.svelte`** | Router state (`activeScenario`, `sessionMode`, `isPreparing`), modal toggles (`showGenerator`, `showReference`, `showSettings`), `localStorage` sync via `useEffect`. | Uses root `AppStore.svelte.ts` context. State managed with `$state`. Modals rendered via declarative snippets. PWA `ReloadPrompt` mounted at root. |
|
||||
| **`index.tsx`** | **`src/main.ts`** | `createRoot(document.getElementById('root')!).render(<App />)`. | `mount(App, { target: document.getElementById('app')! })` using Svelte 5 `mount` API. |
|
||||
| **`index.html`** | **`index.html`** | `<div id="root"></div>`, scripts pointing to `index.tsx`. | `<div id="app"></div>`, script pointing to `/src/main.ts`. Retains PWA manifest and Inter font. |
|
||||
| **`sw.ts`** | **`src/sw.ts`** | Workbox injectManifest precaching + Google Fonts runtime caching. | 100% retained. `vite-plugin-pwa` operates identically with `@sveltejs/vite-plugin-svelte`. |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Design System UI Primitives (`components/ui/` -> `src/lib/components/ui/`)
|
||||
|
||||
All Radix UI primitives are mapped to `bits-ui` (or native accessible HTML where headless UI is redundant):
|
||||
|
||||
| React Component | Svelte 5 Component | Dependencies (React) | Dependencies (Svelte 5) | Recomposition Notes |
|
||||
|:---|:---|:---|:---|:---|
|
||||
| **`ui/button.tsx`** | **`Button.svelte`** | `@radix-ui/react-slot`, `class-variance-authority` | `bits-ui/button` or HTML `<button>` + `tailwind-variants` | Uses Svelte 5 `Snippet` for default children. Supports `variant` and `size` props. |
|
||||
| **`ui/card.tsx`** | **`Card.svelte`**, `CardHeader.svelte`, `CardTitle.svelte`, `CardContent.svelte`, `CardFooter.svelte` | Native `div` containers with Tailwind styling | Native `div` components using `{#snippet children()}` | Clean composable snippets replacing sub-component JSX props. |
|
||||
| **`ui/badge.tsx`** | **`Badge.svelte`** | `class-variance-authority`, `clsx`, `tailwind-merge` | `clsx` + `tailwind-merge` + `$props<{ variant?: 'default' \| 'secondary' \| 'destructive' \| 'outline' }>()` | Native `span` with typed variants. |
|
||||
| **`ui/dialog.tsx`** | **`Dialog.svelte`** | `@radix-ui/react-dialog` | `bits-ui` (`Dialog.Root`, `Dialog.Trigger`, `Dialog.Portal`, `Dialog.Overlay`, `Dialog.Content`) | Retains exact backdrop blur, animations, and focus trapping. |
|
||||
| **`ui/tabs.tsx`** | **`Tabs.svelte`** | `@radix-ui/react-tabs` | `bits-ui` (`Tabs.Root`, `Tabs.List`, `Tabs.Trigger`, `Tabs.Content`) | Controlled tab state via `$bindable()` or internal value. |
|
||||
| **`ui/scroll-area.tsx`** | **`ScrollArea.svelte`** | `@radix-ui/react-scroll-area` | `bits-ui` (`ScrollArea.Root`, `ScrollArea.Viewport`, `ScrollArea.Scrollbar`) | Custom styled scrollbar matching dark/light themes. |
|
||||
| **`ui/tooltip.tsx`** | **`Tooltip.svelte`** | `@radix-ui/react-tooltip` | `bits-ui` (`Tooltip.Root`, `Tooltip.Trigger`, `Tooltip.Content`) | Replaces bespoke `components/Tooltip.tsx` and Radix tooltip with unified Svelte 5 component. |
|
||||
| **`ui/select.tsx`** | **`Select.svelte`** | `@radix-ui/react-select` | `bits-ui` (`Select.Root`, `Select.Trigger`, `Select.Content`, `Select.Item`) | Accessible dropdown selector for audio devices and difficulty. |
|
||||
| **`ui/switch.tsx`** | **`Switch.svelte`** | `@radix-ui/react-switch` | `bits-ui` (`Switch.Root`, `Switch.Thumb`) | Accessible toggle switch. |
|
||||
| **`ui/input.tsx`** | **`Input.svelte`** | Native `<input>` with Tailwind classes | Native `<input>` with `$bindable(value)` and rest props | Type-safe two-way binding. |
|
||||
| **`ui/textarea.tsx`** | **`Textarea.svelte`** | Native `<textarea>` with Tailwind classes | Native `<textarea>` with `$bindable(value)` and auto-resize | Two-way reactive text binding. |
|
||||
| **`ui/separator.tsx`** | **`Separator.svelte`** | `@radix-ui/react-separator` | `bits-ui` (`Separator.Root`) | Semantic divider. |
|
||||
| **`ui/label.tsx`** | **`Label.svelte`** | `@radix-ui/react-label` | `bits-ui` (`Label.Root`) or `<label>` | Semantic form labeling. |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Interactive Domain Feature Components
|
||||
|
||||
| React Component | Svelte 5 Component | Core Mechanics & State | Svelte 5 Architectural Treatment |
|
||||
|:---|:---|:---|:---|
|
||||
| **`ScenarioCard.tsx`** | **`ScenarioCard.svelte`** | Displays scenario metadata, difficulty badge, CBT focus tags. Has "Chat" and "Voice" launcher buttons and custom delete trigger. | `$props<{ scenario: Scenario, onSelect: (s: Scenario, mode: SessionMode) => void, onDelete?: (id: string) => void }>()`. Uses `Badge` and `Button` snippets. Tooltip hover explanations. |
|
||||
| **`ChatSession.tsx`** | **`ChatSession.svelte`** | Message stream rendering, SSE chunk accumulation, automatic scroll-to-bottom on new messages, out-of-band turn supervision, post-session summary. | Driven by `ChatSessionController.svelte.ts`. Message list auto-scroll handled via an action `use:autoScroll`. Sub-components `ChatMessage.svelte` and `ChatInput.svelte`. Out-of-band analysis drawer rendered in flex container. |
|
||||
| **`ChatMessage.tsx`** | **`ChatMessage.svelte`** | Renders user vs AI character dialogue bubbles with timestamps and role avatar. | Pure presentational component. Formats timestamps via `$derived`. Supports markdown rendering through Svelte markdown component. |
|
||||
| **`ChatInput.tsx`** | **`ChatInput.svelte`** | Text input with enter-key send shortcut and disabled state while waiting for AI turn. | Form binding with `$state(text)`. Enter key listener `onkeydown={(e) => e.key === 'Enter' && !e.shiftKey && submit()}`. |
|
||||
| **`VoiceSession.tsx`** | **`VoiceSession.svelte`** | Duplex voice session orchestrator. Coordinates WebAudio capture, FFT visualizer, speech synthesis, live insight drawer, transcripts modal. | Uses `AudioEngine.svelte.ts` service class. Binds visualizer level directly to reactive state without re-rendering container. Coordinates sub-components `VoiceHeader`, `VoiceControls`, `AudioVisualizer`, `TranscriptPanel`. |
|
||||
| **`VoiceHeader.tsx`** | **`VoiceHeader.svelte`** | Scenario title, microphone device selector button, finish session trigger. | Simple header receiving `$props<{ scenario: Scenario, onExit: () => void, onOpenDeviceSelector: () => void }>()`. |
|
||||
| **`VoiceControls.tsx`** | **`VoiceControls.svelte`** | Mute button toggle, finish session button, transcript drawer button. | Reactive buttons bound to `audio.isMuted` and `$state(showTranscript)`. |
|
||||
| **`AnalyzingState.tsx`** | **`AnalyzingState.svelte`** | Animated post-session clinical synthesis loading screen. | SVG animated spinners and pulsing status typography. |
|
||||
| **`ConnectionState.tsx`** | **`ConnectionState.svelte`** | Pre-session audio hardware initialization and calibration screen. | Smooth CSS pulsing visualizer. |
|
||||
| **`AudioVisualizer.tsx`** | **`AudioVisualizer.svelte`** | 19 animated frequency bars dynamically styled based on microphone level or AI vocalization level. | Directly mutates bar heights via `$derived` or inline style calculation. In Svelte 5, updates happen at microtask speed with zero VDOM diffing. |
|
||||
| **`AudioDeviceSelector.tsx`** | **`AudioDeviceSelector.svelte`** | Queries `navigator.mediaDevices.enumerateDevices()`, selects input mic. | Device enumeration encapsulated in a reactive Svelte 5 class or effect. Accessible `Select` dropdown. |
|
||||
| **`LiveInsightPanel.tsx`** | **`LiveInsightPanel.svelte`** | Out-of-band supervisor drawer displaying distortions detected, adaptiveness, evidence quote, efficiency score, reframes, and tactical pivots. | Animates into view via `transition:slide`. Renders structured `AnalysisResult` reactive prop. |
|
||||
| **`SessionReport.tsx`** | **`SessionReport.svelte`** | Comprehensive post-session CBT assessment report with mastery score, trajectory, homework suggestions, transcript accordion, Markdown & JSON downloaders. | Generates download blobs via `sessionStorage.ts`. Accordion transcript drawer. Clipboard copy with reactive `$state(copied)` timeout. |
|
||||
| **`ScenarioGenerator.tsx`** | **`ScenarioGenerator.svelte`** | AI scenario builder dialog. Prompts user for description, selects CBT concepts from glossary, calibrates difficulty, calls OpenRouter JSON API. | Dialog modal using `Dialog.svelte`. Reactive multiselect tag chips with `$state(selectedConcepts)`. Generates validated scenario and appends to `customScenarios`. |
|
||||
| **`CBTReference.tsx`** | **`CBTReference.svelte`** | Interactive searchable CBT glossary covering all 18 clinical terms, definitions, categories, and real-world examples. | Filter tabs (`All`, `Distortions`, `Techniques`, `Concepts`) and search input. Filtered list calculated cleanly via `$derived(filteredGlossary)`. |
|
||||
| **`SettingsModal.tsx`** | **`SettingsModal.svelte`** | Tabs for Voice & Audio (vocal persona, mic device) and OpenRouter AI (API key input, model selector with `google/gemini-3.8-flash`). | Accessible `Tabs.svelte`. Binds `$state(openRouterKey)` and `$state(selectedModel)`. Persists to `UserSettingsStore.svelte.ts`. |
|
||||
| **`TranscriptPanel.tsx`** | **`TranscriptPanel.svelte`** | Slide-over drawer displaying conversation history during voice sessions. | Svelte transition drawer (`transition:fly={{ x: 300 }}`) with `ScrollArea`. |
|
||||
| **`ReloadPrompt.svelte`** | **`ReloadPrompt.svelte`** | PWA update notification toast when Workbox detects a service worker update. | Uses `virtual:pwa-register/svelte` to trigger automatic or prompt-based reload. |
|
||||
| **`VersionBadge.svelte`** | **`VersionBadge.svelte`** | Displays `__APP_VERSION__` and `__BUILD_TIMESTAMP__`. | Reads Vite compile-time definitions. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Reactive Architecture & State Engineering (Svelte 5 Runes)
|
||||
|
||||
In Svelte 5, state management is vastly superior to React's `useState` / `useCallback` / `useRef` paradigm. We will implement three first-class reactive state stores:
|
||||
|
||||
### 3.1 `UserSettingsStore.svelte.ts` (Global Preferences)
|
||||
```typescript
|
||||
import { UserSettings } from './types';
|
||||
import { DEFAULT_OPENROUTER_MODEL, DEFAULT_SUPERVISOR_MODEL } from './services/apiConfig';
|
||||
|
||||
const SETTINGS_KEY = 'cognitive-clarity-settings';
|
||||
|
||||
export class UserSettingsStore {
|
||||
voiceName = $state<UserSettings['voiceName']>('Zephyr');
|
||||
vocalStyle = $state('Helpful');
|
||||
genderPreference = $state<'Male' | 'Female' | 'Neutral'>('Neutral');
|
||||
preferredAudioDeviceId = $state<string | undefined>(undefined);
|
||||
openRouterApiKey = $state<string | undefined>(undefined);
|
||||
roleplayModel = $state(DEFAULT_OPENROUTER_MODEL);
|
||||
supervisorModel = $state(DEFAULT_SUPERVISOR_MODEL);
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load() {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
try {
|
||||
const saved = localStorage.getItem(SETTINGS_KEY);
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
Object.assign(this, parsed);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load settings", e);
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
const data: UserSettings = {
|
||||
voiceName: this.voiceName,
|
||||
vocalStyle: this.vocalStyle,
|
||||
genderPreference: this.genderPreference,
|
||||
preferredAudioDeviceId: this.preferredAudioDeviceId,
|
||||
openRouterApiKey: this.openRouterApiKey,
|
||||
roleplayModel: this.roleplayModel,
|
||||
supervisorModel: this.supervisorModel,
|
||||
};
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
export const userSettings = new UserSettingsStore();
|
||||
```
|
||||
|
||||
### 3.2 `AudioEngine.svelte.ts` (Isolated WebAudio DSP Controller)
|
||||
Replaces `hooks/useLiveSession.ts`. Completely encapsulates hardware access, FFT frequency calculations, and speech synthesis:
|
||||
```typescript
|
||||
import { Scenario, Message, AnalysisResult } from './types';
|
||||
import { UserSettingsStore } from './UserSettingsStore.svelte';
|
||||
import { streamOpenRouterChat, analyzeTurnWithOpenRouter, synthesizeSpeechWithOpenRouter } from './services/openRouterService';
|
||||
|
||||
export class AudioEngine {
|
||||
status = $state<'connecting' | 'active' | 'analyzing' | 'finished' | 'error'>('connecting');
|
||||
errorMessage = $state<string | null>(null);
|
||||
isMuted = $state(false);
|
||||
micLevel = $state(0);
|
||||
aiAudioLevel = $state(0);
|
||||
isAiSpeaking = $state(false);
|
||||
isUserSpeaking = $state(false);
|
||||
messages = $state<Message[]>([]);
|
||||
liveInsight = $state<AnalysisResult | null>(null);
|
||||
isAnalyzingTurn = $state(false);
|
||||
|
||||
// Active level derived cleanly for visualizer
|
||||
activeLevel = $derived(this.isAiSpeaking ? this.aiAudioLevel : this.micLevel);
|
||||
|
||||
private inputCtx: AudioContext | null = null;
|
||||
private outputCtx: AudioContext | null = null;
|
||||
private userAnalyser: AnalyserNode | null = null;
|
||||
private aiAnalyser: AnalyserNode | null = null;
|
||||
private activeSource: AudioBufferSourceNode | null = null;
|
||||
private recognition: any = null;
|
||||
private animFrameId: number | null = null;
|
||||
|
||||
constructor(private scenario: Scenario, private settings: UserSettingsStore) {}
|
||||
|
||||
async start() {
|
||||
// 1. Initialize AudioContexts & Analysers
|
||||
// 2. Setup Web Speech Recognition
|
||||
// 3. Start RAF loop to update this.micLevel and this.aiAudioLevel
|
||||
// 4. Initial scenario greeting
|
||||
}
|
||||
|
||||
stopAllAudio() {
|
||||
this.activeSource?.stop();
|
||||
this.activeSource = null;
|
||||
if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
|
||||
window.speechSynthesis.cancel();
|
||||
}
|
||||
this.isAiSpeaking = false;
|
||||
this.aiAudioLevel = 0;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopAllAudio();
|
||||
if (this.animFrameId) cancelAnimationFrame(this.animFrameId);
|
||||
this.recognition?.stop();
|
||||
this.inputCtx?.close();
|
||||
this.outputCtx?.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Context Injection via Svelte 5 Context API
|
||||
To avoid prop drilling, the root `App.svelte` provisions the stores using `setContext`:
|
||||
```typescript
|
||||
// src/lib/context.ts
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { UserSettingsStore } from './UserSettingsStore.svelte';
|
||||
import { AppStateStore } from './AppStateStore.svelte';
|
||||
|
||||
const SETTINGS_KEY = Symbol('USER_SETTINGS');
|
||||
const APP_STATE_KEY = Symbol('APP_STATE');
|
||||
|
||||
export const setUserSettingsContext = (store: UserSettingsStore) => setContext(SETTINGS_KEY, store);
|
||||
export const getUserSettingsContext = () => getContext<UserSettingsStore>(SETTINGS_KEY);
|
||||
|
||||
export const setAppStateContext = (store: AppStateStore) => setContext(APP_STATE_KEY, store);
|
||||
export const getAppStateContext = () => getContext<AppStateStore>(APP_STATE_KEY);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Tri-Lens Adversarial Swarm Audit (`--audit --swarm`)
|
||||
|
||||
### 4.1 Lens 1: Requirements & Blast Radius (`[Requirements_BlastRadius_Auditor]`)
|
||||
- **Clinical Roleplay Invariant:** Svelte 5 implementation must preserve strict separation between character dialogue and supervisor feedback. System instructions in `ChatSession.svelte` and `AudioEngine.svelte.ts` must maintain anti-meta constraints:
|
||||
- AI character NEVER congratulates the user or breaks character.
|
||||
- Clinical supervision runs strictly out-of-band via parallel OpenRouter promises.
|
||||
- **Scenario & Glossary Parity:** 100% preservation of all 14 scenarios and 18 glossary terms from `constants.ts`.
|
||||
- **Backend API Neutrality:** The existing Azure Functions in `api/` (`/api/chat`, `/api/analysis`, `/api/scenario/generate`) remain 100% compatible and unchanged, since Svelte consumes the same HTTP JSON contracts.
|
||||
|
||||
### 4.2 Lens 2: Performance & Security (`[Performance_Security_Auditor]`)
|
||||
- **WebAudio Garbage Collection & Context Limits:** Browsers enforce a hard cap of ~6 active `AudioContext` instances. `AudioEngine.svelte.ts` must implement strict `$effect` teardown lifecycles calling `audioCtx.close()` upon component unmount.
|
||||
- **Token Streaming Throughput:** Server-Sent Events in `openRouterService.ts` parse chunks at wire speed. In Svelte 5, text accumulation directly appends to `$state(text)` without triggering full component tree re-renders.
|
||||
- **Client Key Security:** OpenRouter API keys remain strictly local to `localStorage` and `Authorization: Bearer <key>` headers. Zero telemetry leakage.
|
||||
|
||||
### 4.3 Lens 3: Test Rigor & QA (`[TestRigor_QA_Auditor]`)
|
||||
- **TDD Contract:** Establish Vitest test suites for:
|
||||
1. `UserSettingsStore.test.ts`: LocalStorage read/write, 4-tier resolution hierarchy.
|
||||
2. `AudioEngine.test.ts`: Mocked WebAudio lifecycle, turn progression, error handling.
|
||||
3. `CBTReference.test.ts`: Reactive search filtering across distortions and techniques.
|
||||
4. `ScenarioGenerator.test.ts`: Schema validation for AI-generated scenarios.
|
||||
|
||||
---
|
||||
|
||||
## 5. Phased Implementation Roadmap & Verification Gates
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Phase1[Phase 1: Setup & Dependencies] --> Phase2[Phase 2: Core Services & Types]
|
||||
Phase2 --> Phase3[Phase 3: Svelte 5 Runes & Stores]
|
||||
Phase3 --> Phase4[Phase 4: Design Primitives bits-ui]
|
||||
Phase4 --> Phase5[Phase 5: Feature Components]
|
||||
Phase5 --> Phase6[Phase 6: Audio & Chat Engines]
|
||||
Phase6 --> Phase7[Phase 7: 5-Gate CI Validation]
|
||||
```
|
||||
|
||||
### Phase 1: Tooling & Dual Workspace Harness
|
||||
1. Scaffold Svelte 5 configuration in the repository:
|
||||
- Add `@sveltejs/vite-plugin-svelte` and `svelte@^5.0.0` to `devDependencies`.
|
||||
- Install `bits-ui` and `lucide-svelte`.
|
||||
- Update `vite.config.ts` to support both Svelte and PWA compilation.
|
||||
- Configure Vitest with `@testing-library/svelte` for Svelte component testing.
|
||||
|
||||
### Phase 2: Domain Services & Types Ingestion
|
||||
1. Re-use 100% framework-agnostic TypeScript files directly:
|
||||
- `types.ts`, `constants.ts`
|
||||
- `services/apiConfig.ts`, `services/openRouterService.ts`, `services/sessionStorage.ts`, `services/audioUtils.ts`
|
||||
2. Validate with existing test suite: `npm test` continues passing 12/12.
|
||||
|
||||
### Phase 3: Svelte 5 Reactive State Layer
|
||||
1. Author `src/lib/stores/UserSettingsStore.svelte.ts`.
|
||||
2. Author `src/lib/stores/AppStateStore.svelte.ts`.
|
||||
3. Author `src/lib/audio/AudioEngine.svelte.ts`.
|
||||
4. Author Vitest unit tests verifying state transitions, defaults (`google/gemini-3.8-flash`), and storage persistence.
|
||||
|
||||
### Phase 4: Svelte Design System (`src/lib/components/ui/`)
|
||||
1. Implement `Button.svelte`, `Badge.svelte`, `Card.svelte`, `Dialog.svelte`, `Tabs.svelte`, `ScrollArea.svelte`, `Tooltip.svelte`, `Input.svelte`, `Textarea.svelte`, `Select.svelte`.
|
||||
2. Verify visual token parity with Tailwind 3.4 / 4 utility classes.
|
||||
|
||||
### Phase 5: Feature Components Migration
|
||||
1. Author `ScenarioCard.svelte`, `CBTReference.svelte`, `SettingsModal.svelte`, `ScenarioGenerator.svelte`.
|
||||
2. Author `SessionReport.svelte` with Markdown and JSON report downloaders.
|
||||
3. Author `LiveInsightPanel.svelte` with reactive distortion pill rendering and star rating.
|
||||
|
||||
### Phase 6: Session Controllers & Audio Visualizer
|
||||
1. Author `ChatSession.svelte`, `ChatMessage.svelte`, `ChatInput.svelte`.
|
||||
2. Author `VoiceSession.svelte`, `VoiceHeader.svelte`, `VoiceControls.svelte`, `AudioVisualizer.svelte`, `TranscriptPanel.svelte`.
|
||||
3. Author `ConnectionState.svelte`, `AnalyzingState.svelte`.
|
||||
4. Test real-time WebAudio microphone frequency analysis and OpenRouter speech playback.
|
||||
|
||||
### Phase 7: App Shell, PWA Precache & 5-Gate Validation
|
||||
1. Compose `src/App.svelte` with hero section, difficulty filter pills, scenario grid, and modal dialogs.
|
||||
2. Update `sw.ts` and `vite.config.ts` PWA injectManifest.
|
||||
3. Run 5-Gate Validation:
|
||||
- Gate 1: Svelte TypeScript check (`svelte-check` / `tsc --noEmit`)
|
||||
- Gate 2: Vitest unit & component test suite (`npm test`)
|
||||
- Gate 3: Scenario schema validator (`npm run validate:scenarios`)
|
||||
- Gate 4: Production build (`npm run build:nobump`)
|
||||
- Gate 5: Backend assembly compilation (`dotnet build api/api.csproj`)
|
||||
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 386 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 515 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,710 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cognitive Clarity — Playwright Visual Snapshot Gallery</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
||||
.gallery-img { cursor: zoom-in; transition: transform 0.2s ease; }
|
||||
.gallery-img:hover { transform: scale(1.01); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-950 text-slate-100 min-h-screen">
|
||||
<header class="border-b border-slate-800 bg-slate-900/60 backdrop-blur-md sticky top-0 z-40 px-6 py-4">
|
||||
<div class="max-w-7xl mx-auto flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold tracking-tight text-white flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-full bg-teal-400 animate-pulse"></span>
|
||||
Cognitive Clarity — Visual Snapshot Gallery
|
||||
</h1>
|
||||
<p class="text-xs text-slate-400 mt-0.5">
|
||||
45 snapshots across 3 viewports (desktop-chrome, ipad-portrait, mobile-iphone)
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2" id="filter-buttons">
|
||||
<button onclick="filterViewport('all')" class="vp-btn px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider bg-teal-600 text-white" data-vp="all">All</button>
|
||||
<button onclick="filterViewport('desktop-chrome')" class="vp-btn px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider bg-slate-800 text-slate-300 hover:bg-slate-700" data-vp="desktop-chrome">desktop-chrome</button><button onclick="filterViewport('ipad-portrait')" class="vp-btn px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider bg-slate-800 text-slate-300 hover:bg-slate-700" data-vp="ipad-portrait">ipad-portrait</button><button onclick="filterViewport('mobile-iphone')" class="vp-btn px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider bg-slate-800 text-slate-300 hover:bg-slate-700" data-vp="mobile-iphone">mobile-iphone</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-7xl mx-auto px-6 py-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="gallery-grid">
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">HOME INITIAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/01-home-initial.png" alt="HOME INITIAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/01-home-initial.png', 'HOME INITIAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>01-home-initial.png</span>
|
||||
<span>352 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">HOME INITIAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/01-home-initial.png" alt="HOME INITIAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/01-home-initial.png', 'HOME INITIAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>01-home-initial.png</span>
|
||||
<span>752 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">HOME INITIAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/01-home-initial.png" alt="HOME INITIAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/01-home-initial.png', 'HOME INITIAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>01-home-initial.png</span>
|
||||
<span>1722 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">FILTER BEGINNER</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/02-filter-beginner.png" alt="FILTER BEGINNER" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/02-filter-beginner.png', 'FILTER BEGINNER')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>02-filter-beginner.png</span>
|
||||
<span>125 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">FILTER BEGINNER</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/02-filter-beginner.png" alt="FILTER BEGINNER" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/02-filter-beginner.png', 'FILTER BEGINNER')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>02-filter-beginner.png</span>
|
||||
<span>279 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">FILTER BEGINNER</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/02-filter-beginner.png" alt="FILTER BEGINNER" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/02-filter-beginner.png', 'FILTER BEGINNER')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>02-filter-beginner.png</span>
|
||||
<span>575 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">FILTER ADVANCED</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/03-filter-advanced.png" alt="FILTER ADVANCED" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/03-filter-advanced.png', 'FILTER ADVANCED')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>03-filter-advanced.png</span>
|
||||
<span>189 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">FILTER ADVANCED</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/03-filter-advanced.png" alt="FILTER ADVANCED" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/03-filter-advanced.png', 'FILTER ADVANCED')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>03-filter-advanced.png</span>
|
||||
<span>403 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">FILTER ADVANCED</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/03-filter-advanced.png" alt="FILTER ADVANCED" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/03-filter-advanced.png', 'FILTER ADVANCED')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>03-filter-advanced.png</span>
|
||||
<span>908 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SETTINGS MODAL VOICE</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/04-settings-modal-voice.png" alt="SETTINGS MODAL VOICE" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/04-settings-modal-voice.png', 'SETTINGS MODAL VOICE')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>04-settings-modal-voice.png</span>
|
||||
<span>391 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SETTINGS MODAL VOICE</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/04-settings-modal-voice.png" alt="SETTINGS MODAL VOICE" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/04-settings-modal-voice.png', 'SETTINGS MODAL VOICE')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>04-settings-modal-voice.png</span>
|
||||
<span>895 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SETTINGS MODAL VOICE</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/04-settings-modal-voice.png" alt="SETTINGS MODAL VOICE" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/04-settings-modal-voice.png', 'SETTINGS MODAL VOICE')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>04-settings-modal-voice.png</span>
|
||||
<span>1786 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SETTINGS MODAL OPENROUTER</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/05-settings-modal-openrouter.png" alt="SETTINGS MODAL OPENROUTER" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/05-settings-modal-openrouter.png', 'SETTINGS MODAL OPENROUTER')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>05-settings-modal-openrouter.png</span>
|
||||
<span>386 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SETTINGS MODAL OPENROUTER</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/05-settings-modal-openrouter.png" alt="SETTINGS MODAL OPENROUTER" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/05-settings-modal-openrouter.png', 'SETTINGS MODAL OPENROUTER')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>05-settings-modal-openrouter.png</span>
|
||||
<span>860 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SETTINGS MODAL OPENROUTER</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/05-settings-modal-openrouter.png" alt="SETTINGS MODAL OPENROUTER" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/05-settings-modal-openrouter.png', 'SETTINGS MODAL OPENROUTER')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>05-settings-modal-openrouter.png</span>
|
||||
<span>1759 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CBT REFERENCE ALL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/06-cbt-reference-all.png" alt="CBT REFERENCE ALL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/06-cbt-reference-all.png', 'CBT REFERENCE ALL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>06-cbt-reference-all.png</span>
|
||||
<span>430 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CBT REFERENCE ALL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/06-cbt-reference-all.png" alt="CBT REFERENCE ALL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/06-cbt-reference-all.png', 'CBT REFERENCE ALL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>06-cbt-reference-all.png</span>
|
||||
<span>1128 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CBT REFERENCE ALL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/06-cbt-reference-all.png" alt="CBT REFERENCE ALL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/06-cbt-reference-all.png', 'CBT REFERENCE ALL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>06-cbt-reference-all.png</span>
|
||||
<span>1756 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CBT REFERENCE SEARCH</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/07-cbt-reference-search.png" alt="CBT REFERENCE SEARCH" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/07-cbt-reference-search.png', 'CBT REFERENCE SEARCH')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>07-cbt-reference-search.png</span>
|
||||
<span>328 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CBT REFERENCE SEARCH</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/07-cbt-reference-search.png" alt="CBT REFERENCE SEARCH" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/07-cbt-reference-search.png', 'CBT REFERENCE SEARCH')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>07-cbt-reference-search.png</span>
|
||||
<span>662 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CBT REFERENCE SEARCH</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/07-cbt-reference-search.png" alt="CBT REFERENCE SEARCH" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/07-cbt-reference-search.png', 'CBT REFERENCE SEARCH')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>07-cbt-reference-search.png</span>
|
||||
<span>1760 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SCENARIO GENERATOR MODAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/08-scenario-generator-modal.png" alt="SCENARIO GENERATOR MODAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/08-scenario-generator-modal.png', 'SCENARIO GENERATOR MODAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>08-scenario-generator-modal.png</span>
|
||||
<span>515 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SCENARIO GENERATOR MODAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/08-scenario-generator-modal.png" alt="SCENARIO GENERATOR MODAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/08-scenario-generator-modal.png', 'SCENARIO GENERATOR MODAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>08-scenario-generator-modal.png</span>
|
||||
<span>1101 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SCENARIO GENERATOR MODAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/08-scenario-generator-modal.png" alt="SCENARIO GENERATOR MODAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/08-scenario-generator-modal.png', 'SCENARIO GENERATOR MODAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>08-scenario-generator-modal.png</span>
|
||||
<span>1898 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SCENARIO GENERATOR FILLED</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/09-scenario-generator-filled.png" alt="SCENARIO GENERATOR FILLED" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/09-scenario-generator-filled.png', 'SCENARIO GENERATOR FILLED')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>09-scenario-generator-filled.png</span>
|
||||
<span>378 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SCENARIO GENERATOR FILLED</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/09-scenario-generator-filled.png" alt="SCENARIO GENERATOR FILLED" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/09-scenario-generator-filled.png', 'SCENARIO GENERATOR FILLED')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>09-scenario-generator-filled.png</span>
|
||||
<span>824 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SCENARIO GENERATOR FILLED</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/09-scenario-generator-filled.png" alt="SCENARIO GENERATOR FILLED" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/09-scenario-generator-filled.png', 'SCENARIO GENERATOR FILLED')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>09-scenario-generator-filled.png</span>
|
||||
<span>1785 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CHAT SESSION INITIAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/10-chat-session-initial.png" alt="CHAT SESSION INITIAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/10-chat-session-initial.png', 'CHAT SESSION INITIAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>10-chat-session-initial.png</span>
|
||||
<span>49 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CHAT SESSION INITIAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/10-chat-session-initial.png" alt="CHAT SESSION INITIAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/10-chat-session-initial.png', 'CHAT SESSION INITIAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>10-chat-session-initial.png</span>
|
||||
<span>109 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CHAT SESSION INITIAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/10-chat-session-initial.png" alt="CHAT SESSION INITIAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/10-chat-session-initial.png', 'CHAT SESSION INITIAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>10-chat-session-initial.png</span>
|
||||
<span>157 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CHAT INTERACTION STREAMED</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/11-chat-interaction-streamed.png" alt="CHAT INTERACTION STREAMED" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/11-chat-interaction-streamed.png', 'CHAT INTERACTION STREAMED')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>11-chat-interaction-streamed.png</span>
|
||||
<span>71 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CHAT INTERACTION STREAMED</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/11-chat-interaction-streamed.png" alt="CHAT INTERACTION STREAMED" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/11-chat-interaction-streamed.png', 'CHAT INTERACTION STREAMED')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>11-chat-interaction-streamed.png</span>
|
||||
<span>160 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">CHAT INTERACTION STREAMED</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/11-chat-interaction-streamed.png" alt="CHAT INTERACTION STREAMED" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/11-chat-interaction-streamed.png', 'CHAT INTERACTION STREAMED')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>11-chat-interaction-streamed.png</span>
|
||||
<span>156 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SESSION REPORT SUMMARY</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/12-session-report-summary.png" alt="SESSION REPORT SUMMARY" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/12-session-report-summary.png', 'SESSION REPORT SUMMARY')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>12-session-report-summary.png</span>
|
||||
<span>78 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SESSION REPORT SUMMARY</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/12-session-report-summary.png" alt="SESSION REPORT SUMMARY" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/12-session-report-summary.png', 'SESSION REPORT SUMMARY')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>12-session-report-summary.png</span>
|
||||
<span>180 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">SESSION REPORT SUMMARY</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/12-session-report-summary.png" alt="SESSION REPORT SUMMARY" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/12-session-report-summary.png', 'SESSION REPORT SUMMARY')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>12-session-report-summary.png</span>
|
||||
<span>252 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">VOICE COCKPIT INITIAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/13-voice-cockpit-initial.png" alt="VOICE COCKPIT INITIAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/13-voice-cockpit-initial.png', 'VOICE COCKPIT INITIAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>13-voice-cockpit-initial.png</span>
|
||||
<span>61 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">VOICE COCKPIT INITIAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/13-voice-cockpit-initial.png" alt="VOICE COCKPIT INITIAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/13-voice-cockpit-initial.png', 'VOICE COCKPIT INITIAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>13-voice-cockpit-initial.png</span>
|
||||
<span>138 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">VOICE COCKPIT INITIAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/13-voice-cockpit-initial.png" alt="VOICE COCKPIT INITIAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/13-voice-cockpit-initial.png', 'VOICE COCKPIT INITIAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>13-voice-cockpit-initial.png</span>
|
||||
<span>189 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">VOICE TRANSCRIPT DRAWER</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/14-voice-transcript-drawer.png" alt="VOICE TRANSCRIPT DRAWER" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/14-voice-transcript-drawer.png', 'VOICE TRANSCRIPT DRAWER')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>14-voice-transcript-drawer.png</span>
|
||||
<span>80 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">VOICE TRANSCRIPT DRAWER</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/14-voice-transcript-drawer.png" alt="VOICE TRANSCRIPT DRAWER" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/14-voice-transcript-drawer.png', 'VOICE TRANSCRIPT DRAWER')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>14-voice-transcript-drawer.png</span>
|
||||
<span>192 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">VOICE TRANSCRIPT DRAWER</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/14-voice-transcript-drawer.png" alt="VOICE TRANSCRIPT DRAWER" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/14-voice-transcript-drawer.png', 'VOICE TRANSCRIPT DRAWER')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>14-voice-transcript-drawer.png</span>
|
||||
<span>230 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="desktop-chrome">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">VOICE AUDIO SETTINGS MODAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">desktop-chrome</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="desktop-chrome/15-voice-audio-settings-modal.png" alt="VOICE AUDIO SETTINGS MODAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('desktop-chrome/15-voice-audio-settings-modal.png', 'VOICE AUDIO SETTINGS MODAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>15-voice-audio-settings-modal.png</span>
|
||||
<span>49 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="ipad-portrait">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">VOICE AUDIO SETTINGS MODAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">ipad-portrait</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="ipad-portrait/15-voice-audio-settings-modal.png" alt="VOICE AUDIO SETTINGS MODAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('ipad-portrait/15-voice-audio-settings-modal.png', 'VOICE AUDIO SETTINGS MODAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>15-voice-audio-settings-modal.png</span>
|
||||
<span>104 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="mobile-iphone">
|
||||
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-slate-200">VOICE AUDIO SETTINGS MODAL</span>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">mobile-iphone</span>
|
||||
</div>
|
||||
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
|
||||
<img src="mobile-iphone/15-voice-audio-settings-modal.png" alt="VOICE AUDIO SETTINGS MODAL" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('mobile-iphone/15-voice-audio-settings-modal.png', 'VOICE AUDIO SETTINGS MODAL')">
|
||||
</div>
|
||||
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
|
||||
<span>15-voice-audio-settings-modal.png</span>
|
||||
<span>185 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Lightbox Modal -->
|
||||
<div id="lightbox" class="fixed inset-0 z-50 bg-black/90 backdrop-blur-md hidden flex flex-col items-center justify-center p-4" onclick="closeLightbox()">
|
||||
<div class="max-w-6xl max-h-[90vh] flex flex-col items-center" onclick="event.stopPropagation()">
|
||||
<div class="flex justify-between items-center w-full mb-3 text-white">
|
||||
<h3 id="lightbox-title" class="font-bold text-base"></h3>
|
||||
<button onclick="closeLightbox()" class="text-slate-400 hover:text-white px-3 py-1 bg-white/10 rounded-lg text-sm">Close ✕</button>
|
||||
</div>
|
||||
<img id="lightbox-img" src="" class="max-h-[80vh] max-w-full rounded-xl shadow-2xl border border-white/10 object-contain">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function filterViewport(vp) {
|
||||
document.querySelectorAll('.vp-btn').forEach(btn => {
|
||||
if (btn.dataset.vp === vp) {
|
||||
btn.className = 'vp-btn px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider bg-teal-600 text-white';
|
||||
} else {
|
||||
btn.className = 'vp-btn px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider bg-slate-800 text-slate-300 hover:bg-slate-700';
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.gallery-card').forEach(card => {
|
||||
if (vp === 'all' || card.dataset.viewport === vp) {
|
||||
card.style.display = 'flex';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openLightbox(src, title) {
|
||||
document.getElementById('lightbox-img').src = src;
|
||||
document.getElementById('lightbox-title').innerText = title;
|
||||
document.getElementById('lightbox').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeLightbox() {
|
||||
document.getElementById('lightbox').classList.add('hidden');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 752 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 895 KiB |
|
After Width: | Height: | Size: 860 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 662 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 824 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 575 KiB |
|
After Width: | Height: | Size: 908 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 185 KiB |