All docs/Feature-Specific

docs/architecture/interview-editor-architecture.md

Interview Editor Architecture

Status: Living document — last updated 2026-02-06

Purpose: Document the shared flow editor system across hello, admin, and library apps. All six structural problems (A–F) have been resolved in the refactor described below.

Table of Contents


1. Component Hierarchy

The interview editor is shared across three apps (library, admin, hello) via the @repo/app-hello package. Each app provides a thin wrapper that wires server actions and auth, then delegates to shared components.

Library and admin app wrappers use the shared useFlowEditHandlers hook to eliminate duplication of asset upload, image generation, and Unsplash handler setup.

Apps (thin shells — Next.js routes + server actions)
├── apps/library/app/edit/[flowId]/
│   ├── page.tsx              → SSR: auth, load FlowConfig, convert
│   └── EditFlowClient.tsx    → Uses useFlowEditHandlers + FlowEditClient
│
├── apps/admin/app/flows/[videoFlowId]/edit/
│   ├── page.tsx                  → SSR: admin auth, load, convert
│   └── AdminEditFlowClient.tsx   → Uses useFlowEditHandlers + FlowEditClient
│
└── apps/hello/app/d/[draftId]/studio/
    ├── page.tsx              → SSR: draft auth, load from Redis
    └── StudioStepForm.tsx    → Different integration path

@repo/app-hello/web (shared package)
├── FlowEditClient.tsx
│   ├── Translation orchestration (handleAddLanguage,
│   │   handleTranslateMissing, handleTranslateAllMissing)
│   │   → Uses registry-based functions (extractTranslatableContent,
│   │     buildFullLanguageTranslationUpdate, buildMissingContent, etc.)
│   │   → Updates store directly via applyFullLanguageTranslation
│   ├── Question generation orchestration
│   │   → Uses extractQuestionTexts, appendQuestions
│   ├── Publish orchestration (build FlowConfig → server action)
│   ├── Analytics tracking
│   └── Renders FlowStudioEditor
│
├── FlowStudioEditor.tsx
│   ├── DraftStore initialization & auto-save hookup
│   ├── Language add/remove (delegates to FlowEditClient callback)
│   ├── Translation dialog (pre-publish check)
│   ├── Version history panel
│   ├── Asset upload / image generation (delegates to store methods)
│   └── Renders StudioStep + WithFlowPreview + OnboardingPanel
│
├── DraftStore (Zustand — global singleton)
│   ├── draft: OnboardingDraft (SINGLE SOURCE OF TRUTH)
│   ├── pendingUpdate: DraftUpdate (accumulated changes)
│   ├── isDirty: boolean
│   ├── _saveGeneration: number (incremented on every updateDraft)
│   ├── editingLanguage, activeTab (UI state)
│   └── Actions: updateDraft, updateQuestions, updateGenerated,
│        updateOrgProfile (partial merge),
│        updateQuestion, updateQuestionRequired, resetQuestionRequired,
│        updateQuestionHint, toggleQuestionHint,
│        addQuestion, removeQuestion, reorderQuestions,
│        updateRewardTitle, updateRewardItem,
│        addAvailableUsp, removeAvailableUsp (cascade),
│        editAvailableUsp (cascade + dup check), toggleSelectedUsp,
│        applyFullLanguageTranslation, appendQuestions,
│        addBackgroundImage, removeBackgroundImage,
│        reorderBackgroundImages, markClean(generation?), etc.
│
├── useAutoSave hook
│   ├── Subscribes to DraftStore changes
│   ├── Debounces 1000ms
│   ├── Captures _saveGeneration before save
│   ├── Calls onSave(draftId, pendingUpdate)
│   └── On success → markClean(generation) → skips if stale
│
├── useFlowEditHandlers hook (shared wrapper logic)
│   ├── Asset upload setup (useAssetUpload)
│   ├── Unsplash handlers (createUnsplashHandlers)
│   └── Returns: assetUpload, unsplashHandlers, imageGeneration
│
└── Translation Pipeline (registry-based)
    ├── TRANSLATABLE_FIELDS registry (single source of truth)
    ├── extractTranslatableContent (reads draft directly, no FlowConfig)
    ├── buildGeneratedTranslationUpdate (applies with !== undefined)
    ├── buildFullLanguageTranslationUpdate (combines genUpdate + questions + rewards)
    ├── extractQuestionTexts (utility for question generation context)
    ├── getRegistryMissingTranslations (registry-driven detection)
    ├── buildMissingContent (extracts only missing items)
    └── buildMissingTranslationUpdates (applies missing translations)

