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,
|
Award,
|
||||||
Share2,
|
Share2,
|
||||||
Repeat,
|
Repeat,
|
||||||
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { ComponentType } from "react";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Phase 10 — single source-of-truth for Studio workshops.
|
* Phase 10 — single source-of-truth for Studio workshops.
|
||||||
|
|
@ -35,7 +35,7 @@ export interface WorkshopDefinition {
|
||||||
/** Inter 400 silver — single line. */
|
/** Inter 400 silver — single line. */
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
/** Lucide icon component, rendered at 32x32 in volt, top-right of the card. */
|
/** 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
|
* Key used by StudioWorkshopDetail to look up the concrete generator
|
||||||
* component. Phase 10 does not rewrite any generator internals — each
|
* component. Phase 10 does not rewrite any generator internals — each
|
||||||
|
|
|
||||||
|
|
@ -1,155 +1,76 @@
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
import { useLocation, useNavigate } from "@/lib/router";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { WorkshopGrid } from "../components/studio/WorkshopGrid";
|
||||||
import { DiagramGeneratePanel } from "../components/DiagramGeneratePanel";
|
import { StudioPromptBar } from "../components/studio/StudioPromptBar";
|
||||||
import { IconGeneratePanel } from "../components/IconGeneratePanel";
|
import { WORKSHOPS, type WorkshopSlug } from "../components/studio/workshops";
|
||||||
import { WallpaperGeneratePanel } from "../components/WallpaperGeneratePanel";
|
import { StudioWorkshopDetail } from "./StudioWorkshopDetail";
|
||||||
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";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
export function ContentStudio() {
|
||||||
const { selectedCompanyId } = useCompany();
|
const location = useLocation();
|
||||||
const companyId = selectedCompanyId ?? "";
|
const navigate = useNavigate();
|
||||||
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);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const workshopSlug = matchWorkshopSlug(location.pathname);
|
||||||
if (themeJob.status === "done" && themeJob.resultAssetId && companyId) {
|
|
||||||
fetch(`/api/companies/${companyId}/assets/${themeJob.resultAssetId}`)
|
if (workshopSlug) {
|
||||||
.then((r) => r.json())
|
return <StudioWorkshopDetail slug={workshopSlug} />;
|
||||||
.then((data) => setThemeBundle(data as typeof themeBundle))
|
}
|
||||||
.catch(() => {});
|
|
||||||
}
|
const handleSelectWorkshop = (slug: WorkshopSlug) => {
|
||||||
}, [themeJob.status, themeJob.resultAssetId, companyId]);
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-6 p-6">
|
<div className="mx-auto w-full max-w-[1200px] px-6 py-8">
|
||||||
<h1 className="text-xl font-semibold">Content Studio</h1>
|
<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">
|
<WorkshopGrid workshops={WORKSHOPS} onSelect={handleSelectWorkshop} />
|
||||||
<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>
|
|
||||||
|
|
||||||
<TabsContent value="diagrams" className="mt-4">
|
<div className="mt-12">
|
||||||
{companyId ? (
|
<StudioPromptBar
|
||||||
<DiagramGeneratePanel companyId={companyId} />
|
onClassified={handleClassified}
|
||||||
) : (
|
onFallbackToAssistant={handleFallbackToAssistant}
|
||||||
<p className="text-sm text-muted-foreground">Select a company to get started.</p>
|
/>
|
||||||
)}
|
</div>
|
||||||
</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>
|
</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 { ArrowLeft, Save, Download, MessageCircle } from "lucide-react";
|
||||||
import { useNavigate, useLocation } from "@/lib/router";
|
import { useNavigate, useLocation } from "@/lib/router";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
|
|
@ -17,7 +17,6 @@ import { ThemePreviewPanel } from "../components/ThemePreviewPanel";
|
||||||
import { ThemeExportTabs } from "../components/ThemeExportTabs";
|
import { ThemeExportTabs } from "../components/ThemeExportTabs";
|
||||||
import { ThemeApplyConfirmDialog } from "../components/ThemeApplyConfirmDialog";
|
import { ThemeApplyConfirmDialog } from "../components/ThemeApplyConfirmDialog";
|
||||||
import { useContentJob } from "../hooks/useContentJob";
|
import { useContentJob } from "../hooks/useContentJob";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
interface StudioWorkshopDetailProps {
|
interface StudioWorkshopDetailProps {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue