Theia rip-out (parent):
- Remove theia submodule entry (the local fork, Gitea repo, Coolify app,
Cloud Run services, and Artifact Registry image are all gone)
- Drop README.md + INFRASTRUCTURE.md (obsolete "Project OS" snapshots
that also leaked API tokens) and setup.sh (Theia clone bootstrap)
- Delete UI-DESIGN-GUIDE.md, BACKEND_AGENTS_PLAN.md, VIBN_BUILD_PLAN.md,
VISUAL_EDITOR_PLAN.md, core-packages.md, ai-packages.md, tools-list.md
(all 100% Theia-specific or superseded)
- Surgical scrubs of remaining Theia mentions in
AGENT_EXECUTION_ARCHITECTURE.md and TURBOREPO_MIGRATION_PLAN.md
Submodule bumps:
- vibn-agent-runner: Theia rip-out + MCP refactor (api/wrapper/server
pattern across shell/file/git/memory/prd/search/agent/gitea/coolify)
- vibn-frontend: Theia rip-out + P5.1 attach E2E + Justine UI WIP
Retire platform/ scaffold:
- Remove platform/backend/ (control-plane, executors, mcp-adapter),
platform/client-ide/ (gcp-productos extension), platform/contracts/,
platform/infra/terraform/, platform/scripts/templates/turborepo/
(replaced by vibn-agent-runner + vibn-frontend + Coolify direct)
- Drop architecture.md, technical_spec.md, vision-ext.md,
"1.Generate Control Plane API scaffold.md" (same era)
Docs / planning snapshots (new):
- AI_CAPABILITIES.md, AI_CAPABILITIES_ROADMAP.md
- AGENT_TELEMETRY_STREAMING_PROJECT.md
- VIBN_PRD.md, product-idea-a.md
Design assets (new):
- branding/{coolify,gitea,ux-testing}/ static brand collateral
- justine/ HTML mockups for the new onboarding/build flows
- preview-assist-ui/ Vite scratch app
- master-ai.code-workspace
Infra helpers (new):
- setup-coolify-montreal.sh provisioner
- gitea-docker-compose.yml
- vibn-coolify-schema.sql for the Coolify Postgres extensions
- prd-agent-prompt.pdf, prompt, root.txt, remixed-9edec9e9.tsx scratch
- flatten.sh helper
.gitignore: ignore **/node_modules, **/.next, **/.turbo, **/coverage
Made-with: Cursor
485 lines
28 KiB
JavaScript
485 lines
28 KiB
JavaScript
// vibn — Projects Dashboard
|
|
// Restyled from original (DM Sans + purple/colour accents) → Ink & parchment
|
|
// Design: Lora serif + Inter sans, #1a1510 ink, #f7f4ee paper, no colour accent
|
|
// Usage: default export, no required props
|
|
|
|
import { useState } from "react";
|
|
|
|
const T = {
|
|
ink: "#1a1510",
|
|
ink2: "#2c2c2a",
|
|
ink3: "#444441",
|
|
mid: "#5f5e5a",
|
|
muted: "#888780",
|
|
stone: "#b4b2a9",
|
|
parch: "#d3d1c7",
|
|
cream: "#f1efe8",
|
|
paper: "#f7f4ee",
|
|
white: "#fdfcfa",
|
|
border: "#e8e2d9",
|
|
border2:"#d3d1c7",
|
|
};
|
|
|
|
const F = { serif: "'Lora', Georgia, serif", sans: "'Inter', sans-serif" };
|
|
|
|
// ─── Shared primitives ─────────────────────────────────────────────────────────
|
|
|
|
function StatusPill({ label, variant = "default" }) {
|
|
const styles = {
|
|
live: { bg: T.cream, text: T.ink3, border: T.border },
|
|
building: { bg: T.cream, text: T.ink3, border: T.border },
|
|
default: { bg: T.paper, text: T.muted, border: T.border },
|
|
invoiced: { bg: T.ink, text: T.paper, border: T.ink },
|
|
unbilled: { bg: T.cream, text: T.ink3, border: T.border },
|
|
scheduled: { bg: T.parch, text: T.ink2, border: T.border2 },
|
|
};
|
|
const s = styles[variant] || styles.default;
|
|
return (
|
|
<span style={{
|
|
fontFamily: F.sans, fontSize: 10.5, fontWeight: 600,
|
|
color: s.text, background: s.bg,
|
|
border: `1px solid ${s.border}`,
|
|
borderRadius: 5, padding: "2px 8px", whiteSpace: "nowrap",
|
|
}}>{label}</span>
|
|
);
|
|
}
|
|
|
|
function InkBtn({ children, onClick, small, outline }) {
|
|
return (
|
|
<button onClick={onClick} style={{
|
|
fontFamily: F.sans, fontWeight: 600,
|
|
fontSize: small ? 12 : 13.5,
|
|
padding: small ? "6px 14px" : "10px 22px",
|
|
background: outline ? "transparent" : T.ink,
|
|
color: outline ? T.ink3 : T.paper,
|
|
border: outline ? `1px solid ${T.border2}` : "none",
|
|
borderRadius: 8, cursor: "pointer",
|
|
}}>{children}</button>
|
|
);
|
|
}
|
|
|
|
// ─── Nav ───────────────────────────────────────────────────────────────────────
|
|
|
|
function Nav({ screen, setScreen }) {
|
|
return (
|
|
<nav style={{
|
|
background: T.white, borderBottom: `1px solid ${T.border}`,
|
|
padding: "0 32px", height: 60, display: "flex",
|
|
alignItems: "center", justifyContent: "space-between",
|
|
}}>
|
|
<div onClick={() => setScreen("projects")} style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer" }}>
|
|
<div style={{ width: 28, height: 28, background: T.ink, borderRadius: 7, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
<span style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 700, color: T.paper }}>V</span>
|
|
</div>
|
|
<span style={{ fontFamily: F.serif, fontSize: 18, fontWeight: 700, color: T.ink, letterSpacing: "-0.02em" }}>vibn</span>
|
|
</div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 22 }}>
|
|
{screen === "billing" && (
|
|
<span onClick={() => setScreen("projects")} style={{ fontFamily: F.sans, fontSize: 13.5, color: T.muted, cursor: "pointer" }}>← All projects</span>
|
|
)}
|
|
<span style={{ fontFamily: F.sans, fontSize: 13.5, color: T.muted, cursor: "pointer" }}>Settings</span>
|
|
<div style={{ width: 30, height: 30, borderRadius: "50%", background: T.ink3, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: F.sans, fontSize: 11, color: T.paper, fontWeight: 700 }}>JD</div>
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
// ─── Data ──────────────────────────────────────────────────────────────────────
|
|
|
|
const PROJECTS = [
|
|
{
|
|
id: "launchpad", label: "Launchpad", initial: "L",
|
|
type: "own", status: "live", url: "launchpad.vibn.app",
|
|
stats: { visitors: "2.4k", signups: 183, mrr: "$840" },
|
|
},
|
|
{
|
|
id: "flowmatic", label: "Flowmatic", initial: "F",
|
|
type: "client", status: "live", url: "flowmatic.app",
|
|
client: "Acme Corp",
|
|
stats: { visitors: "890", signups: 54, mrr: "$210" },
|
|
costs: { total: 48.20, llm: 29.20, compute: 11.60, other: 7.40, billed: false },
|
|
},
|
|
{
|
|
id: "taskly", label: "Taskly", initial: "T",
|
|
type: "client", status: "building", url: null,
|
|
client: "Beta Labs", buildProgress: 60,
|
|
costs: { total: 12.40, llm: 9.20, compute: 3.20, other: 0, billed: false },
|
|
},
|
|
];
|
|
|
|
const ACTIVITY = [
|
|
{ text: "Launchpad — Blog post published:", detail: '"How to launch faster with AI"', time: "2h ago" },
|
|
{ text: "Flowmatic — New signup:", detail: "marcus@email.com", time: "4h ago" },
|
|
{ text: "Taskly — Checkout page built and deployed", detail: "", time: "6h ago" },
|
|
{ text: "Launchpad — Newsletter #12", detail: "scheduled", time: "Yesterday" },
|
|
];
|
|
|
|
const BILLING_ROWS = [
|
|
{ label: "Flowmatic", initial: "F", client: "Acme Corp", llm: 29.20, compute: 11.60, other: 7.40, total: 48.20, billed: false },
|
|
{ label: "Taskly", initial: "T", client: "Beta Labs", llm: 9.20, compute: 3.20, other: 0, total: 12.40, billed: false },
|
|
{ label: "Flowmatic", initial: "F", client: "Acme · Feb", llm: 22.10, compute: 8.40, other: 4.20, total: 34.70, billed: true },
|
|
];
|
|
|
|
const COST_LOG = [
|
|
{ time: "2h ago", desc: "LLM: Homepage copy generation", project: "Flowmatic", cost: 0.82 },
|
|
{ time: "3h ago", desc: "LLM: Checkout page code", project: "Taskly", cost: 1.24 },
|
|
{ time: "5h ago", desc: "LLM: Weekly newsletter draft", project: "Flowmatic", cost: 0.43 },
|
|
{ time: "6h ago", desc: "Compute: Build pipeline run", project: "Taskly", cost: 0.18 },
|
|
{ time: "8h ago", desc: "LLM: Discover phase Q&A", project: "Flowmatic", cost: 0.31 },
|
|
{ time: "Yesterday", desc: "Email delivery · 240 recipients", project: "Flowmatic", cost: 0.96 },
|
|
];
|
|
|
|
// ─── Project card ──────────────────────────────────────────────────────────────
|
|
|
|
function ProjectCard({ project }) {
|
|
const isClient = project.type === "client";
|
|
const isBuilding = project.status === "building";
|
|
|
|
return (
|
|
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 14, overflow: "hidden" }}>
|
|
|
|
{/* Header preview */}
|
|
{isBuilding ? (
|
|
<div style={{ height: 100, background: T.cream, borderBottom: `1px solid ${T.border}`, display: "flex", alignItems: "center", justifyContent: "center", position: "relative" }}>
|
|
<div style={{ textAlign: "center" }}>
|
|
<div style={{ fontFamily: F.sans, fontSize: 11.5, color: T.muted, fontWeight: 500, marginBottom: 10 }}>
|
|
Build phase · {project.buildProgress}% complete
|
|
</div>
|
|
<div style={{ width: 160, height: 4, background: T.parch, borderRadius: 3, overflow: "hidden" }}>
|
|
<div style={{ width: `${project.buildProgress}%`, height: "100%", background: T.ink3, borderRadius: 3 }} />
|
|
</div>
|
|
</div>
|
|
{isClient && (
|
|
<div style={{ position: "absolute", top: 10, right: 12, fontFamily: F.sans, fontSize: 10, fontWeight: 600, color: T.mid, background: T.white, border: `1px solid ${T.border}`, borderRadius: 5, padding: "2px 8px" }}>
|
|
Client
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div style={{ height: 100, background: T.ink, display: "flex", alignItems: "center", justifyContent: "center", position: "relative" }}>
|
|
<div style={{ background: "rgba(247,244,238,0.1)", borderRadius: 8, width: "55%", padding: "10px 14px" }}>
|
|
<div style={{ height: 8, background: "rgba(247,244,238,0.6)", borderRadius: 3, width: "65%", marginBottom: 6 }} />
|
|
<div style={{ height: 5, background: "rgba(247,244,238,0.25)", borderRadius: 3, width: "85%" }} />
|
|
</div>
|
|
<div style={{ position: "absolute", top: 10, right: 12, fontFamily: F.sans, fontSize: 10, fontWeight: 600, color: "rgba(247,244,238,0.6)", background: "rgba(247,244,238,0.1)", borderRadius: 5, padding: "2px 8px" }}>
|
|
{isClient ? "Client" : "My product"}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ padding: "18px 20px" }}>
|
|
{/* Identity row */}
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
<div style={{ width: 28, height: 28, background: T.ink, borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: F.serif, fontSize: 12, color: T.paper, fontWeight: 700 }}>
|
|
{project.initial}
|
|
</div>
|
|
<div>
|
|
<div style={{ fontFamily: F.serif, fontSize: 15, fontWeight: 700, color: T.ink }}>{project.label}</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 11, color: T.muted }}>
|
|
{isClient ? `${project.client} · ` : ""}
|
|
{project.url || "Setting up pages…"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<StatusPill label={isBuilding ? "Building" : "Live"} variant={isBuilding ? "building" : "live"} />
|
|
</div>
|
|
|
|
{/* Cost strip — client + building */}
|
|
{isClient && project.costs && isBuilding && (
|
|
<div style={{ background: T.cream, border: `1px solid ${T.border}`, borderRadius: 9, padding: "10px 14px", marginBottom: 12, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
<div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 10, fontWeight: 600, color: T.muted, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 3 }}>Costs so far</div>
|
|
<div style={{ fontFamily: F.serif, fontSize: 17, fontWeight: 700, color: T.ink }}>${project.costs.total.toFixed(2)}</div>
|
|
</div>
|
|
<StatusPill label="Unbilled" variant="unbilled" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Cost strip — client + live */}
|
|
{isClient && project.costs && !isBuilding && (
|
|
<div style={{ background: T.cream, border: `1px solid ${T.border}`, borderRadius: 9, padding: "10px 14px", marginBottom: 12, display: "flex", alignItems: "center", gap: 10 }}>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ fontFamily: F.sans, fontSize: 10, fontWeight: 600, color: T.muted, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 3 }}>Costs this month</div>
|
|
<div style={{ fontFamily: F.serif, fontSize: 17, fontWeight: 700, color: T.ink }}>${project.costs.total.toFixed(2)}</div>
|
|
</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 11, color: T.mid, lineHeight: 1.7 }}>
|
|
LLM ${project.costs.llm.toFixed(2)}<br />
|
|
Compute ${project.costs.compute.toFixed(2)}
|
|
</div>
|
|
{!project.costs.billed && (
|
|
<button style={{ background: T.ink, border: "none", color: T.paper, borderRadius: 7, padding: "7px 13px", fontFamily: F.sans, fontSize: 11.5, fontWeight: 600, cursor: "pointer" }}>
|
|
Bill →
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Stats */}
|
|
{!isBuilding && project.stats && (
|
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8, marginBottom: 14 }}>
|
|
{[["visitors", project.stats.visitors], ["signups", project.stats.signups], ["MRR", project.stats.mrr]].map(([k, v]) => (
|
|
<div key={k} style={{ textAlign: "center" }}>
|
|
<div style={{ fontFamily: F.serif, fontSize: 16, fontWeight: 700, color: T.ink }}>{v}</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 10, color: T.muted }}>{k}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
{isBuilding ? (
|
|
<button style={{ width: "100%", background: T.ink, border: "none", color: T.paper, borderRadius: 8, padding: 10, fontFamily: F.sans, fontSize: 13, fontWeight: 600, cursor: "pointer" }}>
|
|
Continue building →
|
|
</button>
|
|
) : (
|
|
<div style={{ display: "flex", gap: 6 }}>
|
|
{[["⬡", "Build"], ["◈", "Grow"]].map(([icon, label]) => (
|
|
<div key={label} style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 5, padding: "7px 10px", background: T.cream, border: `1px solid ${T.border}`, borderRadius: 7, cursor: "pointer" }}>
|
|
<span style={{ fontSize: 11 }}>{icon}</span>
|
|
<span style={{ fontFamily: F.sans, fontSize: 11.5, color: T.ink3 }}>{label}</span>
|
|
</div>
|
|
))}
|
|
<div style={{ padding: "7px 12px", background: T.cream, border: `1px solid ${T.border}`, borderRadius: 7, fontFamily: F.sans, fontSize: 11.5, color: T.ink3, cursor: "pointer" }}>
|
|
↗
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Projects screen ───────────────────────────────────────────────────────────
|
|
|
|
function ProjectsScreen({ setScreen }) {
|
|
const totalUnbilled = PROJECTS
|
|
.filter(p => p.type === "client" && p.costs?.billed === false)
|
|
.reduce((s, p) => s + p.costs.total, 0);
|
|
|
|
return (
|
|
<div style={{ maxWidth: 1000, margin: "0 auto", padding: "36px 32px" }}>
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 32 }}>
|
|
<div>
|
|
<h1 style={{ fontFamily: F.serif, fontSize: 26, fontWeight: 700, color: T.ink, letterSpacing: "-0.02em", marginBottom: 4 }}>Your projects</h1>
|
|
<p style={{ fontFamily: F.sans, fontSize: 13.5, color: T.muted }}>3 active · 1 building</p>
|
|
</div>
|
|
<div style={{ display: "flex", gap: 10, alignItems: "center" }}>
|
|
{totalUnbilled > 0 && (
|
|
<button onClick={() => setScreen("billing")} style={{ fontFamily: F.sans, fontSize: 13, color: T.ink3, background: T.cream, border: `1px solid ${T.border}`, borderRadius: 8, padding: "9px 16px", cursor: "pointer" }}>
|
|
${totalUnbilled.toFixed(2)} unbilled →
|
|
</button>
|
|
)}
|
|
<InkBtn>+ New project</InkBtn>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14, marginBottom: 28 }}>
|
|
{PROJECTS.map(p => <ProjectCard key={p.id} project={p} />)}
|
|
|
|
{/* New project CTA card */}
|
|
<div
|
|
style={{ background: "transparent", border: `1px dashed ${T.parch}`, borderRadius: 14, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, padding: 40, cursor: "pointer", minHeight: 220 }}
|
|
onMouseEnter={e => e.currentTarget.style.background = T.cream}
|
|
onMouseLeave={e => e.currentTarget.style.background = "transparent"}
|
|
>
|
|
<div style={{ width: 42, height: 42, borderRadius: 10, border: `1px solid ${T.parch}`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 22, color: T.stone }}>+</div>
|
|
<div style={{ textAlign: "center" }}>
|
|
<div style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink, marginBottom: 4 }}>New project</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 12.5, color: T.muted }}>For yourself or a client</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Activity feed */}
|
|
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 14, padding: "20px 24px" }}>
|
|
<div style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink, marginBottom: 16 }}>Recent activity</div>
|
|
{ACTIVITY.map((a, i) => (
|
|
<div key={i} style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 0", borderBottom: i < ACTIVITY.length - 1 ? `1px solid ${T.border}` : "none" }}>
|
|
<div style={{ width: 7, height: 7, borderRadius: "50%", background: T.ink3, flexShrink: 0 }} />
|
|
<div style={{ flex: 1, fontFamily: F.sans, fontSize: 13.5, color: T.ink }}>
|
|
{a.text}{" "}{a.detail && <span style={{ color: T.muted }}>{a.detail}</span>}
|
|
</div>
|
|
<span style={{ fontFamily: F.sans, fontSize: 11.5, color: T.stone, whiteSpace: "nowrap" }}>{a.time}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Billing screen ────────────────────────────────────────────────────────────
|
|
|
|
function BillingScreen() {
|
|
const [tab, setTab] = useState("billing");
|
|
const unbilled = BILLING_ROWS.filter(r => !r.billed).reduce((s, r) => s + r.total, 0);
|
|
|
|
return (
|
|
<div style={{ maxWidth: 1000, margin: "0 auto", padding: "28px 32px" }}>
|
|
|
|
{/* Sub-tabs */}
|
|
<div style={{ display: "flex", borderBottom: `1px solid ${T.border}`, marginBottom: 28 }}>
|
|
{[["billing", "Client billing"], ["costs", "Cost tracker"]].map(([id, label]) => (
|
|
<button key={id} onClick={() => setTab(id)} style={{
|
|
padding: "10px 18px", border: "none", background: "transparent",
|
|
borderBottom: tab === id ? `2px solid ${T.ink}` : "2px solid transparent",
|
|
fontFamily: F.sans, fontSize: 13.5, cursor: "pointer",
|
|
color: tab === id ? T.ink : T.muted, fontWeight: tab === id ? 600 : 400,
|
|
}}>{label}</button>
|
|
))}
|
|
</div>
|
|
|
|
{tab === "billing" && <>
|
|
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 22 }}>
|
|
<div>
|
|
<h2 style={{ fontFamily: F.serif, fontSize: 22, fontWeight: 700, color: T.ink, marginBottom: 4 }}>Client billing</h2>
|
|
<p style={{ fontFamily: F.sans, fontSize: 13, color: T.muted }}>All costs tracked and ready to invoice</p>
|
|
</div>
|
|
<InkBtn>Generate invoice</InkBtn>
|
|
</div>
|
|
|
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr 1fr", gap: 10, marginBottom: 24 }}>
|
|
{[
|
|
{ label: "Total unbilled", value: `$${unbilled.toFixed(2)}` },
|
|
{ label: "LLM costs", value: "$38.40" },
|
|
{ label: "Compute", value: "$14.80" },
|
|
{ label: "Other", value: "$7.40" },
|
|
].map(c => (
|
|
<div key={c.label} style={{ background: T.cream, borderRadius: 10, padding: "14px 16px" }}>
|
|
<div style={{ fontFamily: F.sans, fontSize: 11, color: T.muted, marginBottom: 5 }}>{c.label}</div>
|
|
<div style={{ fontFamily: F.serif, fontSize: 22, fontWeight: 700, color: T.ink }}>{c.value}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 12, overflow: "hidden" }}>
|
|
<div style={{ padding: "13px 20px", borderBottom: `1px solid ${T.border}`, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
<span style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink }}>Breakdown by client</span>
|
|
<select style={{ border: `1px solid ${T.border}`, borderRadius: 6, padding: "4px 10px", fontFamily: F.sans, fontSize: 12, color: T.muted, background: T.paper, outline: "none" }}>
|
|
<option>March 2026</option>
|
|
</select>
|
|
</div>
|
|
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr 120px", padding: "9px 20px", background: T.cream, borderBottom: `1px solid ${T.border}` }}>
|
|
{["Project / Client", "LLM", "Compute", "Other", "Total", "Status"].map(h => (
|
|
<div key={h} style={{ fontFamily: F.sans, fontSize: 10.5, fontWeight: 600, color: T.muted, textTransform: "uppercase", letterSpacing: "0.05em" }}>{h}</div>
|
|
))}
|
|
</div>
|
|
{BILLING_ROWS.map((r, i) => (
|
|
<div key={i} style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr 120px", padding: "13px 20px", borderBottom: i < BILLING_ROWS.length - 1 ? `1px solid ${T.border}` : "none", alignItems: "center", opacity: r.billed ? 0.5 : 1 }}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
<div style={{ width: 24, height: 24, background: T.ink, borderRadius: 6, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: F.serif, fontSize: 10, color: T.paper, fontWeight: 700 }}>{r.initial}</div>
|
|
<div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 13, fontWeight: 600, color: T.ink }}>{r.label}</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 11, color: T.muted }}>{r.client}</div>
|
|
</div>
|
|
</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 13, color: T.ink }}>${r.llm.toFixed(2)}</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 13, color: T.ink }}>${r.compute.toFixed(2)}</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 13, color: T.ink }}>${r.other.toFixed(2)}</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 13, fontWeight: 600, color: T.ink }}>${r.total.toFixed(2)}</div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
<StatusPill label={r.billed ? "Invoiced" : "Unbilled"} variant={r.billed ? "invoiced" : "unbilled"} />
|
|
{!r.billed && (
|
|
<button style={{ background: "transparent", border: `1px solid ${T.border2}`, borderRadius: 5, padding: "3px 9px", fontFamily: F.sans, fontSize: 11, color: T.muted, cursor: "pointer" }}>Invoice</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>}
|
|
|
|
{tab === "costs" && <>
|
|
<div style={{ marginBottom: 22 }}>
|
|
<h2 style={{ fontFamily: F.serif, fontSize: 22, fontWeight: 700, color: T.ink, marginBottom: 4 }}>Cost tracker</h2>
|
|
<p style={{ fontFamily: F.sans, fontSize: 13, color: T.muted }}>Every dollar spent, broken down by type and project</p>
|
|
</div>
|
|
|
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14, marginBottom: 20 }}>
|
|
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 12, padding: "18px 20px" }}>
|
|
<div style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink, marginBottom: 16 }}>LLM usage</div>
|
|
{[
|
|
{ label: "Code generation", amount: 21.40, pct: 56 },
|
|
{ label: "Content & marketing", amount: 10.20, pct: 27 },
|
|
{ label: "Chat assist", amount: 6.80, pct: 18 },
|
|
].map(r => (
|
|
<div key={r.label} style={{ marginBottom: 14 }}>
|
|
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
|
|
<span style={{ fontFamily: F.sans, fontSize: 12.5, color: T.mid }}>{r.label}</span>
|
|
<span style={{ fontFamily: F.sans, fontSize: 12.5, fontWeight: 600, color: T.ink }}>${r.amount.toFixed(2)}</span>
|
|
</div>
|
|
<div style={{ height: 4, background: T.cream, borderRadius: 2, overflow: "hidden" }}>
|
|
<div style={{ width: `${r.pct}%`, height: "100%", background: T.ink, borderRadius: 2 }} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
<div style={{ marginTop: 14, paddingTop: 12, borderTop: `1px solid ${T.border}`, display: "flex", justifyContent: "space-between" }}>
|
|
<span style={{ fontFamily: F.sans, fontSize: 12, color: T.muted }}>Total LLM</span>
|
|
<span style={{ fontFamily: F.serif, fontSize: 15, fontWeight: 700, color: T.ink }}>$38.40</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 12, padding: "18px 20px" }}>
|
|
<div style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink, marginBottom: 16 }}>Infrastructure</div>
|
|
{[
|
|
{ label: "Hosting & compute", amount: 11.60 },
|
|
{ label: "Database", amount: 3.20 },
|
|
{ label: "Email delivery", amount: 4.20 },
|
|
{ label: "Domain & SSL", amount: 3.20 },
|
|
].map(r => (
|
|
<div key={r.label} style={{ display: "flex", justifyContent: "space-between", padding: "8px 11px", background: T.cream, borderRadius: 7, marginBottom: 7 }}>
|
|
<span style={{ fontFamily: F.sans, fontSize: 13, color: T.mid }}>{r.label}</span>
|
|
<span style={{ fontFamily: F.sans, fontSize: 13, fontWeight: 600, color: T.ink }}>${r.amount.toFixed(2)}</span>
|
|
</div>
|
|
))}
|
|
<div style={{ marginTop: 14, paddingTop: 12, borderTop: `1px solid ${T.border}`, display: "flex", justifyContent: "space-between" }}>
|
|
<span style={{ fontFamily: F.sans, fontSize: 12, color: T.muted }}>Total infra</span>
|
|
<span style={{ fontFamily: F.serif, fontSize: 15, fontWeight: 700, color: T.ink }}>$22.20</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ background: T.white, border: `1px solid ${T.border}`, borderRadius: 12, overflow: "hidden" }}>
|
|
<div style={{ padding: "13px 20px", borderBottom: `1px solid ${T.border}` }}>
|
|
<span style={{ fontFamily: F.serif, fontSize: 14, fontWeight: 600, color: T.ink }}>Recent charges</span>
|
|
</div>
|
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 2fr 1fr 80px", padding: "9px 20px", background: T.cream, borderBottom: `1px solid ${T.border}` }}>
|
|
{["Time", "Description", "Project", "Cost"].map(h => (
|
|
<div key={h} style={{ fontFamily: F.sans, fontSize: 10.5, fontWeight: 600, color: T.muted, textTransform: "uppercase", letterSpacing: "0.05em" }}>{h}</div>
|
|
))}
|
|
</div>
|
|
{COST_LOG.map((row, i) => (
|
|
<div key={i} style={{ display: "grid", gridTemplateColumns: "1fr 2fr 1fr 80px", padding: "11px 20px", borderBottom: i < COST_LOG.length - 1 ? `1px solid ${T.border}` : "none", alignItems: "center" }}>
|
|
<div style={{ fontFamily: F.sans, fontSize: 12, color: T.muted }}>{row.time}</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 13, color: T.ink }}>{row.desc}</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 12, color: T.mid }}>{row.project}</div>
|
|
<div style={{ fontFamily: F.sans, fontSize: 13, fontWeight: 600, color: T.ink }}>${row.cost.toFixed(2)}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Root ──────────────────────────────────────────────────────────────────────
|
|
|
|
export default function Dashboard() {
|
|
const [screen, setScreen] = useState("projects");
|
|
|
|
return (
|
|
<div style={{ background: T.paper, minHeight: "100vh" }}>
|
|
<style>{`
|
|
@import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;0,700;1,400;1,600&family=Inter:wght@400;500;600&display=swap');
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
button { font-family: inherit; cursor: pointer; }
|
|
input, select { font-family: inherit; }
|
|
::-webkit-scrollbar { width: 4px; }
|
|
::-webkit-scrollbar-thumb { background: ${T.parch}; border-radius: 4px; }
|
|
`}</style>
|
|
<Nav screen={screen} setScreen={setScreen} />
|
|
{screen === "projects" && <ProjectsScreen setScreen={setScreen} />}
|
|
{screen === "billing" && <BillingScreen />}
|
|
</div>
|
|
);
|
|
}
|