From c862104e35c871cf9891912f66c9b05541921c53 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Thu, 30 Apr 2026 17:17:22 -0700 Subject: [PATCH] feat(ux): empty-state prompt nudges + workspace delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Plan/Product/Infrastructure: empty states now suggest a concrete AI prompt so non-technical users know exactly what to type rather than staring at a blank category ("Try: Add a Postgres database…") - Workspace settings danger zone: wired Delete Workspace to a new POST /api/workspaces/delete endpoint (deletes projects + chat threads; Coolify resources intentionally untouched) Made-with: Cursor --- .../(home)/infrastructure/page.tsx | 13 +++- .../project/[projectId]/(home)/plan/page.tsx | 30 ++++++-- .../[projectId]/(home)/product/page.tsx | 16 +++-- app/[workspace]/settings/page.tsx | 36 ++++++++-- app/api/workspaces/delete/route.ts | 71 +++++++++++++++++++ 5 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 app/api/workspaces/delete/route.ts diff --git a/app/[workspace]/project/[projectId]/(home)/infrastructure/page.tsx b/app/[workspace]/project/[projectId]/(home)/infrastructure/page.tsx index 5d1f283a..3479c770 100644 --- a/app/[workspace]/project/[projectId]/(home)/infrastructure/page.tsx +++ b/app/[workspace]/project/[projectId]/(home)/infrastructure/page.tsx @@ -57,6 +57,15 @@ interface CategoryDef { dashboards?: Record; } +const INFRA_NUDGE = { + databases: 'No database yet. Try: "Add a Postgres database to my project"', + auth: 'No auth provider connected. Try: "Add Google OAuth to my app"', + email: 'No email provider. Try: "Set up email sending with Resend"', + payments: 'No payment provider. Try: "Connect Stripe to my project"', + llm: 'No LLM connected. Try: "Add an OpenAI key to this project"', + secrets: 'No secrets stored yet. Try: "Add my Stripe secret key"', +} as const; + const CATEGORIES: CategoryDef[] = [ { key: "databases", label: "Databases", icon: Database, @@ -493,7 +502,9 @@ function CategoryDetail({ ({count}) {count === 0 ? ( -
None yet.
+
+ {INFRA_NUDGE[def.key as keyof typeof INFRA_NUDGE] ?? "None yet."} +
) : def.key === "secrets" ? (
{anatomy.infrastructure.secrets.byResource.map(r => ( diff --git a/app/[workspace]/project/[projectId]/(home)/plan/page.tsx b/app/[workspace]/project/[projectId]/(home)/plan/page.tsx index d384542a..6811f29f 100644 --- a/app/[workspace]/project/[projectId]/(home)/plan/page.tsx +++ b/app/[workspace]/project/[projectId]/(home)/plan/page.tsx @@ -468,8 +468,11 @@ function SessionsPanel({
{err}
) : !sessions || sessions.length === 0 ? (
- No sessions yet. Open the chat panel and start a conversation about this project — - it'll show up here. + No sessions yet.{" "} + + Open the chat and ask anything — each conversation becomes a session entry here with an AI-generated summary. + + Try: "What should I build first for this project?"
) : (
    @@ -577,9 +580,15 @@ function TasksPanel({
    {visible.length === 0 && !creating ? (
    - {tasks.length === 0 - ? "No tasks yet. Create one to scope a feature, refactor, or investigation." - : "Nothing in this view."} + {tasks.length === 0 ? ( + <> + No tasks yet. + + Ask the AI to break down your first feature — it will create scoped tasks automatically. + + Try: "Add user authentication to my app" + + ) : "Nothing in this view."}
    ) : (
      @@ -947,7 +956,10 @@ function DecisionsPanel({ )} {plan.decisions.length === 0 ? ( -
      No decisions logged yet.
      +
      + No decisions yet — the AI logs these automatically when you settle on something in chat. + Try: "Which database should I use for this project?" +
      ) : (
        {plan.decisions.map((d) => ( @@ -1267,6 +1279,12 @@ const emptyBox: React.CSSProperties = { background: "#fafaf6", border: `1px dashed ${INK.borderSoft}`, borderRadius: 8, lineHeight: 1.5, textAlign: "center", }; +const promptNudge: React.CSSProperties = { + display: "block", marginTop: 10, + background: "#f3eee4", borderRadius: 5, + padding: "5px 10px", fontSize: "0.76rem", + color: INK.mid, fontStyle: "italic", +}; const errorBox: React.CSSProperties = { padding: "12px 14px", fontSize: "0.85rem", color: "#7a1f15", background: "#fbe9e7", border: `1px solid #f4c2bc`, borderRadius: 8, diff --git a/app/[workspace]/project/[projectId]/(home)/product/page.tsx b/app/[workspace]/project/[projectId]/(home)/product/page.tsx index c78353f2..b1f2cc14 100644 --- a/app/[workspace]/project/[projectId]/(home)/product/page.tsx +++ b/app/[workspace]/project/[projectId]/(home)/product/page.tsx @@ -81,8 +81,8 @@ export default function ProductTab() { {codebases && codebases.length === 0 && ( {reason === "no_repo" - ? "No Gitea repo connected to this project yet." - : "Repo is empty — push a first commit."} + ? <>No codebase yet. Try: "Start building my app" + : <>Repo is empty — push a first commit. Try: "Scaffold a Next.js app"} )} {codebases?.map(cb => { @@ -131,7 +131,8 @@ export default function ProductTab() { {images && images.length === 0 && ( - Self-hosted apps (Twenty, n8n, Plausible…) you adopt as part of the product appear here. + Self-hosted tools (Twenty CRM, n8n, Plausible…) you run appear here. + Try: "Install Twenty CRM for my project" )} {images?.map(img => ( @@ -342,8 +343,13 @@ const countPill: React.CSSProperties = { const railItems: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 10 }; const railEmpty: React.CSSProperties = { padding: "10px 12px", fontSize: "0.74rem", color: INK.muted, - fontStyle: "italic", border: `1px dashed ${INK.borderSoft}`, borderRadius: 8, - lineHeight: 1.4, + border: `1px dashed ${INK.borderSoft}`, borderRadius: 8, + lineHeight: 1.6, +}; +const nudge: React.CSSProperties = { + display: "block", marginTop: 6, fontStyle: "normal", + background: "#f3eee4", borderRadius: 4, padding: "3px 8px", + fontSize: "0.72rem", color: "#7a6a50", }; const flatTile: React.CSSProperties = { display: "flex", alignItems: "center", gap: 10, diff --git a/app/[workspace]/settings/page.tsx b/app/[workspace]/settings/page.tsx index 564c476a..798e9ce8 100644 --- a/app/[workspace]/settings/page.tsx +++ b/app/[workspace]/settings/page.tsx @@ -223,24 +223,48 @@ export default function SettingsPage() {
    Irreversible and destructive actions - + +

    + Deletes all Vibn project and chat data for this workspace.{" "} + Coolify services and databases are not removed — clean those up + separately in Coolify or via the AI before deleting. +

    - - Are you absolutely sure? + Delete workspace “{workspace}”? - This action cannot be undone. This will permanently delete your workspace and all associated data. + This will permanently delete all projects and chat history in this workspace. + Coolify services and databases will remain running — you must stop them + separately. This action cannot be undone. Cancel - - Delete Workspace + { + try { + const r = await fetch("/api/workspaces/delete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ slug: workspace }), + }); + const d = await r.json(); + if (!r.ok) throw new Error(d.error || "Delete failed"); + toast.success("Workspace deleted"); + router.push("/"); + } catch (e) { + toast.error(e instanceof Error ? e.message : "Delete failed"); + } + }} + > + Yes, delete workspace diff --git a/app/api/workspaces/delete/route.ts b/app/api/workspaces/delete/route.ts new file mode 100644 index 00000000..9c78d702 --- /dev/null +++ b/app/api/workspaces/delete/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from "next/server"; +import { authSession } from "@/lib/auth/session-server"; +import { query } from "@/lib/db-postgres"; + +/** + * POST /api/workspaces/delete + * Body: { slug: string } + * + * Deletes the workspace record and all associated projects/threads. + * Coolify resources (services, databases) are NOT deleted — the user + * must clean those up manually or via the AI. This only removes Vibn's + * knowledge of the workspace. + * + * Ownership check: the workspace's owner_user_id must match the + * authenticated user. + */ +export async function POST(request: Request) { + try { + const session = await authSession(); + if (!session?.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { slug } = await request.json(); + if (!slug) { + return NextResponse.json({ error: "slug is required" }, { status: 400 }); + } + + // Find the workspace and verify ownership + const wsRows = await query<{ id: string; slug: string }>( + `SELECT vw.id, vw.slug + FROM vibn_workspaces vw + JOIN fs_users u ON u.id = vw.owner_user_id + WHERE vw.slug = $1 AND u.data->>'email' = $2 + LIMIT 1`, + [slug, session.user.email], + ); + + if (!wsRows.length) { + return NextResponse.json({ error: "Workspace not found or unauthorized" }, { status: 404 }); + } + + const workspaceId = wsRows[0].id; + + // Delete all chat threads scoped to the workspace + await query( + `DELETE FROM fs_chat_threads WHERE workspace = $1`, + [slug], + ); + + // Delete all projects in the workspace + await query( + `DELETE FROM fs_projects WHERE workspace = $1`, + [slug], + ); + + // Delete the workspace itself + await query( + `DELETE FROM vibn_workspaces WHERE id = $1`, + [workspaceId], + ); + + return NextResponse.json({ success: true, message: "Workspace deleted" }); + } catch (error) { + console.error("[POST /api/workspaces/delete]", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Delete failed" }, + { status: 500 }, + ); + } +}