diff --git a/apps/landing/src/components/AppPreview/AppPreview.tsx b/apps/landing/src/components/AppPreview/AppPreview.tsx new file mode 100644 index 00000000000..a0b646d6c6e --- /dev/null +++ b/apps/landing/src/components/AppPreview/AppPreview.tsx @@ -0,0 +1,483 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { + ArrowDownIcon, + ArrowDownUpIcon, + ChevronDownIcon, + ChevronRightIcon, + CornerDownLeftIcon, + EyeIcon, + FolderIcon, + GitBranchIcon, + GlobeIcon, + ImageIcon, + LockIcon, + PencilIcon, + PlusIcon, + SearchIcon, + ShieldIcon, + TerminalIcon, + WrenchIcon, + ZapIcon, +} from "lucide-react"; +import { + ACCESS_LABELS, + MODELS, + previewProjects, + previewThreads, + previewTurns, + type PreviewAccessMode, + type PreviewTurn, +} from "./data"; + +type ToolCallKind = "command" | "read" | "edit" | "search" | "fetch"; + +const TOOL_ICONS: Record = { + command: TerminalIcon, + read: EyeIcon, + edit: PencilIcon, + search: SearchIcon, + fetch: GlobeIcon, +}; + +const ACCESS_ICONS: Record = { + "approval-required": ShieldIcon, + "auto-accept-edits": LockIcon, + "full-access": ZapIcon, +}; + +const ACCESS_ACCENTS: Record = { + "approval-required": "text-sunbyte", + "auto-accept-edits": "text-curious-sky", + "full-access": "text-rebel-mint", +}; + +const MODEL_DOT_BG: Record<"fresh-syntax" | "rebel-mint" | "dream-shift" | "curious-sky", string> = + { + "fresh-syntax": "bg-fresh-syntax", + "rebel-mint": "bg-rebel-mint", + "dream-shift": "bg-dream-shift", + "curious-sky": "bg-curious-sky", + }; + +function ProjectIcon({ icon }: { icon: "marcode" | "round" | "lawn" | "folder" }) { + if (icon === "marcode") { + return ( + + + M + + + ); + } + if (icon === "round") { + return ( + + + + ); + } + if (icon === "lawn") { + return ( + + + + + + ); + } + return ( + + + + ); +} + +function ToolCallRow({ call }: { call: { kind: ToolCallKind; heading: string; preview: string } }) { + const Icon = TOOL_ICONS[call.kind]; + return ( +
+ +
+
{call.heading}
+
{call.preview}
+
+
+ ); +} + +function TurnView({ turn }: { turn: PreviewTurn }) { + if (turn.type === "user") { + return ( +
+
+ {turn.text} +
+
+ ); + } + + if (turn.type === "tool") { + return ( +
+ + + +
+
{turn.title}
+
+ {turn.calls.map((call, i) => ( + + ))} +
+
+
+ ); + } + + return ( +
+ + + M + + +
+ {turn.text} +
+
+ ); +} + +function StatusDot({ status }: { status: "Working" | "Completed" }) { + if (status === "Working") { + return ( + + + + + ); + } + return ; +} + +function PopoverButton({ + label, + value, + icon, + accent, +}: { + label: string; + value: string; + icon?: React.ReactNode; + accent?: string; +}) { + return ( + + ); +} + +export function AppPreview() { + const [activeThreadId, setActiveThreadId] = useState(previewThreads[0]!.id); + const [activeModel, setActiveModel] = useState(MODELS[0]!.model); + const [collapsedProjects, setCollapsedProjects] = useState>(new Set()); + + const activeThread = useMemo( + () => previewThreads.find((t) => t.id === activeThreadId) ?? previewThreads[0]!, + [activeThreadId], + ); + const activeTurns = previewTurns[activeThreadId] ?? []; + const AccessIcon = ACCESS_ICONS[activeThread.access]; + const accessAccent = ACCESS_ACCENTS[activeThread.access]; + + const modelMeta = MODELS.find((m) => m.model === activeModel) ?? MODELS[0]!; + + const toggleProject = (projectId: string) => { + setCollapsedProjects((prev) => { + const next = new Set(prev); + if (next.has(projectId)) next.delete(projectId); + else next.add(projectId); + return next; + }); + }; + + return ( +
+ {/* Decorative glow */} +
+
+
+
+ +
+
+ {/* ── Sidebar ─────────────────────────────────────────── */} + + + {/* ── Main pane ───────────────────────────────────────── */} +
+ {/* Header */} +
+
+
+ {activeThread.title} +
+
+ p.id === activeThread.projectId)?.icon ?? "folder" + } + /> + {previewProjects.find((p) => p.id === activeThread.projectId)?.title} + · + {activeThread.age} +
+
+ {activeThread.status === "Working" ? ( + + + Working + + ) : activeThread.status === "Completed" ? ( + + + Completed + + ) : null} +
+ + {/* Timeline */} +
+
+ {activeTurns.length === 0 ? ( +
+ Send a prompt to begin a new turn. +
+ ) : ( + activeTurns.map((turn, i) => ) + )} + {activeThread.status === "Working" && ( +
+ + Working... +
+ )} +
+
+ + {/* Composer */} +
+
+