diff --git a/packages/ui/docs/DEPLOY_INSTRUCTIONS.md b/packages/ui/docs/DEPLOY_INSTRUCTIONS.md new file mode 100644 index 00000000..b40f8246 --- /dev/null +++ b/packages/ui/docs/DEPLOY_INSTRUCTIONS.md @@ -0,0 +1,39 @@ +# 🚀 Deploy Mux Proxy Function + +The credentials are now hardcoded in the function. You need to redeploy it: + +## Option 1: Via Supabase Dashboard (Easiest) + +1. Go to: https://supabase.com/dashboard/project/ytoadlpbdguriiccjnip/functions +2. Find **mux-proxy** in the functions list +3. Click the **⋮** menu (three dots) next to it +4. Select **"Deploy"** or **"Deploy new version"** +5. Upload the updated function files from `supabase/functions/mux-proxy/` + +## Option 2: Via Supabase CLI + +If you have the CLI installed: + +```bash +supabase functions deploy mux-proxy +``` + +## ✅ After Deployment + +1. Go to http://localhost:5173/playground/video-player +2. Sign in +3. Try uploading a video +4. Should work now! 🎉 + +## ⚠️ Important + +The credentials are currently HARDCODED in the function. This is for testing only! + +**Before deploying to production:** +1. Set MUX_TOKEN_ID and MUX_TOKEN_SECRET as Supabase secrets +2. Uncomment the env loading lines in `supabase/functions/mux-proxy/index.ts` +3. Remove the hardcoded values +4. Redeploy + +See `docs/SETUP_MUX_SECRETS.md` for proper setup instructions. + diff --git a/packages/ui/docs/QUICKSTART_MUX.md b/packages/ui/docs/QUICKSTART_MUX.md new file mode 100644 index 00000000..6db01ab5 --- /dev/null +++ b/packages/ui/docs/QUICKSTART_MUX.md @@ -0,0 +1,223 @@ +# Mux Video Quick Start Guide + +## 🎯 What You Have Now + +You now have a complete Mux video integration with: + +1. **VideoCard Component** - Display videos with Vidstack player +2. **Mux Uploader Integration** - Upload videos directly to Mux +3. **Video Player Playground** - Test at `/playground/video-player` +4. **Supabase Edge Function** - Secure Mux API proxy + +## ⚡ Quick Setup (5 minutes) + +### Step 1: Get Mux Credentials + +1. Go to https://dashboard.mux.com/signup +2. After signing up, go to **Settings** → **Access Tokens** +3. Click **Generate new token** +4. Name it "pm-pics" and enable **Mux Video** permissions +5. Copy the **Token ID** and **Token Secret** + +### Step 2: Configure Supabase Secrets + +You need to add your Mux credentials as secrets to your Supabase project: + +```bash +# Using Supabase CLI +supabase secrets set MUX_TOKEN_ID=your_token_id_here +supabase secrets set MUX_TOKEN_SECRET=your_token_secret_here +``` + +Or via Supabase Dashboard: +1. Go to your Supabase project dashboard +2. Navigate to **Project Settings** → **Edge Functions** → **Secrets** +3. Add `MUX_TOKEN_ID` and `MUX_TOKEN_SECRET` + +### Step 3: Deploy the Edge Function + +```bash +supabase functions deploy mux-proxy +``` + +### Step 4: Test It Out + +1. Start your dev server: `npm run dev` +2. Navigate to http://localhost:5173/playground/video-player +3. Sign in (required for uploads) +4. Go to the "Upload Video" tab +5. Drag & drop a video or click to select one +6. Watch it upload, process, and play! + +## 📝 How It Works + +### The Upload Flow + +``` +User Selects Video + ↓ +Frontend calls /functions/v1/mux-proxy (create-upload) + ↓ +Edge Function calls Mux API + ↓ +Returns signed upload URL + ↓ +MuxUploader uploads video directly to Mux + ↓ +Video processes on Mux servers + ↓ +Poll for asset creation + ↓ +Get playback ID + ↓ +Play video using Vidstack player +``` + +### Key Concepts + +- **Upload ID**: Temporary ID for tracking the upload +- **Asset ID**: Permanent ID for managing the video in Mux +- **Playback ID**: Public ID used to stream the video + +### URLs You Get + +After uploading, you get these URLs: + +**HLS Stream (for playback):** +``` +https://stream.mux.com/{PLAYBACK_ID}.m3u8 +``` + +**Thumbnail:** +``` +https://image.mux.com/{PLAYBACK_ID}/thumbnail.jpg +``` + +**MP4 Download (if enabled):** +``` +https://stream.mux.com/{PLAYBACK_ID}/high.mp4 +``` + +## 💾 Save to Database + +The playground has a "Save to Database" button that stores: + +```typescript +{ + user_id: current_user.id, + title: "Video Title", + description: "Description", + video_url: "https://stream.mux.com/{playback_id}.m3u8", + thumbnail_url: "https://image.mux.com/{playback_id}/thumbnail.jpg", + meta: { + mux_asset_id: "asset_abc123", + mux_playback_id: "playback_xyz789" + } +} +``` + +## 🎨 Using in Your App + +### Upload Component + +```tsx +import MuxUploader from "@mux/mux-uploader-react"; +import { supabase } from "@/integrations/supabase/client"; + +function MyUploader() { + const fetchUploadUrl = async () => { + const response = await fetch( + `${supabase.supabaseUrl}/functions/v1/mux-proxy`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${session.access_token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ action: 'create-upload' }), + } + ); + + const { data } = await response.json(); + return data.url; + }; + + return ( + console.log('Done!', e.detail)} + /> + ); +} +``` + +### Video Player + +```tsx +import VideoCard from "@/components/VideoCard"; + +function MyVideo({ video }) { + return ( + + ); +} +``` + +## 🆓 Pricing + +Mux offers **$20/month in free credits**, which includes: +- ~40 minutes of video encoding +- ~100 hours of video streaming + +Perfect for testing and small projects! + +## 🔧 Troubleshooting + +**Upload button doesn't appear** +- Make sure you're signed in +- Check that edge function is deployed + +**Upload fails** +- Verify Mux credentials are set as Supabase secrets +- Check browser console for errors +- Make sure edge function has correct environment variables + +**Video stuck processing** +- Large videos take time (can be 5-10 minutes for HD) +- Check Mux dashboard: https://dashboard.mux.com +- Look for the asset in the "Assets" section + +**Video won't play** +- Verify the HLS URL format: `https://stream.mux.com/{playback_id}.m3u8` +- Check that playback policy is "public" +- Try the URL directly in your browser + +## 📚 More Info + +See `docs/mux-integration.md` for detailed documentation including: +- Complete API reference +- Webhook setup +- Advanced configuration +- Production best practices + +## 🎉 You're Done! + +You now have: +- ✅ VideoCard component for displaying videos +- ✅ Mux upload integration +- ✅ Secure API proxy via Edge Functions +- ✅ Video player playground +- ✅ Database storage ready + +Go to `/playground/video-player` and start uploading! 🎬 + diff --git a/packages/ui/docs/SETUP_MUX_SECRETS.md b/packages/ui/docs/SETUP_MUX_SECRETS.md new file mode 100644 index 00000000..a5c4fb66 --- /dev/null +++ b/packages/ui/docs/SETUP_MUX_SECRETS.md @@ -0,0 +1,89 @@ +# 🔐 Setup Mux Secrets in Supabase + +## Your Credentials + +``` +MUX_TOKEN_ID: 3ceb1723-1274-48ed-bc1d-0ab967f2dda5 +MUX_TOKEN_SECRET: kYuAFBuOEiA+XZD8qRfgv6rcLVTJWdOLUTrLhiYagVej8UCRdjSzxOAFpvFQJHePcDd/KhqFXcE +``` + +## ⚡ Quick Setup (2 minutes) + +### Step 1: Open Supabase Dashboard + +Go to: https://supabase.com/dashboard/project/ytoadlpbdguriiccjnip/settings/functions + +### Step 2: Add Secrets + +Look for **"Secrets"** or **"Environment Variables"** section. + +Click **"New secret"** or **"Add secret"** and add: + +**First Secret:** +- Name: `MUX_TOKEN_ID` +- Value: `3ceb1723-1274-48ed-bc1d-0ab967f2dda5` + +**Second Secret:** +- Name: `MUX_TOKEN_SECRET` +- Value: `kYuAFBuOEiA+XZD8qRfgv6rcLVTJWdOLUTrLhiYagVej8UCRdjSzxOAFpvFQJHePcDd/KhqFXcE` + +### Step 3: Save & Verify + +1. Click **Save** or **Add** +2. You should see both secrets listed (values will be hidden) + +### Step 4: Redeploy Function (if needed) + +If the function still doesn't work after adding secrets: + +1. Go to: https://supabase.com/dashboard/project/ytoadlpbdguriiccjnip/functions +2. Find **mux-proxy** in the list +3. Click the **⋮** menu (three dots) +4. Select **"Redeploy"** or **"Deploy new version"** + +## ✅ Test It + +1. Go to http://localhost:5173/playground/video-player +2. Sign in +3. Try uploading a video +4. Should work now! 🎉 + +## 🔍 Troubleshooting + +### Still getting "Mux credentials not configured"? + +**Check #1: Are secrets set?** +- Dashboard → Settings → Functions → Secrets +- You should see `MUX_TOKEN_ID` and `MUX_TOKEN_SECRET` listed + +**Check #2: Is function deployed?** +- Dashboard → Edge Functions +- `mux-proxy` should show as "Active" or "Deployed" + +**Check #3: Redeploy** +- Sometimes secrets don't update until you redeploy +- Click the ⋮ menu next to mux-proxy → Redeploy + +**Check #4: Browser console** +- Open DevTools (F12) +- Look for detailed error messages + +### Different error? + +Check the browser console and edge function logs: +- Dashboard → Edge Functions → mux-proxy → Logs + +## 📝 Notes + +- **Local .env file**: Only used for local development, NOT for edge functions +- **Edge function secrets**: Must be set in Supabase Dashboard +- **Security**: Secrets are encrypted and never exposed to the client +- **Updates**: If you change secrets, redeploy the function + +## 🎯 Quick Links + +- Mux Dashboard: https://dashboard.mux.com +- Supabase Project: https://supabase.com/dashboard/project/ytoadlpbdguriiccjnip +- Edge Functions: https://supabase.com/dashboard/project/ytoadlpbdguriiccjnip/functions +- Function Settings: https://supabase.com/dashboard/project/ytoadlpbdguriiccjnip/settings/functions + diff --git a/packages/ui/docs/caching.md b/packages/ui/docs/caching.md new file mode 100644 index 00000000..af11ecd0 --- /dev/null +++ b/packages/ui/docs/caching.md @@ -0,0 +1,49 @@ + +# Caching Strategy + +## 1. Server-Side Caching (The Fast Layer) +**Goal**: Reduce DB load by caching public reads (Feeds, Profiles). + +### Cache Adapter Interface +We will use a platform-agnostic interface to support both Memory (Dev/Single-Node) and Redis (Prod/Cluster). + +```typescript +// server/src/commons/cache/types.ts +export interface CacheAdapter { + get(key: string): Promise; + set(key: string, value: T, ttl?: number): Promise; + del(key: string): Promise; + flush(pattern?: string): Promise; +} +``` + +### Implementations +- [ ] **MemoryCache**: Use `lru-cache`. Default for local dev. +- [ ] **RedisCache**: Use `ioredis`. Enabled if `REDIS_URL` is present. + +### usage in `ServingProduct` +- **Feed**: Cache `home-feed` for 60 seconds (Target: [`server/src/products/serving/index.ts`](../server/src/products/serving/index.ts)). +- **Profile**: Cache `profile-{id}` for 5 minutes (Target: [`server/src/products/serving/index.ts`](../server/src/products/serving/index.ts)). Invalidate on profile update webhook. + +--- + +## 2. Client-Side Caching (The Smart Layer) +**Goal**: Eliminate "Double Fetching" and provide instant navigation (Back/Forward). + +### TanStack Query (React Query) +- [ ] **Config**: Set global `staleTime` to 5 minutes for "Content" (Posts, Pictures) in [`src/App.tsx`](../src/App.tsx). +- [ ] **Prefetching**: + - On hover of a User Link, `queryClient.prefetchQuery(['profile', id])`. + - On hover of a Post Card, `queryClient.prefetchQuery(['post', id])`. +- [ ] **Hydration**: + - Use `HydrationBoundary` to ingest `window.__INITIAL_STATE__` served by the optimized Server Injection in [`src/App.tsx`](../src/App.tsx). + +### Optimistic Updates +- [ ] **Likes**: Update UI immediately. Rollback on error in [`src/components/LikeButton.tsx`](../src/components/LikeButton.tsx) (or relevant component). +- [ ] **Edits**: Update Local Cache immediately. Background sync. + +--- + +## 3. CDN & Static Assets +- [ ] Ensure Supabase Storage bucket is behind a CDN (Cloudflare or Supabase built-in). +- [ ] **Thumbnails**: Use the Resizing Proxy (`/api/images/cache/...`) which natively caches processed images on disk/nginx. diff --git a/packages/ui/docs/canvas-edit.md b/packages/ui/docs/canvas-edit.md new file mode 100644 index 00000000..a8559bf6 --- /dev/null +++ b/packages/ui/docs/canvas-edit.md @@ -0,0 +1,117 @@ +# Canvas Inline Editing & Extension Slots Proposal + +## Objective + +Enable a rich "Design Mode" experience where widget properties (text, images) can be edited directly on the canvas (Inline Editing) and widgets can define "Slots" for nested or extended content. + +## 1. Inline Editing + +Instead of relying solely on the sidebar or modal settings, we can make properties directly editable within the visual representation of the widget. + +### Concept + +* **Property Mapping**: Map specific DOM elements in the widget template to widget properties (e.g., `${content}`, `${image-0}`). +* **Editor Activation**: When in "Edit Mode", these elements become interactive targets (contenteditable for text, click-to-pick for images). +* **Data Binding**: Changes to the inline element immediately update the underlying widget property map. + +### 1.1 Text Editing + +**Example: `text.html`** +Current: + +```html +

