import { appApiPath } from "@agent-native/core/client/api-path"; import { useActionQuery } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { openAgentSidebar } from "@agent-native/core/client/navigation"; import { withBuilderUtmTrackingParams } from "@agent-native/core/shared"; import { useSetPageTitle, useSetHeaderActions, } from "@agent-native/toolkit/app-shell"; import { IconArrowLeft, IconBrandGithub, IconBrandFigma, IconUpload, IconFolder, IconX, IconWorld, IconFileDescription, IconPhoto, IconPalette, IconCheck, IconExternalLink, } from "@tabler/icons-react"; import { useState, useCallback, useRef, useMemo, useEffect } from "react"; import { Link, useNavigate, useSearchParams } from "react-router"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import { Textarea } from "@/components/ui/textarea"; import { sendToDesignAgentChat } from "@/lib/agent-chat"; interface GitHubLink { id: string; url: string; } interface UploadedFile { id: string; name: string; type: string; size: number; textContent?: string; } interface BuilderIndexResult { ok: boolean; source: "builder"; suggestedTitle: string; projectId: string; jobId: string; designSystemId: string; builderUrl: string; status: "in-progress"; localDesignSystemId?: string; uploadedFileCount?: number; instructions?: string; } async function readJsonResponse(res: Response): Promise { const text = await res.text(); if (!text.trim()) return {}; try { return JSON.parse(text); } catch { return { error: res.ok ? "The server returned an invalid response." : text.slice(0, 240), }; } } export default function DesignSystemSetup() { const t = useT(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const sourceId = searchParams.get("source") ?? ""; const [companyInfo, setCompanyInfo] = useState(""); const [websiteUrl, setWebsiteUrl] = useState(""); const [websiteUrls, setWebsiteUrls] = useState([]); const [githubUrl, setGithubUrl] = useState(""); const [githubLinks, setGithubLinks] = useState([]); const [codeFiles, setCodeFiles] = useState([]); const [docFiles, setDocFiles] = useState([]); const [imageFiles, setImageFiles] = useState([]); const [assets, setAssets] = useState([]); const [selectedProjectId, setSelectedProjectId] = useState(""); const [notes, setNotes] = useState(""); const [customInstructions, setCustomInstructions] = useState(""); const [validationError, setValidationError] = useState(null); const docInputRef = useRef(null); const imageInputRef = useRef(null); const assetInputRef = useRef(null); const codeInputRef = useRef(null); const appliedSourceIdRef = useRef(null); const { data: designsData } = useActionQuery<{ designs: Array<{ id: string; title: string; designSystemId?: string }>; }>("list-designs"); const { data: designSystemsData } = useActionQuery<{ designSystems: Array<{ id: string; title: string }>; }>("list-design-systems"); const existingProjects = designsData?.designs ?? []; const existingSystems = designSystemsData?.designSystems ?? []; // --- Figma .fig import (Builder design-system indexing) ----------------- const realFigInputRef = useRef(null); const [builderIndexing, setBuilderIndexing] = useState(false); const [builderIndexResult, setBuilderIndexResult] = useState(null); const [builderIndexError, setBuilderIndexError] = useState( null, ); const handleBuilderIndexUpload = useCallback( async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; e.target.value = ""; if (!file) return; if (!file.name.toLowerCase().endsWith(".fig")) { setBuilderIndexError(t("designSystemSetup.errors.chooseFig")); return; } setBuilderIndexError(null); setBuilderIndexResult(null); setBuilderIndexing(true); try { const body = new FormData(); body.append("file", file); const res = await fetch( appApiPath("/api/index-design-system-with-builder"), { method: "POST", body, }, ); const json = await readJsonResponse(res); if (!res.ok || json?.error) { throw new Error(json?.error || `Upload failed (${res.status})`); } setBuilderIndexResult(json as BuilderIndexResult); } catch (err) { setBuilderIndexError( err instanceof Error ? err.message : t("designSystemSetup.errors.parseFig"), ); } finally { setBuilderIndexing(false); } }, [t], ); useEffect(() => { if (!sourceId || appliedSourceIdRef.current === sourceId) return; const sourceExists = existingSystems.some((system) => system.id === sourceId) || existingProjects.some((project) => project.id === sourceId); if (!sourceExists) return; setSelectedProjectId(sourceId); appliedSourceIdRef.current = sourceId; }, [sourceId, existingProjects, existingSystems]); const hasAnySources = useMemo(() => { return ( companyInfo.trim() || websiteUrl.trim() || websiteUrls.length > 0 || githubUrl.trim() || githubLinks.length > 0 || codeFiles.length > 0 || builderIndexResult || docFiles.length > 0 || imageFiles.length > 0 || assets.length > 0 || selectedProjectId || notes.trim() || customInstructions.trim() ); }, [ companyInfo, websiteUrl, websiteUrls, githubUrl, githubLinks, codeFiles, builderIndexResult, docFiles, imageFiles, assets, selectedProjectId, notes, customInstructions, ]); const addWebsiteUrl = useCallback(() => { const url = websiteUrl.trim(); if (!url) { setValidationError(t("designSystemSetup.errors.enterWebsite")); return; } if (!isHttpUrl(url)) { setValidationError(t("designSystemSetup.errors.websiteProtocol")); return; } setWebsiteUrls((prev) => [...prev, url]); setWebsiteUrl(""); setValidationError(null); }, [websiteUrl, t]); const addGithubLink = useCallback(() => { const url = githubUrl.trim(); if (!url) { setValidationError(t("designSystemSetup.errors.enterGithub")); return; } if (!isGithubRepoUrl(url)) { setValidationError(t("designSystemSetup.errors.githubUrl")); return; } setGithubLinks((prev) => [...prev, { id: crypto.randomUUID(), url }]); setGithubUrl(""); setValidationError(null); }, [githubUrl, t]); const removeGithubLink = useCallback((id: string) => { setGithubLinks((prev) => prev.filter((l) => l.id !== id)); }, []); const readTextFiles = useCallback( ( fileList: FileList, setter: React.Dispatch>, ) => { const newFiles: UploadedFile[] = []; const promises: Promise[] = []; Array.from(fileList).forEach((f) => { const file: UploadedFile = { id: crypto.randomUUID(), name: f.name, type: f.type, size: f.size, }; if ( f.size < 200 * 1024 && (f.name.match( /\.(css|scss|sass|less|ts|tsx|js|jsx|json|html|svg|xml|md|markdown|mdx|txt)$/i, ) || f.type.startsWith("text/")) ) { promises.push( f.text().then((text) => { file.textContent = text; }), ); } newFiles.push(file); }); Promise.all(promises).then(() => { setter((prev) => [...prev, ...newFiles]); }); }, [], ); const handleCodeUpload = useCallback( (e: React.ChangeEvent) => { if (!e.target.files) return; readTextFiles(e.target.files, setCodeFiles); e.target.value = ""; }, [readTextFiles], ); const handleDocUpload = useCallback( (e: React.ChangeEvent) => { if (!e.target.files) return; const newFiles: UploadedFile[] = Array.from(e.target.files).map((f) => ({ id: crypto.randomUUID(), name: f.name, type: f.type || f.name.split(".").pop() || "", size: f.size, })); setDocFiles((prev) => [...prev, ...newFiles]); e.target.value = ""; }, [], ); const handleImageUpload = useCallback( (e: React.ChangeEvent) => { if (!e.target.files) return; const newFiles: UploadedFile[] = Array.from(e.target.files).map((f) => ({ id: crypto.randomUUID(), name: f.name, type: f.type, size: f.size, })); setImageFiles((prev) => [...prev, ...newFiles]); e.target.value = ""; }, [], ); const handleAssetUpload = useCallback( (e: React.ChangeEvent) => { if (!e.target.files) return; const newAssets: UploadedFile[] = Array.from(e.target.files).map((f) => ({ id: crypto.randomUUID(), name: f.name, type: f.type, size: f.size, })); setAssets((prev) => [...prev, ...newAssets]); e.target.value = ""; }, [], ); const handleFolderDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (!e.dataTransfer.files) return; readTextFiles(e.dataTransfer.files, setCodeFiles); }, [readTextFiles], ); const handleContinue = useCallback(() => { if (!hasAnySources) { setValidationError(t("designSystemSetup.errors.noSources")); return; } const pendingWebsiteUrl = websiteUrl.trim(); const pendingGithubUrl = githubUrl.trim(); if (pendingWebsiteUrl && !isHttpUrl(pendingWebsiteUrl)) { setValidationError(t("designSystemSetup.errors.websiteProtocol")); return; } if (pendingGithubUrl && !isGithubRepoUrl(pendingGithubUrl)) { setValidationError(t("designSystemSetup.errors.githubUrl")); return; } const normalizedWebsiteUrls = pendingWebsiteUrl ? [...websiteUrls, pendingWebsiteUrl] : websiteUrls; const normalizedGithubLinks = pendingGithubUrl ? [...githubLinks, { id: "pending", url: pendingGithubUrl }] : githubLinks; const readableCodeFiles = codeFiles.filter((f) => f.textContent); const designMdFiles = readableCodeFiles.filter(isDesignMdFile); const builderCodeFiles = readableCodeFiles.filter( (file) => !isDesignMdFile(file), ); const unreadableCodeFiles = codeFiles.filter((f) => !f.textContent); const parts: string[] = []; parts.push( "Set up a design system from the following sources. Use Builder Design System Intelligence (DSI) as the source of truth for reusable Figma/code/design.md indexing. Analyze each source, extract design tokens (colors, fonts, spacing, borders), and create a cohesive design system.", ); if (companyInfo.trim()) { parts.push(`\n## Company / Brand\n${companyInfo.trim()}`); } if (normalizedWebsiteUrls.length > 0) { parts.push( `\n## Website URLs\nExtract design tokens from these websites:\n${normalizedWebsiteUrls.map((u) => `- ${u}`).join("\n")}\n\n**Best approach:** Call \`activate-browser\` first, then use chrome-devtools MCP tools to navigate each URL and extract computed styles (colors, fonts, spacing, CSS custom properties) via \`evaluate_script\`. This captures the real rendered design — including JS-injected styles, CSS-in-JS, and SPA content that plain HTML fetch misses. Take a screenshot too for visual reference. If Builder is not connected, fall back to \`import-from-url\` for each URL (limited to static HTML parsing).`, ); } if (normalizedGithubLinks.length > 0) { parts.push( `\n## Connect Code: GitHub Repositories\nStart Builder DSI indexing for each repository with \`index-design-system-with-builder\`:\n${normalizedGithubLinks.map((l) => `- ${l.url}`).join("\n")}\n\nBuilder is the source of truth for repo/code design-system indexing. The action also creates a local selectable proxy design system for Design flows. If Builder is not connected, stop and tell me to connect Builder from Settings instead of asking me to paste repository credentials into chat.`, ); } if (codeFiles.length > 0) { if (builderCodeFiles.length > 0) { parts.push( `\n## Connect Code: Code Files (${builderCodeFiles.length} files with content)\nStart Builder DSI indexing with \`index-design-system-with-builder\` using these files as the \`codeFiles\` argument:`, ); for (const f of builderCodeFiles) { parts.push( `\n### ${f.name}\n\`\`\`\n${f.textContent!.slice(0, 5000)}\n\`\`\``, ); } } if (designMdFiles.length > 0) { parts.push( `\n## Optional design.md (${designMdFiles.length} file${designMdFiles.length === 1 ? "" : "s"})\nPass this content as the \`designMd\` argument to \`index-design-system-with-builder\` alongside any Figma/code sources:`, ); for (const f of designMdFiles) { parts.push( `\n### ${f.name}\n\`\`\`md\n${f.textContent!.slice(0, 5000)}\n\`\`\``, ); } } if (unreadableCodeFiles.length > 0) { parts.push( `\nBinary code files (could not read):\n${unreadableCodeFiles.map((f) => `- ${f.name}`).join("\n")}`, ); } } if (builderIndexResult) { parts.push( `\n## Connect Figma: Builder-Indexed Figma File\nBuilder DSI indexing has already started.\n- Design system: ${builderIndexResult.designSystemId}\n- Local selectable design system: ${builderIndexResult.localDesignSystemId ?? "(not returned)"}\n- Project: ${builderIndexResult.projectId}\n- Job: ${builderIndexResult.jobId}\n- URL: ${builderIndexResult.builderUrl}\n\nUse Builder as the source of truth for indexed tokens, assets, components, and guidance. Do not call \`create-design-system\` again for this Builder-indexed source.`, ); } if (docFiles.length > 0) { parts.push( `\n## Documents\nExtract brand cues from these documents. Call \`import-document\` with metadata:\n${docFiles.map((f) => `- ${f.name} (${f.type}, ${formatSize(f.size)})`).join("\n")}`, ); } if (imageFiles.length > 0) { parts.push( `\n## Visual References\nUse these images to inform the design system (color palette, typography, mood):\n${imageFiles.map((f) => `- ${f.name}`).join("\n")}`, ); } if (assets.length > 0) { parts.push( `\n## Brand Assets (logos, fonts, etc.)\n${assets.map((a) => `- ${a.name} (${a.type})`).join("\n")}`, ); } if (selectedProjectId) { const project = existingProjects.find((p) => p.id === selectedProjectId); const system = existingSystems.find((s) => s.id === selectedProjectId); if (project) { parts.push( `\n## Import from Existing Project\nExtract design tokens from "${project.title}". Call \`import-design-project --designId ${selectedProjectId}\``, ); } else if (system) { parts.push( `\n## Fork Existing Design System\nClone "${system.title}" as a starting point. Call \`import-design-project --designId _ --designSystemId ${selectedProjectId}\``, ); } } if (notes.trim()) { parts.push(`\n## Additional Notes\n${notes.trim()}`); } if (customInstructions.trim()) { parts.push( `\n## Custom Instructions (durable — store on the design system)\nIf you create a local design system from non-Builder sources, pass these verbatim as the \`customInstructions\` argument. They will be re-applied every time the design system is used to generate a design:\n\n${customInstructions.trim()}`, ); } parts.push( `\n---\nAfter processing all sources, if you started Builder DSI indexing, report the Builder job/design-system URL plus the local selectable design-system id returned by \`index-design-system-with-builder\`. Do not call \`create-design-system\` again for Builder-indexed Figma/code/design.md sources. If you processed non-Builder sources into concrete tokens, call \`create-design-system\` with the combined tokens${ customInstructions.trim() ? " AND the verbatim --customInstructions string from above" : "" }. Present a summary for review.`, ); openAgentSidebar(); sendToDesignAgentChat({ message: parts.join("\n"), submit: true, newTab: true, }); navigate("/design-systems"); }, [ hasAnySources, companyInfo, websiteUrl, websiteUrls, githubUrl, githubLinks, codeFiles, builderIndexResult, docFiles, imageFiles, assets, selectedProjectId, notes, customInstructions, existingProjects, existingSystems, navigate, t, ]); useSetPageTitle(

{t("navigation.setupDesignSystem")}

, ); useSetHeaderActions( , ); return ( <>

{t("designSystemSetup.title")}

{t("designSystemSetup.description")}

{validationError && (
{validationError}
)}
{/* Start from a Figma file via Builder DSI. */}
{!builderIndexResult ? ( <> {builderIndexError && (
{builderIndexError}
)} ) : ( { setBuilderIndexResult(null); setBuilderIndexError(null); }} /> )}
{/* Company / Brand */}