Mermaid: Component Ownership


2. Data Flow

Edit → Save → Publish

Translation Flow: Add Language

FlowEditClient owns the full translation orchestration — it extracts, translates, and applies results to the store in one operation. FlowStudioEditor simply delegates to the callback and receives a success/error status.


3. Per-App Integration

AspectHelloLibraryAdmin
Draft sourceRedis (OnboardingDraftRegistry)FlowConfig → buildDraftFromFlowConfigSame as library
Auto-save targetRedis (persists)No-op (client-only)No-op (client-only)
Publish actionCreates NEW flowUpdates existing (versioned)Updates existing (versioned)
Version historyNoYesYes
Auth modelHMAC-signed cookieClerk org ownershipClerk admin role
Client wrapperStudioStepForm (different)EditFlowClient (uses useFlowEditHandlers)AdminEditFlowClient (uses useFlowEditHandlers)
Translation actiontranslateContentActiontranslateFlowContentActiontranslateAdminFlowContentAction

4. Translation Pipeline (Registry-Based)

TRANSLATABLE_FIELDS Registry

The registry is the single source of truth for all simple translatable fields. Adding a new translatable field requires adding one line to the registry — extraction, detection, and application all derive from it automatically.

// packages/app-hello/src/web/utils/translatable-field-registry.ts

export const TRANSLATABLE_FIELDS: readonly TranslatableField[] = [
  { key: "welcomeTitle",       path: (d) => d.generated.welcomeTitle },
  { key: "welcomeSubtitle",    path: (d) => d.generated.welcomeSubtitle },
  { key: "welcomeDescription", path: (d) => d.generated.welcomeDescription },
  { key: "tipsTitle",          path: (d) => d.generated.tipsTitle },
  { key: "confirmTitle",       path: (d) => d.generated.confirmTitle },
  { key: "rewardsTitle",       path: (d) => d.generated.rewardsTitle, guard: (d) => !!d.flow.rewardsEnabled },
  { key: "rewardsSubtitle",    path: (d) => d.generated.rewardsSubtitle, guard: (d) => !!d.flow.rewardsEnabled },
] as const;

Functions Derived from Registry

FunctionPurpose
extractTranslatableContent(draft, lang)Extract all translatable content from draft for a given language. Reads draft directly — no FlowConfig round-trip.
buildGeneratedTranslationUpdate(draft, lang, translated)Build Partial<DraftGenerated> from translated content. Uses !== undefined — empty strings are preserved.
buildFullLanguageTranslationUpdate(draft, lang, translated)Combined operation: builds genUpdate + maps questions + maps rewards from translated content. Returns { genUpdate, questions?, rewards? } for use with applyFullLanguageTranslation.
extractQuestionTexts(draft)Extract question texts in the default language. Used as context for question generation/regeneration.
getRegistryMissingTranslations(draft, lang)Detect all fields/questions/rewards/customFields missing translations for a language.
registryHasMissingTranslations(draft, lang)Boolean: does this language have any missing translations?
getRegistryMissingTranslationCount(draft, lang)Count of all missing items.
getRegistryLanguagesWithMissingTranslations(draft)List of languages (excluding default) that have missing translations.
buildMissingContent(draft, lang)Extract content for only the missing items (for translate-missing workflow).
buildMissingTranslationUpdates(draft, lang, translated, missing)Apply translated content for only the missing items.