${content}

+``` + +**Proposed Implementation:** +The renderer (HtmlWidget) detects text property placeholders and wraps them in a span that handles the click/edit event. + +**Rendered Output (Design Mode):** + +```html +

+ + ast3 + +

+``` + +### 1.2 Image Editing + +**Example: `image_col_3.html`** +Current: + +```html + +``` + +**Proposed Implementation:** +The renderer identifies `` tags where the `src` attribute is bound to a variable (e.g., `${image-0}`). It attaches an `onClick` handler to these images in Design Mode. + +**Interaction Flow:** + +1. **Hover**: Image overlay indicates "Change Image" (e.g., camera icon, dashed border). +2. **Click**: Triggers `ImagePickerDialog.tsx` popup found in `src/components/widgets/ImagePickerDialog.tsx`. +3. **Select**: User picks an image from the gallery (or uploads new). +4. **Update**: On selection (`onSelectPicture`), the `image_url` is mapped back to the widget's property (e.g., `props['image-0'] = picture.image_url`). + +## 2. Extension Slots + +Widgets should be able to define "Slots" where other widgets or specialized content can be inserted. This effectively allows widgets to act as layout containers for specific sections. + +### Concept + +* **Slot Definition**: Define areas within a widget template that serve as drop zones or modification points. +* **Extensions**: Other widgets or "Feature Blocks" that can be plugged into these slots. + +### Example: `text.html` with Slots + +We might want to allow injecting a "Call to Action" button or a "Divider" *inside* the text block structure, or perhaps an "Action Bar" slot that appears on hover. + +**Template Definition (`text.html`):** + +```html + +

${content}

