diff --git a/echo/.mcp.json b/echo/.mcp.json new file mode 100644 index 00000000..3b0a9552 --- /dev/null +++ b/echo/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "linear": { + "type": "http", + "url": "https://mcp.linear.app/mcp" + }, + "context7": { + "command": "npx", + "args": ["-y", "@upstash/context7-mcp"] + } + } +} diff --git a/echo/AGENTS.md b/echo/AGENTS.md index b1fe6795..6c8f3046 100644 --- a/echo/AGENTS.md +++ b/echo/AGENTS.md @@ -10,13 +10,17 @@ ECHO is an event-driven platform for collective sense-making. Users run discrete ``` echo/ +├── brand/ # Brand guidelines and assets +│ ├── STYLE_GUIDE.md # Comprehensive brand/UI guidelines +│ ├── colors.json # Machine-readable color tokens +│ ├── README.md # Quick reference + source links +│ └── logos/ # Logo files (PNG, SVG when available) ├── frontend/ # React + Vite frontend │ ├── src/ │ │ ├── components/ │ │ ├── routes/ │ │ ├── locales/ # Translation .po files │ │ └── config.ts # Feature flags -│ └── COPY_GUIDE.md # UI copy style guide ├── server/ # Python FastAPI backend │ └── dembrane/ │ ├── api/ # API endpoints @@ -27,20 +31,25 @@ echo/ ## Key Conventions -### UI Copy (IMPORTANT) +### Brand & UI Copy (IMPORTANT) -Always follow [frontend/COPY_GUIDE.md](frontend/COPY_GUIDE.md) when writing user-facing text: +Always follow [brand/STYLE_GUIDE.md](brand/STYLE_GUIDE.md) when writing user-facing text or making design decisions: -- **Shortest possible, highest clarity** -- **No jargon** — use plain language users understand -- **No corporate speak** — write like explaining to a colleague -- **Never say "successfully"** — just state what happened +- Shortest possible, highest clarity +- No jargon, no corporate speak +- Write like explaining to a colleague +- Never say "successfully" (just state what happened) +- Never use bold text for emphasis (use Royal Blue or italics) +- "dembrane" always lowercase, even at sentence start +- Say "language model" not "AI" when describing platform features Examples: - "Context limit reached" → "Selection too large" - "Successfully saved" → "Saved" - "Please wait while we process" → "Processing..." +Color tokens available in `brand/colors.json` for programmatic use. + ### Translations See [docs/frontend_translations.md](docs/frontend_translations.md) for the full workflow. @@ -89,7 +98,7 @@ Convention: Use `ENABLE_*` naming pattern for feature flags. ### Adding Translations -1. Write copy following COPY_GUIDE.md +1. Write copy following `brand/STYLE_GUIDE.md` 2. Use `` component or `t` template literal 3. Run `pnpm messages:extract` 4. Fill in translations in all `.po` files @@ -109,7 +118,8 @@ cd server && uv sync && uv run uvicorn dembrane.main:app --reload | File | Purpose | |------|---------| -| `frontend/COPY_GUIDE.md` | UI copy style guide | +| `brand/STYLE_GUIDE.md` | Brand guidelines, UI copy, colors, typography | +| `brand/colors.json` | Machine-readable color tokens | | `frontend/src/config.ts` | Frontend feature flags | | `server/dembrane/settings.py` | Backend configuration | | `docs/frontend_translations.md` | Translation workflow | diff --git a/echo/brand/README.md b/echo/brand/README.md new file mode 100644 index 00000000..46c12c64 --- /dev/null +++ b/echo/brand/README.md @@ -0,0 +1,27 @@ +# dembrane brand + +This folder contains brand guidelines and assets for building dembrane products. + +## Contents + +- `STYLE_GUIDE.md` - comprehensive brand and UI guidelines +- `colors.json` - machine-readable color tokens +- `logos/` - logo files (SVG preferred) + +## Source files + +- **Figma**: Brand Guidelines deck (visual specs, button states, layouts) +- **Notion**: [Build Brand pillar](https://www.notion.so/dembrane/Build-Brand-03a9b1a8b5e44f81aa48a6c3c4ee476c) +- **Coolors**: [Palette](https://coolors.co/f6f4f1-2d2d2c-4169e1-ffc2ff-00ffff-1effa1-f4ff81-ffd166-ff9aa2) + +## Quick reference + +- **Typography**: DM Sans (with stylistic alternates ss01-ss06) +- **Primary action**: Royal Blue `#4169e1` +- **Background**: Parchment `#f6f4f1` +- **Text**: Graphite `#2d2d2c` +- **Archetype**: 80% Everyman + 20% Explorer + +## Naming + +Always write "dembrane" in lowercase. Even at the start of a sentence. diff --git a/echo/brand/STYLE_GUIDE.md b/echo/brand/STYLE_GUIDE.md new file mode 100644 index 00000000..76fa2c34 --- /dev/null +++ b/echo/brand/STYLE_GUIDE.md @@ -0,0 +1,384 @@ +# dembrane style guide + +This is the north star for how dembrane looks, sounds, and feels. + +Use your gut. If something serves the brand better by bending a guideline, bend it. + +When in doubt, ask: Does this feel approachable, grounded, and human? Does it invite people in? + +--- + +## Brand foundation + +### Core belief + +PEOPLE KNOW HOW. + +Communities already hold the knowledge to solve their challenges. They just need better ways to surface it, connect it, and act on it. dembrane doesn't add intelligence to groups. It reveals the intelligence already there. + +### Archetype + +80% Everyman + 20% Explorer. + +Think IKEA meets Patagonia. Reliable, accessible, unpretentious. But with a spark of adventure and purpose. + +### What we stand for + +- *Hope over cynicism* - democracy works when people can actually talk +- *Critical AI* - language models as tools, not oracles +- *People powered* - the humans in the room hold the answers +- *Complexity welcome* - real problems are messy, we don't pretend otherwise +- *Institutional respect* - working with systems, not against them + +--- + +## Naming + +### dembrane + +Always lowercase. Even at the start of a sentence. + +- "dembrane helps groups..." (correct) +- "Dembrane helps groups..." (incorrect) +- "DEMBRANE" (never) + +### ECHO + +The platform feature. Not the brand. Use sparingly in external contexts. + +### Vocabulary + +| Don't say | Say instead | +|-----------|-------------| +| AI | language model (when describing features) | +| Users | participants and hosts | +| Customers | partners and clients | +| The tool | the platform, dembrane | +| Facilitate deliberation | help groups have better conversations | +| Collective sense-making | make sense of big messy conversations | + +### Words we use carefully + +- "Stakeholders" - fine, but prefer "everyone affected" or "the people involved" +- "AI" - fine in general context, but be specific: "the language model" or "the transcription model" + +--- + +## Voice and tone + +### Tone spectrum + +Warm but not gushing. Direct but not cold. Smart but not showing off. + +We sound like a trusted colleague, not a corporate announcement or a tech bro pitch deck. + +### Writing rules + +1. Shortest possible, highest clarity +2. If you wouldn't say it out loud, rewrite it +3. No jargon, no corporate speak +4. Write for humans who are busy and smart + +### Never say in UI + +- "We are pleased to inform you..." +- "Please be advised..." +- "In order to..." +- "Successfully" (just state what happened) +- "We apologize for any inconvenience this may have caused" +- "Click here to..." + +--- + +## Colors + +Use brand colors in these approximate proportions: + +| Color | Hex | Usage | +|-------|-----|-------| +| Parchment | `#f6f4f1` | Default background, canvas | +| Graphite | `#2d2d2c` | Primary text, mood | +| Royal Blue | `#4169e1` | Primary action, links, emphasis | + +### Accent colors (for categories/tags) + +| Color | Hex | Category | +|-------|-----|----------| +| Cyan | `#00ffff` | VALUE | +| Spring Green | `#1effa1` | DESIGN | +| Mauve | `#ffc2ff` | ENGINE | +| Lime Cream | `#f4ff81` | ADMIN | + +### System states (never for branding) + +| Color | Hex | Usage | +|-------|-----|-------| +| Golden Pollen | `#ffd166` | Warning | +| Cotton Candy | `#ff9aa2` | Error | + +--- + +## Typography + +### Font + +DM Sans. With stylistic alternates enabled (ss01 through ss06) for characters: a, g, u, y, Q. + +```css +font-family: 'DM Sans', sans-serif; +font-feature-settings: 'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06'; +``` + +### Hierarchy + +Keep it simple. One or two font weights max. Let whitespace do the work. + +| Level | Size | Weight | +|-------|------|--------| +| Display | 48-64px | Regular | +| Headline | 32-40px | Regular | +| Title | 24-28px | Regular | +| Body | 16-18px | Regular | +| Caption | 12-14px | Regular | + +### Rules + +1. Left-align text (unless very short and wrapped in a border) +2. Avoid orphans - single words alone on a line +3. Prefer hanging punctuation - quotes and bullets "hang" outside the text box +4. Never use *bold*. Use Royal Blue or *italics* for emphasis + +--- + +## Buttons + +### Primary + +- Default: Royal Blue background, white text, rounded corners +- Hover: Graphite background +- Click: Graphite background + +### Secondary + +- Default: Transparent with Royal Blue border, Royal Blue text +- Hover: 10% Royal Blue opacity fill +- Click: 20% Royal Blue opacity fill + +### Tertiary + +- Default: Text only, Royal Blue, no border +- Hover: 10% Royal Blue opacity background +- Click: 20% Royal Blue opacity background + +### Disabled + +- Parchment background (darker), Graphite text at 50% opacity + +### Copy patterns + +| Do | Don't | +|----|-------| +| Upload | Click here to upload | +| Save | Save your changes | +| Delete | Delete this item | + +--- + +## Components + +### Modals + +- Clear, direct title (action-oriented when appropriate) +- Body text: one or two sentences max +- Warning callouts: Golden Pollen background, brief text +- Actions: Primary on right, Cancel on left + +### Forms + +- Labels above inputs, left-aligned +- Placeholder text is not a substitute for labels +- Validation errors inline, close to the field +- Don't disable submit buttons while form is incomplete. Show errors on attempt + +### Cards + +- Parchment background by default +- Minimal borders (1px if needed) +- Generous padding +- One clear action per card + +### Tables + +- Left-align text, right-align numbers +- Zebra striping optional (only if many rows) +- Row hover state for clickable rows + +### Navigation + +- Keep it flat when possible +- Clear current-state indication +- Breadcrumbs for deep nesting only + +--- + +## UI copy patterns + +### Labels + +| Do | Don't | +|----|-------| +| Project name | Please enter project name | +| Start date | Name of the project | + +### Errors + +| Do | Don't | +|----|-------| +| File too large. Max 100MB. | An error occurred. The file exceeds... | +| Something went wrong. Try again. | We apologize for any inconvenience... | + +### Success states + +| Do | Don't | +|----|-------| +| Saved | Successfully saved | +| File uploaded | Your file has been successfully uploaded | + +### Empty states + +| Do | Don't | +|----|-------| +| No conversations yet. Start your first one. | You have not created any conversations | + +### Loading/processing + +| Do | Don't | +|----|-------| +| Analyzing... | Please wait while we process your request | +| Processing audio... | Your request is being processed | + +--- + +## Layout + +### Grid + +12 columns. 6 row zones. + +### Spacing + +- 8px base unit +- Generous whitespace. Let content breathe +- Group related items, separate unrelated ones +- Mobile-first responsive design + +--- + +## Photography and imagery + +### Principles + +- No stock photos +- No AI-generated images +- Warmth over polish +- Groups over individuals +- Real situations, real people +- Candid over posed + +### When showing the product + +- Real data where possible (anonymized) +- Avoid empty states in marketing materials +- Show the work, not just the interface + +--- + +## Logo + +### Usage + +- Use the full logo (dembrane wordmark) when space allows +- Use the logomark (d symbol) for favicons, app icons, constrained spaces +- Maintain clear space around the logo equal to the height of the "d" + +### Variants + +- Light (for dark backgrounds) +- Dark (for light backgrounds) +- Transparent (for overlays) + +Logo files live in `logos/`. SVG preferred. + +--- + +## Dutch localization + +### Core rules + +- Use "je/jij/jou" - never "u/uw" (always informal) +- Natural phrasing, not word-for-word translation +- Compound words: audiobestand (not "audio bestand") +- Keep English terms when they sound better: Dashboard, Upload, Chat + +### Key glossary + +| English | Dutch | +|---------|-------| +| Dashboard | Dashboard | +| Upload | Uploaden | +| Chat | Chat | +| Conversation | Gesprek | +| Audio file | Audiobestand | +| Participant | Deelnemer | +| Settings | Instellingen | +| Save | Opslaan | +| Delete | Verwijderen | +| It's not working | Doet het niet | +| We're fixing it | We zijn het aan het fixen | + +### Examples + +- Bad: "Gelieve uw bestand te uploaden" +- Good: "Upload je bestand" + +- Bad: "We zijn verheugd u te informeren dat de functionaliteit hersteld is" +- Good: "Chat doet het weer" + +--- + +## Platform context + +ECHO is event-driven, not daily-use software. Hosts run discrete engagement sessions: workshops, consultations, civic forums, employee feedback rounds. + +Typical flow: Prepare event > Run session > Analyze conversations > Generate report > Return for next event + +This means: + +- Don't add friction to infrequent tasks +- Remind people where they left off +- Make re-onboarding seamless +- Celebrate completed analyses, not login streaks + +--- + +## Icons + +Use [Phosphor Icons](https://phosphoricons.com/). + +- Regular weight for most UI +- Keep icons simple and recognizable +- Don't rely on icons alone. Pair with text labels where clarity matters + +--- + +## Quick checklist + +Before shipping any UI: + +- [ ] Can I say this in fewer words? +- [ ] Would I say this to a colleague? +- [ ] Is it clear what to do next? +- [ ] Does it feel approachable and human? +- [ ] Have I avoided bold text? +- [ ] Are error states helpful, not scary? +- [ ] Does it work in Dutch? diff --git a/echo/brand/colors.json b/echo/brand/colors.json new file mode 100644 index 00000000..ccc16d6f --- /dev/null +++ b/echo/brand/colors.json @@ -0,0 +1,62 @@ +{ + "categories": { + "admin": "limeCream", + "design": "springGreen", + "engine": "mauve", + "value": "cyan" + }, + "palette": { + "cottonCandy": { + "hex": "#ff9aa2", + "rgb": [255, 154, 162], + "usage": "Error states" + }, + "cyan": { + "hex": "#00ffff", + "rgb": [0, 255, 255], + "usage": "Accent, VALUE category" + }, + "goldenPollen": { + "hex": "#ffd166", + "rgb": [255, 209, 102], + "usage": "Warning states" + }, + "graphite": { + "hex": "#2d2d2c", + "rgb": [45, 45, 44], + "usage": "Primary text, mood, button hover" + }, + "limeCream": { + "hex": "#f4ff81", + "rgb": [244, 255, 129], + "usage": "Accent, ADMIN category" + }, + "mauve": { + "hex": "#ffc2ff", + "rgb": [255, 194, 255], + "usage": "Accent, ENGINE category" + }, + "parchment": { + "hex": "#f6f4f1", + "rgb": [246, 244, 241], + "usage": "Default background, canvas" + }, + "royalBlue": { + "hex": "#4169e1", + "rgb": [65, 105, 225], + "usage": "Primary action, links, emphasis" + }, + "springGreen": { + "hex": "#1effa1", + "rgb": [30, 255, 161], + "usage": "Accent, DESIGN category" + } + }, + "semantic": { + "action": "royalBlue", + "background": "parchment", + "error": "cottonCandy", + "text": "graphite", + "warning": "goldenPollen" + } +} diff --git a/echo/brand/logos/logo-dark-transparent.png b/echo/brand/logos/logo-dark-transparent.png new file mode 100644 index 00000000..29cac37d Binary files /dev/null and b/echo/brand/logos/logo-dark-transparent.png differ diff --git a/echo/brand/logos/logo-dark.png b/echo/brand/logos/logo-dark.png new file mode 100644 index 00000000..ab6b31a5 Binary files /dev/null and b/echo/brand/logos/logo-dark.png differ diff --git a/echo/brand/logos/logo-light-transparent.png b/echo/brand/logos/logo-light-transparent.png new file mode 100644 index 00000000..a6cd62b4 Binary files /dev/null and b/echo/brand/logos/logo-light-transparent.png differ diff --git a/echo/brand/logos/logo-light.png b/echo/brand/logos/logo-light.png new file mode 100644 index 00000000..49bde560 Binary files /dev/null and b/echo/brand/logos/logo-light.png differ diff --git a/echo/brand/logos/logo-square.png b/echo/brand/logos/logo-square.png new file mode 100644 index 00000000..fe356c30 Binary files /dev/null and b/echo/brand/logos/logo-square.png differ diff --git a/echo/brand/logos/logomark-dark-transparent.png b/echo/brand/logos/logomark-dark-transparent.png new file mode 100644 index 00000000..374936ac Binary files /dev/null and b/echo/brand/logos/logomark-dark-transparent.png differ diff --git a/echo/brand/logos/logomark-dark.png b/echo/brand/logos/logomark-dark.png new file mode 100644 index 00000000..d4620701 Binary files /dev/null and b/echo/brand/logos/logomark-dark.png differ diff --git a/echo/brand/logos/logomark-light-transparent.png b/echo/brand/logos/logomark-light-transparent.png new file mode 100644 index 00000000..c7cab266 Binary files /dev/null and b/echo/brand/logos/logomark-light-transparent.png differ diff --git a/echo/brand/logos/logomark-light.png b/echo/brand/logos/logomark-light.png new file mode 100644 index 00000000..dcab326c Binary files /dev/null and b/echo/brand/logos/logomark-light.png differ diff --git a/echo/directus/sync/collections/folders.json b/echo/directus/sync/collections/folders.json index ab83040a..ad3d1d96 100644 --- a/echo/directus/sync/collections/folders.json +++ b/echo/directus/sync/collections/folders.json @@ -3,5 +3,15 @@ "name": "Public", "parent": null, "_syncId": "74232676-80e7-4f8c-8012-c0d59e6d0a24" + }, + { + "name": "custom_logos", + "parent": null, + "_syncId": "9358abf8-aa7d-464e-9ea5-5e998f3cb807" + }, + { + "name": "custom_logos", + "parent": null, + "_syncId": "eabbb779-e43b-4c78-baa5-5a5cd102a596" } ] diff --git a/echo/directus/sync/snapshot/fields/conversation/chunks.json b/echo/directus/sync/snapshot/fields/conversation/chunks.json index e0b45493..db2fe066 100644 --- a/echo/directus/sync/snapshot/fields/conversation/chunks.json +++ b/echo/directus/sync/snapshot/fields/conversation/chunks.json @@ -24,7 +24,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 12, + "sort": 13, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/conversation_artifacts.json b/echo/directus/sync/snapshot/fields/conversation/conversation_artifacts.json index 8b8d9b7c..5104cc09 100644 --- a/echo/directus/sync/snapshot/fields/conversation/conversation_artifacts.json +++ b/echo/directus/sync/snapshot/fields/conversation/conversation_artifacts.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 25, + "sort": 26, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/conversation_segments.json b/echo/directus/sync/snapshot/fields/conversation/conversation_segments.json index 695a9411..62ab63be 100644 --- a/echo/directus/sync/snapshot/fields/conversation/conversation_segments.json +++ b/echo/directus/sync/snapshot/fields/conversation/conversation_segments.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 16, + "sort": 17, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/duration.json b/echo/directus/sync/snapshot/fields/conversation/duration.json index fc9d53bf..daebb4d7 100644 --- a/echo/directus/sync/snapshot/fields/conversation/duration.json +++ b/echo/directus/sync/snapshot/fields/conversation/duration.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 17, + "sort": 18, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/conversation/is_all_chunks_transcribed.json b/echo/directus/sync/snapshot/fields/conversation/is_all_chunks_transcribed.json index d0584b36..dadd0a1e 100644 --- a/echo/directus/sync/snapshot/fields/conversation/is_all_chunks_transcribed.json +++ b/echo/directus/sync/snapshot/fields/conversation/is_all_chunks_transcribed.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 22, + "sort": 23, "special": [ "cast-boolean" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/is_audio_processing_finished.json b/echo/directus/sync/snapshot/fields/conversation/is_audio_processing_finished.json index 2f7a3bef..b4666d93 100644 --- a/echo/directus/sync/snapshot/fields/conversation/is_audio_processing_finished.json +++ b/echo/directus/sync/snapshot/fields/conversation/is_audio_processing_finished.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 21, + "sort": 22, "special": [ "cast-boolean" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/is_finished.json b/echo/directus/sync/snapshot/fields/conversation/is_finished.json index 7e6352bb..fe093e1a 100644 --- a/echo/directus/sync/snapshot/fields/conversation/is_finished.json +++ b/echo/directus/sync/snapshot/fields/conversation/is_finished.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 20, + "sort": 21, "special": [ "cast-boolean" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/linked_conversations.json b/echo/directus/sync/snapshot/fields/conversation/linked_conversations.json index 38198ad2..af48f54d 100644 --- a/echo/directus/sync/snapshot/fields/conversation/linked_conversations.json +++ b/echo/directus/sync/snapshot/fields/conversation/linked_conversations.json @@ -21,7 +21,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 23, + "sort": 24, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/linking_conversations.json b/echo/directus/sync/snapshot/fields/conversation/linking_conversations.json index 6449e4d3..6a8cd255 100644 --- a/echo/directus/sync/snapshot/fields/conversation/linking_conversations.json +++ b/echo/directus/sync/snapshot/fields/conversation/linking_conversations.json @@ -20,7 +20,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 24, + "sort": 25, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/merged_audio_path.json b/echo/directus/sync/snapshot/fields/conversation/merged_audio_path.json index b29a3865..ed528e3c 100644 --- a/echo/directus/sync/snapshot/fields/conversation/merged_audio_path.json +++ b/echo/directus/sync/snapshot/fields/conversation/merged_audio_path.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 19, + "sort": 20, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/conversation/merged_transcript.json b/echo/directus/sync/snapshot/fields/conversation/merged_transcript.json index 3657c7b2..a2167210 100644 --- a/echo/directus/sync/snapshot/fields/conversation/merged_transcript.json +++ b/echo/directus/sync/snapshot/fields/conversation/merged_transcript.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 18, + "sort": 19, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/conversation/participant_email.json b/echo/directus/sync/snapshot/fields/conversation/participant_email.json index 51f29ebe..ef2966f2 100644 --- a/echo/directus/sync/snapshot/fields/conversation/participant_email.json +++ b/echo/directus/sync/snapshot/fields/conversation/participant_email.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 7, + "sort": 8, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/conversation/participant_name.json b/echo/directus/sync/snapshot/fields/conversation/participant_name.json index 23f25715..e02cb4bb 100644 --- a/echo/directus/sync/snapshot/fields/conversation/participant_name.json +++ b/echo/directus/sync/snapshot/fields/conversation/participant_name.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 6, + "sort": 7, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/conversation/participant_user_agent.json b/echo/directus/sync/snapshot/fields/conversation/participant_user_agent.json index dbe094af..93db99fc 100644 --- a/echo/directus/sync/snapshot/fields/conversation/participant_user_agent.json +++ b/echo/directus/sync/snapshot/fields/conversation/participant_user_agent.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 8, + "sort": 9, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/conversation/project_chat_messages.json b/echo/directus/sync/snapshot/fields/conversation/project_chat_messages.json index c9e5837c..1b6d3dab 100644 --- a/echo/directus/sync/snapshot/fields/conversation/project_chat_messages.json +++ b/echo/directus/sync/snapshot/fields/conversation/project_chat_messages.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 14, + "sort": 15, "special": [ "m2m" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/project_chats.json b/echo/directus/sync/snapshot/fields/conversation/project_chats.json index cd1d5847..0e63edfb 100644 --- a/echo/directus/sync/snapshot/fields/conversation/project_chats.json +++ b/echo/directus/sync/snapshot/fields/conversation/project_chats.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 13, + "sort": 14, "special": [ "m2m" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/replies.json b/echo/directus/sync/snapshot/fields/conversation/replies.json index cf040c13..e3d54be9 100644 --- a/echo/directus/sync/snapshot/fields/conversation/replies.json +++ b/echo/directus/sync/snapshot/fields/conversation/replies.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 15, + "sort": 16, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/source.json b/echo/directus/sync/snapshot/fields/conversation/source.json index 5aacc6f5..ee1a857b 100644 --- a/echo/directus/sync/snapshot/fields/conversation/source.json +++ b/echo/directus/sync/snapshot/fields/conversation/source.json @@ -29,7 +29,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 11, + "sort": 12, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/conversation/summary.json b/echo/directus/sync/snapshot/fields/conversation/summary.json index c54e0805..1c6ab5f1 100644 --- a/echo/directus/sync/snapshot/fields/conversation/summary.json +++ b/echo/directus/sync/snapshot/fields/conversation/summary.json @@ -31,7 +31,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 10, + "sort": 11, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/conversation/tags.json b/echo/directus/sync/snapshot/fields/conversation/tags.json index ba140876..1bde2897 100644 --- a/echo/directus/sync/snapshot/fields/conversation/tags.json +++ b/echo/directus/sync/snapshot/fields/conversation/tags.json @@ -20,7 +20,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 9, + "sort": 10, "special": [ "m2m" ], diff --git a/echo/directus/sync/snapshot/fields/conversation/title.json b/echo/directus/sync/snapshot/fields/conversation/title.json new file mode 100644 index 00000000..eb12807b --- /dev/null +++ b/echo/directus/sync/snapshot/fields/conversation/title.json @@ -0,0 +1,44 @@ +{ + "collection": "conversation", + "field": "title", + "type": "text", + "meta": { + "collection": "conversation", + "conditions": null, + "display": null, + "display_options": null, + "field": "title", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 6, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "title", + "table": "conversation", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/directus_users/whitelabel_logo.json b/echo/directus/sync/snapshot/fields/directus_users/whitelabel_logo.json new file mode 100644 index 00000000..601813c5 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/directus_users/whitelabel_logo.json @@ -0,0 +1,48 @@ +{ + "collection": "directus_users", + "field": "whitelabel_logo", + "type": "uuid", + "meta": { + "collection": "directus_users", + "conditions": null, + "display": null, + "display_options": null, + "field": "whitelabel_logo", + "group": null, + "hidden": false, + "interface": "file", + "note": null, + "options": { + "folder": "eabbb779-e43b-4c78-baa5-5a5cd102a596" + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 5, + "special": [ + "file" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "whitelabel_logo", + "table": "directus_users", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "directus_files", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/project/anonymize_transcripts.json b/echo/directus/sync/snapshot/fields/project/anonymize_transcripts.json new file mode 100644 index 00000000..549a4f4e --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project/anonymize_transcripts.json @@ -0,0 +1,46 @@ +{ + "collection": "project", + "field": "anonymize_transcripts", + "type": "boolean", + "meta": { + "collection": "project", + "conditions": null, + "display": null, + "display_options": null, + "field": "anonymize_transcripts", + "group": null, + "hidden": false, + "interface": "boolean", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 35, + "special": [ + "cast-boolean" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "anonymize_transcripts", + "table": "project", + "data_type": "boolean", + "default_value": false, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project/conversation_title_prompt.json b/echo/directus/sync/snapshot/fields/project/conversation_title_prompt.json new file mode 100644 index 00000000..b6eeccb5 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project/conversation_title_prompt.json @@ -0,0 +1,44 @@ +{ + "collection": "project", + "field": "conversation_title_prompt", + "type": "text", + "meta": { + "collection": "project", + "conditions": null, + "display": null, + "display_options": null, + "field": "conversation_title_prompt", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 34, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "conversation_title_prompt", + "table": "project", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project/conversations.json b/echo/directus/sync/snapshot/fields/project/conversations.json index c730aa94..ac8e81fa 100644 --- a/echo/directus/sync/snapshot/fields/project/conversations.json +++ b/echo/directus/sync/snapshot/fields/project/conversations.json @@ -26,7 +26,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 16, + "sort": 17, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/project/custom_verification_topics.json b/echo/directus/sync/snapshot/fields/project/custom_verification_topics.json index 060a91dd..e5a24233 100644 --- a/echo/directus/sync/snapshot/fields/project/custom_verification_topics.json +++ b/echo/directus/sync/snapshot/fields/project/custom_verification_topics.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 30, + "sort": 31, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/project/default_conversation_ask_for_participant_email.json b/echo/directus/sync/snapshot/fields/project/default_conversation_ask_for_participant_email.json new file mode 100644 index 00000000..1ac5aa65 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project/default_conversation_ask_for_participant_email.json @@ -0,0 +1,46 @@ +{ + "collection": "project", + "field": "default_conversation_ask_for_participant_email", + "type": "boolean", + "meta": { + "collection": "project", + "conditions": null, + "display": "boolean", + "display_options": null, + "field": "default_conversation_ask_for_participant_email", + "group": null, + "hidden": false, + "interface": "boolean", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 10, + "special": [ + "cast-boolean" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "default_conversation_ask_for_participant_email", + "table": "project", + "data_type": "boolean", + "default_value": false, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project/default_conversation_description.json b/echo/directus/sync/snapshot/fields/project/default_conversation_description.json index aef337d2..3762f13a 100644 --- a/echo/directus/sync/snapshot/fields/project/default_conversation_description.json +++ b/echo/directus/sync/snapshot/fields/project/default_conversation_description.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 12, + "sort": 13, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/project/default_conversation_finish_text.json b/echo/directus/sync/snapshot/fields/project/default_conversation_finish_text.json index bf8036e9..8742fffb 100644 --- a/echo/directus/sync/snapshot/fields/project/default_conversation_finish_text.json +++ b/echo/directus/sync/snapshot/fields/project/default_conversation_finish_text.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 14, + "sort": 15, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/project/default_conversation_title.json b/echo/directus/sync/snapshot/fields/project/default_conversation_title.json index 1ab55d9b..01dca870 100644 --- a/echo/directus/sync/snapshot/fields/project/default_conversation_title.json +++ b/echo/directus/sync/snapshot/fields/project/default_conversation_title.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 11, + "sort": 12, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/project/default_conversation_transcript_prompt.json b/echo/directus/sync/snapshot/fields/project/default_conversation_transcript_prompt.json index 04c4e66b..942fdba7 100644 --- a/echo/directus/sync/snapshot/fields/project/default_conversation_transcript_prompt.json +++ b/echo/directus/sync/snapshot/fields/project/default_conversation_transcript_prompt.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 13, + "sort": 14, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/project/default_conversation_tutorial_slug.json b/echo/directus/sync/snapshot/fields/project/default_conversation_tutorial_slug.json index d7303f59..521971a8 100644 --- a/echo/directus/sync/snapshot/fields/project/default_conversation_tutorial_slug.json +++ b/echo/directus/sync/snapshot/fields/project/default_conversation_tutorial_slug.json @@ -31,7 +31,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 10, + "sort": 11, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/project/directus_user_id.json b/echo/directus/sync/snapshot/fields/project/directus_user_id.json index 091dd5cb..785d847f 100644 --- a/echo/directus/sync/snapshot/fields/project/directus_user_id.json +++ b/echo/directus/sync/snapshot/fields/project/directus_user_id.json @@ -18,7 +18,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 18, + "sort": 19, "special": [ "m2o" ], diff --git a/echo/directus/sync/snapshot/fields/project/divider-n6xep9.json b/echo/directus/sync/snapshot/fields/project/divider-n6xep9.json index 6c00c9d6..5378881d 100644 --- a/echo/directus/sync/snapshot/fields/project/divider-n6xep9.json +++ b/echo/directus/sync/snapshot/fields/project/divider-n6xep9.json @@ -19,7 +19,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 21, + "sort": 22, "special": [ "alias", "no-data" diff --git a/echo/directus/sync/snapshot/fields/project/enable_ai_title_and_tags.json b/echo/directus/sync/snapshot/fields/project/enable_ai_title_and_tags.json new file mode 100644 index 00000000..19cb7cc0 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project/enable_ai_title_and_tags.json @@ -0,0 +1,46 @@ +{ + "collection": "project", + "field": "enable_ai_title_and_tags", + "type": "boolean", + "meta": { + "collection": "project", + "conditions": null, + "display": null, + "display_options": null, + "field": "enable_ai_title_and_tags", + "group": null, + "hidden": false, + "interface": "boolean", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 33, + "special": [ + "cast-boolean" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "enable_ai_title_and_tags", + "table": "project", + "data_type": "boolean", + "default_value": false, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project/get_reply_mode.json b/echo/directus/sync/snapshot/fields/project/get_reply_mode.json index 4ab6f721..79ad0ce2 100644 --- a/echo/directus/sync/snapshot/fields/project/get_reply_mode.json +++ b/echo/directus/sync/snapshot/fields/project/get_reply_mode.json @@ -58,7 +58,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 25, + "sort": 26, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/project/get_reply_prompt.json b/echo/directus/sync/snapshot/fields/project/get_reply_prompt.json index 499fad69..09f02b0e 100644 --- a/echo/directus/sync/snapshot/fields/project/get_reply_prompt.json +++ b/echo/directus/sync/snapshot/fields/project/get_reply_prompt.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 24, + "sort": 25, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/project/is_conversation_allowed.json b/echo/directus/sync/snapshot/fields/project/is_conversation_allowed.json index a8326ec7..cb028847 100644 --- a/echo/directus/sync/snapshot/fields/project/is_conversation_allowed.json +++ b/echo/directus/sync/snapshot/fields/project/is_conversation_allowed.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 22, + "sort": 23, "special": [ "cast-boolean" ], diff --git a/echo/directus/sync/snapshot/fields/project/is_enhanced_audio_processing_enabled.json b/echo/directus/sync/snapshot/fields/project/is_enhanced_audio_processing_enabled.json index 2c5097a9..22ee522e 100644 --- a/echo/directus/sync/snapshot/fields/project/is_enhanced_audio_processing_enabled.json +++ b/echo/directus/sync/snapshot/fields/project/is_enhanced_audio_processing_enabled.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 26, + "sort": 27, "special": [ "cast-boolean" ], diff --git a/echo/directus/sync/snapshot/fields/project/is_get_reply_enabled.json b/echo/directus/sync/snapshot/fields/project/is_get_reply_enabled.json index 2d2e0ef8..fedbf32c 100644 --- a/echo/directus/sync/snapshot/fields/project/is_get_reply_enabled.json +++ b/echo/directus/sync/snapshot/fields/project/is_get_reply_enabled.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 23, + "sort": 24, "special": [ "cast-boolean" ], diff --git a/echo/directus/sync/snapshot/fields/project/is_project_notification_subscription_allowed.json b/echo/directus/sync/snapshot/fields/project/is_project_notification_subscription_allowed.json index 543b35dc..3f2caba4 100644 --- a/echo/directus/sync/snapshot/fields/project/is_project_notification_subscription_allowed.json +++ b/echo/directus/sync/snapshot/fields/project/is_project_notification_subscription_allowed.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 27, + "sort": 28, "special": [ "cast-boolean" ], diff --git a/echo/directus/sync/snapshot/fields/project/is_verify_enabled.json b/echo/directus/sync/snapshot/fields/project/is_verify_enabled.json index 01276f01..e4454087 100644 --- a/echo/directus/sync/snapshot/fields/project/is_verify_enabled.json +++ b/echo/directus/sync/snapshot/fields/project/is_verify_enabled.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 29, + "sort": 30, "special": [ "cast-boolean" ], diff --git a/echo/directus/sync/snapshot/fields/project/processing_status.json b/echo/directus/sync/snapshot/fields/project/processing_status.json index c745544a..c4b81d17 100644 --- a/echo/directus/sync/snapshot/fields/project/processing_status.json +++ b/echo/directus/sync/snapshot/fields/project/processing_status.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 28, + "sort": 29, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/project/project_analysis_runs.json b/echo/directus/sync/snapshot/fields/project/project_analysis_runs.json index 3e8b6be7..bba605b0 100644 --- a/echo/directus/sync/snapshot/fields/project/project_analysis_runs.json +++ b/echo/directus/sync/snapshot/fields/project/project_analysis_runs.json @@ -24,7 +24,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 17, + "sort": 18, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/project/project_chats.json b/echo/directus/sync/snapshot/fields/project/project_chats.json index 56d33b81..5f398a49 100644 --- a/echo/directus/sync/snapshot/fields/project/project_chats.json +++ b/echo/directus/sync/snapshot/fields/project/project_chats.json @@ -16,7 +16,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 19, + "sort": 20, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/project/project_reports.json b/echo/directus/sync/snapshot/fields/project/project_reports.json index e6876257..d629d15c 100644 --- a/echo/directus/sync/snapshot/fields/project/project_reports.json +++ b/echo/directus/sync/snapshot/fields/project/project_reports.json @@ -21,7 +21,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 20, + "sort": 21, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/fields/project/selected_verification_key_list.json b/echo/directus/sync/snapshot/fields/project/selected_verification_key_list.json index ef9722d6..37b12b4c 100644 --- a/echo/directus/sync/snapshot/fields/project/selected_verification_key_list.json +++ b/echo/directus/sync/snapshot/fields/project/selected_verification_key_list.json @@ -19,7 +19,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 31, + "sort": 32, "special": null, "translations": null, "validation": null, diff --git a/echo/directus/sync/snapshot/fields/project/tags.json b/echo/directus/sync/snapshot/fields/project/tags.json index e7163eeb..fbfeb4ae 100644 --- a/echo/directus/sync/snapshot/fields/project/tags.json +++ b/echo/directus/sync/snapshot/fields/project/tags.json @@ -20,7 +20,7 @@ "readonly": false, "required": false, "searchable": true, - "sort": 15, + "sort": 16, "special": [ "o2m" ], diff --git a/echo/directus/sync/snapshot/relations/directus_users/whitelabel_logo.json b/echo/directus/sync/snapshot/relations/directus_users/whitelabel_logo.json new file mode 100644 index 00000000..b22571ed --- /dev/null +++ b/echo/directus/sync/snapshot/relations/directus_users/whitelabel_logo.json @@ -0,0 +1,25 @@ +{ + "collection": "directus_users", + "field": "whitelabel_logo", + "related_collection": "directus_files", + "meta": { + "junction_field": null, + "many_collection": "directus_users", + "many_field": "whitelabel_logo", + "one_allowed_collections": null, + "one_collection": "directus_files", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": null, + "sort_field": null + }, + "schema": { + "table": "directus_users", + "column": "whitelabel_logo", + "foreign_key_table": "directus_files", + "foreign_key_column": "id", + "constraint_name": "directus_users_whitelabel_logo_foreign", + "on_update": "NO ACTION", + "on_delete": "SET NULL" + } +} diff --git a/echo/frontend/AGENTS.md b/echo/frontend/AGENTS.md index dfcda28a..f22b2bfe 100644 --- a/echo/frontend/AGENTS.md +++ b/echo/frontend/AGENTS.md @@ -74,6 +74,20 @@ - If there is a type error with ".count" with Directus, add it to the typesDirectus.ts. You can add to the fields `count("")` to obtain `_count` in the response - When a user request feels ambiguous, pause and confirm the intended action with them before touching code or docs; err on the side of over-communicating. +## Brand Guidelines + +All UI copy, colors, and visual decisions should follow `../brand/STYLE_GUIDE.md`: + +- Shortest possible, highest clarity +- "dembrane" always lowercase +- Say "language model" not "AI" for platform features +- Never use bold for emphasis (use Royal Blue `#4169e1` or italics) +- Colors: Parchment `#f6f4f1` (background), Graphite `#2d2d2c` (text), Royal Blue `#4169e1` (action) +- Typography: DM Sans with stylistic alternates (ss01-ss06) +- Dutch localization: use "je/jij" not "u/uw" + +Machine-readable tokens in `../brand/colors.json`. Logo files in `../brand/logos/`. + ## Theming & Styling Patterns ### CSS Variables for Dynamic Theming diff --git a/echo/frontend/COPY_GUIDE.md b/echo/frontend/COPY_GUIDE.md deleted file mode 100644 index b457e6d6..00000000 --- a/echo/frontend/COPY_GUIDE.md +++ /dev/null @@ -1,257 +0,0 @@ -# dembrane UI Style Guide - -This is the north star for how dembrane looks, sounds, and feels. - -It's not a rulebook. Use your gut. If something serves the brand better by bending a guideline, bend it. - -When in doubt, ask: Does this feel approachable, grounded, and human? Does it invite people in? - ---- - -## Colors - -Use brand colors in these approximate proportions: - -| Color | Name | Usage | Hex | -|-------|------|-------|-----| -| Off-white | Parchment | Default background | `#F5F5F0` | -| Dark grey | Graphite | Mood/text | `#2D2D2D` | -| Blue | Institution Blue | Action/primary | `#4169E1` | -| Light grey | Grey | Disabled states | `#B8B8B8` | - -**Accent colors (for categories/tags):** - -| Color | Name | Usage | -|-------|------|-------| -| Cyan | Cyan | VALUE | -| Bright green | Spring Green | DESIGN | -| Pink | Mauve | ENGINE | -| Yellow | Lime Yellow | ADMIN | - -**System states only (never for branding):** - -| Color | Name | Usage | -|-------|------|-------| -| Orange | Peach | Warning | -| Pink-red | Salmon | Error | - ---- - -## Buttons - -### Primary -- Default: Institution Blue background, white text, rounded corners -- Hover: Graphite background -- Click: Graphite background - -### Secondary -- Default: Transparent with blue border, blue text -- Hover: 10% blue opacity fill -- Click: 20% blue opacity fill - -### Tertiary -- Default: Text only, blue, no border -- Hover: 10% blue opacity background -- Click: 20% blue opacity background - -### Disabled -- Grey background, darker grey text - ---- - -## Typography - -### Rules -1. Always keep text left aligned (unless very short and wrapped in a border) -2. Avoid orphans — single words alone on a line. Adjust text box width. -3. Prefer hanging punctuation — quotes and bullets "hang" outside the text box -4. Never use bold. Use *blue* or *italics* for emphasis. - -### Hierarchy -- Keep it simple: one or two font weights max -- Let whitespace do the work, not bold text - ---- - -## Writing: Product UI - -**Golden rule:** Shortest possible, highest clarity. - -### Tone -- Accessible, friendly, specific -- Write like you're explaining to a colleague, not presenting to a board -- No jargon, no corporate speak -- If you wouldn't say it out loud, rewrite it - -### Vocabulary - -| Don't say | Say instead | -|-----------|-------------| -| Collective sense-making | Make sense of big messy conversations | -| Users | Participants and hosts | -| Customers | Partners and clients | -| The tool | The platform / dembrane | -| Facilitate deliberation | Help groups have better conversations | -| AI | Language model (when describing our features) | - -### Words we keep but explain plainly -- "Stakeholders" → fine, but prefer "everyone affected" or "the people involved" -- "AI" → fine in general context, but be specific: "the language model" or "the transcription model" - -### Never say in UI -- "We are pleased to inform you..." -- "Please be advised..." -- "In order to..." -- "Successfully" (just state what happened) -- "We apologize for any inconvenience this may have caused" -- "Click here to..." - ---- - -## UI Copy Patterns - -### Buttons -✅ Upload -✅ Save -✅ Delete -❌ Click here to upload -❌ Save your changes - -### Labels -✅ Project name -✅ Start date -❌ Please enter project name -❌ Name of the project - -### Errors -✅ "File too large. Max 100MB." -✅ "Something went wrong. Try again." -❌ "An error occurred. The file exceeds the maximum allowed size..." - -### Success states -✅ "Saved" -✅ "File uploaded" -❌ "Successfully saved" -❌ "Your file has been successfully uploaded" - -### Empty states -✅ "No conversations yet. Start your first one." -❌ "You have not created any conversations" - -### Loading/Processing -✅ "Analyzing..." -✅ "Processing audio..." -❌ "Please wait while we process your request" - ---- - -## Dutch Localization - -### Core rules -- Use "je/jij/jou" — never "u/uw" (always informal) -- Natural phrasing, not word-for-word translation -- Compound words: audiobestand (not "audio bestand") -- Keep English terms when they sound better: Dashboard, Upload, Chat - -### Key glossary - -| English | Dutch | -|---------|-------| -| Dashboard | Dashboard | -| Upload | Uploaden | -| Chat | Chat | -| Conversation | Gesprek | -| Audio file | Audiobestand | -| Participant | Deelnemer | -| Settings | Instellingen | -| Save | Opslaan | -| Delete | Verwijderen | -| It's not working | Doet het niet | -| We're fixing it | We zijn het aan het fixen | - -### Examples - -**Bad:** "Gelieve uw bestand te uploaden" -**Good:** "Upload je bestand" - -**Bad:** "We zijn verheugd u te informeren dat de functionaliteit hersteld is" -**Good:** "Chat doet het weer" - ---- - -## Component Guidelines - -### Modals -- Clear, direct title (action-oriented when appropriate) -- Body text: one or two sentences max -- Warning callouts: use Peach background, keep text brief -- Actions: Primary on right, Cancel on left - -### Forms -- Labels above inputs, left-aligned -- Placeholder text is not a substitute for labels -- Validation errors inline, close to the field -- Don't disable submit buttons while form is incomplete — show errors on attempt - -### Cards -- Parchment background by default -- Minimal borders (1px if needed) -- Generous padding -- One clear action per card - -### Tables -- Left-align text, right-align numbers -- Zebra striping optional (only if many rows) -- Row hover state for clickable rows - -### Navigation -- Keep it flat when possible -- Clear current-state indication -- Breadcrumbs for deep nesting only - ---- - -## Spacing & Layout - -- Use consistent spacing scale (8px base) -- Generous whitespace — let content breathe -- Group related items, separate unrelated ones -- Mobile-first responsive design - ---- - -## Icons - -Use [Phosphor Icons](https://phosphoricons.com/) for all iconography. - -- Regular weight for most UI -- Keep icons simple and recognizable -- Don't rely on icons alone — pair with text labels where clarity matters - ---- - -## Platform Context - -ECHO is event-driven, not daily-use software. Users run discrete engagement sessions: workshops, consultations, civic forums, employee feedback rounds. - -**Typical flow:** Prepare event → Run session → Analyze conversations → Generate report → Return for next event - -This means: -- Don't add friction to infrequent tasks -- Remind users where they left off -- Make re-onboarding seamless -- Celebrate completed analyses, not login streaks - ---- - -## Quick Checklist - -Before shipping any UI: - -- [ ] Can I say this in fewer words? -- [ ] Would I say this to a colleague? -- [ ] Is it clear what to do next? -- [ ] Does it feel approachable and human? -- [ ] Have I avoided bold text? -- [ ] Are error states helpful, not scary? -- [ ] Does it work in Dutch? \ No newline at end of file diff --git a/echo/frontend/docs/button_design_system.md b/echo/frontend/docs/button_design_system.md new file mode 100644 index 00000000..18d3559b --- /dev/null +++ b/echo/frontend/docs/button_design_system.md @@ -0,0 +1,197 @@ +# Button Design System + +This document describes the button design system implemented in the ECHO frontend. + +## Quick Reference + +| Button Type | Mantine Variant | Example Usage | +|-------------|-----------------|---------------| +| **Primary** | `filled` (default) | `` | +| **Secondary** | `outline` | `` | +| **Tertiary** | `subtle` | `` | +| **Disabled** | Any + `disabled` | `` | + +## Design Spec + +### Primary Button (filled) +- **Default**: Institution Blue (`#4169E1`) background, white text, **pill-shaped (fully rounded)** +- **Hover**: Graphite (`#2D2D2C`) background +- **Click/Active**: Graphite (`#2D2D2C`) background +- **Loading**: Graphite (`#2D2D2C`) background with white spinner + +### Secondary Button (outline) +- **Default**: Institution Blue border, Institution Blue text, transparent background, **standard corners** +- **Hover**: 10% Institution Blue background +- **Click/Active**: 20% Institution Blue background + +### Tertiary Button (subtle) +- **Default**: No border, Institution Blue text, transparent background, **standard corners** +- **Hover**: 10% Institution Blue background +- **Click/Active**: 20% Institution Blue background + +### Disabled Button +- **Default**: Gray background, Graphite text +- **Hover**: Gray background + 1px Peach (`#FFD166`) border +- **Click/Active**: Gray background + 1px Salmon (`#FF9AA2`) border +- **Note**: Loading buttons are technically disabled but use their own styling (see Primary/Loading above) + +## Usage Examples + +```tsx +// Primary button (default) + + +// Secondary button + + +// Tertiary button + + +// Disabled button (shows interactive borders) + + +// Loading button (graphite background with spinner) + + +// Custom color (overrides primary) + +``` + +## Brand Colors + +All brand colors are defined in [`src/colors.ts`](../src/colors.ts) as the single source of truth. + +### Available Colors + +| Name | Base Color | Mantine Usage | Tailwind Usage | Purpose | +|------|------------|---------------|----------------|---------| +| **Primary** | `#4169E1` | `color="primary.6"` | `bg-primary-500` | Buttons, links, accents | +| **Cyan** | `#00FFFF` | `color="cyan.6"` | `bg-cyan-500` | Deep Dive mode accent | +| **Graphite** | `#2D2D2C` | `color="graphite.6"` | `bg-graphite` | Text (DM Sans theme) | +| **Lime Yellow** | `#F4FF81` | `color="limeYellow.6"` | `bg-limeYellow-500` | Highlights | +| **Mauve** | `#FFC2FF` | `color="mauve.6"` | `bg-mauve-500` | Accent color | +| **Parchment** | `#F6F4F1` | `color="parchment.6"` | `bg-parchment` | Background (DM Sans theme) | +| **Peach** | `#FFD166` | `color="peach.6"` | `bg-peach-500` | Warnings, alerts | +| **Salmon** | `#FF9AA2` | `color="salmon.6"` | `bg-salmon-500` | Error states | +| **Spring Green** | `#1EFFA1` | `color="springGreen.6"` | `bg-springGreen-500` | Success, Overview mode | + +### Using Brand Colors + +**In Mantine Components:** +```tsx + +Error +Success! +``` + +**In Tailwind Classes:** +```tsx +
+ Content +
+``` + +**In Inline Styles:** +```tsx +import { baseColors } from "@/colors"; + +
+ Content +
+``` + +## Implementation Details + +### File Structure + +- **[`src/colors.ts`](../src/colors.ts)**: Single source of truth for all brand colors +- **[`src/theme.tsx`](../src/theme.tsx)**: Mantine theme configuration with button defaults +- **[`src/styles/button.module.css`](../src/styles/button.module.css)**: Custom button variant styles +- **[`tailwind.config.js`](../../tailwind.config.js)**: Tailwind configuration with brand colors + +### How It Works + +1. **Colors are defined once** in `colors.ts` with 10 shades per color +2. **Mantine imports** `mantineColors` (array format, 0-9 indices) +3. **Tailwind imports** `tailwindColors` (object format, 50-900 keys) +4. **Button styles** are applied via CSS modules attached to the Button component in the theme + +### Default Button Behavior + +All buttons automatically get: +- `color="primary"` (Institution Blue) +- `variant="filled"` (Primary style) + +**Only primary (filled) buttons are pill-shaped.** Secondary and tertiary buttons use standard rounded corners. + +Override these by passing props: +```tsx + + + +``` + +## Exceptions + +The following buttons are exempt from the design system and use custom CSS: +- **Refine button** (custom animated loading state) +- Other buttons with explicit custom `className` or `classNames` props + +## Migration Guide + +### Updating from Old Button Styles + +**Before:** +```tsx + +``` + +**After:** +```tsx + // Uses primary (Institution Blue) by default +``` + +### Reviewing `variant="default"` Usage + +The `default` variant is not part of the design system. Consider replacing with: +- `variant="outline"` for secondary actions +- `variant="subtle"` for low-emphasis actions + +### Adding New Colors + +To add a new brand color: + +1. Add the 10-shade array to `brandColors` in `src/colors.ts` +2. Add to `toTailwindPalette()` conversion in `tailwindColors` +3. Add base color to `baseColors` export +4. Colors will automatically be available in both Mantine and Tailwind + +## Testing + +Test button states in development: +- Hover over buttons to see hover states +- Click buttons to see active states +- Try disabled buttons to see interactive border feedback (Peach on hover, Salmon on click) + +## Questions? + +For questions about the design system, refer to: +- [Frontend Style Guides](../../docs/style-guides/) +- [AGENTS.md](../AGENTS.md) for general frontend patterns +- [COPY_GUIDE.md](../COPY_GUIDE.md) for button text guidelines diff --git a/echo/frontend/package.json b/echo/frontend/package.json index 3b150d25..d85eaae5 100644 --- a/echo/frontend/package.json +++ b/echo/frontend/package.json @@ -23,7 +23,6 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@fontsource-variable/dm-sans": "^5.2.8", "@fontsource-variable/space-grotesk": "^5.2.8", "@formkit/auto-animate": "^0.8.2", "@hookform/resolvers": "^3.10.0", @@ -39,6 +38,7 @@ "@mantine/modals": "^7.17.8", "@mantine/notifications": "^7.17.8", "@mdxeditor/editor": "^3.40.0", + "@phosphor-icons/react": "^2.1.10", "@react-pdf/renderer": "^4.3.0", "@sentry/react": "^8.55.0", "@tabler/icons-react": "^3.34.1", diff --git a/echo/frontend/pnpm-lock.yaml b/echo/frontend/pnpm-lock.yaml index 2118ee4e..479b6cc2 100644 --- a/echo/frontend/pnpm-lock.yaml +++ b/echo/frontend/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.0.0) - '@fontsource-variable/dm-sans': - specifier: ^5.2.8 - version: 5.2.8 '@fontsource-variable/space-grotesk': specifier: ^5.2.8 version: 5.2.8 @@ -74,6 +71,9 @@ importers: '@mdxeditor/editor': specifier: ^3.40.0 version: 3.40.0(@codemirror/language@6.11.2)(@lezer/highlight@1.2.1)(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(yjs@13.6.22) + '@phosphor-icons/react': + specifier: ^2.1.10 + version: 2.1.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@react-pdf/renderer': specifier: ^4.3.0 version: 4.3.0(react@19.0.0) @@ -1169,9 +1169,6 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} - '@fontsource-variable/dm-sans@5.2.8': - resolution: {integrity: sha512-AxkvMTvNWgfrmlyjiV05vlHYJa+nRQCf1EfvIrQAPBpFJW0O9VTz7oAFr9S3lvbWdmnFoBk7yFqQL86u64nl2g==} - '@fontsource-variable/space-grotesk@5.2.8': resolution: {integrity: sha512-ei9jNXzZVgBGEBVfHZqPe6F9ZxpPUG8kJYrtlLsivlWJZLCfrfSxcayjnMYAmslEGvvfjth7qybl7PNNqE8ZHw==} @@ -1577,6 +1574,13 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@phosphor-icons/react@2.1.10': + resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -5610,8 +5614,6 @@ snapshots: '@floating-ui/utils@0.2.9': {} - '@fontsource-variable/dm-sans@5.2.8': {} - '@fontsource-variable/space-grotesk@5.2.8': {} '@formkit/auto-animate@0.8.2': {} @@ -6273,6 +6275,11 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@phosphor-icons/react@2.1.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@pkgjs/parseargs@0.11.0': optional: true diff --git a/echo/frontend/public/brand-logo-dark.png b/echo/frontend/public/brand-logo-dark.png new file mode 100644 index 00000000..ab6b31a5 Binary files /dev/null and b/echo/frontend/public/brand-logo-dark.png differ diff --git a/echo/frontend/public/brand-logomark-dark.png b/echo/frontend/public/brand-logomark-dark.png new file mode 100644 index 00000000..d4620701 Binary files /dev/null and b/echo/frontend/public/brand-logomark-dark.png differ diff --git a/echo/frontend/public/dembrane-logomark-cropped.png b/echo/frontend/public/dembrane-logomark-cropped.png new file mode 100644 index 00000000..1292781e Binary files /dev/null and b/echo/frontend/public/dembrane-logomark-cropped.png differ diff --git a/echo/frontend/public/dembrane-logomark.png b/echo/frontend/public/dembrane-logomark.png new file mode 100644 index 00000000..c7cab266 Binary files /dev/null and b/echo/frontend/public/dembrane-logomark.png differ diff --git a/echo/frontend/public/dembrane-wordmark.png b/echo/frontend/public/dembrane-wordmark.png new file mode 100644 index 00000000..a6cd62b4 Binary files /dev/null and b/echo/frontend/public/dembrane-wordmark.png differ diff --git a/echo/frontend/src/App.tsx b/echo/frontend/src/App.tsx index e6f30bb8..3ca64694 100644 --- a/echo/frontend/src/App.tsx +++ b/echo/frontend/src/App.tsx @@ -1,4 +1,3 @@ -import "@fontsource-variable/dm-sans"; import "@fontsource-variable/space-grotesk"; import "@mantine/core/styles.css"; import "@mantine/dropzone/styles.css"; @@ -11,6 +10,7 @@ import { RouterProvider } from "react-router/dom"; import { I18nProvider } from "./components/layout/I18nProvider"; import { USE_PARTICIPANT_ROUTER } from "./config"; import { AppPreferencesProvider } from "./hooks/useAppPreferences"; +import { WhitelabelLogoProvider } from "./hooks/useWhitelabelLogo"; import { analytics } from "./lib/analytics"; import { mainRouter, participantRouter } from "./Router"; import { theme } from "./theme"; @@ -79,12 +79,14 @@ export const App = () => { return ( - + {/* */} - - - + + + + + diff --git a/echo/frontend/src/Router.tsx b/echo/frontend/src/Router.tsx index 41c91efb..10c9b9db 100644 --- a/echo/frontend/src/Router.tsx +++ b/echo/frontend/src/Router.tsx @@ -103,6 +103,10 @@ const UserSettingsRoute = createLazyNamedRoute( () => import("./routes/settings/UserSettingsRoute"), "UserSettingsRoute", ); +const HostGuidePage = createLazyNamedRoute( + () => import("./routes/project/HostGuidePage"), + "HostGuidePage", +); export const mainRouter = createBrowserRouter([ { @@ -159,6 +163,15 @@ export const mainRouter = createBrowserRouter([ ), path: "verify-email", }, + { + // Host Guide - standalone page, protected but no header/layout + element: ( + + + + ), + path: "projects/:projectId/host-guide", + }, { children: [ { diff --git a/echo/frontend/src/assets/dembrane-logo-new.svg b/echo/frontend/src/assets/dembrane-logo-new.svg new file mode 100644 index 00000000..4881f9b2 --- /dev/null +++ b/echo/frontend/src/assets/dembrane-logo-new.svg @@ -0,0 +1,99 @@ + + + + + + + + + diff --git a/echo/frontend/src/assets/logomark-no-bg.svg b/echo/frontend/src/assets/logomark-no-bg.svg new file mode 100644 index 00000000..d6a7fa6d --- /dev/null +++ b/echo/frontend/src/assets/logomark-no-bg.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/echo/frontend/src/assets/wordmark-no-padding.svg b/echo/frontend/src/assets/wordmark-no-padding.svg new file mode 100644 index 00000000..a18da5cc --- /dev/null +++ b/echo/frontend/src/assets/wordmark-no-padding.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/echo/frontend/src/colors.ts b/echo/frontend/src/colors.ts new file mode 100644 index 00000000..31b40dcb --- /dev/null +++ b/echo/frontend/src/colors.ts @@ -0,0 +1,208 @@ +/** + * Brand Color Palettes + * Single source of truth for colors used in both Mantine and Tailwind + * + * Mantine uses 10-shade arrays (index 0-9, base at index 6) + * Tailwind uses object with keys 50-900 (base at 500) + */ + +// Mantine-style color arrays (10 shades, base at index 6) +export const brandColors = { + // Cyan (base: #00FFFF) + cyan: [ + "#f0ffff", + "#e5ffff", + "#ccffff", + "#99ffff", + "#66ffff", + "#33ffff", + "#00FFFF", // base at position 6 + "#00e6e6", + "#00cccc", + "#00b3b3", + ], + // Graphite (solid - same across all shades) + graphite: [ + "#2D2D2C", + "#2D2D2C", + "#2D2D2C", + "#2D2D2C", + "#2D2D2C", + "#2D2D2C", + "#2D2D2C", + "#2D2D2C", + "#2D2D2C", + "#2D2D2C", + ], + // Institution Blue (alias for primary) + institutionBlue: [ + "#f0f5ff", + "#e9f1ff", + "#d4dffe", + "#a8bbf4", + "#7996eb", + "#5176e4", + "#4169e1", // base at position 6 + "#2957df", + "#1a48c6", + "#1040b2", + ], + // Lime Yellow (base: #F4FF81) + limeYellow: [ + "#fefff5", + "#fdfff0", + "#fbffe1", + "#f8ffc3", + "#f6ffa5", + "#f5ff93", + "#F4FF81", // base at position 6 + "#dce674", + "#c4cc67", + "#acb35a", + ], + // Mauve (base: #FFC2FF) + mauve: [ + "#fffaff", + "#fff5ff", + "#ffe8ff", + "#ffd6ff", + "#ffc8ff", + "#ffc5ff", + "#FFC2FF", // base at position 6 + "#e6aee6", + "#cc9acc", + "#b386b3", + ], + // Parchment (solid - same across all shades) + parchment: [ + "#F6F4F1", + "#F6F4F1", + "#F6F4F1", + "#F6F4F1", + "#F6F4F1", + "#F6F4F1", + "#F6F4F1", + "#F6F4F1", + "#F6F4F1", + "#F6F4F1", + ], + // Peach (base: #FFD166) + peach: [ + "#fffcf5", + "#fff8ec", + "#fff1d9", + "#ffe3b3", + "#ffd68c", + "#ffc866", + "#FFD166", // base at position 6 + "#e6bc5c", + "#cca752", + "#b39248", + ], + // Primary / Institution Blue (base: #4169E1) + primary: [ + "#f0f5ff", + "#e9f1ff", + "#d4dffe", + "#a8bbf4", + "#7996eb", + "#5176e4", + "#4169e1", // base at position 6 + "#2957df", + "#1a48c6", + "#1040b2", + ], + // Salmon (base: #FF9AA2) + salmon: [ + "#fffafc", + "#fff5f6", + "#ffebec", + "#ffd7da", + "#ffc3c7", + "#ffafb5", + "#FF9AA2", // base at position 6 + "#e68b92", + "#cc7c82", + "#b36d72", + ], + // Spring Green (base: #1EFFA1) + springGreen: [ + "#f0fffb", + "#e8fff5", + "#d1ffeb", + "#a3ffd7", + "#75ffc3", + "#47ffaf", + "#1EFFA1", // base at position 6 + "#1be691", + "#18cc81", + "#15b371", + ], +} as const; + +// Type for Mantine color tuple (10 shades) +export type MantineColorTuple = readonly [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, +]; + +// Mantine-compatible colors export +export const mantineColors: Record = + brandColors as Record; + +/** + * Helper to convert Mantine array (10 shades) to Tailwind object (50-900 keys) + */ +function toTailwindPalette( + colors: readonly string[], +): Record { + const tailwindKeys = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]; + const palette: Record = {}; + + colors.forEach((color, index) => { + if (index < tailwindKeys.length) { + palette[tailwindKeys[index]] = color; + } + }); + + // Add DEFAULT as the base color (index 6 = key 500 in Tailwind) + palette.DEFAULT = colors[6]; + + return palette; +} + +// Tailwind-compatible colors export +export const tailwindColors = { + cyan: toTailwindPalette(brandColors.cyan), + graphite: toTailwindPalette(brandColors.graphite), + institutionBlue: toTailwindPalette(brandColors.institutionBlue), + limeYellow: toTailwindPalette(brandColors.limeYellow), + mauve: toTailwindPalette(brandColors.mauve), + parchment: toTailwindPalette(brandColors.parchment), + peach: toTailwindPalette(brandColors.peach), + primary: toTailwindPalette(brandColors.primary), + salmon: toTailwindPalette(brandColors.salmon), + springGreen: toTailwindPalette(brandColors.springGreen), +}; + +// Base color values for quick access (e.g., in CSS-in-JS or inline styles) +export const baseColors = { + cyan: "#00FFFF", + graphite: "#2D2D2C", + institutionBlue: "#4169E1", + limeYellow: "#F4FF81", + mauve: "#FFC2FF", + parchment: "#F6F4F1", + peach: "#FFD166", + primary: "#4169E1", + salmon: "#FF9AA2", + springGreen: "#1EFFA1", +} as const; diff --git a/echo/frontend/src/components/announcement/AnnouncementDrawerHeader.tsx b/echo/frontend/src/components/announcement/AnnouncementDrawerHeader.tsx index 570edf19..59863527 100644 --- a/echo/frontend/src/components/announcement/AnnouncementDrawerHeader.tsx +++ b/echo/frontend/src/components/announcement/AnnouncementDrawerHeader.tsx @@ -21,7 +21,7 @@ export const AnnouncementDrawerHeader = ({ - Announcements + Announcements { disabled={(unreadCount || 0) === 0} withBorder > - + {isLoading ? ( ) : ( - )} diff --git a/echo/frontend/src/components/announcement/TopAnnouncementBar.tsx b/echo/frontend/src/components/announcement/TopAnnouncementBar.tsx index 8496bb69..d7ab3b27 100644 --- a/echo/frontend/src/components/announcement/TopAnnouncementBar.tsx +++ b/echo/frontend/src/components/announcement/TopAnnouncementBar.tsx @@ -85,7 +85,7 @@ export function TopAnnouncementBar() { return ( diff --git a/echo/frontend/src/components/auth/hooks/index.ts b/echo/frontend/src/components/auth/hooks/index.ts index 1f802ba5..38471735 100644 --- a/echo/frontend/src/components/auth/hooks/index.ts +++ b/echo/frontend/src/components/auth/hooks/index.ts @@ -31,6 +31,7 @@ export const useCurrentUser = ({ "email", "disable_create_project", "tfa_secret", + "whitelabel_logo", ], }), ); diff --git a/echo/frontend/src/components/chat/ChatAccordion.tsx b/echo/frontend/src/components/chat/ChatAccordion.tsx index 26ac8563..0953c47b 100644 --- a/echo/frontend/src/components/chat/ChatAccordion.tsx +++ b/echo/frontend/src/components/chat/ChatAccordion.tsx @@ -48,8 +48,6 @@ export const ChatModeIndicator = ({ const isOverview = effectiveMode === "overview"; const colors = MODE_COLORS[effectiveMode]; - const iconSize = size === "xs" ? 14 : 16; - return ( {isOverview ? ( - + + + ) : ( - + > + + )} @@ -254,17 +268,12 @@ export const ChatAccordionMain = ({ projectId }: { projectId: string }) => { const chatMode = (item as ProjectChat & { chat_mode?: string }) .chat_mode as "overview" | "deep_dive" | null | undefined; const isActive = item.id === activeChatId; - const effectiveMode = chatMode ?? "deep_dive"; - const activeBorderColor = isActive - ? MODE_COLORS[effectiveMode].border - : undefined; return ( diff --git a/echo/frontend/src/components/chat/ChatModeBanner.tsx b/echo/frontend/src/components/chat/ChatModeBanner.tsx index bac17ca0..4dbfef97 100644 --- a/echo/frontend/src/components/chat/ChatModeBanner.tsx +++ b/echo/frontend/src/components/chat/ChatModeBanner.tsx @@ -38,17 +38,7 @@ export const ChatModeBanner = ({ )} {isOverview && ( - + Beta )} diff --git a/echo/frontend/src/components/chat/ChatModeSelector.tsx b/echo/frontend/src/components/chat/ChatModeSelector.tsx index 3d33807e..4f13d874 100644 --- a/echo/frontend/src/components/chat/ChatModeSelector.tsx +++ b/echo/frontend/src/components/chat/ChatModeSelector.tsx @@ -122,15 +122,14 @@ const ModeCard = ({ {isThisLoading ? ( - + ) : ( - + )} @@ -139,17 +138,7 @@ const ModeCard = ({ {title} {isBeta && ( - + Beta )} @@ -176,7 +165,7 @@ const ModeCard = ({ diff --git a/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx b/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx index 9a7d0f31..aefbb5d7 100644 --- a/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx +++ b/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx @@ -80,11 +80,7 @@ const SuggestionPill = ({ )} > - + {suggestion.label} diff --git a/echo/frontend/src/components/common/Breadcrumbs.tsx b/echo/frontend/src/components/common/Breadcrumbs.tsx index 22538ab0..beac25e8 100644 --- a/echo/frontend/src/components/common/Breadcrumbs.tsx +++ b/echo/frontend/src/components/common/Breadcrumbs.tsx @@ -30,7 +30,7 @@ export const Breadcrumbs = ({ items }: BreadcrumbsProps) => { } return ( - + {item.label} ); diff --git a/echo/frontend/src/components/common/DembraneLoadingSpinner/index.tsx b/echo/frontend/src/components/common/DembraneLoadingSpinner/index.tsx index dc31c35d..9a187bff 100644 --- a/echo/frontend/src/components/common/DembraneLoadingSpinner/index.tsx +++ b/echo/frontend/src/components/common/DembraneLoadingSpinner/index.tsx @@ -3,7 +3,7 @@ import type React from "react"; import { useEffect, useState } from "react"; import "./DembraneLoading.css"; import { cn } from "@/lib/utils"; -import dembraneLogoHQ from "../../../assets/dembrane-logo-hq.png"; +import { DembraneLogomark } from "../Logo"; interface DembraneLoadingSpinnerProps { isLoading: boolean; @@ -62,7 +62,7 @@ const DembraneLoadingSpinner: React.FC = ({ )} > Spinning Dembrane Logo to indicate loading diff --git a/echo/frontend/src/components/common/Logo.tsx b/echo/frontend/src/components/common/Logo.tsx index 1b343890..efcf4c60 100644 --- a/echo/frontend/src/components/common/Logo.tsx +++ b/echo/frontend/src/components/common/Logo.tsx @@ -1,39 +1,33 @@ -import { Group, type GroupProps, Title } from "@mantine/core"; +import { Group, type GroupProps } from "@mantine/core"; import aiconlLogo from "@/assets/aiconl-logo.png"; import aiconlLogoHQ from "@/assets/aiconl-logo-hq.png"; -import dembranelogo from "@/assets/dembrane-logo-hq.png"; -import { cn } from "@/lib/utils"; + +import dembraneLogoFull from "@/assets/dembrane-logo-new.svg"; +import dembraneLogomark from "@/assets/logomark-no-bg.svg"; +import { useWhitelabelLogo } from "@/hooks/useWhitelabelLogo"; type LogoProps = { hideLogo?: boolean; hideTitle?: boolean; - textAfterLogo?: string | React.ReactNode; + alwaysDembrane?: boolean; } & GroupProps; -export const LogoDembrane = ({ - hideLogo, - hideTitle, - textAfterLogo, - ...props -}: LogoProps) => ( - - {!hideLogo && ( - Dembrane Logo - )} - {!hideTitle && ( - - <span className={cn("font-medium", textAfterLogo && "mr-1")}> - Dembrane - </span> - {textAfterLogo && <span>{textAfterLogo}</span>} - - )} - -); +export const LogoDembrane = ({ hideLogo, hideTitle, alwaysDembrane, ...props }: LogoProps) => { + const { logoUrl } = useWhitelabelLogo(); + const effectiveLogoUrl = alwaysDembrane ? null : logoUrl; + + return ( + + {!hideLogo && ( + Logo + )} + + ); +}; const LogoAiCoNL = ({ hideLogo, hideTitle, ...props }: LogoProps) => ( @@ -44,11 +38,6 @@ const LogoAiCoNL = ({ hideLogo, hideTitle, ...props }: LogoProps) => ( className="h-full object-contain" /> )} - {/* {!hideTitle && ( - - AICONL - - )} */} ); @@ -61,3 +50,6 @@ export const Logo = (props: LogoProps) => { ); }; + +// Export logomark for use in spinners and loading indicators +export const DembraneLogomark = dembraneLogomark; diff --git a/echo/frontend/src/components/common/QRCode.tsx b/echo/frontend/src/components/common/QRCode.tsx index 23751ff7..cc165de4 100644 --- a/echo/frontend/src/components/common/QRCode.tsx +++ b/echo/frontend/src/components/common/QRCode.tsx @@ -13,11 +13,13 @@ export const QRCode = (props: { value: string; ref?: any }) => { ref={props.ref} logoImage={ CURRENT_BRAND === "dembrane" - ? "/dembrane-logo-hq.png" + ? "/dembrane-logomark-cropped.png" : "/aiconl-logo-hq.png" } + logoWidth={200} + logoHeight={200} eyeColor={"#000000"} - logoPadding={2} + logoPadding={16} removeQrCodeBehindLogo logoPaddingStyle="circle" size={1024} diff --git a/echo/frontend/src/components/conversation/AutoSelectConversations.tsx b/echo/frontend/src/components/conversation/AutoSelectConversations.tsx index f1f16593..1795771b 100644 --- a/echo/frontend/src/components/conversation/AutoSelectConversations.tsx +++ b/echo/frontend/src/components/conversation/AutoSelectConversations.tsx @@ -163,7 +163,7 @@ export const AutoSelectConversations = () => { diff --git a/echo/frontend/src/components/conversation/ConversationAccordion.tsx b/echo/frontend/src/components/conversation/ConversationAccordion.tsx index e23e3a52..c714bf70 100644 --- a/echo/frontend/src/components/conversation/ConversationAccordion.tsx +++ b/echo/frontend/src/components/conversation/ConversationAccordion.tsx @@ -39,7 +39,8 @@ import { IconArrowsUpDown, IconChevronDown, IconChevronUp, - IconRosetteDiscountCheckFilled, + IconInfoCircle, + IconRosetteDiscountCheck, IconSearch, IconSelectAll, IconTags, @@ -67,6 +68,8 @@ import { useProjectById, } from "@/components/project/hooks"; import { ENABLE_CHAT_AUTO_SELECT, ENABLE_CHAT_SELECT_ALL } from "@/config"; +import { analytics } from "@/lib/analytics"; +import { AnalyticsEvents as events } from "@/lib/analyticsEvents"; import { testId } from "@/lib/testUtils"; import { BaseSkeleton } from "../common/BaseSkeleton"; import { NavigationButton } from "../common/NavigationButton"; @@ -516,11 +519,6 @@ const ConversationAccordionItem = ({ return null; } - const isLocked = chatContextQuery.data?.conversations?.find( - (c) => c.conversation_id === conversation.id && c.locked, - ); - - const isAutoSelectEnabled = chatContextQuery.data?.auto_select_bool ?? false; const chatMode = chatContextQuery.data?.chat_mode; // Hide checkboxes when: @@ -538,26 +536,10 @@ const ConversationAccordionItem = ({ (artefact) => (artefact as ConversationArtifact).approved_at, ); - // In overview mode, show a subtle "included" indicator - const isOverviewMode = chatMode === "overview"; - - // Mode-based styling - const isDeepDiveWithSelection = - inChatMode && !isNewChatRoute && chatMode === "deep_dive" && isLocked; - return ( - {conversation.participant_email ?? conversation.participant_name} + {conversation.participant_name || conversation.title} + {conversation.title && conversation.participant_name && ( + + + + )} {hasVerifiedArtefacts && ( - + )} @@ -927,6 +914,8 @@ export const ConversationAccordion = ({ // Handle select all const handleSelectAllClick = () => { + try { analytics.trackEvent(events.SELECT_ALL_CLICK); } + catch (error) { console.warn("Analytics tracking failed:", error); } setSelectAllModalOpened(true); setSelectAllResult(null); }; @@ -938,6 +927,9 @@ export const ConversationAccordion = ({ return; } + try { analytics.trackEvent(events.SELECT_ALL_CONFIRM); } + catch (error) { console.warn("Analytics tracking failed:", error); } + setSelectAllLoading(true); try { const result = await selectAllMutation.mutateAsync({ @@ -948,7 +940,11 @@ export const ConversationAccordion = ({ verifiedOnly: showOnlyVerified || undefined, }); setSelectAllResult(result); + try { analytics.trackEvent(events.SELECT_ALL_SUCCESS); } + catch (error) { console.warn("Analytics tracking failed:", error); } } catch (_error) { + try { analytics.trackEvent(events.SELECT_ALL_ERROR); } + catch (error) { console.warn("Analytics tracking failed:", error); } toast.error(t`Failed to add conversations to context`); setSelectAllModalOpened(false); } finally { @@ -1091,7 +1087,7 @@ export const ConversationAccordion = ({ }} {...testId("conversation-search-clear-button")} > - + ) } @@ -1106,8 +1102,6 @@ export const ConversationAccordion = ({ setShowFilterActions((prev) => !prev)} aria-label={t`Options`} {...testId("conversation-filter-options-toggle")} @@ -1146,9 +1140,8 @@ export const ConversationAccordion = ({ > - - + )} @@ -1391,7 +1381,7 @@ export const ConversationAccordion = ({ disabled={remainingCount > 0} > + + )} + + } + description={t`Topic-based title describing what was discussed`} + placeholder={t`Auto-generated or enter manually`} + disabled={isGeneratingTitle} + {...register("title")} + {...testId("conversation-edit-title-input")} + /> + + {projectTags && projectTags.length > 0 ? ( ) : ( - <> + @@ -206,10 +384,7 @@ export const ConversationEdit = ({ - - No tags found - - + )} diff --git a/echo/frontend/src/components/conversation/MoveConversationButton.tsx b/echo/frontend/src/components/conversation/MoveConversationButton.tsx index 1c514377..a3319a32 100644 --- a/echo/frontend/src/components/conversation/MoveConversationButton.tsx +++ b/echo/frontend/src/components/conversation/MoveConversationButton.tsx @@ -138,7 +138,7 @@ export const MoveConversationButton = ({ {...testId("conversation-move-button")} > - + Beta Move to Another Project diff --git a/echo/frontend/src/components/conversation/OngoingConversationsSummaryCard.tsx b/echo/frontend/src/components/conversation/OngoingConversationsSummaryCard.tsx index cc517917..97f6c903 100644 --- a/echo/frontend/src/components/conversation/OngoingConversationsSummaryCard.tsx +++ b/echo/frontend/src/components/conversation/OngoingConversationsSummaryCard.tsx @@ -1,7 +1,8 @@ import { aggregate } from "@directus/sdk"; import { t } from "@lingui/core/macro"; import { ActionIcon, Group, Stack, Text } from "@mantine/core"; -import { IconRefresh, IconUsersGroup } from "@tabler/icons-react"; +import { UsersThreeIcon } from "@phosphor-icons/react"; +import { IconRefresh } from "@tabler/icons-react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { directus } from "@/lib/directus"; @@ -68,7 +69,7 @@ export const OngoingConversationsSummaryCard = ({ return ( } + icon={} label={ } + icon={} label={t`Open for Participation?`} value={ { + setUsePiiRedaction(projectAnonymize); + }, [projectAnonymize]); + const navigate = useI18nNavigate(); const handleRetranscribe = async () => { @@ -123,7 +131,7 @@ export const RetranscribeConversationModal = ({ title={ {t`Retranscribe Conversation`} - + Beta @@ -159,7 +167,11 @@ export const RetranscribeConversationModal = ({ /> .`} + description={ + projectAnonymize + ? t`Project default: enabled. This will replace personally identifiable information with .` + : t`This will replace personally identifiable information with .` + } checked={usePiiRedaction} onChange={(e) => setUsePiiRedaction(e.currentTarget.checked)} {...testId("transcript-retranscribe-pii-toggle")} diff --git a/echo/frontend/src/components/conversation/SelectAllConfirmationModal.tsx b/echo/frontend/src/components/conversation/SelectAllConfirmationModal.tsx index 4205bec7..fb3684c7 100644 --- a/echo/frontend/src/components/conversation/SelectAllConfirmationModal.tsx +++ b/echo/frontend/src/components/conversation/SelectAllConfirmationModal.tsx @@ -19,7 +19,7 @@ import { IconAlertTriangle, IconCheck, IconFileOff, - IconRosetteDiscountCheckFilled, + IconRosetteDiscountCheck, IconScale, IconX, } from "@tabler/icons-react"; @@ -106,7 +106,7 @@ const FilterDisplay = ({ color="blue" variant="light" size="md" - leftSection={} + rightSection={} style={{ width: "fit-content" }} > Verified @@ -439,9 +439,7 @@ export const SelectAllConfirmationModal = ({ size="sm" variant="light" color="blue" - leftSection={ - - } + rightSection={} > Verified @@ -510,8 +508,8 @@ export const SelectAllConfirmationModal = ({ {result.added.length > 0 && ( } - rightSection={ + rightSection={} + leftSection={ 0 && ( } - rightSection={ + rightSection={} + leftSection={ 0 && ( } - rightSection={ + rightSection={} + leftSection={ {getReasonLabel(conv.reason)} @@ -661,7 +659,7 @@ export const SelectAllConfirmationModal = ({ color={getReasonColor(conv.reason)} size="sm" variant="light" - leftSection={getReasonIcon(conv.reason)} + rightSection={getReasonIcon(conv.reason)} className="flex-shrink-0" > {getReasonLabel(conv.reason)} @@ -693,7 +691,7 @@ export const SelectAllConfirmationModal = ({ - - + {!isStopping && chunks?.data && chunks.data.length > 0 && ( )} diff --git a/echo/frontend/src/components/participant/ParticipantConversationText.tsx b/echo/frontend/src/components/participant/ParticipantConversationText.tsx index 3693bd36..13586106 100644 --- a/echo/frontend/src/components/participant/ParticipantConversationText.tsx +++ b/echo/frontend/src/components/participant/ParticipantConversationText.tsx @@ -161,11 +161,9 @@ export const ParticipantConversationText = () => { - - + {text.trim() === "" && chunks.data && chunks.data.length > 0 && ( - )} + + <Trans id="participant.ready.to.begin">Ready to Begin?</Trans> + + + + ) : ( + <>
- {React.createElement(currentCard.icon, { - className: "text-blue-500", - size: 64, - })} -
+ {currentCard?.type === "microphone" && ( + + )} + {currentCard.icon && ( +
+ {React.createElement(currentCard.icon, { + className: "text-blue-500", + size: 64, + })} +
+ )} -