Backward Compatibility

translationUtils.ts re-exports registry-backed functions under the original names (getMissingTranslations, hasMissingTranslations, etc.) for backward compatibility. FlowConfig-based functions (extractContentForTranslation, mergeTranslationsIntoFlowConfig, hasContentToTranslate) remain for non-draft callers.

Adding a New Translatable Field

  1. Add one entry to TRANSLATABLE_FIELDS in translatable-field-registry.ts
  2. If the field has a guard condition (like rewardsEnabled), add the guard function
  3. Done — all extraction, detection, application, and status hooks pick it up automatically

5. Resolved Structural Problems

All six structural problems identified in the original architecture review have been resolved:

Problem A: Translation mapping drops fields silently ✅ FIXED

Solution: The registry-based functions (buildGeneratedTranslationUpdate, buildMissingTranslationUpdates) use !== undefined checks by construction. All field iteration happens through registry loops, so no field can accidentally use truthiness checks.

Problem B: welcomeDescription not wired through pipeline ✅ FIXED

Solution: welcomeDescription is in the TRANSLATABLE_FIELDS registry. All six pipeline points (extraction, add-language application, translate-missing extraction, translate-missing application, store helper, detection) derive from the registry and include it automatically.

Problem C: Auto-save race condition with markClean ✅ FIXED

Solution: DraftStore._saveGeneration counter increments on every updateDraft. markClean(savedGeneration?) skips clearing if the generation has changed since the save started. useAutoSave captures the generation before saving and passes it to markClean.

Problem D: 6-place changes per translatable field ✅ FIXED

Solution: The TRANSLATABLE_FIELDS registry is the single source of truth. Adding a new field is one line. All pipeline functions derive from the registry.

Problem E: Duplicated app wrappers ✅ FIXED

Solution: useFlowEditHandlers hook in @repo/app-hello/web extracts the shared asset upload, image generation, and Unsplash handler setup. Both EditFlowClient (library) and AdminEditFlowClient (admin) use this hook, eliminating ~40 lines of duplicated callback code each.

Problem F: Wasteful Draft→FlowConfig→extract round-trip ✅ FIXED

Solution: extractTranslatableContent(draft, lang) reads directly from the draft object. No buildFlowConfigFromDraftextractContentForTranslation conversion needed.


6. Key Files

FileRole
packages/app-hello/src/web/utils/translatable-field-registry.tsTRANSLATABLE_FIELDS registry — single source of truth for translatable fields. All translation functions.
packages/app-hello/src/web/utils/__tests__/translatable-field-registry.test.tsTests for registry-based translation functions (19 tests)
packages/app-hello/src/web/components/FlowEditClient.tsxTranslation orchestration, publish, analytics — uses registry functions
packages/app-hello/src/web/components/FlowStudioEditor.tsxStore init, auto-save, language add/remove, UI
packages/app-hello/src/web/stores/draft-store.tsDraftStore — state, mutations, generation-aware markClean
packages/app-hello/src/web/hooks/use-auto-save.tsDebounced auto-save — captures generation before save
packages/app-hello/src/web/hooks/use-flow-edit-handlers.tsShared wrapper hook for library/admin app wrappers
packages/app-hello/src/web/utils/translationUtils.tsBackward-compatible re-exports + FlowConfig-based functions
packages/app-hello/src/web/stores/draft-hooks.tsTranslation status hooks — delegates to registry
packages/app-hello/src/web/types/onboarding.tsTranslationContent type
apps/library/app/edit/[flowId]/EditFlowClient.tsxLibrary app wrapper (uses useFlowEditHandlers)
apps/admin/app/flows/[videoFlowId]/edit/AdminEditFlowClient.tsxAdmin app wrapper (uses useFlowEditHandlers)