+ +
+ +
+ +``` + +### Configuration (`library.json`) + +```json +{ + "name": "Text", + "template": "./text.html", + "slots": { + "footer": { + "allowedWidgets": ["button", "link"], + "maxItems": 1 + } + } +} +``` + +## 3. Combined Workflow (Design Mode) + +1. **Select Widget**: The toolbar shows standard settings. +2. **Edit Content**: Strings are editable inline; Images trigger the picker. +3. **Slots**: defined "Slots" highlight for drag-and-drop insertion. +4. **Drop Widget**: Dragging a "Button" widget onto the "footer" slot nests it within the Text widget's data structure (e.g., `props.slots.footer = [ButtonWidget]`). + +## Next Steps + +1. **Parser Update**: Update `HtmlWidget` to: + * Inject `contenteditable` wrappers for text vars. + * Attach `onClick` to `` tags mapped to variables. + * Parse `data-slot` and render `SlotContainer`. +2. **Interaction Layer**: Connect `ImagePickerDialog` and `onClick` handlers in `GenericCanvas`/`HtmlWidget`. +3. **Data Model**: Update widget property structure to support nested slot content. diff --git a/packages/ui/docs/canvas-html.md b/packages/ui/docs/canvas-html.md new file mode 100644 index 00000000..4e59858a --- /dev/null +++ b/packages/ui/docs/canvas-html.md @@ -0,0 +1,82 @@ +# Canvas & HTML Export Architecture + +This document outlines the architecture of the **Playground Canvas**, its dependency on the **Unified Layout Manager**, and the **HTML Email Export** workflow. + +## 1. Core Architecture: Unified Layout Manager + +**Source:** [src/lib/unifiedLayoutManager.ts](../src/lib/unifiedLayoutManager.ts) + +The `UnifiedLayoutManager` is the brain of the layout system. It handles the data model and all state mutations. + +### Data Model + +- **`PageLayout`**: The root object representing a page. Contains a list of `LayoutContainer`s. +- **`LayoutContainer`**: A recursive structure that can contain `WidgetInstance`s or child `LayoutContainer`s. It defines the grid system (`columns`, `gap`). +- **`WidgetInstance`**: A reference to a registered widget (`widgetId`), containing specific properties (`props`) and order. + +### Responsibilities + +- **CRUD Operations**: `addWidgetToContainer`, `removeWidget`, `updateWidgetProps`. +- **Grid Management**: Logic for `moveWidgetInContainer` (directional swapping) and `updateContainerColumns`. +- **Persistence**: Handles saving/loading from storage/API and import/export to JSON. + +## 2. Rendering: Generic Canvas + +**Source:** [src/components/hmi/GenericCanvas.tsx](../src/components/hmi/GenericCanvas.tsx) + +The `GenericCanvas` is the React presentation layer that consumes the layout data. + +- **Recursive Rendering**: It iterates through the `PageLayout` and renders `LayoutContainer` components. +- **Context Usage**: Consumes `useLayout` hook to interact with `UnifiedLayoutManager` without direct coupling. +- **Modes**: + - **Edit Mode**: Renders controls for adding containers, widgets, and drag-and-drop handles. + - **View Mode**: Renders only the final content. +- **Auto-Logging (Hook)**: [src/pages/PlaygroundCanvas.tsx](../src/pages/PlaygroundCanvas.tsx) uses `usePlaygroundLogic` to automatically log the current layout state to the WebSocket server whenever it changes. + +## 3. Custom Widgets (Email Bundle) + +**Location:** [public/widgets/email/](../public/widgets/email/) + +Widgets are defined via an external bundle system, allowing dynamic loading. + +- **`library.json`**: The manifest file defining the bundle. + - **`root`**: Path to the root HTML template (e.g., `./body.html`). + - **`widgets`**: List of available widgets, mapping names to template files (e.g., `Text` -> `./text.html`). +- **Templates**: HTML files containing the widget structure and placeholders. + - **Placeholders**: `[[propName]]` syntax is used for dynamic content substitution. + +## 4. HTML Email Export + +**Source:** [src/lib/emailExporter.ts](../src/lib/emailExporter.ts) + +The export machinery transforms the abstract `PageLayout` into a table-based HTML string suitable for email clients. + +### Workflow + +1. **Fetch Root**: Downloads the `rootTemplate` (e.g., `body.html`). +2. **Generate Body**: + - Iterate through `layout.containers`. + - Construct a `` structure for each container. + - Within containers, iterate through `widgets`. + - Handle columns by creating `
` cells with calculated widths. +3. **Render Widgets**: + - For each widget, fetch its specific HTML template (defined in the registry/bundle). + - **Substitution**: Replace `[[key]]` placeholders in the template with values from `widget.props`. +4. **Assembly**: Inject the generated body HTML into the root template (replacing `${SOURCE}` or appending to ``). + +### Example Flow + +1. **Layout**: Container (1 col) -> Text Widget (`props: { content: "Hello" }`). +2. **Exporter**: + - Fetches `body.html`. + - Fetches `text.html`: `
[[content]]
`. + - Substitutes: `
Hello
`. + - Wraps in Table: `
Hello
`. + - Injects into Body. + +## Key Paths + +- **Layout Logic**: [src/lib/unifiedLayoutManager.ts](../src/lib/unifiedLayoutManager.ts) +- **Canvas UI**: [src/components/hmi/GenericCanvas.tsx](../src/components/hmi/GenericCanvas.tsx) +- **Exporter**: [src/lib/emailExporter.ts](../src/lib/emailExporter.ts) +- **Email Widgets**: [public/widgets/email/](../public/widgets/email/) diff --git a/packages/ui/docs/context-aware-content.md b/packages/ui/docs/context-aware-content.md new file mode 100644 index 00000000..cf89b3a2 --- /dev/null +++ b/packages/ui/docs/context-aware-content.md @@ -0,0 +1,62 @@ +# Creating Brand/Context Aware Articles + +The AI Page Generator allows you to create highly consistent, on-brand content by combining **visual context** (Reference Images) with **structural context** (Context Templates). This workflow ensures that generated articles not only look like your brand but also follow your specific formatting and content standards. + +## The Workflow + +### 1. Visual Context (Reference Images) + +Use **Reference Images** to establish the visual identity of your article. + +* **What to use**: Upload brand assets, logo variations, previous diagram styles, or product photos. +* **How it works**: The AI "sees" these images and uses them to: + * **Style Match**: Generate new images that match the color palette and artistic style of your references. + * **Contextual Description**: Accurately describe visual details in the text (e.g., "As shown in the diagram..."). + * **Brand Alignment**: Ensure generated visuals align with your brand's aesthetic. + +### 2. Structural Context (Context Templates) + +Use a **Context Template** in your prompt to define the exact structure and tone of the article. This serves as a "skeleton" for the AI to fill in. + +* **What is a Context Template?**: A markdown structure that defines headers, required sections, image placement, and key points, without the final text. +* **How to use**: Paste a structured template into the prompt area. + +#### Example Context Template + +```markdown +# [Article Title] + +## Overview +[Brief summary of the topic] + +## Core Concepts +* Concept A: [Description] +* Concept B: [Description] + +## Visual Breakdown +[Instruction: Generate an exploded view diagram here similar to the reference image] +* **Part 1**: Details... +* **Part 2**: Details... + +## Technical Specifications +| Spec | Value | +|------|-------| +| [Key Spec] | [Value] | + +## Conclusion +[Summarize benefits] +``` + +## Step-by-Step Guide + +1. **Open Page Generator**: Click "Create Page" -> "Generate with AI". +2. **Add Reference Images**: Click the "Add" button and select your brand assets or style references. +3. **Input Context Template**: Paste your structured markdown template into the prompt box. +4. **Refine Prompt**: Add specific instructions above or below the template (e.g., "Fill out this template for a new shelving unit product using the attached technical drawings as reference"). +5. **Generate**: The AI will combine your **Visual Context** (images) and **Structural Context** (template) to produce a production-ready article that feels authentic to your brand. + +## Best Practices + +* **Consistency**: Keep a library of standard templates for different content types (e.g., "Product Launch", "Technical Guide", "Case Study"). +* **Quality References**: High-resolution, clear reference images yield better results. +* **Explicit Instructions**: Tell the AI *how* to use the references (e.g., "Use the color scheme from Image 1 for all generated diagrams"). diff --git a/packages/ui/docs/custom-canvas.md b/packages/ui/docs/custom-canvas.md new file mode 100644 index 00000000..574eb433 --- /dev/null +++ b/packages/ui/docs/custom-canvas.md @@ -0,0 +1,45 @@ +# Custom Canvas & Widget Extension + +## Overview + +This document describes the mechanism for extending the playground canvas with custom widgets loaded on-demand. + +## Widget Library Structure + +External widget libraries are defined in a `library.json` file. +Example: `public/widgets/email/library.json` + +```json +{ + "root": "./body.html", + "name": "email", + "description": "Email widgets", + "widgets": [ + { + "name": "HR", + "template": "./hr.html" + }, + ... + ] +} +``` + +## HtmlWidget Proxy + +To render HTML templates within the React-based canvas, we use a generic `HtmlWidget` component. +This component: + +1. Accepts a `templateUrl` prop. +2. Fetches the HTML content from the given URL. +3. Renders the content using `dangerouslySetInnerHTML`. +4. Wraps the content in a container that preserves styles/layout. + +## Runtime Registration + +Widgets are registered dynamically using `widgetRegistry.register()`. +When a context (e.g., "Email") is loaded: + +1. The `library.json` is fetched and parsed. +2. For each widget, a wrapper component is created using `HtmlWidget`. +3. The component is registered via `widgetRegistry.register()` with a unique ID (e.g., `email.hr`). +4. The `WidgetPalette` will automatically reflect these new widgets upon next render (open). diff --git a/packages/ui/docs/database-todos.md b/packages/ui/docs/database-todos.md new file mode 100644 index 00000000..3c16a6b9 --- /dev/null +++ b/packages/ui/docs/database-todos.md @@ -0,0 +1,48 @@ + +# Database & Architecture Todos + +## Server-Side & Schema Tasks + +### Schema Changes (Postgres/Supabase) +- [ ] **Split `profiles` Table**: + - [ ] Create `user_secrets` table (Columns: `user_id` (PK, FK), `openai_api_key`, `bria_api_key`, `replicate_api_key`, `settings`, `google_api_key`). + - [ ] Migrate data from `profiles` to `user_secrets` (Ref: [`src/integrations/supabase/types.ts`](../src/integrations/supabase/types.ts)). + - [ ] Drop secret columns from `profiles`. + - [ ] Rename `profiles` to `profiles_public` (optional, or just restrict access). +- [ ] **Create `page_collaborators` Table**: + - [ ] Columns: `page_id` (FK), `user_id` (FK), `role` (enum: 'viewer', 'editor', 'owner'), `created_at`. + - [ ] Add unique constraint on `(page_id, user_id)`. +- [ ] **RLS Policies Update**: + - [ ] `user_secrets`: Enable RLS. Policy: `auth.uid() = user_id`. + - [ ] `profiles`: Policy: Public read. Update strictly limited to owner. + - [ ] `pages`: Policy: + - Read: `is_public` OR `auth.uid() = owner` OR `auth.uid() IN (select user_id from page_collaborators)`. + - Update: `auth.uid() = owner` OR `auth.uid() IN (select user_id from page_collaborators where role IN ('editor', 'owner'))`. + +### Server Logic (Node/Hono) +- [ ] **Implement `ServingProduct` Endpoints** (Ref: [`server/src/products/serving/index.ts`](../server/src/products/serving/index.ts)): + - [ ] `GET /api/feed`: Returns hydrated feed (Posts + Authors + Cover Images). + - [ ] `GET /api/profile/:id`: Returns public profile + recent posts. + - [ ] `GET /api/me/secrets`: (Secure) Returns user secrets for settings page. +- [ ] **Server-Side Injection**: + - [ ] Update `handleServeApp` in [`ServingProduct`](../server/src/products/serving/index.ts) to pre-fetch User & Feed. + - [ ] Inject into `index.html` as `window.__INITIAL_STATE__`. + +--- + +## Client-Side Tasks + +### `src/lib/db.ts` Refactor +- [ ] **Deprecate Direct Selects**: Identify all `supabase.from('posts').select(...)` calls in [`src/lib/db.ts`](../src/lib/db.ts). +- [ ] **Implement Proxy Clients**: + - [ ] Create `fetchFeedFromProxy()` calling `/api/feed` in [`src/lib/db.ts`](../src/lib/db.ts). + - [ ] Create `fetchProfileFromProxy(id)` calling `/api/profile/:id` in [`src/lib/db.ts`](../src/lib/db.ts). +- [ ] **Hydration Logic**: + - [ ] Check `window.__INITIAL_STATE__` on app boot to populate React Query cache before fetching. + +### Component Updates +- [ ] **Post Page**: + - [ ] Use `fetchPostFromProxy` (or standard `db.fetchPostById` redirected to proxy) in [`src/pages/Post.tsx`](../src/pages/Post.tsx). + - [ ] Handle 404s gracefully (See Security.md for details). +- [ ] **PageManager**: + - [ ] Update [`src/components/PageManager.tsx`](../src/components/PageManager.tsx) to fetch "My Pages" AND "Shared Pages". diff --git a/packages/ui/docs/db-caching.md b/packages/ui/docs/db-caching.md new file mode 100644 index 00000000..4dd5f8ce --- /dev/null +++ b/packages/ui/docs/db-caching.md @@ -0,0 +1,169 @@ +# Short Term DB Caching Proposal + +## Objective +Reduce database load and improve response times for high-traffic, read-heavy routes by implementing a short-term caching layer using a **Generically Safe Decorator Pattern**. + +## Proposed Solution +Implement a **Generic CachedHandler Utility** (`server/src/commons/decorators.ts`) that: +1. **Auto-Generates Keys**: Defaults to URL + Query. +2. **Auth Protection**: Skips caching for Authenticated requests by default. +3. **Size Protection**: Skips caching for responses larger than a threshold (e.g. 1MB). +4. **Memory Protection**: Enforces LRU/Limits in `MemoryCache`. + +### 1. Functional Decorator +```typescript +import { Context } from 'hono'; +import { getCache } from '../commons/cache/index.js'; + +type KeyGenerator = (c: Context) => string; + +const defaultKeyInfo = (c: Context) => { + const url = new URL(c.req.url); + // Deterministic Sort: key=a&key=b vs key=b&key=a + // 1. Sort keys + url.searchParams.sort(); + return `auto-cache:${c.req.method}:${url.pathname}${url.search}`; +}; + +export const CachedHandler = ( + handler: (c: Context) => Promise, + options: { + ttl: number, + keyGenerator?: KeyGenerator, + skipAuth?: boolean, // Default true + maxSizeBytes?: number // Default: 1MB + } +) => async (c: Context) => { + // defaults + const ttl = options.ttl; + const skipAuth = options.skipAuth !== false; + const maxSizeBytes = options.maxSizeBytes || 1024 * 1024; // 1MB + const keyGen = options.keyGenerator || defaultKeyInfo; + + // 1. Auth Bypass + if (skipAuth && c.req.header('Authorization')) { + return handler(c); + } + + const cache = getCache(); + const key = keyGen(c); + const bypass = c.req.query('cache') === 'false'; + + // 2. Hit + if (!bypass) { + const cached = await cache.get(key); + if (cached) { + c.header('X-Cache', 'HIT'); + if (cached.contentType) c.header('Content-Type', cached.contentType); + return c.body(cached.data); + } + } + + // 3. Miss + const response = await handler(c); + + // 4. Save + if (response instanceof Response && response.ok) { + const cloned = response.clone(); + try { + const contentType = response.headers.get('Content-Type') || 'application/json'; + let data: any; + + // Check content length if available + const contentLength = cloned.headers.get('Content-Length'); + if (contentLength && parseInt(contentLength) > maxSizeBytes) { + // Too big, skip cache + return response; + } + + if (contentType.includes('application/json')) { + const jsonObj = await cloned.json(); + data = JSON.stringify(jsonObj); + } else { + data = await cloned.text(); + } + + // Double check actual size after reading + if (data.length > maxSizeBytes) { + // Too big, skip cache + return response; + } + + await cache.set(key, { data, contentType }, ttl); + c.header('X-Cache', bypass ? 'BYPASS' : 'MISS'); + } catch (e) { + console.error('Cache interception failed', e); + } + } + + return response; +} +``` + +### 2. Usage Implementation +In `server/src/products/serving/index.ts`: + +```typescript +// 5 minute cache, auto-key, skip if auth, max 500kb +this.routes.push({ + definition: getApiUserPageRoute, + handler: CachedHandler(handleGetApiUserPage, { ttl: 300, maxSizeBytes: 500 * 1024 }) +}); +``` + +### 3. MemoryCache Protection (Limit) +Update `server/src/commons/cache/MemoryCache.ts`: + +```typescript +// Add limit +const MAX_KEYS = 1000; + +async set(key: string, value: any, ttlSeconds: number): Promise { + this.prune(); + if (this.cache.size >= MAX_KEYS) { + const first = this.cache.keys().next().value; + this.cache.delete(first); + } + // ... set logic +} +``` + +### 4. Summary of Protections +| Protection | Mechanism | Benefit | +| :--- | :--- | :--- | +| **Data Leak** | `skipAuth: true` | Prevents private data being cached/served to public. | +| **Stale Data** | `ttl` | Ensures updates propagate eventually. | +| **OOM (Large Item)** | `maxSizeBytes` | Prevents caching huge responses (e.g. giant JSONs). | +| **OOM (Many Items)** | `MAX_KEYS` | Prevents unlimited growth of the cache map. | +| **Performance** | `X-Cache` | Visibility into hit rates. | + +### 5. Sequence Diagram (Final) +```mermaid +sequenceDiagram + participant Client + participant Dec as CachedHandler + participant Cache as MemoryCache + participant H as Handler + + Client->>Dec: GET /api/data + Dec->>Dec: Check Auth Header? + opt Authenticated + Dec->>H: Invoke Handler Directly + H-->>Client: Returns Private Data + end + + Dec->>Cache: get(key) + alt Hit + Cache-->>Client: Returns Data (HIT) + else Miss + Dec->>H: Invoke Handler + H-->>Dec: Returns Response + Dec->>Dec: Check Size < 1MB? + alt Small Enough + Dec->>Cache: set(key, data) + Dec-->>Client: Returns (MISS) + else Too Big + Dec-->>Client: Returns (MISS - No Cache) + end + end +``` diff --git a/packages/ui/docs/db.md b/packages/ui/docs/db.md new file mode 100644 index 00000000..f842f11e --- /dev/null +++ b/packages/ui/docs/db.md @@ -0,0 +1,9 @@ + +# [DEPRECATED] Database Consolidation Plan + +> **Note**: This document has been split into more specific task lists. Please refer to: +> - [Database Todos & Schema](./database-todos.md) +> - [Security & Auth Plans](./security.md) +> - [Caching Strategy](./caching.md) + +This file remains for historical context but may be out of date. diff --git a/packages/ui/docs/editor.md b/packages/ui/docs/editor.md new file mode 100644 index 00000000..5f286bb6 --- /dev/null +++ b/packages/ui/docs/editor.md @@ -0,0 +1,113 @@ +# MDXEditor Content Modification Deep Dive + +This document outlines the core concepts behind modifying content in the `MDXEditor` and provides a recommended approach for building custom extensions, based on a deep dive into its source code. + +## Key Findings: Reactive Architecture (Gurx) + +The editor is built on a reactive architecture powered by a library called `@mdxeditor/gurx`. This is the most critical concept to understand. + +- **Signals, not direct calls**: Instead of directly calling methods to change content (like `editor.bold()` or `editor.insertText('foo')`), the editor's UI components **publish** their intent to signals (also called "publishers" or "nodes"). + +- **State management**: The editor's state, including the current markdown content and selection, is managed within a reactive "realm." Various plugins subscribe to signals within this realm. + +- **Decoupled logic**: When a signal is published, the corresponding plugin logic is executed. This decouples the UI (e.g., a toolbar button) from the actual implementation of the feature. + +## How Toolbar Actions Work (e.g., Bold) + +Let's trace the "bold" action, as it's a perfect example of this architecture in action: + +1. **UI Component**: `BoldItalicUnderlineToggles.tsx` contains the UI button for toggling bold. +2. **Publishing an action**: When the bold button is clicked, it doesn't directly modify the editor. Instead, it calls `applyFormat('bold')`, which is a publisher for the `applyFormat$` signal. +3. **Core plugin subscription**: The core editor plugin subscribes to `applyFormat$`. When it receives the 'bold' signal, it executes the logic to apply or remove the bold format to the current text selection within the Lexical editor instance. + +This approach is highly reliable because it leverages the editor's internal state management system. The UI simply declares what needs to happen, and the editor's core logic handles the update when it's ready. + +## The Problem with `insertMarkdown` and Polling + +Our initial approach to inserting markdown had issues because it was imperative and fought against the editor's reactive nature: + +1. **`insertMarkdown$` is a signal**: The `insertMarkdown` method provided on the editor ref is, under the hood, a publisher for the `insertMarkdown$` signal. It's a "fire-and-forget" operation from the outside. +2. **Asynchronous execution**: When we call `editorRef.current.insertMarkdown('some text')`, we're publishing to a signal. The actual insertion happens asynchronously within the editor's update cycle. +3. **Why polling is bad**: Attempting to immediately read the new markdown with `getMarkdown()` fails because the update hasn't been processed yet. Our polling with `requestAnimationFrame` and `setTimeout` was a fragile attempt to guess when the update would complete. + +## The Correct Approach: Using the Signal Architecture + +To build reliable extensions or custom functionality, we must follow the editor's architectural pattern: + +1. **Create a custom signal**: If you need to perform a custom action, the best approach is to define a new signal within a custom plugin. +2. **Publish from your component**: Your React component (e.g., a custom button or a side panel) should get a publisher for your signal and publish to it when the user takes an action. +3. **Subscribe within a plugin**: The plugin that defines the signal should also contain the logic that subscribes to it. This logic will receive the signal and can then safely interact with the editor state. + +By following this pattern, your custom logic will be integrated into the editor's reactive flow, ensuring that state updates are handled correctly and reliably, without the need for hacks like `setTimeout` or `focus()` workarounds. + +## Sequence Diagram: Content Modification Flow + +Here is a sequence diagram illustrating the flow for a typical toolbar action, like applying bold formatting, in `MarkdownEditorEx`. + +```mermaid +sequenceDiagram + participant User + participant ToolbarUI as "React Component (e.g., BoldItalicUnderlineToggles)" + participant Gurx as "Gurx Reactive Realm" + participant CorePlugin as "MDXEditor Core Plugin" + participant Lexical as "Lexical Editor Instance" + + User->>ToolbarUI: Clicks 'Bold' button + ToolbarUI->>Gurx: Publishes to 'applyFormat$' signal with 'bold' payload + Gurx-->>CorePlugin: Notifies subscriber of 'applyFormat$' signal + CorePlugin->>Lexical: Dispatches 'FORMAT_TEXT_COMMAND' with 'bold' + Lexical->>Lexical: Updates editor state (applies format) + Lexical-->>Gurx: Broadcasts updated state (e.g., 'currentFormat$') + Gurx-->>ToolbarUI: Notifies component of state change + ToolbarUI->>User: Re-renders with 'Bold' button in active state +``` + +## Knowing When an Action is "Done" + +The editor's architecture is based on signals. Actions like applying formats or inserting markdown are published to the reactive system, which then processes them asynchronously. There is no `Promise` or callback returned from methods like `insertMarkdown` to let you know when the operation is complete. + +So, how do we know? + +1. **The `onChange` Prop is the Key**: The primary mechanism for an external component to be notified of state changes is the `onChange` prop. When the editor's internal Lexical state is updated and converted back to markdown, `onChange` is triggered with the new content. This is our confirmation that the editor has processed an update. + +2. **The Challenge**: The problem is linking a specific action (e.g., our call to `insertMarkdown`) to a subsequent `onChange` event. A simple `useEffect` on the content is not enough, as `onChange` can fire for many reasons (user typing, other plugins, etc.). + +3. **The Solution: A Transactional Approach**: We can create a temporary "transaction" to bridge the gap between our action and the resulting state change. The flow looks like this: + + 1. **Before Action**: Before calling `insertMarkdown`, we store the text we're about to insert and a success callback (e.g., to show a toast) in a `useRef`. + 2. **Fire Action**: We call `editorRef.current.insertMarkdown(...)`. + 3. **Wait for Confirmation**: The `onChange` handler (`handleContentChange`) is now responsible for checking if the new content contains the text from our transaction. + 4. **On Confirmation**: If the text is found, we know our specific insertion was successful. We can then execute the success callback from the ref and clear the transaction. + 5. **Safety Net**: A `setTimeout` can be used as a fallback. If `onChange` doesn't fire within a reasonable time, we can assume the update failed or timed out and notify the user accordingly. + +This pattern respects the editor's asynchronous and reactive nature while giving us the reliable completion confirmation we need for a smooth user experience. + +## Modifying Content Without Stealing Focus + +A common requirement is to insert or modify editor content programmatically (e.g., from an AI suggestion) without pulling the user's focus away from their current task (e.g., typing in a prompt input). + +- **`insertMarkdown()` Steals Focus**: The `editorRef.current.insertMarkdown()` method is designed for direct user actions. It internally manages focus and selection to place content where the cursor is. As a result, it will almost always steal focus. + +- **`setMarkdown()` Does NOT Steal Focus**: The `editorRef.current.setMarkdown()` method is the correct tool for this job. It is designed for programmatic updates and works like setting a prop on a controlled component. It replaces the entire editor content with the new markdown string you provide, without affecting the user's focus. + +### Recommended Usage + +- **For "Append" or "Replace All"**: `setMarkdown` is perfect. You can get the current content via `getMarkdown()`, construct the new content string (`newContent = oldContent + appendedText`), and then call `setMarkdown(newContent)`. The user's focus remains untouched. + +- **For "Insert at Cursor"**: This is trickier. Since `setMarkdown` replaces everything and doesn't know about the cursor, you cannot use it to insert at the current selection. For this specific use case, stealing focus via `insertMarkdown` is often the intended and most intuitive behavior from a user's perspective. + +## Summary: Implementing Merge Operations + +To robustly implement `append`, `insert`, and `replace`, you need the following from your `MarkdownEditorEx` component: + +1. **`editorRef`**: Essential for calling `getMarkdown()`, `setMarkdown()`, and `insertMarkdown()`. +2. **`onSelectionChange` callback**: Required for the **replace** operation to know what text is currently selected. +3. **`onChange` callback**: The key to confirming that any operation has completed successfully, using the "transactional" pattern described above. + +### Cheatsheet: + +| Operation | Goal | Recommended Method | Steals Focus? | Key Dependency | +| :-------- | :---------------------------------------- | :-------------------- | :------------ | :--------------------- | +| **Append** | Add content to the end of the document | `setMarkdown()` | No | `getMarkdown()` | +| **Insert** | Add content at the user's cursor | `insertMarkdown()` | Yes | User's cursor position | +| **Replace** | Swap selected text with new content | `setMarkdown()` | No | `onSelectionChange` | diff --git a/packages/ui/docs/embed.md b/packages/ui/docs/embed.md new file mode 100644 index 00000000..1a59696a --- /dev/null +++ b/packages/ui/docs/embed.md @@ -0,0 +1,100 @@ +# Embed Implementation Plan + +## Goal +Enable third-party sites to embed posts from our platform using an iframe. This requires a lightweight, dedicated build of the application that renders a single post with minimal distractions (no navigation, no sidebar, etc.). + +## Architecture + +### 1. Dedicated Output Build +We will create a separate Vite build for the embed view to ensure the bundle size is minimal and isolated from the main application's complexity. + +- **Config**: `vite.config.embed.ts` +- **Entry**: `src/main-embed.tsx` +- **Output**: `dist/client/embed/` +- **Feature Flags**: Use Vite `define` to set `__IS_EMBED__` constant to `true` at build time. This allows dead-code elimination (tree-shaking) of unused components in `CompactRenderer`. + +### 2. Client-Side Entry Point (`src/main-embed.tsx`) +A simplified entry point that: +- Does **not** include the full `App` router. +- Reads initial state from `window.__INITIAL_STATE__`. +- Renders `EmbedApp`. + +### 3. Component Strategy (`EmbedRenderer.tsx`) +Create `src/pages/Post/renderers/EmbedRenderer.tsx`. This component will be: +- **Lightweight**: Minimal imports, no heavy third-party libs (unless critical). +- **Read-Only**: No edit controls, no comments, no wizard. +- **Navigation**: + - Image click → Opens `window.open(postUrl, '_blank')`. + - Title/Author click → Opens `window.open(profileUrl, '_blank')`. +- **Layout**: + - Responsive Media (Image/Video). + - Filmstrip (for galleries). + - Simple footer (Like count, Share button). + +### 4. Server-Side Serving (`server/src/products/serving/index.ts`) +A new route `GET /embed/:postId` will be added to the serving product. + +- **Logic**: + 1. Fetch the post/media. + 2. Inject into `window.__INITIAL_STATE__`. + 3. Serve `embed.html` (which points to `main-embed.tsx`). + 4. Set headers to allow embedding. + +### 5. UI Trigger (`ArticleRenderer.tsx`) +Add an "Embed" action button next to "Export to Markdown". +- **Action**: Opens a modal with the iframe code snippet. + + +## Implementation Steps + +### 1. Build Configuration +Create `vite.config.embed.ts`: +```typescript +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist/embed', + rollupOptions: { + input: 'embed.html', // We might need a dedicated HTML or use index.html with a different entry + }, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); +``` + +### 2. Embed Entry Point +Create `src/embed.html` (copy of index.html pointing to `src/main-embed.tsx`). +Create `src/main-embed.tsx`. +Create `src/EmbedApp.tsx`. + +### 3. Server Route +Update `server/src/products/serving/index.ts`: +```typescript +// Add to routes +this.routes.push({ definition: { method: 'get', path: '/embed/:id' }, handler: this.handleGetEmbed.bind(this) }); + +// Handler +async handleGetEmbed(c: Context) { + const id = c.req.param('id'); + // ... fetch post ... + // ... load embed/index.html ... + // ... inject data ... + return c.html(injected); +} +``` + +### 4. UI Component +Modify `src/pages/Post/renderers/ArticleRenderer.tsx` to add the Embed button. + +## Considerations +- **Styling**: Ensure global styles (`index.css`) are included but verify they don't assume a full page layout that breaks inside a small iframe. +- **Analytics**: Embed views might need distinct tracking. +- **Links**: All links inside the embed should open in a new tab (`target="_blank"`) to avoid navigating the iframe itself. diff --git a/packages/ui/docs/feed.md b/packages/ui/docs/feed.md new file mode 100644 index 00000000..b0772d8c --- /dev/null +++ b/packages/ui/docs/feed.md @@ -0,0 +1,79 @@ +# Instagram-like Feed Implementation Plan + +## Objective +Create a responsive, immersive feed experience that adapts to device size: +- **Desktop/Large Screens**: Retain the current `PhotoGrid` (grid view). +- **Mobile**: Implement a new `Feed` view (vertical list) similar to Instagram. +- **Carousel**: Support horizontal swiping (left/right) through multiple pictures within a single post. +- **Performance**: Implement "load ahead" strategy (buffer ~5 posts) to ensure smooth scrolling without loading the entire database. + +## Architecture & Components + +### 1. Data Layer Enhancements +Current `PhotoGrid` logic fetches posts and selects a single "cover" image. +We need to modify the data transformation to pass *all* visible pictures to the UI components. + +- **Query**: Keep fetching `posts` with `pictures`. +- **Transformation**: Instead of flattening to a single `MediaItem`, we need a structure that preserves the list of pictures for each post. + ```typescript + interface FeedPost { + id: string; // Post ID + user_id: string; // Author + pictures: MediaItemType[]; // Array of pictures in the post + // ... other post metadata (title, description, etc.) + } + ``` + +### 2. New `Feed` Component (Mobile) +A new component `src/components/Feed.tsx` will be created for the mobile view. +- **Layout**: Vertical list of full-width cards. +- **Virtualization**: Use `react-window` or simpler intersection observer-based rendering to only render posts in (and slightly outside) the viewport. +- **Preloading**: Ensure the next 5 image/video assets are preloaded. + +### 3. Updated `MediaCard` / New `FeedCard` +`MediaCard` currently handles a single media item. We have two options: +1. **Refactor `MediaCard`**: Add support for an array of media and internal carousel logic. +2. **Create `FeedCard`**: A specialized card for the Feed view that wraps `MediaCard` or implements its own carousel. + * *Decision*: Use `FeedCard` (or `PostCard`) to encapsulate the carousel logic (Embla Carousel or similar) and use `MediaCard` for individual slides if needed, or implement a lighter slide view. + * **Carousel**: Must support touch gestures for left/right swiping. + +### 4. `PhotoGrid` Updates +- **Logic Separation**: Extract the data fetching hook (e.g., `useFeedMedia`) so both `PhotoGrid` and `Feed` can share the same data source and state (likes, etc.). +- **Responsive Switch**: In `Index.tsx`, conditionally render `PhotoGrid` (desktop) or `Feed` (mobile). Or render both and hide via CSS (better for SSR/hydration matching, but heavier on DOM). Better to use a valid hook for `isMobile`. + +## Implementation Steps + +### Phase 1: Data & Hooks +1. Create `useFeedQuery` hook to fetch posts + pictures. +2. Implement pagination (infinite scroll) logic (load 10, load next 10 when bottom reached). +3. Preloading utility: Function to preload images `n` indexes ahead of the current viewport item. + +### Phase 2: Carousel Component +1. Implement a Swipe/Carousel component (using `embla-carousel-react` or purely custom CSS scroll-snap). +2. Ensure it handles image aspect ratios gracefully (Instagram usually restricts to 4:5 or square, but we might support flexible). + +### Phase 3: `MobileFeed` Component +1. Create the vertical list layout. +2. Implement the "Load 5 ahead" logic (prefetching images for the next 5 cards). +3. Integrate the Carousel for multi-image posts. + +### Phase 4: Integration +1. Update `Index.tsx` to switch between `PhotoGrid` and `MobileFeed`. +2. Ensure shared state (Likes, Comments) works in both views. + +## Technical Details + +### "Load 5 Ahead" Strategy +- **Intersection Observer**: Watch the last rendered element to trigger fetching the next page. +- **Image Preloading**: Watch the *currently visible* post index. Automatically create `Link rel="preload"` or `new Image()` for the `cover` images of the next 5 posts. +- **Carousel Preloading**: If a user stops on a post, prioritize loading the *next* slide of that specific post. + +### Swiping Interaction +- **Carousel (Inner)**: Swiping horizontally moves between pictures of the *same* post. +- **Feed (Outer)**: Scrolling vertically moves between *different* posts. + +## Proposed File Structure +- `src/components/feed/Feed.tsx` +- `src/components/feed/FeedCard.tsx` (Handles the carousel) +- `src/components/feed/FeedCarousel.tsx` (The actual swiper) +- `src/hooks/useFeed.ts` (Data fetching logic) diff --git a/packages/ui/docs/layouts/readme.md b/packages/ui/docs/layouts/readme.md new file mode 100644 index 00000000..b96f21e1 --- /dev/null +++ b/packages/ui/docs/layouts/readme.md @@ -0,0 +1,44 @@ +# Layouts + +## 2 Columns : PhotoCard & Text + +```json + +{ + "id": "playground-canvas-demo", + "name": "Playground Canvas", + "containers": [ + { + "id": "container-1769374479197-drxdzewil", + "type": "container", + "columns": 2, + "gap": 16, + "widgets": [ + { + "id": "widget-1769374641802-b2cj664rs", + "widgetId": "photo-card", + "props": { + "pictureId": "41185796-3600-4e0b-988f-e1ac59592492" + }, + "order": 0 + }, + { + "id": "widget-1769374667509-kxctp5bix", + "widgetId": "markdown-text", + "props": { + "content": "sdfdf", + "placeholder": "Enter your text here...", + "templates": [] + }, + "order": 1 + } + ], + "children": [], + "order": 0 + } + ], + "createdAt": 1769374479197, + "updatedAt": 1769374676394 +} + +``` diff --git a/packages/ui/docs/m3u8.md b/packages/ui/docs/m3u8.md new file mode 100644 index 00000000..061aa174 --- /dev/null +++ b/packages/ui/docs/m3u8.md @@ -0,0 +1,79 @@ +# HLS (m3u8) Implementation Plan + +## Objective +Transition internal video processing and serving from single MP4 files to HLS (HTTP Live Streaming) to improve playback performance, seekability, and support adaptive bitrate streaming. + +## Technical Changes + +### 1. Video Processing (Worker) +**File**: `server/src/products/videos/worker.ts` + +**Current**: Generates single `jobId.mp4`. +**New**: Generate HLS playlist and segments. + +**FFmpeg Command**: +```bash +ffmpeg -i input.mp4 \ + -b:v 1M \ + -g 60 \ + -hls_time 2 \ + -hls_list_size 0 \ + -hls_segment_size 500000 \ + output.m3u8 +``` + +**Storage Structure**: +Instead of flat files in `videos/`, use a directory per job: +``` +videos/ + ├── [jobId]/ + │ ├── playlist.m3u8 + │ ├── output0.ts + │ ├── output1.ts + │ └── ... +``` + +### 2. API Endpoints (Product) +**File**: `server/src/products/videos/routes.ts` & `index.ts` + +Create new endpoints to serve HLS assets. We need to serve both the playlist (`.m3u8`) and the segment files (`.ts`). + +#### Endpoints: +1. **Playlist**: `GET /api/videos/jobs/:id/hls/playlist.m3u8` + * **Handler**: Looks up directory `videos/:id/`, serves `playlist.m3u8`. + * **Content-Type**: `application/vnd.apple.mpegurl` or `application/x-mpegURL`. + +2. **Segments**: `GET /api/videos/jobs/:id/hls/:segment` + * **Handler**: Looks up directory `videos/:id/`, serves requested `.ts` file. + * **Content-Type**: `video/MP2T`. + +**Note**: The playlist generated by ffmpeg usually contains relative paths (e.g., `output0.ts`). If the browser loads the playlist from `.../hls/playlist.m3u8`, it will resolve segments relative to that base, `.../hls/output0.ts`, which fits the proposed endpoint structure perfectly. + +### 3. Frontend (Playground & Components) +**File**: `src/pages/VideoPlayerPlaygroundIntern.tsx` + +* Update `pollJob` or `handleUpload` completion logic. +* Instead of setting `videoUrl` to the MP4 download link, set it to the HLS playlist endpoint: + * `http://localhost:3333/api/videos/jobs/[jobId]/hls/playlist.m3u8` +* Pass type `application/x-mpegurl` (or `application/vnd.apple.mpegurl`) to `VideoCard`. + +**File**: `src/components/VideoCard.tsx` + +* Ensure `Vidstack` / `MediaPlayer` handles the HLS mime type correctly. Vidstack has built-in HLS support (often requires `hls.js` but it's usually bundled or easily added). +* Verify `src` prop handling for HLS. + +### 4. Database / Metadata +* Update `resultUrl` in the job completion data (in `pg-boss` or returned to frontend) to point to the `.m3u8` file. +* Supabase `pictures` table: Update `image_url` to store the HLS playlist URL instead of the MP4 download URL. + +## Migration Strategy +1. **Implement new Worker logic**: Create a new job name or update existing `video-processing` to output HLS. +2. **Implement new Endpoints**: Add HLS routes to `VideosProduct`. +3. **Update Frontend**: Point playground to new endpoints. +4. **Verify**: Test playback. +5. **Cleanup**: Remove MP4 download endpoint and old worker logic once verified. + +## Outstanding Questions +* **Storage Cleanup**: Deleting a video now means deleting a directory. Ensure `handleDelete` is updated. +* **Legacy Content**: Old MP4s will stop working if we aggressively remove the old endpoint. + * *Decision*: Keep the old `download` endpoint for backward compatibility if needed, or migration script to convert old videos (out of scope for now). diff --git a/packages/ui/docs/markdown-html-widget.md b/packages/ui/docs/markdown-html-widget.md new file mode 100644 index 00000000..ef6b1bbe --- /dev/null +++ b/packages/ui/docs/markdown-html-widget.md @@ -0,0 +1,64 @@ +# Markdown HTML Widget & Property Type + +This document outlines the implementation of the `markdown` property type for widgets in the Polymech Pictures system. This feature allows widget developers to define properties that accept Markdown input, which is then rendered as HTML in the canvas and exported as HTML for emails. + +## 1. Configuration Schema + +To use the Markdown property type, update your widget's `library.json` definition. Use `"type": "markdown"` in the `configSchema`. + +```json +{ + "name": "Text", + "template": "./text.html", + "configSchema": { + "content": { + "type": "markdown", + "label": "Content", + "default": "# Hello World\nThis is **markdown**.", + "description": "The main text content." + } + } +} +``` + +## 2. Widget Properties UI + +The `WidgetPropertiesForm` component has been updated to handle the `markdown` type: + +* **Input**: Renders a `Textarea` for quick edits. +* **Fullscreen Editor**: Includes a "Fullscreen" button that opens a modal with a rich Markdown editor (`MarkdownEditorEx`). +* **Functionality**: + * Supports splitting view (Editor/Preview). + * Integrates with `ImagePickerDialog` for inserting images. + +## 3. Rendering Pipeline + +The rendering of Markdown content occurs in two places: + +### A. Canvas Rendering (`HtmlWidget.tsx`) + +* **Dependency**: Uses `marked` library for parsing. +* **Logic**: + * The `HtmlWidget` checks the widget definition (looked up via `widgetDefId`) for any properties defined as `type: "markdown"`. + * If found, it asynchronously parses the markdown string into HTML using `marked.parse()`. + * The resulting HTML is injected into the template. +* **Inline Editing**: Unlike standard text properties, Markdown properties are **excluded** from the inline `contenteditable` wrappers. This prevents users from accidentally breaking the HTML structure by editing raw HTML output directly on the canvas. + +### B. Email Export (`emailExporter.ts`) + +* **Logic**: + * During the export process (`generateEmailHtml`), the system iterates through widget properties. + * It checks the `configSchema` for `markdown` types. + * It converts the Markdown content to HTML *before* performing variable substitution in the template. + * This ensures that the final email HTML contains properly formatted tags (e.g., `

