From b04680150d4cc2b4b8780413aeb0ffb0b027e526 Mon Sep 17 00:00:00 2001 From: Sameer Pashikanti Date: Fri, 3 Apr 2026 10:26:03 +0000 Subject: [PATCH 01/17] fix: change report buttons from teal to blue per brand guidelines --- ...template-community-report-anonymization.md | 695 ++++++++++++++++++ ...e-community-report-anonymization-design.md | 130 ++++ .../components/report/CreateReportForm.tsx | 4 +- .../report/UpdateReportModalButton.tsx | 6 +- .../project/report/ProjectReportRoute.tsx | 4 +- 5 files changed, 832 insertions(+), 7 deletions(-) create mode 100644 echo/docs/superpowers/plans/2026-04-03-template-community-report-anonymization.md create mode 100644 echo/docs/superpowers/specs/2026-04-03-template-community-report-anonymization-design.md diff --git a/echo/docs/superpowers/plans/2026-04-03-template-community-report-anonymization.md b/echo/docs/superpowers/plans/2026-04-03-template-community-report-anonymization.md new file mode 100644 index 000000000..f5d293c40 --- /dev/null +++ b/echo/docs/superpowers/plans/2026-04-03-template-community-report-anonymization.md @@ -0,0 +1,695 @@ +# Template Fixes, Community Removal, Report Colors, Anonymization UX + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix template quick access bugs by simplifying to JSON storage, remove dead community marketplace code, fix report button colors to match brand, and add anonymization UX for participants and hosts. + +**Architecture:** Four independent changes. Template preferences move from a separate Directus collection to a JSON field on `directus_users`. Community code is fully deleted. Report buttons change from teal to blue. Anonymization adds a muted notice in the participant recording view and a confirmation modal in the host portal editor. + +**Tech Stack:** Python/FastAPI, React/Mantine, Lingui i18n, Directus SDK + +--- + +## File Map + +| Change | Files to Modify | Files to Delete | +|--------|----------------|-----------------| +| Template preferences | `server/dembrane/api/template.py`, `frontend/src/lib/api.ts`, `frontend/src/components/chat/TemplatesModal.tsx`, `frontend/src/components/chat/templateKey.ts` | `frontend/src/components/chat/QuickAccessConfigurator.tsx` (if unused elsewhere) | +| Community removal | `server/dembrane/api/template.py`, `frontend/src/lib/api.ts`, `frontend/src/components/chat/TemplatesModal.tsx` | `frontend/src/components/chat/hooks/useCommunityTemplates.ts`, `frontend/src/components/chat/PublishTemplateForm.tsx` | +| Report colors | `frontend/src/components/report/CreateReportForm.tsx`, `frontend/src/components/report/UpdateReportModalButton.tsx`, `frontend/src/routes/project/report/ProjectReportRoute.tsx` | None | +| Anonymization UX | `frontend/src/components/participant/ParticipantBody.tsx`, `frontend/src/components/participant/ParticipantConversationAudio.tsx`, `frontend/src/components/participant/ParticipantConversationText.tsx`, `frontend/src/components/project/ProjectPortalEditor.tsx`, `frontend/src/locales/*.po` | None | + +--- + +### Task 1: Report Buttons -- Teal to Blue + +The simplest change. All primary buttons in the report feature use `color="teal"` but should use `color="blue"` per brand guidelines. + +**Files:** +- Modify: `frontend/src/components/report/CreateReportForm.tsx:258,295` +- Modify: `frontend/src/components/report/UpdateReportModalButton.tsx:149,274,311` +- Modify: `frontend/src/routes/project/report/ProjectReportRoute.tsx:548,992` + +- [ ] **Step 1: Change CreateReportForm.tsx buttons** + +In `frontend/src/components/report/CreateReportForm.tsx`, change both instances of `color="teal"` to `color="blue"`: + +Line 258 (Schedule Report button): +```tsx +color="blue" +``` + +Line 295 (Generate now button): +```tsx +color="blue" +``` + +- [ ] **Step 2: Change UpdateReportModalButton.tsx buttons** + +In `frontend/src/components/report/UpdateReportModalButton.tsx`, change all three instances of `color="teal"` to `color="blue"`: + +Line 149 (Update Report trigger button): +```tsx +color="blue" +``` + +Line 274 (Schedule Report button): +```tsx +color="blue" +``` + +Line 311 (Generate now button): +```tsx +color="blue" +``` + +- [ ] **Step 3: Change ProjectReportRoute.tsx buttons** + +In `frontend/src/routes/project/report/ProjectReportRoute.tsx`, change both instances of `color="teal"` to `color="blue"`: + +Line 548 (Confirm reschedule button): +```tsx +color="blue" +``` + +Line 992 (Publish toggle Switch): +```tsx +color="blue" +``` + +- [ ] **Step 4: Visual check** + +Run `cd frontend && pnpm build` to confirm no build errors. + +- [ ] **Step 5: Commit** + +```bash +git add frontend/src/components/report/CreateReportForm.tsx frontend/src/components/report/UpdateReportModalButton.tsx frontend/src/routes/project/report/ProjectReportRoute.tsx +git commit -m "fix: change report buttons from teal to blue per brand guidelines" +``` + +--- + +### Task 2: Remove Community Marketplace Code + +Full deletion of all community endpoints, schemas, frontend hooks, and API client functions. The `prompt_template_rating` Directus collection stays in the database (no destructive migration). + +**Files:** +- Modify: `server/dembrane/api/template.py` +- Delete: `frontend/src/components/chat/hooks/useCommunityTemplates.ts` +- Delete: `frontend/src/components/chat/PublishTemplateForm.tsx` +- Modify: `frontend/src/components/chat/TemplatesModal.tsx` +- Modify: `frontend/src/lib/api.ts` + +- [ ] **Step 1: Remove community schemas from template.py** + +In `server/dembrane/api/template.py`, delete lines 67-102 (the following schemas and constant): + +```python +# DELETE these blocks entirely: + +class PromptTemplateRatingIn(BaseModel): + prompt_template_id: str + rating: Literal[1, 2] # 1 = thumbs down, 2 = thumbs up + chat_message_id: Optional[str] = None + + +class PromptTemplateRatingOut(BaseModel): + id: str + prompt_template_id: str + rating: int + chat_message_id: Optional[str] = None + date_created: Optional[str] = None + + +ALLOWED_TAGS = ["Workshop", "Interview", "Focus Group", "Meeting", "Research", "Community", "Education", "Analysis"] + + +class CommunityTemplateOut(BaseModel): + id: str + title: str + description: Optional[str] = None + content: str + tags: Optional[List[str]] = None + language: Optional[str] = None + author_display_name: Optional[str] = None + star_count: int = 0 + use_count: int = 0 + date_created: Optional[str] = None + is_own: bool = False + + +class PublishTemplateIn(BaseModel): + description: Optional[str] = Field(default=None, max_length=500) + tags: Optional[List[str]] = Field(default=None) + language: Optional[str] = Field(default=None, max_length=10) + is_anonymous: bool = False +``` + +- [ ] **Step 2: Remove community endpoints from template.py** + +Delete the entire `# -- Community Marketplace --` section (lines 228-505), which contains these endpoints: +- `GET /community` (list_community_templates) +- `GET /community/my-stars` (get_my_community_stars) +- `POST /{id}/publish` (publish_template) +- `POST /{id}/unpublish` (unpublish_template) +- `POST /{id}/star` (toggle_star) +- `POST /{id}/copy` (copy_template) + +- [ ] **Step 3: Remove ratings endpoints from template.py** + +Delete the entire `# -- Ratings --` section (lines 584-681), which contains: +- `POST /ratings` (rate_prompt_template) +- `DELETE /ratings/{id}` (delete_rating) +- `GET /ratings` (list_my_ratings) + +- [ ] **Step 4: Clean up unused imports in template.py** + +After deletions, check if `List`, `Literal`, `Field` are still needed. `List` is used in remaining endpoints. `Literal` is used in `AiSuggestionsToggleIn` and remaining schemas. `Field` is used in remaining schemas. Keep all imports. + +- [ ] **Step 5: Delete frontend community hook file** + +Delete the entire file `frontend/src/components/chat/hooks/useCommunityTemplates.ts`. + +- [ ] **Step 6: Delete PublishTemplateForm component** + +Delete the entire file `frontend/src/components/chat/PublishTemplateForm.tsx`. + +- [ ] **Step 7: Remove community imports and code from TemplatesModal.tsx** + +In `frontend/src/components/chat/TemplatesModal.tsx`: + +Remove the community hooks import (lines 54-61): +```tsx +// DELETE: +import { + useCommunityTemplates, + useCopyTemplate, + useMyCommunityStars, + usePublishTemplate, + useToggleStar, + useUnpublishTemplate, +} from "./hooks/useCommunityTemplates"; +``` + +Remove the PublishTemplateForm import (line 62): +```tsx +// DELETE: +import { PublishTemplateForm } from "./PublishTemplateForm"; +``` + +Remove the `showCommunity` variable (lines 228-230): +```tsx +// DELETE: +// Community features disabled until Directus fields are created +// (author_display_name, use_count, star_count, copied_from) +const showCommunity = false; +``` + +Then search through the component for all references to `showCommunity`, community hooks (`useCommunityTemplates`, `useMyCommunityStars`, `usePublishTemplate`, `useUnpublishTemplate`, `useToggleStar`, `useCopyTemplate`), and community-related UI sections (any JSX gated by `showCommunity` or referencing community templates). Remove them all. + +Also remove unused imports from `@phosphor-icons/react` that were only used by community UI (e.g., `Globe`, `ShareNetwork`, `Star`, `Copy` -- check each is not used elsewhere in the file before removing). + +- [ ] **Step 8: Remove community API functions from api.ts** + +In `frontend/src/lib/api.ts`, delete the following sections: + +The `// -- Community Marketplace --` section (around lines 1849-1923): +```tsx +// DELETE all of: +export type CommunityTemplateResponse = { ... } +export type CommunityTemplateParams = { ... } +export const getCommunityTemplates = async (...) +export const getMyCommunityStars = async (...) +export const publishTemplate = async (...) +export const unpublishTemplate = async (...) +export const toggleTemplateStar = async (...) +export const copyTemplate = async (...) +``` + +The `// -- Prompt Template Ratings --` section (around lines 1925-1955): +```tsx +// DELETE all of: +export type PromptTemplateRatingResponse = { ... } +export const ratePromptTemplate = async (...) +export const deletePromptTemplateRating = async (...) +export const getMyRatings = async (...) +``` + +- [ ] **Step 9: Verify build** + +```bash +cd frontend && pnpm build +``` + +Fix any remaining import errors or references to deleted code. + +- [ ] **Step 10: Run server lint** + +```bash +cd /workspaces/echo && ./check-code.sh +``` + +- [ ] **Step 11: Commit** + +```bash +git add -A +git commit -m "feat: remove community marketplace code (endpoints, hooks, UI, API client)" +``` + +--- + +### Task 3: Simplify Template Quick Access to JSON + +Replace the `prompt_template_preference` Directus collection with a JSON field on `directus_users`. + +**Files:** +- Modify: `server/dembrane/api/template.py` +- Modify: `frontend/src/lib/api.ts` +- Modify: `frontend/src/components/chat/TemplatesModal.tsx` +- Modify: `frontend/src/components/chat/templateKey.ts` +- Delete: `frontend/src/components/chat/QuickAccessConfigurator.tsx` (if unused) + +**Prerequisite:** User must create a JSON field `quick_access_preferences` on `directus_users` in Directus admin. Type: JSON, default: `[]`, nullable: true. + +- [ ] **Step 1: Replace backend quick-access endpoints in template.py** + +Delete the old `# -- Quick-Access Preferences --` section (lines 508-581 after community removal) and replace with: + +```python +# -- Quick-Access Preferences -- + + +class QuickAccessItemIn(BaseModel): + type: Literal["static", "user"] + id: str + + +@TemplateRouter.get("/quick-access") +async def get_quick_access( + auth: DependencyDirectusSession, +) -> list: + """Get the user's quick access preferences as a JSON array.""" + try: + users = directus.get_users( + { + "query": { + "filter": {"id": {"_eq": auth.user_id}}, + "fields": ["quick_access_preferences"], + "limit": 1, + } + } + ) + if not isinstance(users, list) or len(users) == 0: + return [] + prefs = users[0].get("quick_access_preferences") + if not isinstance(prefs, list): + return [] + return prefs + except Exception as e: + logger.error(f"Failed to get quick access preferences: {e}") + raise HTTPException(status_code=500, detail="Failed to get preferences") from None + + +@TemplateRouter.put("/quick-access") +async def save_quick_access( + body: List[QuickAccessItemIn], + auth: DependencyDirectusSession, +) -> list: + """Save the user's quick access preferences as a JSON array.""" + if len(body) > 5: + raise HTTPException(status_code=400, detail="Maximum 5 quick access items") + + # Validate no duplicates + seen = set() + for item in body: + key = (item.type, item.id) + if key in seen: + raise HTTPException(status_code=400, detail=f"Duplicate item: {item.type}:{item.id}") + seen.add(key) + + # Validate user templates exist and belong to user + for item in body: + if item.type == "user": + try: + template = directus.get_item("prompt_template", item.id) + if not template or template.get("user_created") != auth.user_id: + raise HTTPException(status_code=400, detail=f"Template not found: {item.id}") + except HTTPException: + raise + except Exception: + raise HTTPException(status_code=400, detail=f"Template not found: {item.id}") from None + + prefs = [{"type": item.type, "id": item.id} for item in body] + + try: + directus.update_user(auth.user_id, {"quick_access_preferences": prefs}) + return prefs + except Exception as e: + logger.error(f"Failed to save quick access preferences: {e}") + raise HTTPException(status_code=500, detail="Failed to save preferences") from None +``` + +Also delete the old schemas that are no longer needed: +- `PromptTemplatePreferenceOut` (lines 48-53) +- `QuickAccessPreferenceIn` (lines 56-60) + +- [ ] **Step 2: Update frontend API client** + +In `frontend/src/lib/api.ts`, find the quick-access preference functions (around lines 1818-1836) and replace with: + +```typescript +// -- Quick-Access Preferences -- + +export type QuickAccessPreference = { + type: "static" | "user"; + id: string; +}; + +export const getQuickAccessPreferences = async (): Promise< + QuickAccessPreference[] +> => { + return api.get("/templates/quick-access"); +}; + +export const saveQuickAccessPreferences = async ( + preferences: QuickAccessPreference[], +): Promise => { + return api.put( + "/templates/quick-access", + preferences, + ); +}; +``` + +Also delete the old types that referenced the Directus collection shape (e.g., `QuickAccessPreferenceResponse` with `template_type`, `static_template_id`, `prompt_template_id`, `sort` fields). Search for any type that mentions `template_type` or `static_template_id` in the quick-access context. + +- [ ] **Step 3: Update TemplatesModal.tsx to use new preference shape** + +The modal currently converts between the old Directus shape (`template_type`, `static_template_id`, `prompt_template_id`) and the internal `QuickAccessItem` format. With the new JSON shape (`type`, `id`), the conversion becomes trivial. + +Update the hook/query that fetches preferences to map the new shape. The `QuickAccessItem` type from `QuickAccessConfigurator.tsx` has: +```typescript +type QuickAccessItem = { + type: "static" | "user"; + id: string; + title: string; +}; +``` + +The new API returns `{ type, id }` directly -- just add `title` by looking up the template. Update all code that previously transformed `template_type` -> `type` and `static_template_id`/`prompt_template_id` -> `id`. + +Also update the save function to send the new shape directly instead of converting to the old format. + +- [ ] **Step 4: Move QuickAccessItem type inline** + +If `QuickAccessConfigurator.tsx` is only imported for its `QuickAccessItem` type, move the type definition into `TemplatesModal.tsx` or `templateKey.ts` and delete `QuickAccessConfigurator.tsx`. + +Check first: +```bash +grep -r "QuickAccessConfigurator" frontend/src/ --include="*.tsx" --include="*.ts" +``` + +If only imported as a type in TemplatesModal.tsx (line 63), delete the file and inline the type. + +- [ ] **Step 5: Verify build and lint** + +```bash +cd /workspaces/echo && ./check-code.sh +``` + +- [ ] **Step 6: Commit** + +```bash +git add -A +git commit -m "feat: simplify quick access preferences to JSON field on directus_users" +``` + +--- + +### Task 4: Anonymization Notice for Participants + +Add a muted text notice in the participant recording view when the conversation is anonymized. + +**Files:** +- Modify: `frontend/src/components/participant/ParticipantBody.tsx` +- Modify: `frontend/src/components/participant/ParticipantConversationAudio.tsx` +- Modify: `frontend/src/components/participant/ParticipantConversationText.tsx` +- Modify: `frontend/src/locales/en-US.po` (and all other locale files) + +- [ ] **Step 1: Add isAnonymized prop to ParticipantBody** + +In `frontend/src/components/participant/ParticipantBody.tsx`, add an `isAnonymized` prop: + +At line 28-40 (component signature), add the prop: +```tsx +export const ParticipantBody = ({ + projectId, + conversationId, + viewResponses = false, + children, + interleaveMessages = true, + isRecording = false, + isAnonymized = false, +}: PropsWithChildren<{ + projectId: string; + conversationId: string; + viewResponses?: boolean; + interleaveMessages?: boolean; + isRecording?: boolean; + isAnonymized?: boolean; +}>) => { +``` + +- [ ] **Step 2: Add muted anonymization notice in ParticipantBody** + +After the existing `SystemMessage` for recording instructions (line 196), add a conditional notice: + +```tsx + + + {isAnonymized && ( + + + Your transcription will be anonymized and your host will not be able to listen to your recording. + + + )} +``` + +Add `Text` to the Mantine imports at line 4 if not already imported (it's not currently in the import list -- `Title` is imported but not `Text`). + +- [ ] **Step 3: Pass isAnonymized from ParticipantConversationAudio** + +In `frontend/src/components/participant/ParticipantConversationAudio.tsx`, the `conversationQuery` is already available (line 76). Find where `ParticipantBody` is rendered via `ParticipantConversationAudioContent.tsx` and pass the prop. + +Check the chain: `ParticipantConversationAudio` -> renders `ParticipantConversationAudioContent` -> renders `ParticipantBody`. + +In `frontend/src/components/participant/ParticipantConversationAudioContent.tsx` (line 136), add the prop: +```tsx + +``` + +The `isAnonymized` value needs to be passed down from `ParticipantConversationAudio.tsx`. Add it to the props of `ParticipantConversationAudioContent` and derive it from: +```tsx +const isAnonymized = conversationQuery.data?.is_anonymized ?? false; +``` + +- [ ] **Step 4: Pass isAnonymized from ParticipantConversationText** + +In `frontend/src/components/participant/ParticipantConversationText.tsx` (line 189), the component also renders `ParticipantBody`. Add the same pattern: + +```tsx +const conversationQuery = useConversationQuery(projectId, conversationId); +const isAnonymized = conversationQuery.data?.is_anonymized ?? false; +``` + +Then pass to `ParticipantBody`: +```tsx + +``` + +Check if `useConversationQuery` is already imported and used in this file. If not, add the import from `./hooks`. + +- [ ] **Step 5: Extract and compile translations** + +```bash +cd frontend +pnpm messages:extract +``` + +Edit each locale file to add the translation for `participant.anonymization.notice`: + +- `en-US.po`: "Your transcription will be anonymized and your host will not be able to listen to your recording." +- `nl-NL.po`: "Je transcriptie wordt geanonimiseerd en je host kan niet naar je opname luisteren." +- `de-DE.po`: "Ihre Transkription wird anonymisiert und Ihr Host kann Ihre Aufnahme nicht anhoren." +- `fr-FR.po`: "Votre transcription sera anonymisee et votre hote ne pourra pas ecouter votre enregistrement." +- `es-ES.po`: "Tu transcripcion sera anonimizada y tu anfitrion no podra escuchar tu grabacion." +- `it-IT.po`: "La tua trascrizione sara anonimizzata e il tuo host non potra ascoltare la tua registrazione." + +```bash +pnpm messages:compile +``` + +- [ ] **Step 6: Verify build** + +```bash +cd frontend && pnpm build +``` + +- [ ] **Step 7: Commit** + +```bash +git add -A +git commit -m "feat: add anonymization notice for participants on recording page" +``` + +--- + +### Task 5: Host Confirmation Modal for Disabling Anonymization + +Add a confirmation modal when the project owner toggles anonymization from ON to OFF. + +**Files:** +- Modify: `frontend/src/components/project/ProjectPortalEditor.tsx` +- Modify: `frontend/src/locales/en-US.po` (and all other locale files) + +- [ ] **Step 1: Add modal state and imports** + +In `frontend/src/components/project/ProjectPortalEditor.tsx`, add `useDisclosure` from `@mantine/hooks` (if not already imported) and `Modal` from `@mantine/core` (if not already imported). + +Add modal state near the top of the component: +```tsx +const [anonymizeModalOpened, { open: openAnonymizeModal, close: closeAnonymizeModal }] = useDisclosure(false); +``` + +- [ ] **Step 2: Modify the anonymize toggle onChange handler** + +Replace the current simple toggle (lines 1496-1498): +```tsx +onChange={(e) => + field.onChange(e.currentTarget.checked) +} +``` + +With a handler that intercepts turning OFF: +```tsx +onChange={(e) => { + const newValue = e.currentTarget.checked; + if (!newValue && field.value) { + // Turning OFF -- show confirmation + openAnonymizeModal(); + } else { + field.onChange(newValue); + } +}} +``` + +- [ ] **Step 3: Add the confirmation modal** + +Right after the `` for anonymize_transcripts (after line 1502), add: + +```tsx + + + + + Turning off anonymization while recordings are ongoing may + have unintended consequences. Active conversations will also + be affected mid-recording. Please use this with caution. + + + + + + + + +``` + +Note: Check how `setValue` is obtained from `useForm` in this component. It should already be destructured alongside `control` and `formState`. If not, add it to the destructuring. + +- [ ] **Step 4: Extract and compile translations** + +```bash +cd frontend +pnpm messages:extract +``` + +Add translations for `portal.anonymization.disable.warning` and `portal.anonymization.disable.confirm` in all locale files: + +- `en-US.po`: (as written above) +- `nl-NL.po`: Warning: "Het uitschakelen van anonimisering terwijl opnames gaande zijn kan onbedoelde gevolgen hebben. Actieve gesprekken worden ook beinvloed tijdens de opname. Gebruik dit met voorzichtigheid." / Confirm: "Uitschakelen" +- `de-DE.po`: Warning: "Das Deaktivieren der Anonymisierung wahrend laufender Aufnahmen kann unbeabsichtigte Folgen haben. Aktive Gesprache werden auch wahrend der Aufnahme betroffen. Bitte verwenden Sie dies mit Vorsicht." / Confirm: "Ausschalten" +- `fr-FR.po`: Warning: "La desactivation de l'anonymisation pendant les enregistrements en cours peut avoir des consequences inattendues. Les conversations actives seront egalement affectees en cours d'enregistrement. Veuillez utiliser cette option avec prudence." / Confirm: "Desactiver" +- `es-ES.po`: Warning: "Desactivar la anonimizacion mientras hay grabaciones en curso puede tener consecuencias no deseadas. Las conversaciones activas tambien se veran afectadas durante la grabacion. Por favor, use esto con precaucion." / Confirm: "Desactivar" +- `it-IT.po`: Warning: "Disattivare l'anonimizzazione durante le registrazioni in corso puo avere conseguenze indesiderate. Anche le conversazioni attive saranno interessate durante la registrazione. Si prega di usare questa opzione con cautela." / Confirm: "Disattiva" + +```bash +pnpm messages:compile +``` + +- [ ] **Step 5: Verify build** + +```bash +cd frontend && pnpm build +``` + +- [ ] **Step 6: Commit** + +```bash +git add -A +git commit -m "feat: add confirmation modal when disabling transcript anonymization" +``` + +--- + +### Task 6: Final Verification + +- [ ] **Step 1: Run full check** + +```bash +cd /workspaces/echo && ./check-code.sh +``` + +- [ ] **Step 2: Commit any remaining fixes** + +If check-code.sh reveals issues, fix and commit. + +- [ ] **Step 3: Tell user about Directus changes needed** + +Remind user to: +1. Create `quick_access_preferences` JSON field on `directus_users` (default: `[]`, nullable: true) +2. Delete the `prompt_template_preference` collection from Directus diff --git a/echo/docs/superpowers/specs/2026-04-03-template-community-report-anonymization-design.md b/echo/docs/superpowers/specs/2026-04-03-template-community-report-anonymization-design.md new file mode 100644 index 000000000..0b579feb7 --- /dev/null +++ b/echo/docs/superpowers/specs/2026-04-03-template-community-report-anonymization-design.md @@ -0,0 +1,130 @@ +# Design: Template Fixes, Community Removal, Report Colors, Anonymization UX + +**Date**: 2026-04-03 +**Status**: Approved + +## 1. Template Quick Access -- Simplify to JSON + +### Problem + +The current `prompt_template_preference` collection stores each quick access item as a separate Directus record. This causes: +- Duplicate entries (same template appearing multiple times) +- Orphaned entries (`template_type: "user"` with `prompt_template_id: null`) +- No backend validation (saves whatever frontend sends) +- Unnecessary complexity for a simple ordered list + +### Solution + +Replace separate Directus records with a single JSON field on the user settings. + +**New field**: `quick_access_preferences` (JSON) on `directus_users` or appropriate user settings collection. + +**Schema**: +```json +[ + {"type": "static", "id": "summarize"}, + {"type": "static", "id": "meeting-notes"}, + {"type": "user", "id": "abc-123"} +] +``` + +**Backend**: +- New `GET /templates/quick-access` endpoint: reads the JSON field, returns the array (empty array default) +- New `PUT /templates/quick-access` endpoint: validates and writes the JSON field +- Validation rules: + - Max 5 items + - No duplicate (type, id) pairs + - Each entry must have `type` in ("static", "user") and non-empty `id` + - For `type: "user"`, verify the template exists and belongs to the user +- Remove old preference CRUD endpoints that interact with `prompt_template_preference` collection +- Remove `PromptTemplatePreferenceOut` and `QuickAccessPreferenceIn` schemas + +**Frontend**: +- Update API client functions to use new endpoints +- Update hooks (`useQuickAccessPreferences` or equivalent) to work with the new JSON shape +- Add client-side dedup as a safety net before saving +- Remove `QuickAccessConfigurator.tsx` if unused + +**Directus field needed** (user to create manually): +- Collection: `directus_users` (or user settings collection -- confirm with user) +- Field name: `quick_access_preferences` +- Type: JSON +- Default: `[]` +- Nullable: true + +## 2. Community Code -- Full Removal + +### What to delete + +**Backend** (`server/dembrane/api/template.py`): +- Endpoints: `GET /community`, `GET /community/my-stars`, `POST /{id}/publish`, `POST /{id}/unpublish`, `POST /{id}/star`, `POST /{id}/copy`, `POST /ratings`, `DELETE /ratings/{id}`, `GET /ratings` +- Schemas: `CommunityTemplateOut`, `PublishTemplateIn`, `PromptTemplateRatingIn`, `PromptTemplateRatingOut`, `ALLOWED_TAGS` + +**Frontend**: +- Hook file: `frontend/src/components/chat/hooks/useCommunityTemplates.ts` (delete entire file) +- Community sections in `TemplatesModal.tsx` (community tab/list, publish/unpublish UI, star UI, copy UI) +- API client functions in `frontend/src/lib/api.ts` (community-related fetch/mutation functions) + +**Not deleted**: The `prompt_template_rating` and related Directus collections remain in the database (no destructive migration). They just become unused. + +## 3. Report Buttons -- Teal to Blue + +### Problem + +All primary action buttons in the report feature use `color="teal"`. The brand style guide specifies Royal Blue (#4169e1) for primary buttons. + +### Fix + +Change `color="teal"` to `color="blue"` in these 6 instances: + +1. `frontend/src/components/report/CreateReportForm.tsx` -- "Schedule Report" button +2. `frontend/src/components/report/CreateReportForm.tsx` -- "Generate now" button +3. `frontend/src/components/report/UpdateReportModalButton.tsx` -- "Update Report"/"New Report" trigger button +4. `frontend/src/components/report/UpdateReportModalButton.tsx` -- "Schedule Report" button +5. `frontend/src/components/report/UpdateReportModalButton.tsx` -- "Generate now" button +6. `frontend/src/routes/project/report/ProjectReportRoute.tsx` -- "Confirm reschedule" button + +Mantine's `color="blue"` maps to a blue range that aligns with Royal Blue. + +## 4. Anonymization UX + +### 4a. Participant Anonymization Notice (Recording Page) + +**Where**: The initial message/instruction area shown to participants before they start recording (in `ParticipantConversationAudio` and `ParticipantConversationText` or their shared parent). + +**When shown**: When the conversation's `is_anonymized` flag is true. + +**Content**: Muted text appended to the existing welcome/instruction content: +> "Your transcription will be anonymized and your host will not be able to listen to your recording." + +**Style**: +- Muted/dimmed text (e.g., `c="dimmed"`, smaller font size) +- Part of the existing instruction flow, not a separate banner or alert +- Should feel informational, not alarming + +**Translations**: All 6 languages (en-US, nl-NL, de-DE, fr-FR, es-ES, it-IT). + +**Data flow**: The `is_anonymized` field is already on the conversation object returned by the API. The participant components already have access to conversation data. + +### 4b. Host Confirmation Modal (Portal Editor) + +**Where**: `ProjectPortalEditor.tsx`, on the anonymize transcripts toggle. + +**When triggered**: Only when toggling from ON to OFF (not when turning ON). + +**Modal content**: +- Title: "Turn off anonymization?" +- Body: "Turning off anonymization while recordings are ongoing may have unintended consequences. Active conversations will also be affected mid-recording. Please use this with caution." +- Buttons: "Cancel" (default) and "Turn off" (destructive/red) + +**Behavior**: +- If user confirms: proceed with the toggle, save the setting +- If user cancels: revert the toggle, no change saved +- No modal when turning ON (enabling anonymization is always safe) + +## Out of Scope + +- Migration of existing `prompt_template_preference` data to new JSON format (old records can be ignored; users get fresh defaults) +- Delete the `prompt_template_preference` Directus collection (replaced by JSON field) +- Deleting `prompt_template_rating` Directus collection +- Changing other button colors outside the report feature diff --git a/echo/frontend/src/components/report/CreateReportForm.tsx b/echo/frontend/src/components/report/CreateReportForm.tsx index 65f24fe81..a72be9fa1 100644 --- a/echo/frontend/src/components/report/CreateReportForm.tsx +++ b/echo/frontend/src/components/report/CreateReportForm.tsx @@ -255,7 +255,7 @@ export const CreateReportForm = ({ onSuccess }: { onSuccess: () => void }) => { loading={isPending} disabled={isPending || !scheduledDate} fullWidth - color="teal" + color="blue" {...testId("report-create-button")} > Schedule Report @@ -292,7 +292,7 @@ export const CreateReportForm = ({ onSuccess }: { onSuccess: () => void }) => { onClick={() => handleCreate(false)} loading={isPending} disabled={isPending} - color="teal" + color="blue" style={{ flex: 7 }} {...testId("report-create-button")} > diff --git a/echo/frontend/src/components/report/UpdateReportModalButton.tsx b/echo/frontend/src/components/report/UpdateReportModalButton.tsx index 7570a4e2c..921d1ad68 100644 --- a/echo/frontend/src/components/report/UpdateReportModalButton.tsx +++ b/echo/frontend/src/components/report/UpdateReportModalButton.tsx @@ -146,7 +146,7 @@ export const UpdateReportModalButton = ({ > @@ -308,7 +308,7 @@ export const UpdateReportModalButton = ({ onClick={() => handleSubmit(false)} loading={isPending} disabled={isPending} - color="teal" + color="blue" style={{ flex: 7 }} {...testId("report-generate-button")} > diff --git a/echo/frontend/src/routes/project/report/ProjectReportRoute.tsx b/echo/frontend/src/routes/project/report/ProjectReportRoute.tsx index 97a3b5d33..88e81526b 100644 --- a/echo/frontend/src/routes/project/report/ProjectReportRoute.tsx +++ b/echo/frontend/src/routes/project/report/ProjectReportRoute.tsx @@ -545,7 +545,7 @@ function ScheduledReportView({ loading={isRescheduling} disabled={!newDate || isRescheduling} fullWidth - color="teal" + color="blue" > Confirm reschedule @@ -989,7 +989,7 @@ export const ProjectReportRoute = () => { } checked={data.status === "published"} - color="teal" + color="blue" size="sm" onChange={(e) => { const isPublishing = e.target.checked; From 2e7569cf4ebb3b2ae2008070f3c6b81e6d0c64fc Mon Sep 17 00:00:00 2001 From: Sameer Pashikanti Date: Fri, 3 Apr 2026 10:38:46 +0000 Subject: [PATCH 02/17] feat: remove community marketplace code (endpoints, hooks, UI, API client) --- .../src/components/chat/ChatTemplatesMenu.tsx | 23 - .../src/components/chat/CommunityTab.tsx | 144 ------ .../components/chat/CommunityTemplateCard.tsx | 159 ------- .../components/chat/PublishTemplateForm.tsx | 184 -------- .../src/components/chat/TemplatesModal.tsx | 215 +-------- .../chat/hooks/useCommunityTemplates.ts | 107 ----- .../components/chat/hooks/useUserTemplates.ts | 1 - echo/frontend/src/lib/api.ts | 75 ---- .../routes/project/chat/ProjectChatRoute.tsx | 32 -- echo/server/dembrane/api/template.py | 418 ------------------ 10 files changed, 11 insertions(+), 1347 deletions(-) delete mode 100644 echo/frontend/src/components/chat/CommunityTab.tsx delete mode 100644 echo/frontend/src/components/chat/CommunityTemplateCard.tsx delete mode 100644 echo/frontend/src/components/chat/PublishTemplateForm.tsx delete mode 100644 echo/frontend/src/components/chat/hooks/useCommunityTemplates.ts diff --git a/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx b/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx index 7765791b4..34bec6e01 100644 --- a/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx +++ b/echo/frontend/src/components/chat/ChatTemplatesMenu.tsx @@ -71,25 +71,12 @@ type ChatTemplatesMenuProps = { // AI suggestions toggle hideAiSuggestions?: boolean; onToggleAiSuggestions?: (hide: boolean) => void; - // Favorites - favoriteTemplateIds?: Set; - onToggleFavorite?: (promptTemplateId: string, isFavorited: boolean) => void; // External open control externalOpen?: boolean; onExternalClose?: () => void; // Save as template prefill saveAsTemplateContent?: string | null; onClearSaveAsTemplate?: () => void; - // Community publish context - userTemplateDetails?: Array<{ - id: string; - is_public: boolean; - star_count: number; - copied_from: string | null; - author_display_name: string | null; - }>; - defaultLanguage?: string; - userName?: string | null; }; // Reusable chip for both dynamic suggestions and pinned templates @@ -175,13 +162,8 @@ export const ChatTemplatesMenu = ({ isSavingQuickAccess = false, hideAiSuggestions = false, onToggleAiSuggestions, - favoriteTemplateIds = new Set(), - onToggleFavorite, externalOpen = false, onExternalClose, - userTemplateDetails = [], - defaultLanguage, - userName, saveAsTemplateContent, onClearSaveAsTemplate, }: ChatTemplatesMenuProps) => { @@ -398,11 +380,6 @@ export const ChatTemplatesMenu = ({ isSavingQuickAccess={isSavingQuickAccess} hideAiSuggestions={hideAiSuggestions} onToggleAiSuggestions={onToggleAiSuggestions} - favoriteTemplateIds={favoriteTemplateIds} - onToggleFavorite={onToggleFavorite} - userTemplateDetails={userTemplateDetails} - defaultLanguage={defaultLanguage} - userName={userName} saveAsTemplateContent={saveAsTemplateContent} onClearSaveAsTemplate={onClearSaveAsTemplate} /> diff --git a/echo/frontend/src/components/chat/CommunityTab.tsx b/echo/frontend/src/components/chat/CommunityTab.tsx deleted file mode 100644 index 928805ddf..000000000 --- a/echo/frontend/src/components/chat/CommunityTab.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { t } from "@lingui/core/macro"; -import { Trans } from "@lingui/react/macro"; -import { - Badge, - Group, - ScrollArea, - Select, - Stack, - Text, -} from "@mantine/core"; -import { useDebouncedValue } from "@mantine/hooks"; -import { useState } from "react"; -import { CommunityTemplateCard } from "./CommunityTemplateCard"; -import { - useCommunityTemplates, - useCopyTemplate, - useMyCommunityStars, - useToggleStar, -} from "./hooks/useCommunityTemplates"; - -const ALLOWED_TAGS = [ - "Workshop", - "Interview", - "Focus Group", - "Meeting", - "Research", - "Community", - "Education", - "Analysis", -]; - -const getSortOptions = () => [ - { value: "newest", label: t`Newest` }, - { value: "most_starred", label: t`Most popular` }, - { value: "most_used", label: t`Most used` }, -]; - -type CommunityTabProps = { - searchQuery: string; -}; - -export const CommunityTab = ({ searchQuery }: CommunityTabProps) => { - const [selectedTag, setSelectedTag] = useState(null); - const [sortBy, setSortBy] = useState("newest"); - const [expandedId, setExpandedId] = useState(null); - const sortOptions = getSortOptions(); - - const [debouncedSearch] = useDebouncedValue(searchQuery, 300); - - const communityQuery = useCommunityTemplates({ - search: debouncedSearch || undefined, - tag: selectedTag ?? undefined, - sort: sortBy as "newest" | "most_starred" | "most_used", - }); - - const starsQuery = useMyCommunityStars(); - const toggleStarMutation = useToggleStar(); - const copyMutation = useCopyTemplate(); - - const starredIds = starsQuery.data ?? new Set(); - const templates = communityQuery.data ?? []; - - return ( - - {/* Filters row */} - - - - setSelectedTag(null)} - > - All - - {ALLOWED_TAGS.map((tag) => ( - - setSelectedTag(selectedTag === tag ? null : tag) - } - > - {tag} - - ))} - - -