- {currentCard.title} -

+ + {currentCard.title} + - {currentCard.content && ( -

{currentCard.content}

- )} + {currentCard.content && ( + + {currentCard.content} + + )} - {currentCard.extraHelp && ( -

{currentCard.extraHelp}

- )} + {currentCard.extraHelp && ( + + {currentCard.extraHelp} + + )} - {currentCard.component && ( -
- -
- )} - - {currentCard.link && ( - - )} - - {currentCard.checkbox && ( -
- + +
+ )} + + {currentCard.link && ( + + {currentCard.link.label} + + )} + + {currentCard.checkbox && ( + - - - )} - + )} + -
- {currentCard?.type === "microphone" ? ( - <> - - - - ) : ( - <> - - {!isLastSlide && ( +
+ {currentCard?.type === "microphone" ? ( + <> + - )} - - )} -
- -
-
- {allSlides.map((slide, index) => ( -
- ))} + + ) : ( + <> + + {!isLastSlide && ( + + )} + + )} +
+ +
+
+ {allSlides.map((slide, index) => ( +
+ ))} +
-
- - )} + + )} +
); }; diff --git a/echo/frontend/src/components/participant/SpikeMessage.tsx b/echo/frontend/src/components/participant/SpikeMessage.tsx index c41d4917..b494a072 100644 --- a/echo/frontend/src/components/participant/SpikeMessage.tsx +++ b/echo/frontend/src/components/participant/SpikeMessage.tsx @@ -21,7 +21,7 @@ const SpikeMessage = ({ title={
- +
} diff --git a/echo/frontend/src/components/participant/StopRecordingConfirmationModal.tsx b/echo/frontend/src/components/participant/StopRecordingConfirmationModal.tsx index 0dc808f4..0d09bf4a 100644 --- a/echo/frontend/src/components/participant/StopRecordingConfirmationModal.tsx +++ b/echo/frontend/src/components/participant/StopRecordingConfirmationModal.tsx @@ -69,19 +69,17 @@ export const StopRecordingConfirmationModal = ({ onClick={handleClose} disabled={isStopping} miw={100} - radius="md" size="md" {...testId("portal-audio-stop-resume-button")} > Resume diff --git a/echo/frontend/src/components/participant/verify/VerifySelection.tsx b/echo/frontend/src/components/participant/verify/VerifySelection.tsx index 535147d3..dff4a68c 100644 --- a/echo/frontend/src/components/participant/verify/VerifySelection.tsx +++ b/echo/frontend/src/components/participant/verify/VerifySelection.tsx @@ -197,9 +197,9 @@ export const VerifySelection = () => { > {/* Main content */} - - <Trans id="participant.concrete.selection.title"> - What do you want to make concrete? + <Title order={3} className="font-semibold"> + <Trans id="participant.verify.selection.title"> + What do you want to verify? </Trans> @@ -208,7 +208,7 @@ export const VerifySelection = () => { {isLoading && (
- +
)} @@ -254,7 +254,7 @@ export const VerifySelection = () => { {isLoading ? ( Loading… ) : ( - Next + Next )}
diff --git a/echo/frontend/src/components/project/HostGuideDownload.tsx b/echo/frontend/src/components/project/HostGuideDownload.tsx new file mode 100644 index 00000000..7e4b0a79 --- /dev/null +++ b/echo/frontend/src/components/project/HostGuideDownload.tsx @@ -0,0 +1,31 @@ +import { Trans } from "@lingui/react/macro"; +import { Button } from "@mantine/core"; +import { IconPresentation } from "@tabler/icons-react"; + +interface HostGuideDownloadProps { + project: Project; +} + +export const HostGuideDownload = ({ project }: HostGuideDownloadProps) => { + const handleOpenHostGuide = () => { + if (!project) return; + // Open host guide in new tab + const hostGuideUrl = `/projects/${project.id}/host-guide`; + window.open(hostGuideUrl, "_blank"); + }; + + if (!project?.is_conversation_allowed) { + return null; + } + + return ( + + ); +}; diff --git a/echo/frontend/src/components/project/HostGuidePDF.tsx b/echo/frontend/src/components/project/HostGuidePDF.tsx new file mode 100644 index 00000000..9b458c30 --- /dev/null +++ b/echo/frontend/src/components/project/HostGuidePDF.tsx @@ -0,0 +1,323 @@ +import { + Document, + Page, + View, + Text, + Image, + StyleSheet, + Font, +} from "@react-pdf/renderer"; + +// Brand colors from colors.json +const colors = { + parchment: "#f6f4f1", + graphite: "#2d2d2c", + royalBlue: "#4169e1", + goldenPollen: "#ffd166", + springGreen: "#1effa1", +}; + +// Hardcoded translations for 6 languages +const translations = { + en: { + title: "How to Record", + step1: "Scan the QR code", + step2_name: "Enter your name or topic", + step2_no_name: "Tap Start", + step3: "Start recording - make sure everyone is heard", + step4: "Done? Press Finish Recording", + important: "IMPORTANT", + warning1: "Turn OFF battery saver", + warning2: "Screen must stay ON during recording", + warning3: "Screen black? It's not recording.", + tip: "Need a break? Press STOP, then Resume when ready.", + }, + nl: { + title: "Hoe op te nemen", + step1: "Scan de QR-code", + step2_name: "Vul je naam of onderwerp in", + step2_no_name: "Tik op Start", + step3: "Start opname - zorg dat iedereen te horen is", + step4: "Klaar? Druk op Opname Afronden", + important: "BELANGRIJK", + warning1: "Zet batterijbesparing UIT", + warning2: "Scherm moet AAN blijven tijdens opname", + warning3: "Scherm zwart? Dan neemt hij niet op.", + tip: "Pauze nodig? Druk op STOP, dan Hervatten als je klaar bent.", + }, + de: { + title: "So nehmen Sie auf", + step1: "QR-Code scannen", + step2_name: "Namen oder Thema eingeben", + step2_no_name: "Auf Start tippen", + step3: "Aufnahme starten - alle sollten zu horen sein", + step4: "Fertig? Aufnahme beenden drucken", + important: "WICHTIG", + warning1: "Energiesparmodus AUS", + warning2: "Bildschirm muss AN bleiben", + warning3: "Bildschirm schwarz? Keine Aufnahme.", + tip: "Pause notig? STOP drucken, dann Fortsetzen.", + }, + fr: { + title: "Comment enregistrer", + step1: "Scannez le QR code", + step2_name: "Entrez votre nom ou sujet", + step2_no_name: "Appuyez sur Demarrer", + step3: "Demarrez - assurez-vous que tout le monde est entendu", + step4: "Termine? Appuyez sur Terminer", + important: "IMPORTANT", + warning1: "Desactivez le mode economie", + warning2: "L'ecran doit rester ALLUME", + warning3: "Ecran noir? Pas d'enregistrement.", + tip: "Besoin d'une pause? STOP, puis Reprendre.", + }, + es: { + title: "Como grabar", + step1: "Escanea el codigo QR", + step2_name: "Ingresa tu nombre o tema", + step2_no_name: "Toca Iniciar", + step3: "Inicia - asegurate de que todos sean escuchados", + step4: "Listo? Presiona Finalizar", + important: "IMPORTANTE", + warning1: "Desactiva el ahorro de bateria", + warning2: "La pantalla debe permanecer ENCENDIDA", + warning3: "Pantalla negra? No esta grabando.", + tip: "Necesitas un descanso? DETENER, luego Continuar.", + }, + it: { + title: "Come registrare", + step1: "Scansiona il codice QR", + step2_name: "Inserisci il tuo nome o argomento", + step2_no_name: "Tocca Inizia", + step3: "Inizia - assicurati che tutti siano sentiti", + step4: "Finito? Premi Termina", + important: "IMPORTANTE", + warning1: "Disattiva il risparmio energetico", + warning2: "Lo schermo deve rimanere ACCESO", + warning3: "Schermo nero? Non sta registrando.", + tip: "Hai bisogno di una pausa? STOP, poi Riprendi.", + }, +} as const; + +type LanguageCode = keyof typeof translations; + +// Styles for A4 Landscape PDF with brand colors +const styles = StyleSheet.create({ + page: { + flexDirection: "column", + backgroundColor: colors.parchment, + padding: 50, + fontFamily: "Helvetica", + }, + header: { + marginBottom: 30, + }, + title: { + fontSize: 36, + color: colors.graphite, + marginBottom: 8, + }, + projectName: { + fontSize: 14, + color: colors.graphite, + opacity: 0.6, + }, + mainContent: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-start", + marginBottom: 30, + flex: 1, + }, + stepsContainer: { + flex: 1, + marginRight: 50, + }, + stepRow: { + flexDirection: "row", + alignItems: "flex-start", + marginBottom: 20, + }, + stepNumber: { + fontSize: 32, + color: colors.royalBlue, + marginRight: 16, + minWidth: 40, + }, + stepText: { + fontSize: 24, + color: colors.graphite, + flex: 1, + paddingTop: 6, + }, + qrContainer: { + width: 200, + height: 200, + backgroundColor: "#FFFFFF", + padding: 12, + borderRadius: 8, + }, + qrImage: { + width: "100%", + height: "100%", + }, + qrPlaceholder: { + width: "100%", + height: "100%", + backgroundColor: "#EEEEEE", + justifyContent: "center", + alignItems: "center", + }, + qrPlaceholderText: { + fontSize: 12, + color: colors.graphite, + opacity: 0.5, + }, + bottomSection: { + flexDirection: "row", + gap: 20, + }, + warningsSection: { + flex: 1, + backgroundColor: "#FFFFFF", + padding: 20, + borderRadius: 8, + }, + warningHeader: { + flexDirection: "row", + alignItems: "center", + marginBottom: 12, + }, + warningIcon: { + fontSize: 16, + color: colors.goldenPollen, + marginRight: 8, + }, + warningTitle: { + fontSize: 14, + color: colors.goldenPollen, + }, + warningList: { + paddingLeft: 24, + }, + warningItem: { + fontSize: 13, + color: colors.graphite, + marginBottom: 6, + }, + tipSection: { + flex: 1, + backgroundColor: colors.springGreen, + padding: 20, + borderRadius: 8, + justifyContent: "center", + }, + tipText: { + fontSize: 14, + color: colors.graphite, + }, + footer: { + flexDirection: "row", + justifyContent: "flex-end", + alignItems: "center", + marginTop: 20, + paddingTop: 20, + borderTopWidth: 1, + borderTopColor: "#E5E7EB", + }, + brandingContainer: { + flexDirection: "row", + alignItems: "center", + }, + brandingLogo: { + width: 100, + height: 28, + }, +}); + +export type HostGuidePDFProps = { + projectName: string; + language: string; + askForParticipantName: boolean; + qrCodeDataUrl: string; + logoDataUrl?: string; +}; + +export const HostGuidePDF = ({ + projectName, + language, + askForParticipantName, + qrCodeDataUrl, + logoDataUrl, +}: HostGuidePDFProps) => { + // Normalize language code to 2-letter code + const langCode = (language?.slice(0, 2) || "en") as LanguageCode; + const t = translations[langCode] || translations.en; + + // Build steps array based on whether we ask for participant name + const steps = askForParticipantName + ? [t.step1, t.step2_name, t.step3, t.step4] + : [t.step1, t.step2_no_name, t.step3, t.step4]; + + return ( + + + {/* Header with title */} + + {t.title} + {projectName} + + + {/* Main content: Steps + QR Code */} + + + {steps.map((step, index) => ( + + {index + 1}. + {step} + + ))} + + + + {qrCodeDataUrl ? ( + + ) : ( + + QR Code + + )} + + + + {/* Bottom Section: Warnings + Tip */} + + + + ! + {t.important} + + + - {t.warning1} + - {t.warning2} + - {t.warning3} + + + + + {t.tip} + + + + {/* Footer with logo */} + + + {logoDataUrl && ( + + )} + + + + + ); +}; diff --git a/echo/frontend/src/components/project/ProjectDangerZone.tsx b/echo/frontend/src/components/project/ProjectDangerZone.tsx index 598a9772..f6d47e31 100644 --- a/echo/frontend/src/components/project/ProjectDangerZone.tsx +++ b/echo/frontend/src/components/project/ProjectDangerZone.tsx @@ -88,7 +88,6 @@ export const ProjectDangerZone = ({ project }: { project: Project }) => { + + + + {project && } + ); }; diff --git a/echo/frontend/src/components/project/ProjectPortalEditor.tsx b/echo/frontend/src/components/project/ProjectPortalEditor.tsx index ae0d5147..53545a64 100644 --- a/echo/frontend/src/components/project/ProjectPortalEditor.tsx +++ b/echo/frontend/src/components/project/ProjectPortalEditor.tsx @@ -40,12 +40,16 @@ import { useProjectSharingLink } from "./ProjectQRCode"; import { ProjectTagsInput } from "./ProjectTagsInput"; const FormSchema = z.object({ + anonymize_transcripts: z.boolean(), + conversation_title_prompt: z.string(), + default_conversation_ask_for_participant_email: z.boolean(), default_conversation_ask_for_participant_name: z.boolean(), default_conversation_description: z.string(), default_conversation_finish_text: z.string(), default_conversation_title: z.string(), default_conversation_transcript_prompt: z.string(), default_conversation_tutorial_slug: z.string(), + enable_ai_title_and_tags: z.boolean(), get_reply_mode: z.string(), get_reply_prompt: z.string(), is_get_reply_enabled: z.boolean(), @@ -121,15 +125,25 @@ const ProperNounInput = ({ return ( + + + <Trans>Specific Context</Trans> + + {isDirty && ( +
+ )} + + + + Add key terms or proper nouns to improve transcript quality and + accuracy. + + } - description={ - - Add key terms or proper nouns to improve transcript quality and - accuracy. - - } value={nounInput} onChange={(e) => setNounInput(e.currentTarget.value)} placeholder={t`Enter a key term or proper noun`} @@ -238,6 +252,10 @@ const ProjectPortalEditorComponent: React.FC = ({ : "none"; return { + anonymize_transcripts: project.anonymize_transcripts ?? false, + conversation_title_prompt: project.conversation_title_prompt ?? "", + default_conversation_ask_for_participant_email: + project.default_conversation_ask_for_participant_email ?? false, default_conversation_ask_for_participant_name: project.default_conversation_ask_for_participant_name ?? false, default_conversation_description: @@ -248,6 +266,7 @@ const ProjectPortalEditorComponent: React.FC = ({ default_conversation_transcript_prompt: project.default_conversation_transcript_prompt ?? "", default_conversation_tutorial_slug: normalizedTutorialSlug ?? "none", + enable_ai_title_and_tags: project.enable_ai_title_and_tags ?? false, get_reply_mode: project.get_reply_mode ?? "summarize", get_reply_prompt: project.get_reply_prompt ?? "", is_get_reply_enabled: project.is_get_reply_enabled ?? false, @@ -292,6 +311,16 @@ const ProjectPortalEditorComponent: React.FC = ({ name: "is_verify_enabled", }); + const watchedAskForEmail = useWatch({ + control, + name: "default_conversation_ask_for_participant_email", + }); + + const watchedAiTitleEnabled = useWatch({ + control, + name: "enable_ai_title_and_tags", + }); + const updateProjectMutation = useUpdateProjectByIdMutation(); const onSave = useCallback( @@ -405,6 +434,13 @@ const ProjectPortalEditorComponent: React.FC = ({ }; }, [watch]); // Only depend on watch + // Auto-disable report notifications when ask_for_email is turned off + useEffect(() => { + if (!watchedAskForEmail) { + setValue("is_project_notification_subscription_allowed", false); + } + }, [watchedAskForEmail, setValue]); + const refreshPreview = useCallback(() => { setPreviewKey((prev) => prev + 1); }, []); @@ -428,7 +464,7 @@ const ProjectPortalEditorComponent: React.FC = ({