`, `
    `, ``) instead of raw markdown characters. + +## 4. Technical Implementation Details + +* **Dependencies**: Added `marked` to `package.json`. +* **Layout Context**: `LayoutContainer` passes `widgetDefId` to widget components to ensure correct schema lookup in the registry. +* **Registry**: The `WidgetRegistry` is the source of truth for property types, enabling the renderer to know *which* props to compile. + +## 5. Usage Notes + +* **Preview Mode**: The behavior in the canvas now matches the "Preview" mode more closely for markdown fields. +* **Sanitization**: Standard `marked` parsing is used. Ensure trusted input if extending to user-generated content in the future. diff --git a/packages/ui/docs/mux-integration.md b/packages/ui/docs/mux-integration.md new file mode 100644 index 00000000..e471f025 --- /dev/null +++ b/packages/ui/docs/mux-integration.md @@ -0,0 +1,306 @@ +# Mux Video Integration + +This project integrates [Mux](https://www.mux.com) for professional video upload, processing, and streaming capabilities. + +## Overview + +Mux provides: +- **Video Upload**: Drag & drop or click to upload video files +- **Automatic Processing**: Videos are automatically transcoded and optimized +- **HLS Streaming**: Adaptive bitrate streaming for smooth playback +- **Thumbnail Generation**: Automatic thumbnails and poster images +- **Analytics**: Track video views and engagement (optional) + +## Architecture + +### Flow + +1. **Client requests upload URL** → Frontend calls our Supabase Edge Function +2. **Edge Function creates upload** → Calls Mux API to generate signed upload URL +3. **User uploads video** → Mux Uploader handles the upload with progress tracking +4. **Mux processes video** → Transcodes video, creates HLS stream, generates thumbnails +5. **Get playback ID** → Poll for asset creation, retrieve playback ID +6. **Play video** → Use Vidstack player with Mux HLS stream URL + +### Components + +- **MuxUploader**: React component for uploading videos (`@mux/mux-uploader-react`) +- **VideoCard**: Component for displaying videos with Vidstack player +- **mux-proxy**: Supabase Edge Function that interfaces with Mux API + +## Setup + +### 1. Get Mux Credentials + +1. Sign up at [mux.com](https://www.mux.com) +2. Navigate to **Settings** → **Access Tokens** +3. Create a new access token with permissions: + - `Mux Video` - Read and Write +4. Copy the **Token ID** and **Token Secret** + +### 2. Configure Environment Variables + +Add these to your Supabase Edge Function environment variables: + +```bash +MUX_TOKEN_ID=your_token_id_here +MUX_TOKEN_SECRET=your_token_secret_here +``` + +To set them in Supabase: + +```bash +# Using Supabase CLI +supabase secrets set MUX_TOKEN_ID=your_token_id +supabase secrets set MUX_TOKEN_SECRET=your_token_secret + +# Or via Supabase Dashboard +# Project Settings → Edge Functions → Secrets +``` + +### 3. Deploy Edge Function + +```bash +supabase functions deploy mux-proxy +``` + +## Usage + +### Upload Video + +```tsx +import MuxUploader from "@mux/mux-uploader-react"; +import { supabase } from "@/integrations/supabase/client"; + +const fetchUploadUrl = async () => { + const response = await fetch( + `${supabase.supabaseUrl}/functions/v1/mux-proxy`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${session.access_token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ action: 'create-upload' }), + } + ); + + const { data } = await response.json(); + return data.url; +}; + +function VideoUpload() { + return ( + { + console.log('Upload complete!', event.detail); + }} + /> + ); +} +``` + +### Play Video + +Once you have the playback ID from Mux, you can play the video: + +```tsx +import VideoCard from "@/components/VideoCard"; + +function VideoPlayer({ playbackId }: { playbackId: string }) { + const videoUrl = `https://stream.mux.com/${playbackId}.m3u8`; + const thumbnailUrl = `https://image.mux.com/${playbackId}/thumbnail.jpg`; + + return ( + + ); +} +``` + +## Mux API Actions + +### create-upload + +Creates a new direct upload URL. + +**Request:** +```json +{ + "action": "create-upload" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "id": "upload_abc123", + "url": "https://storage.googleapis.com/...", + "status": "waiting" + } +} +``` + +### get-upload + +Get the status of an upload and check if asset was created. + +**Request:** +```json +{ + "action": "get-upload", + "uploadId": "upload_abc123" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "id": "upload_abc123", + "status": "asset_created", + "asset_id": "asset_xyz789" + } +} +``` + +### get-asset + +Get asset details including playback IDs. + +**Request:** +```json +{ + "action": "get-asset", + "assetId": "asset_xyz789" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "id": "asset_xyz789", + "status": "ready", + "playback_ids": [ + { + "id": "playback_def456", + "policy": "public" + } + ] + } +} +``` + +## Database Schema + +Store Mux video data in your `videos` table: + +```sql +CREATE TABLE videos ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL, + title TEXT NOT NULL, + description TEXT, + video_url TEXT NOT NULL, -- https://stream.mux.com/{playback_id}.m3u8 + thumbnail_url TEXT, -- https://image.mux.com/{playback_id}/thumbnail.jpg + meta JSONB, -- { mux_asset_id, mux_playback_id } + created_at TIMESTAMP DEFAULT NOW() +); +``` + +Store in meta: +- `mux_asset_id`: For managing the asset via Mux API +- `mux_playback_id`: For generating stream/thumbnail URLs + +## Mux URLs + +### Stream URL (HLS) +``` +https://stream.mux.com/{PLAYBACK_ID}.m3u8 +``` + +This is an HLS stream that works with Vidstack, Mux Player, and most video players. + +### Thumbnail URL +``` +https://image.mux.com/{PLAYBACK_ID}/thumbnail.jpg +``` + +Query parameters: +- `?width=1280` - Set width +- `?height=720` - Set height +- `?time=10` - Thumbnail at 10 seconds + +### MP4 URL (if enabled) +``` +https://stream.mux.com/{PLAYBACK_ID}/high.mp4 +``` + +Available qualities: `low.mp4`, `medium.mp4`, `high.mp4` + +## Webhooks (Optional) + +For production, set up Mux webhooks to get notified when: +- Upload completes (`video.upload.asset_created`) +- Video is ready (`video.asset.ready`) +- Errors occur (`video.asset.errored`) + +This is more efficient than polling. See [Mux Webhooks Docs](https://docs.mux.com/guides/listen-for-webhooks). + +## Playground + +Test the integration at `/playground/video-player`: +- **Upload tab**: Upload videos using Mux +- **Test with URL tab**: Test Vidstack player with any video URL + +## Pricing + +Mux charges based on: +- **Encoding**: Minutes of video processed +- **Streaming**: Minutes of video delivered +- **Storage**: GB-months of video stored + +See [Mux Pricing](https://www.mux.com/pricing) for current rates. + +Free tier includes: +- $20/month in free credits +- Enough for ~40 minutes of encoding + 100 hours of streaming + +## Troubleshooting + +### Upload fails immediately +- Check that MUX_TOKEN_ID and MUX_TOKEN_SECRET are set in Supabase +- Verify the edge function is deployed +- Check browser console for CORS errors + +### Video stuck in "processing" +- Large videos can take several minutes to process +- Check Mux dashboard for asset status +- Verify the upload completed successfully + +### Video won't play +- Check that playback policy is set to "public" +- Verify the HLS URL format is correct +- Check browser console for player errors + +## Resources + +- [Mux Documentation](https://docs.mux.com) +- [Mux Uploader Docs](https://www.mux.com/docs/guides/mux-uploader) +- [Vidstack Player Docs](https://vidstack.io) +- [Mux Dashboard](https://dashboard.mux.com) + diff --git a/packages/ui/docs/overview-todos.md b/packages/ui/docs/overview-todos.md new file mode 100644 index 00000000..038f20a6 --- /dev/null +++ b/packages/ui/docs/overview-todos.md @@ -0,0 +1,53 @@ + +# Master Implementation Plan + +This document serves as the central roadmap, referencing tasks from: +- [`database-todos.md`](./database-todos.md) (DB) +- [`security.md`](./security.md) (SEC) +- [`caching.md`](./caching.md) (CACHE) + +## Phase 1: Foundation (Schema & Data Security) +*Goal: Secure the data layer and enable collaboration primitives.* + +- [ ] **[DB] Split `profiles` into `profiles_public` & `user_secrets`** + - [ ] Create table & Migrate data (Ref: [`src/integrations/supabase/types.ts`](../src/integrations/supabase/types.ts)). + - [ ] **[SEC]** Apply RLS to `user_secrets` (`user_id = auth.uid()`). +- [ ] **[DB] Create `page_collaborators` Table** + - [ ] Define columns & Unique Constraints. + - [ ] **[SEC]** Implement RLS for shared Page access (Viewer/Editor logic). + +## Phase 2: Server Core & API +*Goal: Build the "Smart Proxy" layer to handle data fetching and caching.* + +- [ ] **[CACHE] Implement `CacheAdapter`** + - [ ] Create Interface (Target: `server/src/commons/cache/types.ts`). + - [ ] Implement `MemoryCache` (default) & `RedisCache` (optional). +- [ ] **[DB] Implement Server Endpoints in [`ServingProduct`](../server/src/products/serving/index.ts)** + - [ ] `GET /api/feed` (Hydrated View-Ready Feed). + - [ ] `GET /api/profile/:id` (Public Profile). + - [ ] `GET /api/me/secrets` (Secure Settings access). +- [ ] **[CACHE] Apply Caching to Endpoints** + - [ ] Cache Feed (60s) & Profiles (5m). + +## Phase 3: Client Security & Refactor +*Goal: Stop leaking keys and move to the Proxy.* + +- [ ] **[SEC] Critical: Remove Client-Side Key Fetching** + - [ ] Scrub `profiles` selects in [`Profile.tsx`](../src/pages/Profile.tsx) and [`db.ts`](../src/lib/db.ts). + - [ ] Remove API Key inputs from Profile UI in [`Profile.tsx`](../src/pages/Profile.tsx). +- [ ] **[DB] Client Data Layer Refactor** + - [ ] Update [`db.ts`](../src/lib/db.ts) to use `fetchFeedFromProxy` / `fetchProfileFromProxy`. + - [ ] Deprecate direct Supabase `select` calls for core content. +- [ ] **[SEC] Hardening** + - [ ] **[SEC]** Handle 404s/403s in [`Post.tsx`](../src/pages/Post.tsx) correctly. + +## Phase 4: Performance & Optimization +*Goal: Instant loads and "feels native" speed.* + +- [ ] **[DB] Server-Side Injection (SSR-Lite)** + - [ ] Inject `window.__INITIAL_STATE__` into `index.html` via [`ServingProduct`](../server/src/products/serving/index.ts). +- [ ] **[CACHE] Client Hydration** + - [ ] Configure React Query to hydrate from `__INITIAL_STATE__`. + - [ ] Set global `staleTime` to 5m. +- [ ] **[SEC] Rate Limiting** + - [ ] Add limits to API endpoints. diff --git a/packages/ui/docs/overview.md b/packages/ui/docs/overview.md new file mode 100644 index 00000000..9c327846 --- /dev/null +++ b/packages/ui/docs/overview.md @@ -0,0 +1,142 @@ +# Polymech Media App +> +> *The next-generation platform for AI-powered content creation, organization, and immersive consumption.* + +Our application bridges the gap between professional content management and generative AI creativity. Built for speed, flexibility, and visual impact. + +## 🎨 AI Image Wizard + +The heart of our creative suite. The **Image Wizard** isn't just an uploader—it's an intelligent studio. + +- **Generative AI & Editing**: Create stunning visuals from text prompts or edit existing images using advanced AI models. +- **Voice-to-Prompt**: Speak your ideas. Our integrated voice recorder transcribes and optimizes your speech into high-quality image prompts. +- **Automated Workflows**: Run complex multi-step actions like "Optimize & Enhance" or "Style Transfer" with a single click. +- **Smart Presets**: Save and reuse your best prompt engineerings as templates. + +![AI Image Wizard Interface](placeholder_wizard_interface.png) + +## 🚀 Seamless Ingestion + +Getting content into your library has never been easier. + +- **Global Drag & Drop**: Drag files anywhere on the screen to instantly start a new post or upload to your gallery. +- **"Share to" Support**: deeply integrated with mobile OS share sheets. Send images, videos, or text from other apps directly to our **New Post** flow. +- **Universal Media Support**: Native handling for images and videos, including special optimization for short-form content like **TikTok** and **YouTube Shorts**. + +## 📝 Rich Content Editor + +Go beyond simple captions with our **Markdown Editor Ex**. + +- **Hybrid Editing**: Switch instantly between a visual WYSIWYG editor and raw Markdown for precision control. +- **Integrated Asset Picker**: Insert images from your library directly into your articles without leaving the flow. +- **Fullscreen Focus Mode**: Write without distractions. + +### 🧠 AI Text Generator + +Enhance your writing with our context-aware AI assistant. + +- **Context Aware**: Use "Selected Text" or "Entire Document" as context for generation, or just raw prompts. +- **Smart Application**: Choose how to apply the result — **Replace** selection, **Insert** at cursor, or **Append** to the end. +- **Web & Visuals**: Toggle **Web Search** for up-to-date info or **Image Tools** to have the AI generate and embed images directly in your text. +- **Voice Input**: Dictate your prompts for hands-free operation. + +![Markdown Editor](placeholder_editor.png) + +## 📤 Sharing & Export + +Your content isn't locked in. Share it anywhere, in any format. + +- **Universal Export**: + - **PDF**: Generate print-ready documents. + - **Markdown**: Export raw source for use in other editors. + - **ZIP Archive**: Download a complete offline package including all media assets. + - **Email HTML**: Get a pre-rendered HTML template ready for your email marketing tools. +- **Embeds**: Copy a simple iframe code snippet to embed your posts on any external website. +- **Social Sharing**: Native OS sharing integration for quick social posting. + +## 📱 Immersive Consumption + +Experience content exactly how it was meant to be seen. + +- **Compact Renderer**: A responsive, high-density view that adapts to your device. + - **Desktop**: Side-by-side media and content view for efficient browsing. + - **Mobile**: Vertical scrolling "Grouped Feed" optimized for touch. +- **Filmstrip Navigation**: Quickly browse through multi-image galleries. +- **Adaptive Video Player**: Smart playback for both landscape and vertical (9:16) video formats. + +## 📂 Organization & Management + +- **Page Manager**: Create, organize, and publish rich pages. Control visibility (Public/Private) with ease. +- **Multi-Tenancy**: Built-in support for Organizations, allowing separated workspaces and shared resources. +- **Collections & Tags**: Powerful categorization variables to keep your library structured. + +## 🎯 Use Cases + +### 🏢 Internal Social Platform + +Perfect for companies needing a secure, private space for culture and communication. + +- **Share Updates**: Post announcements, welcome new hires, or share event photos. +- **Privacy First**: Unlike public social media, your data stays within your organization. +- **Rich Media**: Share training videos, PDF policy documents, and branded assets in one place. + +### 🏪 Small Business Media Library + +A dedicated Digital Asset Management (DAM) solution without the enterprise price tag. + +- **Centralized Assets**: Keep product photos, marketing materials, and brand guides organized. +- **Easy Retrieval**: Find assets quickly with tags and collections compared to messy folder structures. +- **Quick Sharing**: Generate ZIPs or public links for press kits and partners instantly. + +### 🎨 Product Design Iterations + +Accelerate visual development workflows. + +- **AI Prototyping**: Use the Image Wizard to rapidly generate concepts and variations. +- **Version History**: Keep track of design evolution over time. +- **Contextual Feedback**: Annotate designs with rich text descriptions for clear communication. + +## 🆚 Comparison + +| Feature | Polymech Media App | Google Photos | Instagram | WordPress | +| :--- | :--- | :--- | :--- | :--- | +| **Primary Focus** | **Content Creation & Org** | Storage & Backup | Social Networking | Blogging / CMS | +| **Generative AI** | **Native & Deeply Integrated** | Basic Magic Editor | Filters / Basic AI | Plugin Required | +| **Context** | **Rich Text / Markdown** | Minimal Captions | Short Captions | Rich Text | +| **Privacy** | **Private / Org-based** | Private / Shared Albums | Public / Social | Public / Private | +| **Media Types** | **Mixed (Video, Img, Doc)** | Photo / Video | Photo / Video | Any (Plugin dep.) | +| **Workflow** | **Iterative & Pro** | Consumption focused | Engagement focused | Publishing focused | + +## 🔮 Future Workflows (Roadmap) + +We are constantly expanding our capabilities. Here is what is on the horizon: + +- [ ] **Collaborative Editing**: Real-time multiplayer editing for Markdown and canvas. +- [ ] **Chrome Extension**: One-click clipping of images, full pages, or selected text from any website directly to your library. +- [ ] **OS Explorer Integration**: Mount your library as a native virtual drive (Windows/FUSE). Drag-and-drop support directly from your desktop. +- [ ] **Figma Integration**: Direct import/sync of frames for seamless design reviews. +- [ ] **Slack/Discord Bot**: Push notifications and "Publish from Chat" workflows. +- [ ] **Mobile Native App**: Dedicated iOS and Android experiences with system-level sharing and camera integration. +- [ ] **Advanced Analytics**: Engagement metrics for internal comms (views, read time). + +- [ ] **Advanced Analytics**: Engagement metrics for internal comms (views, read time). + +## 💰 Monetization Options + +### For the Developer (Platform Owner) + +- **SaaS Subscription Models**: + - **Free Tier**: Basic features, storage limits, community support. + - **Pro Tier**: Advanced AI tools, unlimited storage, priority support. + - **Enterprise**: SSO, dedicated hosting, SLA, audit logs. +- **AI Credit System**: Pay-per-use model for high-compute generative AI tasks (Image generation, Voice-to-text). +- **White-Label Licensing**: Rebrandable instances for agencies to resell to their clients. + +### For Customers (End Users) + +- **Digital Asset Marketplace**: Sell templates, prompt packs, stock photos, or design assets directly from your library. +- **Premium Content Gates**: Create "Subscriber Only" pages or collections (Substack-style) for exclusive tutorials or resources. +- **Service Integration**: Showcase your portfolio and include "Hire Me" buttons to monetize your skills directly. + +--- +*Built with React, Vite, and Supabase.* diff --git a/packages/ui/docs/page-gen.md b/packages/ui/docs/page-gen.md new file mode 100644 index 00000000..c319b294 --- /dev/null +++ b/packages/ui/docs/page-gen.md @@ -0,0 +1,50 @@ +# Reference Image Integration in Page Generator + +This document explains how user-selected reference images influence the generation process for both text and images within the AI Page Generator. + +## Overview + +When a user selects reference images in the AI Page Generator, these images are passed to the AI model (LLM) as part of the conversation context. This enables **multimodal generation**, where the AI can "see" the selected images and use that visual understanding to guide its output. + +## data Flow + +1. **Selection**: Users select images via the `ImagePickerDialog`. These are stored as `referenceImages` state in `AIPageGenerator`. +2. **Submission**: When "Generate" is clicked, the image URLs are collected and passed through `CreationWizardPopup` -> `usePageGenerator` -> `runTools`. +3. **Context Injection**: In `src/lib/openai.ts`, the `runTools` function detects the presence of images. It constructs a **multimodal user message** for the OpenAI API: + + ```json + { + "role": "user", + "content": [ + { "type": "text", "text": "User's text prompt..." }, + { "type": "image_url", "image_url": { "url": "https://..." } }, + { "type": "image_url", "image_url": { "url": "https://..." } } + ] + } + ``` + +## Impact on Generation + +### 1. Text Generation (Direct Visual Context) + +The LLM (e.g., GPT-4o) directly processes the image data. This allows it to: + +* Describe the visible content of the reference images in the generated page. +* Match the tone, style, and mood of the text to the visual aesthetics of the images. +* Extract specific details (colors, objects, setting) from the images and incorporate them into the narrative. + +### 2. Image Generation (Indirect Prompt Alignment) + +Currently, **reference images are NOT passed as direct inputs** (img2img) to the underlying image generation tools (`generate_image` or `generate_markdown_image`). + +Instead, the reference images influence image generation **indirectly via the LLM**: + +1. The LLM "sees" the reference images and understands their style, composition, and subject matter. +2. When the LLM decides to generating *new* images for the page (using `generate_text_with_images`), it writes the **image generation prompts** based on this visual understanding. +3. **Result**: The newly generated images are likely to be stylistically consistent with the reference images because the prompts used to generate them were crafted by an AI that "saw" the references. + +## Schema Reference + +* **`runTools` (`openai.ts`)**: Accepts `images: string[]` and builds the multimodal message. +* **`generate_text_with_images` (`markdownImageTools.ts`)**: Accepts text prompts for new images, but does not accept input images. +* **`generate_image` (`openai.ts`)**: Accepts text prompts, count, and model, but does not accept input images. diff --git a/packages/ui/docs/pages-wizard.md b/packages/ui/docs/pages-wizard.md new file mode 100644 index 00000000..7b8ec59b --- /dev/null +++ b/packages/ui/docs/pages-wizard.md @@ -0,0 +1,132 @@ +# AI Page Generation Wizard + +## 1. Overview + +The AI Page Generation Wizard introduces a high-level, AI-driven workflow for creating complete pages, not just individual images. It leverages the existing voice input and image generation capabilities to offer a seamless "voice-to-page" experience. Users can dictate an idea, and the AI will generate a fully-formed page containing rich text content, embedded images, and appropriate metadata like tags and a title. + +This feature will be accessed through a new, unified creation popup in the header, which will serve as a central starting point for both the existing Image Wizard and the new Page Wizard. + +## 2. User Interface & Flow + +### New Entry Popup + +A new button will be added to the `Header`, triggering a "Creation Wizard" popup. This popup will be the primary entry point for AI-assisted content creation. + +**Popup Design:** + +- **Title:** What would you like to create? +- **Two Main Options:** + 1. **Generate Image:** For creating standalone images. + - **Fast & Direct:** Opens the standard Image Wizard. + - **Smart & Optimized:** Opens the Image Wizard in Agent mode. + - **Voice + AI:** Opens the Image Wizard's voice agent popup directly. + 2. **Create Page:** For generating entire pages. + - **From Scratch:** Opens a new page editor. + - **AI Agent:** A future feature for more complex, multi-step page generation. + - **Voice + AI:** This is the primary new flow. It opens a voice recording UI to start the voice-to-page process. + +### Voice-to-Page Flow + +1. **Initiation:** The user clicks the "Voice + AI" button under "Create Page". +2. **Recording:** A voice recording modal appears (reusing the component from `ImageWizard`). The user describes the page they want to create (e.g., "Write a tutorial on how to brew the perfect cup of green tea, include an image of a serene tea setup"). +3. **Processing:** The UI shows a status progression: `Transcribing...` -> `Generating content...` -> `Creating page...`. +4. **Completion:** Once the page is created, the user is automatically redirected to the new page in view mode. A success toast notification confirms the creation. + +## 3. Dependencies + +This feature will leverage many existing parts of the application and introduce a few new components. + +### Existing Components & Modules to Reuse: + +- **`Header.tsx`**: To add the new wizard trigger button. +- **`ImageWizard.tsx` / `VoiceRecordingPopup.tsx`**: The UI for voice recording and transcription. +- **`lib/openai.ts`**: The core `runTools` function, `zodFunction` helper, and existing tool definitions (`transcribeAudioTool`). We will add a new preset and tools here. +- **`lib/markdownImageTools.ts`**: The `generateTextWithImagesTool` will be crucial for the AI to generate the main content of the page. +- **`integrations/supabase/client.ts`**: For database interactions within the new page creation tool. +- **`pages/UserPage.tsx`**: The destination view for the newly created page. + +### New Components & Modules to Create: + +- **`components/CreationWizardPopup.tsx`**: The new modal that serves as the entry point. +- **`hooks/usePageGenerator.ts`**: A new hook to orchestrate the multi-step voice-to-page generation process. +- **`lib/pageTools.ts`**: A new file to house the AI tool(s) responsible for page creation to keep concerns separated. + +## 4. Implementation Plan + +The implementation can be broken down into the following tasks: + +1. **Task 1: Create New AI Tools for Page Management** + - Create a new file: `src/lib/pageTools.ts`. + - Define a new tool `createPageTool` using `zodFunction`. + - **Schema:** `({ title: string, content: string, tags: string[], slug: string, is_public?: boolean, visible?: boolean })`. + - **Functionality:** + - It will accept the page title, markdown content, and tags. + - It must format the markdown content into the required page JSON structure (with `containers`, `widgets`, and `widgetId: "markdown-text"`). + - It will insert a new row into the `pages` table in Supabase. + - It will return the `slug` of the newly created page so the UI can navigate to it. + +2. **Task 2: Define a New `runTools` Preset** + - In `src/lib/openai.ts`, create a new preset called `'page-generator'`. + - **Tools:** This preset will include `generateTextWithImagesTool` (from `markdownImageTools.ts`) and the new `createPageTool` (from `pageTools.ts`). + - **System Prompt:** A detailed system prompt will guide the LLM through the process: + 1. First, understand the user's request from the transcribed text. + 2. Use the `generateTextWithImagesTool` to create rich markdown content, including one or more relevant images. + 3. From the generated content, derive a concise title and a list of relevant tags. + 4. Generate a URL-friendly slug from the title. + 5. Finally, call the `createPageTool` with the title, slug, tags, and the full markdown content to save the page. + +3. **Task 3: Develop the Orchestration Logic** + - Create a new hook `usePageGenerator` (`src/hooks/usePageGenerator.ts`). + - This hook will manage the state of the voice-to-page flow (`isTranscribing`, `isGenerating`, `isCreating`). + - It will contain a function, e.g., `generatePageFromVoice(audioFile)`, which: + 1. Calls `transcribeAudio`. + 2. Calls `runTools` with the `'page-generator'` preset and the transcribed text. + 3. Processes the result from `createPageTool` to get the new page slug. + 4. Uses the `navigate` function from `react-router-dom` to redirect the user. + +4. **Task 4: Build the UI Components** + - Create the `CreationWizardPopup.tsx` component with the layout described in the UI section. + - Add a new state and button to `Header.tsx` to open this popup. + - The "Voice + AI" button for page creation will trigger the `usePageGenerator` logic and display the status to the user. + +## 5. Sequence Diagram (Mermaid) + +This diagram illustrates the full voice-to-page workflow. + +```mermaid +sequenceDiagram + participant User + participant HeaderUI + participant CreationWizardPopup + participant VoiceUI + participant PageGeneratorHook + participant OpenAIApi as OpenAI API + participant SupabaseDB as Supabase DB + + User->>HeaderUI: Clicks "Create" button + HeaderUI->>CreationWizardPopup: Opens popup + + User->>CreationWizardPopup: Selects "Create Page" -> "Voice + AI" + CreationWizardPopup->>VoiceUI: Opens voice recorder + User->>VoiceUI: Records voice command + VoiceUI-->>PageGeneratorHook: onTranscriptionComplete(audioBlob) + + PageGeneratorHook->>OpenAIApi: transcribeAudio(audioBlob) + OpenAIApi-->>PageGeneratorHook: Returns transcribed text + + PageGeneratorHook->>OpenAIApi: runTools('page-generator', transcribedText) + note right of OpenAIApi: System prompt instructs AI to:
    1. Call generateTextWithImagesTool
    2. Call createPageTool + + OpenAIApi->>OpenAIApi: 1. generateTextWithImagesTool(prompt) + note right of OpenAIApi: This internally calls
    createImage and uploads to storage + OpenAIApi-->>OpenAIApi: Returns markdown with image URLs + + OpenAIApi->>OpenAIApi: 2. createPageTool(title, content, ...) + OpenAIApi-->>PageGeneratorHook: Tool call to create page + + PageGeneratorHook->>SupabaseDB: INSERT INTO pages (title, content, ...) + SupabaseDB-->>PageGeneratorHook: Returns new page data (incl. slug) + + PageGeneratorHook->>CreationWizardPopup: Page creation successful (returns slug) + CreationWizardPopup->>User: Navigates to new page URL (/user/.../pages/new-slug) +``` diff --git a/packages/ui/docs/pm.md b/packages/ui/docs/pm.md new file mode 100644 index 00000000..25d094c3 --- /dev/null +++ b/packages/ui/docs/pm.md @@ -0,0 +1,31 @@ +# Polymech Server Operations + +## Hot Reloading via API + +To update the server code without requiring root access or SSH: + +1. **Deploy Code**: Sync the new `dist/` folder to the server (e.g., using `scripts/deploy.sh` or `rclone`). +2. **Trigger Restart**: Send a POST request to the restart endpoint with an admin token. + +```bash +curl -X POST https://api.polymech.info/api/admin/system/restart \ + -H "Authorization: Bearer " +``` + +### How it works + +- The endpoint `/api/admin/system/restart` is protected by admin authentication. +- When called, it waits 1 second (to flush the response) and then calls `process.exit(0)`. +- The systemd service is configured with `Restart=always` (or `on-failure`), so it automatically restarts the node process. +- The new process loads the updated code from the disk. + +### Systemd Configuration + +Ensure your `pm-pics.service` file includes: + +```ini +[Service] +Restart=always +# or +Restart=on-failure +``` diff --git a/packages/ui/docs/posts.md b/packages/ui/docs/posts.md new file mode 100644 index 00000000..45b5c740 --- /dev/null +++ b/packages/ui/docs/posts.md @@ -0,0 +1,73 @@ +# Plan: Support Multiple Images/Videos per Post + +## Overview +Currently, the application treats every image or video as an independent entity (a `picture` or linked `video`). We want to introduce a higher-level concept of a **Post** which can contain one or more media items (images or videos). + +This allows: +- Grouping related photos (e.g., a photo dump, or variations). +- A unified description and comment section for the group. +- Preserving individual interactions (likes/comments) on specific images within the post if desired (as per requirements). + +## Database Schema Changes + +We will introduce a new `posts` table and link existing `pictures` to it. + +### 1. New Table: `posts` +This table will hold the content for the "container". + +| Column | Type | Notes | +|Ref|---|---| +| `id` | `uuid` | Primary Key, default `gen_random_uuid()` | +| `user_id` | `uuid` | FK to `auth.users` (or profiles) | +| `title` | `text` | Main title of the post | +| `description` | `text` | Description/Caption for the whole post | +| `created_at` | `timestamptz` | default `now()` | +| `updated_at` | `timestamptz` | | +| `metadata` | `jsonb` | Flexible field for extra data | + +### 2. Update Table: `pictures` +We link media items to the post. + +- Add column `post_id`: `uuid`, FK to `posts(id)`. +- Add column `position`: `integer`, default 0. To order images within a post. + +> **Note**: Videos are stored in the `pictures` table with `type='mux-video'`, so this change covers both images and videos. The separate `videos` table in Supabase appears unused by the current frontend. + +### 3. Update Table: `comments` and `likes` +Currently, `comments` and `likes` reference `picture_id`. +- **Requirement**: "we might have also comments, and descriptions for the parent 'post'". +- **Approach**: + - Add `post_id` to `comments` and `likes` tables (nullable). + - Or create `post_comments` / `post_likes` tables if cleaner. + - *Decision*: We will start with a simple structure where `posts` have their own `description` (already in table). For comments, we might need a unified comment system later or link comments to posts. For now, let's focus on `posts` containing `pictures`. + +## Migration Strategy (SQL) + +According to user feedback, **no backfill is required**. Old pictures will simply not be displayed in the new "Post" feed which will rely on the `posts` table. + +1. **Create `posts` table.** +2. **Alter `pictures` table**: Add `post_id` column. + +## UI/UX Updates + +### Feed (`PhotoGrid.tsx`) +- Query `posts` instead of `pictures`. +- Fetch the first linked picture for the thumbnail. + +### Post Detail (`Post.tsx`) +- Route `/post/:id` will now accept a **Post ID**. +- Fetch Post metadata. +- Fetch associated Media Items (`select * from pictures where post_id = :id order by position`). + +### Creation Wizard +- Allow granular updates: "Select Multiple Files". +- Create Post -> Upload all files -> Create Picture records linked to Post. + +## Step-by-Step Implementation + +1. **Supabase Migration**: Create tables, run backfill script. +2. **Codebase - Types**: Update `types.ts` (re-run codegen). +3. **Codebase - API**: Update any fetch functions to use `posts`. +4. **UI - Feed**: Switch `PhotoGrid` to use `posts`. +5. **UI - Detail**: Rewrite `Post.tsx` to handle `Post` + `Media[]`. +6. **UI - Create**: Update upload logic. diff --git a/packages/ui/docs/pwa-sharing.md b/packages/ui/docs/pwa-sharing.md new file mode 100644 index 00000000..a2e855bc --- /dev/null +++ b/packages/ui/docs/pwa-sharing.md @@ -0,0 +1,58 @@ +# PWA Sharing & Mobile Integration Options + +## Overview +We have implemented a **Progressive Web App (PWA)** with the **Web Share Target API** to allow sharing text, images, and videos from mobile apps directly to the Polymech Pics app. + +## Implementation Details + +### 1. Manifest +The manifest is manually managed in `public/manifest.webmanifest`. +It includes the `share_target` configuration: +```json +"share_target": { + "action": "/upload-share-target", + "method": "POST", + "enctype": "multipart/form-data", + "params": { + "title": "title", + "text": "text", + "url": "url", + "files": [ + { + "name": "file", + "accept": ["image/*", "video/*"] + } + ] + } +} +``` + +### 2. Service Worker (`src/sw.ts`) +The Service Worker intercepts `POST` requests to `/upload-share-target`: +1. Catches the request. +2. Parses `FormData` (files, title, text). +3. Stores data in **IndexedDB** (using `idb-keyval`, key: `share-target`). +4. Redirects the user to `/new?shared=true`. + +### 3. Frontend (`src/pages/NewPost.tsx`) +On the "New Post" page: +1. Checks for `?shared=true`. +2. Reads from IndexedDB (`share-target`). +3. Pre-loads the images/text into the wizard. +4. Clears the IndexedDB entry. + +### 4. Build Configuration (`vite.config.ts`) +- `vite-plugin-pwa` is configured with `manifest: false` to respect the manual file in `public/`. +- Strategy is `injectManifest` using `src/sw.ts`. +- Updates are handled via `workbox-core`'s `skipWaiting()` and `clientsClaim()` for immediate activation. + +## Installation (Add to Home Screen) +The app meets the criteria for "Add to Home Screen" (A2HS): +- **Manifest**: Correctly linked with `name`, `icons` (192px/512px), `start_url`, and `display: standalone`. +- **Service Worker**: Registered and functioning offline (precaching cached assets). +- **HTTPS**: Required for PWA installation (ensure your deployment provider supports this). + +### Testing A2HS +- **Desktop Chrome**: Look for the "Install" icon in the address bar. +- **Android**: Open Chrome -> Menu -> "Add to Home screen". +- **iOS**: Open Safari -> **Share** button -> "Add to Home Screen" (Note: iOS doesn't prompt automatically). diff --git a/packages/ui/docs/render-post.md b/packages/ui/docs/render-post.md new file mode 100644 index 00000000..e5170701 --- /dev/null +++ b/packages/ui/docs/render-post.md @@ -0,0 +1,630 @@ +# Post Rendering Flow Documentation + +This document traces the complete data flow and rendering pipeline for opening and displaying a post in the pm-pics application, from the PhotoGrid component through to the CompactMediaViewer. + +--- + +## Overview + +The post rendering system follows this component chain: + +``` +PhotoGrid → Post.tsx → CompactRenderer → CompactMediaViewer +``` + +Each layer transforms and enriches the data, handling navigation state, media types, and rendering options. + +--- + +## 1. Entry Point: PhotoGrid Component + +**File**: [PhotoGrid.tsx](../src/components/PhotoGrid.tsx) + +### Data Source + +PhotoGrid receives media items from two possible sources: + +1. **Custom Pictures** (props): Pre-loaded media items passed directly +2. **Feed Data** (hook): Fetched via `useFeedData` hook with parameters: + - `source`: 'home' | 'collection' | 'tag' | 'user' | 'widget' + - `sourceId`: Identifier for the source + - `sortBy`: 'latest' | other sort options + - `categorySlugs`: Optional category filtering + +### Data Structure: MediaItemType + +```typescript +interface MediaItemType { + id: string; + picture_id?: string; + title: string; + description: string | null; + image_url: string; + thumbnail_url: string | null; + type: MediaType; + meta: any | null; + likes_count: number; + created_at: string; + user_id: string; + comments: { count: number }[]; + + // Enriched fields + author?: UserProfile; + job?: any; + responsive?: any; + versionCount?: number; +} +``` + +### Navigation Data Setup + +When a user clicks on a media item (line 258-274): + +```typescript +const handleMediaClick = (mediaId: string, type: MediaType, index: number) => { + // Special handling for internal pages + if (type === 'page-intern') { + navigate(`/user/${username}/pages/${slug}`); + return; + } + + // Update navigation context with current index + setNavigationData({ ...navigationData, currentIndex: index }); + + // Navigate to post view + navigate(`/post/${mediaId}`); +} +``` + +**Navigation Context** includes: +- `posts`: Array of post metadata (id, title, image_url, user_id, type) +- `currentIndex`: Position in the feed +- `source`: Where the user came from +- `sourceId`: Source identifier + +--- + +## 2. Post Container: Post.tsx + +**File**: [Post.tsx](../src/pages/Post.tsx) + +### Data Fetching Strategy + +The `fetchMedia` function (lines 477-674) implements a **3-tier fallback strategy**: + +#### Tier 1: Fetch as Post +```typescript +const postData = await db.fetchPostById(id); +``` + +Returns a `PostItem` with: +- Post metadata (title, description, settings) +- Array of `pictures` (sorted by position) +- User information + +#### Tier 2: Fetch as Picture +```typescript +const pictureData = await db.fetchPictureById(id); +``` + +If found and has `post_id`, fetches the full post. Otherwise creates a **pseudo-post** with single picture. + +#### Tier 3: Fetch as Page +```typescript +const pageData = await db.fetchPageById(id); +``` + +Creates a pseudo-post for internal page content with `type: 'page-intern'`. + +### Version Resolution + +After fetching, the system resolves image versions (lines 478-526): + +```typescript +const resolveVersions = async (items: any[]) => { + // 1. Extract root IDs (parent_id || id) + const rootIds = items.map(i => i.parent_id || i.id); + + // 2. Fetch all related versions + const allVersions = await db.fetchRelatedVersions(rootIds); + + // 3. Preserve original positions + // 4. Sort by position, then created_at + // 5. Deduplicate by ID +} +``` + +This ensures that: +- AI-generated variations are grouped with originals +- User-selected versions are displayed +- Position order is maintained + +### State Management + +Key state variables: + +```typescript +const [post, setPost] = useState(null); +const [mediaItems, setMediaItems] = useState([]); +const [mediaItem, setMediaItem] = useState(null); // Currently displayed +const [viewMode, setViewMode] = useState<'compact' | 'article' | 'thumbs'>('compact'); +``` + +### View Mode Synchronization + +View mode is synchronized with URL parameters (lines 82-124): + +```typescript +// Initialize from URL +const viewParam = searchParams.get('view'); +if (viewParam === 'article' || viewParam === 'compact' || viewParam === 'thumbs') { + return viewParam; +} + +// Sync state to URL +useEffect(() => { + const newParams = new URLSearchParams(searchParams); + newParams.set('view', viewMode); + setSearchParams(newParams, { replace: true }); +}, [viewMode]); +``` + +### Data Types + +#### PostItem +```typescript +interface PostItem { + id: string; + title: string; + description: string | null; + user_id: string; + created_at: string; + updated_at: string; + pictures?: PostMediaItem[]; + settings?: Json; + meta?: Json; + isPseudo?: boolean; // For single pictures without posts + type?: string; // 'page-intern' for internal pages +} +``` + +#### PostMediaItem +```typescript +interface PostMediaItem { + id: string; + title: string; + description: string | null; + image_url: string; + thumbnail_url: string | null; + user_id: string; + type: MediaType; + created_at: string; + position: number | null; + post_id: string | null; + parent_id: string | null; // For versions + meta: Json | null; + likes_count: number; + visible: boolean; + is_liked?: boolean; // Enriched by user context + renderKey: string; // Unique key for React rendering +} +``` + +--- + +## 3. Renderer Selection: CompactRenderer + +**File**: [CompactRenderer.tsx](../src/pages/Post/renderers/CompactRenderer.tsx) + +### Props Interface + +```typescript +interface PostRendererProps { + post: PostItem; + authorProfile: UserProfile; + mediaItems: PostMediaItem[]; + mediaItem: PostMediaItem; // Currently selected + + // State + isOwner: boolean; + isLiked: boolean; + likesCount: number; + currentImageIndex: number; + + // Media-specific + videoPlaybackUrl?: string; + videoPosterUrl?: string; + versionImages: ImageFile[]; + + // Callbacks + onMediaSelect: (item: PostMediaItem) => void; + onExpand: (item: PostMediaItem) => void; + onLike: () => void; + onDeletePicture: (id: string) => void; + // ... many more action handlers + + // Navigation + navigationData: any; + handleNavigate: (direction: 'next' | 'prev') => void; + + // Edit mode + isEditMode?: boolean; + localPost?: { title: string; description: string }; + localMediaItems?: any[]; + // ... edit-related handlers +} +``` + +### Layout Strategy + +CompactRenderer implements a **responsive 2-column layout**: + +#### Desktop (lg breakpoint): +- **Left Column**: Media viewer + filmstrip +- **Right Column**: Header + details (scrollable) + +#### Mobile: +- **Stacked Layout**: Header → Media feed → Details + +### Component Breakdown + +``` +CompactRenderer +├── CompactPostHeader (mobile top, desktop right) +│ ├── User info, title, description +│ ├── View mode switcher +│ └── Action menu (edit, delete, export) +│ +├── Left Column (Desktop) +│ ├── CompactMediaViewer (main media display) +│ └── CompactFilmStrip (thumbnail navigation) +│ +├── MobileGroupedFeed (Mobile only) +│ └── Vertical scrolling media cards +│ +└── Right Column (Desktop) + ├── CompactPostHeader (repeated) + └── CompactMediaDetails + ├── Like/comment stats + ├── Description + ├── Comments section + └── Action buttons +``` + +--- + +## 4. Media Display: CompactMediaViewer + +**File**: [CompactMediaViewer.tsx](../src/pages/Post/renderers/components/CompactMediaViewer.tsx) + +### Media Type Handling + +The viewer handles **6 distinct media types**: + +#### 1. Internal Videos (`video`, `video-intern`) +```typescript + +``` + +#### 2. TikTok Videos (`tiktok`) +```typescript +