/** * 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 { ErrorBanner, StatusBadge, useAsyncResource, } from "./interactive/ui"; const TABS = [ { 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, }) { 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, }) { 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, }) { return (); }