refactor(nexus): rewire content studio to workshop grid
This commit is contained in:
parent
f20fd0ec8d
commit
cd6c172d48
3 changed files with 69 additions and 149 deletions
|
|
@ -7,8 +7,8 @@ import {
|
|||
Award,
|
||||
Share2,
|
||||
Repeat,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
/**
|
||||
* Phase 10 — single source-of-truth for Studio workshops.
|
||||
|
|
@ -35,7 +35,7 @@ export interface WorkshopDefinition {
|
|||
/** Inter 400 silver — single line. */
|
||||
subtitle: string;
|
||||
/** Lucide icon component, rendered at 32x32 in volt, top-right of the card. */
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
icon: LucideIcon;
|
||||
/**
|
||||
* Key used by StudioWorkshopDetail to look up the concrete generator
|
||||
* component. Phase 10 does not rewrite any generator internals — each
|
||||
|
|
|
|||
|
|
@ -1,155 +1,76 @@
|
|||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { DiagramGeneratePanel } from "../components/DiagramGeneratePanel";
|
||||
import { IconGeneratePanel } from "../components/IconGeneratePanel";
|
||||
import { WallpaperGeneratePanel } from "../components/WallpaperGeneratePanel";
|
||||
import { SocialPostPanel } from "../components/SocialPostPanel";
|
||||
import { DocumentGeneratePanel } from "../components/DocumentGeneratePanel";
|
||||
import { BrandKitPanel } from "../components/BrandKitPanel";
|
||||
import { PresentationPanel } from "../components/PresentationPanel";
|
||||
import { ThemeSeedInput } from "../components/ThemeSeedInput";
|
||||
import { ThemePaletteGrid, type PaletteRole } from "../components/ThemePaletteGrid";
|
||||
import { ThemePreviewPanel } from "../components/ThemePreviewPanel";
|
||||
import { ThemeExportTabs } from "../components/ThemeExportTabs";
|
||||
import { ThemeApplyConfirmDialog } from "../components/ThemeApplyConfirmDialog";
|
||||
import { useContentJob } from "../hooks/useContentJob";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useLocation, useNavigate } from "@/lib/router";
|
||||
import { WorkshopGrid } from "../components/studio/WorkshopGrid";
|
||||
import { StudioPromptBar } from "../components/studio/StudioPromptBar";
|
||||
import { WORKSHOPS, type WorkshopSlug } from "../components/studio/workshops";
|
||||
import { StudioWorkshopDetail } from "./StudioWorkshopDetail";
|
||||
|
||||
/**
|
||||
* Phase 10 — Studio home.
|
||||
*
|
||||
* Replaces the legacy 7-tab ContentStudio with a grid of 8 workshop cards
|
||||
* plus a freeform prompt bar at the bottom. Clicking a card navigates to
|
||||
* `/<prefix>/content-studio/<slug>`. The prompt bar routes classifiable
|
||||
* prompts to the matching workshop (with the prompt as a query param) and
|
||||
* falls through to the Assistant for anything unclassified.
|
||||
*
|
||||
* App.tsx currently only exposes a single `content-studio` route with no
|
||||
* sub-route param (Phase 10 is forbidden from editing App.tsx). To render
|
||||
* StudioWorkshopDetail without a dedicated sub-route, this component
|
||||
* path-matches `location.pathname` and conditionally renders the detail
|
||||
* view. The controller should replace this with a proper
|
||||
* `content-studio/:workshopSlug` route after Wave 2.
|
||||
*/
|
||||
export function ContentStudio() {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const companyId = selectedCompanyId ?? "";
|
||||
const themeJob = useContentJob(companyId);
|
||||
const [showApplyDialog, setShowApplyDialog] = useState(false);
|
||||
const [seedColor, setSeedColor] = useState("var(--primary)");
|
||||
const [themeBundle, setThemeBundle] = useState<{
|
||||
palette: PaletteRole[];
|
||||
exports: { css: string; tailwind: string; vscode: string; json: string };
|
||||
} | null>(null);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (themeJob.status === "done" && themeJob.resultAssetId && companyId) {
|
||||
fetch(`/api/companies/${companyId}/assets/${themeJob.resultAssetId}`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => setThemeBundle(data as typeof themeBundle))
|
||||
.catch(() => {});
|
||||
}
|
||||
}, [themeJob.status, themeJob.resultAssetId, companyId]);
|
||||
const workshopSlug = matchWorkshopSlug(location.pathname);
|
||||
|
||||
if (workshopSlug) {
|
||||
return <StudioWorkshopDetail slug={workshopSlug} />;
|
||||
}
|
||||
|
||||
const handleSelectWorkshop = (slug: WorkshopSlug) => {
|
||||
navigate(`content-studio/${slug}`);
|
||||
};
|
||||
|
||||
const handleClassified = (slug: WorkshopSlug, prefilledPrompt: string) => {
|
||||
navigate(`content-studio/${slug}?prompt=${encodeURIComponent(prefilledPrompt)}`);
|
||||
};
|
||||
|
||||
const handleFallbackToAssistant = (prompt: string) => {
|
||||
navigate(`assistant?prompt=${encodeURIComponent(prompt)}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<h1 className="text-xl font-semibold">Content Studio</h1>
|
||||
<div className="mx-auto w-full max-w-[1200px] px-6 py-8">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-[14px] font-semibold uppercase tracking-[0.1em] text-primary">
|
||||
STUDIO
|
||||
</h1>
|
||||
<p className="mt-2 max-w-[560px] text-[14px] text-muted-foreground">
|
||||
Eight workshops. Pick one or describe what you need.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<Tabs defaultValue="diagrams" className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="diagrams">Diagrams</TabsTrigger>
|
||||
<TabsTrigger value="icons">Icons</TabsTrigger>
|
||||
<TabsTrigger value="themes">Themes</TabsTrigger>
|
||||
<TabsTrigger value="wallpapers">Wallpapers</TabsTrigger>
|
||||
<TabsTrigger value="social">Social</TabsTrigger>
|
||||
<TabsTrigger value="documents">Documents</TabsTrigger>
|
||||
<TabsTrigger value="brand">Brand</TabsTrigger>
|
||||
<TabsTrigger value="presentations">Presentations</TabsTrigger>
|
||||
</TabsList>
|
||||
<WorkshopGrid workshops={WORKSHOPS} onSelect={handleSelectWorkshop} />
|
||||
|
||||
<TabsContent value="diagrams" className="mt-4">
|
||||
{companyId ? (
|
||||
<DiagramGeneratePanel companyId={companyId} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Select a company to get started.</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="icons" className="mt-4">
|
||||
{companyId ? (
|
||||
<IconGeneratePanel companyId={companyId} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Select a company to get started.</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="themes" className="mt-4">
|
||||
{companyId ? (
|
||||
<div className="flex flex-col gap-6">
|
||||
<ThemeSeedInput
|
||||
value={seedColor}
|
||||
onChange={setSeedColor}
|
||||
/>
|
||||
<button
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
disabled={themeJob.status === "running" || themeJob.status === "queued"}
|
||||
onClick={() => {
|
||||
setThemeBundle(null);
|
||||
themeJob.submit("theme-palette", { seedColor });
|
||||
}}
|
||||
>
|
||||
{themeJob.status === "running" || themeJob.status === "queued" ? "Generating..." : "Generate Palette"}
|
||||
</button>
|
||||
{themeBundle && (
|
||||
<>
|
||||
<ThemePaletteGrid palette={themeBundle.palette} />
|
||||
<ThemePreviewPanel palette={themeBundle.palette} variant="light" />
|
||||
<ThemeExportTabs exports={themeBundle.exports} />
|
||||
<button
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
onClick={() => setShowApplyDialog(true)}
|
||||
>
|
||||
Apply to Nexus
|
||||
</button>
|
||||
<ThemeApplyConfirmDialog
|
||||
open={showApplyDialog}
|
||||
onConfirm={() => {
|
||||
setShowApplyDialog(false);
|
||||
}}
|
||||
onCancel={() => setShowApplyDialog(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Select a company to get started.</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="wallpapers" className="mt-4">
|
||||
{companyId ? (
|
||||
<WallpaperGeneratePanel companyId={companyId} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Select a company to get started.</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="social" className="mt-4">
|
||||
{companyId ? (
|
||||
<SocialPostPanel companyId={companyId} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Select a company to get started.</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="documents" className="mt-4">
|
||||
{companyId ? (
|
||||
<DocumentGeneratePanel companyId={companyId} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Select a company to get started.</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="brand" className="mt-4">
|
||||
{companyId ? (
|
||||
<BrandKitPanel companyId={companyId} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Select a company to get started.</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="presentations" className="mt-4">
|
||||
{companyId ? (
|
||||
<PresentationPanel companyId={companyId} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Select a company to get started.</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<div className="mt-12">
|
||||
<StudioPromptBar
|
||||
onClassified={handleClassified}
|
||||
onFallbackToAssistant={handleFallbackToAssistant}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `/:companyPrefix/content-studio/<slug>` out of a pathname. Returns
|
||||
* null for the Studio home. This is the Phase 10 workaround for the
|
||||
* missing `content-studio/:workshopSlug` route in App.tsx.
|
||||
*/
|
||||
export function matchWorkshopSlug(pathname: string): string | null {
|
||||
const match = pathname.match(/\/content-studio\/([^/?#]+)/);
|
||||
return match ? match[1]! : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, type ComponentType } from "react";
|
||||
import { useEffect, useMemo, useState, type ComponentType } from "react";
|
||||
import { ArrowLeft, Save, Download, MessageCircle } from "lucide-react";
|
||||
import { useNavigate, useLocation } from "@/lib/router";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
|
|
@ -17,7 +17,6 @@ import { ThemePreviewPanel } from "../components/ThemePreviewPanel";
|
|||
import { ThemeExportTabs } from "../components/ThemeExportTabs";
|
||||
import { ThemeApplyConfirmDialog } from "../components/ThemeApplyConfirmDialog";
|
||||
import { useContentJob } from "../hooks/useContentJob";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface StudioWorkshopDetailProps {
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue