/** * InteractiveEditor — tabbed editor for one interactive experience. * * UI-2a placeholder — just renders the project header and a tab * strip with the six future sub-panels disabled. The real panel * bodies land in UI-3 / UI-4 / UI-5: * * UI-3: Graph — DAG visualizer over ix_nodes + ix_edges * Catalog — action_catalog CRUD with level/scheme gates * UI-4: Rules — personalization-rule DSL builder * QA — run checks, render issues * Publish — channel picker + publish flow * UI-5: Analytics — experience + session rollups * * The editor owns the currently-selected tab in local state; * switching tabs never unmounts the project header so the * breadcrumb + status badge stay stable. */ import React, { useMemo, useState } from "react"; import { BarChart3, CheckSquare, GitBranch, ListChecks, Play, Send, Sparkles, } from "lucide-react"; import { createInteractiveApi } from "./interactive/api"; import { AnalyticsPanel } from "./interactive/AnalyticsPanel"; import { CatalogPanel } from "./interactive/CatalogPanel"; import { GraphPanel } from "./interactive/GraphPanel"; import { PublishPanel } from "./interactive/PublishPanel"; import { QAPanel } from "./interactive/QAPanel"; import { RulesPanel } from "./interactive/RulesPanel"; import type { Experience } from "./interactive/types"; import { ErrorBanner, StatusBadge, useAsyncResource, } from "./interactive/ui"; export interface InteractiveEditorProps { backendUrl: string; apiKey?: string; projectId: string; /** * Optional callback to launch the player for this experience. * Shown as a prominent "Play" button in the editor header when * provided. Host decides routing (App.tsx wires it to the same * ``interactivePlayId`` state the landing grid Play CTA uses). */ onPlay?: () => void; } type TabKey = "graph" | "catalog" | "rules" | "qa" | "publish" | "analytics"; const TABS: Array<{ key: TabKey; label: string; icon: React.ReactNode; description: string }> = [ { key: "graph", label: "Graph", icon: , description: "Scenes, branches, and transitions" }, { key: "catalog", label: "Catalog", icon: , description: "Actions viewers can take" }, { key: "rules", label: "Rules", icon: , description: "Personalization routing" }, { key: "qa", label: "QA", icon: , description: "Readiness checks" }, { key: "publish", label: "Publish", icon: , description: "Channels + versions" }, { key: "analytics", label: "Analytics", icon: , description: "Viewer engagement" }, ]; export function InteractiveEditor({ backendUrl, apiKey, projectId, onPlay, }: InteractiveEditorProps) { const api = useMemo(() => createInteractiveApi(backendUrl, apiKey), [backendUrl, apiKey]); const [activeTab, setActiveTab] = useState("graph"); const exp = useAsyncResource( (signal) => api.getExperience(projectId, signal), [api, projectId], ); // Same shell pattern as the wizard: project header + tab strip // are pinned; only the active panel scrolls. Keeps the tab row // visible at all times even when a panel (analytics cards, // catalog table) is tall. return (
{exp.error ? ( ) : exp.loading || !exp.data ? ( ) : ( )}
{activeTab === "graph" && } {activeTab === "catalog" && } {activeTab === "rules" && } {activeTab === "qa" && } {activeTab === "publish" && } {activeTab === "analytics" && }
); } // ──────────────────────────────────────────────────────────────── // Project header (title, status, description) // ──────────────────────────────────────────────────────────────── function ProjectHeader({ experience, onPlay, }: { experience: Experience; onPlay?: () => void; }) { return (

{experience.title || "(untitled)"}

{(experience.description || experience.objective) && (

{experience.description || experience.objective}

)}
mode: {experience.experience_mode} profile: {experience.policy_profile_id} {typeof experience.branch_count === "number" && ( {experience.branch_count} {experience.branch_count === 1 ? "branch" : "branches"} )} {typeof experience.max_depth === "number" && ( depth {experience.max_depth} )}
{onPlay && ( )}
); } function ProjectHeaderSkeleton() { return (
); } // ──────────────────────────────────────────────────────────────── // Tab strip // ──────────────────────────────────────────────────────────────── function TabStrip({ active, onChange, }: { active: TabKey; onChange: (key: TabKey) => void; }) { return ( ); }