Save frontend state (layout, sidebar, chat panel, preview refresh fix) before rollback
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
"use client";
|
||||
|
||||
import { Bot, Plus, FileText, Wrench, Plug } from "lucide-react";
|
||||
|
||||
export default function AgentsPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 40 }}>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
Agents
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
Manage agents and users
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
marginTop: 64,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
background: "#fafafa",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<Bot size={24} color="#18181b" />
|
||||
</div>
|
||||
<h2
|
||||
style={{ fontSize: "1.25rem", fontWeight: 600, margin: "0 0 8px 0" }}
|
||||
>
|
||||
Create your first agent
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.9rem",
|
||||
color: "#71717a",
|
||||
textAlign: "center",
|
||||
maxWidth: 480,
|
||||
margin: "0 0 40px 0",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Agents talk to your users, work with your data, and run on schedules –
|
||||
all guided by clear instructions you define in your own words.
|
||||
</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 16,
|
||||
padding: "24px 32px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{ fontSize: "0.85rem", fontWeight: 500, color: "#71717a" }}
|
||||
>
|
||||
Suggested for your app
|
||||
</span>
|
||||
<button
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
color: "#a1a1aa",
|
||||
}}
|
||||
>
|
||||
<RefreshIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}
|
||||
>
|
||||
{/* Blurred out cards */}
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
height: 110,
|
||||
border: "1px solid #e4e4e7",
|
||||
filter: "blur(2px)",
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 12,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 6,
|
||||
width: "60%",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
style={{
|
||||
height: 8,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 4,
|
||||
width: "100%",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
style={{
|
||||
height: 8,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 4,
|
||||
width: "80%",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
height: 110,
|
||||
border: "1px solid #e4e4e7",
|
||||
filter: "blur(2px)",
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 12,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 6,
|
||||
width: "50%",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
style={{
|
||||
height: 8,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 4,
|
||||
width: "90%",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
style={{
|
||||
height: 8,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 4,
|
||||
width: "70%",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
height: 110,
|
||||
border: "1px solid #e4e4e7",
|
||||
filter: "blur(2px)",
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 12,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 6,
|
||||
width: "70%",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
style={{
|
||||
height: 8,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 4,
|
||||
width: "100%",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
style={{
|
||||
height: 8,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 4,
|
||||
width: "60%",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
{/* Create from scratch */}
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
height: 110,
|
||||
border: "1px solid #e4e4e7",
|
||||
cursor: "pointer",
|
||||
transition: "border-color 0.2s",
|
||||
}}
|
||||
className="hover:border-blue-500"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 600, fontSize: "0.95rem" }}>
|
||||
Create from scratch
|
||||
</span>
|
||||
<div
|
||||
style={{ background: "#f4f4f5", borderRadius: 6, padding: 4 }}
|
||||
>
|
||||
<Plus size={14} color="#71717a" />
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.8rem",
|
||||
color: "#71717a",
|
||||
margin: 0,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
Create your own agent, defining how it works, responds, and
|
||||
integrates with your data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ width: "100%", marginTop: 48 }}>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: "0.9rem",
|
||||
fontWeight: 500,
|
||||
color: "#71717a",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
How you can use agents
|
||||
</h3>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr 1fr",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<FileText size={16} color="#18181b" />
|
||||
<span style={{ fontWeight: 600, fontSize: "0.9rem" }}>
|
||||
Guidelines
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.8rem",
|
||||
color: "#71717a",
|
||||
lineHeight: 1.5,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Define the agent's behavior, knowledge, and AI model.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<Wrench size={16} color="#18181b" />
|
||||
<span style={{ fontWeight: 600, fontSize: "0.9rem" }}>
|
||||
Tools
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.8rem",
|
||||
color: "#71717a",
|
||||
lineHeight: 1.5,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Configure what tools and data the agent can access.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<Plug size={16} color="#18181b" />
|
||||
<span style={{ fontWeight: 600, fontSize: "0.9rem" }}>
|
||||
Connectors
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.8rem",
|
||||
color: "#71717a",
|
||||
lineHeight: 1.5,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Connect the agent to Gmail, Calendar & more.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RefreshIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
|
||||
<path d="M3 3v5h5"></path>
|
||||
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path>
|
||||
<path d="M16 21v-5h5"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { BarChart2 } from "lucide-react";
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
Analytics
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
Track traffic, usage, and events.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px dashed #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "80px 32px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 32,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
background: "#fafafa",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<BarChart2 size={24} color="#18181b" />
|
||||
</div>
|
||||
<h2
|
||||
style={{ fontSize: "1.25rem", fontWeight: 600, margin: "0 0 12px 0" }}
|
||||
>
|
||||
No data available yet
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
color: "#71717a",
|
||||
textAlign: "center",
|
||||
maxWidth: 460,
|
||||
margin: "0 0 24px 0",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Once your app is live and receiving traffic, your analytics metrics
|
||||
will appear here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
"use client";
|
||||
|
||||
import { Copy, Key } from "lucide-react";
|
||||
|
||||
export default function ApiPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
API & Webhooks
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
Connect external services to your application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "24px",
|
||||
marginBottom: 32,
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: "1rem", fontWeight: 600, margin: "0 0 16px 0" }}>
|
||||
REST API Endpoint
|
||||
</h2>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
background: "#fafafa",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 8,
|
||||
padding: "10px 16px",
|
||||
fontSize: "0.9rem",
|
||||
color: "#71717a",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "#18181b" }}>
|
||||
https://api.steadfast-camp-core-flow.vibn.app/v1
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<Copy size={16} color="#71717a" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "24px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: "1rem", fontWeight: 600, margin: 0 }}>
|
||||
API Keys
|
||||
</h2>
|
||||
<button
|
||||
style={{
|
||||
background: "#18181b",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
padding: "8px 16px",
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Generate Key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
padding: "16px 0",
|
||||
borderBottom: "1px solid #e4e4e7",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
background: "#fafafa",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Key size={16} color="#18181b" />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 4 }}>
|
||||
Production Key
|
||||
</div>
|
||||
<div style={{ fontSize: "0.85rem", color: "#71717a" }}>
|
||||
Created 2 days ago
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
||||
fontSize: "0.85rem",
|
||||
color: "#71717a",
|
||||
background: "#fafafa",
|
||||
padding: "4px 8px",
|
||||
borderRadius: 6,
|
||||
border: "1px solid #e4e4e7",
|
||||
}}
|
||||
>
|
||||
pk_live_*******************
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
export default function AutomationsPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
Automations
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
Build and manage automations in your app.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "16px 24px",
|
||||
borderBottom: "1px solid #e4e4e7",
|
||||
display: "flex",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ fontSize: "0.95rem", fontWeight: 600 }}>
|
||||
Automations
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
color: "#f97316",
|
||||
border: "1px solid #ffedd5",
|
||||
background: "#fff7ed",
|
||||
padding: "2px 6px",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
Builder+
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: "80px 32px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: 600,
|
||||
margin: "0 0 12px 0",
|
||||
}}
|
||||
>
|
||||
Unlock automations
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
color: "#71717a",
|
||||
textAlign: "center",
|
||||
maxWidth: 460,
|
||||
margin: "0 0 24px 0",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
To run automations in your app, you need backend functions enabled.
|
||||
Upgrade to enable backend functions and start using automations.
|
||||
</p>
|
||||
<button
|
||||
style={{
|
||||
background: "#f97316",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
padding: "10px 24px",
|
||||
fontSize: "0.9rem",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
View plans
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function DataPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace: string; projectId: string }>;
|
||||
}) {
|
||||
const { workspace, projectId } = await params;
|
||||
redirect(`/${workspace}/project/${projectId}/data/tables`);
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import {
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Database,
|
||||
CircleDot,
|
||||
} from "lucide-react";
|
||||
import { DatabaseTableTree } from "@/components/project/database-table-tree";
|
||||
import { TableViewer } from "@/components/project/table-viewer";
|
||||
import { useAnatomy } from "@/components/project/use-anatomy";
|
||||
|
||||
type Selection = {
|
||||
kind: "table";
|
||||
dbUuid: string;
|
||||
schema: string;
|
||||
name: string;
|
||||
} | null;
|
||||
|
||||
export default function DataTablesPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const projectId = params.projectId as string;
|
||||
const targetDbId = searchParams.get("db");
|
||||
|
||||
const { anatomy, loading, error } = useAnatomy(projectId);
|
||||
|
||||
const databases = anatomy?.infrastructure?.databases ?? [];
|
||||
|
||||
// If targetDbId is in the URL, only show that database.
|
||||
// Otherwise, default to the first database in the list if available.
|
||||
const activeDbId =
|
||||
targetDbId || (databases.length > 0 ? databases[0].uuid : null);
|
||||
const activeDatabases = databases.filter((db) => db.uuid === activeDbId);
|
||||
|
||||
const [selection, setSelection] = useState<Selection>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSelection(null);
|
||||
}, [projectId, targetDbId]);
|
||||
|
||||
const showLoading = loading && !anatomy;
|
||||
|
||||
return (
|
||||
<div style={pageWrap}>
|
||||
<div style={grid}>
|
||||
{/* ── Left rail ── */}
|
||||
<section style={leftCol}>
|
||||
{showLoading && (
|
||||
<Inline>
|
||||
<Loader2 size={13} className="animate-spin" /> Loading…
|
||||
</Inline>
|
||||
)}
|
||||
{error && !showLoading && (
|
||||
<Inline>
|
||||
<AlertCircle size={13} /> {error}
|
||||
</Inline>
|
||||
)}
|
||||
|
||||
{anatomy && (
|
||||
<RailGroup title="Databases" count={activeDatabases.length}>
|
||||
{activeDatabases.length === 0 && (
|
||||
<RailEmpty>
|
||||
No databases yet.
|
||||
<span style={nudge}>
|
||||
Try: "Add a Postgres database to my project"
|
||||
</span>
|
||||
</RailEmpty>
|
||||
)}
|
||||
{activeDatabases.map((db) => {
|
||||
return (
|
||||
<article key={db.uuid} style={codebaseTile}>
|
||||
<div style={tileHeader}>
|
||||
<span style={chevronCell}>
|
||||
<ChevronDown size={13} style={{ color: INK.mid }} />
|
||||
</span>
|
||||
<Database
|
||||
size={13}
|
||||
style={{ color: INK.mid, flexShrink: 0 }}
|
||||
/>
|
||||
<div style={{ minWidth: 0, textAlign: "left", flex: 1 }}>
|
||||
<div style={tileLabel}>{db.name}</div>
|
||||
<div style={tileHint}>{db.type}</div>
|
||||
</div>
|
||||
<CircleDot
|
||||
size={9}
|
||||
style={{ color: statusColor(db.status), flexShrink: 0 }}
|
||||
/>
|
||||
</div>
|
||||
<div style={tileBody}>
|
||||
<DatabaseTableTree
|
||||
projectId={projectId}
|
||||
dbUuid={db.uuid}
|
||||
selectedTable={
|
||||
selection?.kind === "table" &&
|
||||
selection.dbUuid === db.uuid
|
||||
? {
|
||||
schema: selection.schema,
|
||||
name: selection.name,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onSelectTable={({ schema, name }) =>
|
||||
setSelection({
|
||||
kind: "table",
|
||||
dbUuid: db.uuid,
|
||||
schema,
|
||||
name,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</RailGroup>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ── Right pane ── */}
|
||||
<aside style={rightCol}>
|
||||
<h3 style={heading}>{paneHeading(selection)}</h3>
|
||||
<div style={panel}>
|
||||
{selection?.kind === "table" && (
|
||||
<TableViewer
|
||||
projectId={projectId}
|
||||
dbUuid={selection.dbUuid}
|
||||
schema={selection.schema}
|
||||
table={selection.name}
|
||||
/>
|
||||
)}
|
||||
{!selection && (
|
||||
<Empty>Select a table on the left to preview data.</Empty>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Bits
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
function RailGroup({
|
||||
title,
|
||||
count,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
count: number;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div style={railGroup}>
|
||||
<header style={railGroupHeader}>
|
||||
<span style={railGroupTitle}>{title}</span>
|
||||
<span style={countPill}>{count}</span>
|
||||
</header>
|
||||
<div style={railItems}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RailEmpty({ children }: { children: React.ReactNode }) {
|
||||
return <div style={railEmpty}>{children}</div>;
|
||||
}
|
||||
|
||||
function Inline({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "12px 14px",
|
||||
fontSize: "0.82rem",
|
||||
color: INK.mid,
|
||||
background: INK.cardBg,
|
||||
border: `1px solid ${INK.borderSoft}`,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Empty({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: INK.mid,
|
||||
fontSize: "0.85rem",
|
||||
padding: "32px 16px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function paneHeading(s: Selection): string {
|
||||
if (!s) return "Preview";
|
||||
if (s.kind === "table")
|
||||
return `Preview · ${s.schema === "public" ? s.name : `${s.schema}.${s.name}`}`;
|
||||
return "Preview";
|
||||
}
|
||||
|
||||
function statusColor(status: string) {
|
||||
const s = (status ?? "").toLowerCase();
|
||||
if (s.includes("running") || s.includes("healthy")) return "#2e7d32";
|
||||
if (s.includes("starting") || s.includes("deploying")) return "#d4a04a";
|
||||
if (s.includes("exit") || s.includes("fail") || s.includes("unhealthy"))
|
||||
return "#c5392b";
|
||||
return "#a09a90";
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Tokens
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
const INK = {
|
||||
ink: "#1a1a1a",
|
||||
mid: "#5f5e5a",
|
||||
muted: "#a09a90",
|
||||
border: "#e8e4dc",
|
||||
borderSoft: "#efebe1",
|
||||
cardBg: "#fff",
|
||||
fontSans: '"Outfit", "Inter", ui-sans-serif, sans-serif',
|
||||
} as const;
|
||||
|
||||
const pageWrap: React.CSSProperties = {
|
||||
padding: "28px 48px 48px",
|
||||
fontFamily: INK.fontSans,
|
||||
color: INK.ink,
|
||||
};
|
||||
const grid: React.CSSProperties = {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "minmax(280px, 360px) minmax(0, 1fr)",
|
||||
gap: 28,
|
||||
maxWidth: 1400,
|
||||
margin: "0 auto",
|
||||
alignItems: "stretch",
|
||||
};
|
||||
const leftCol: React.CSSProperties = {
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 18,
|
||||
};
|
||||
const rightCol: React.CSSProperties = {
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
};
|
||||
const heading: React.CSSProperties = {
|
||||
fontSize: "0.72rem",
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.12em",
|
||||
textTransform: "uppercase",
|
||||
color: INK.muted,
|
||||
margin: "0 0 14px",
|
||||
};
|
||||
const railGroup: React.CSSProperties = {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
};
|
||||
const railGroupHeader: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "0 4px 8px",
|
||||
};
|
||||
const railGroupTitle: React.CSSProperties = {
|
||||
fontSize: "0.68rem",
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.12em",
|
||||
textTransform: "uppercase",
|
||||
color: INK.muted,
|
||||
};
|
||||
const countPill: React.CSSProperties = {
|
||||
fontSize: "0.7rem",
|
||||
fontWeight: 600,
|
||||
color: INK.mid,
|
||||
padding: "1px 7px",
|
||||
borderRadius: 999,
|
||||
background: "#f3eee4",
|
||||
};
|
||||
const railItems: React.CSSProperties = {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
};
|
||||
const railEmpty: React.CSSProperties = {
|
||||
padding: "10px 12px",
|
||||
fontSize: "0.74rem",
|
||||
color: INK.muted,
|
||||
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 codebaseTile: React.CSSProperties = {
|
||||
background: INK.cardBg,
|
||||
border: `1px solid ${INK.borderSoft}`,
|
||||
borderRadius: 10,
|
||||
overflow: "hidden",
|
||||
};
|
||||
const tileHeader: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
width: "100%",
|
||||
padding: "12px 14px",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
font: "inherit",
|
||||
color: "inherit",
|
||||
};
|
||||
const tileLabel: React.CSSProperties = {
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 600,
|
||||
color: INK.ink,
|
||||
marginBottom: 2,
|
||||
};
|
||||
const tileHint: React.CSSProperties = {
|
||||
fontSize: "0.74rem",
|
||||
color: INK.mid,
|
||||
lineHeight: 1.4,
|
||||
textTransform: "capitalize",
|
||||
};
|
||||
const tileBody: React.CSSProperties = {
|
||||
padding: "8px 10px 12px",
|
||||
borderTop: `1px solid ${INK.borderSoft}`,
|
||||
};
|
||||
const chevronCell: React.CSSProperties = {
|
||||
width: 14,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
};
|
||||
const panel: React.CSSProperties = {
|
||||
background: INK.cardBg,
|
||||
border: `1px solid ${INK.border}`,
|
||||
borderRadius: 10,
|
||||
padding: 16,
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
"use client";
|
||||
|
||||
import { Copy } from "lucide-react";
|
||||
|
||||
export default function DomainsPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 32 }}>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
Domains
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
Buy, connect and manage your domains.{" "}
|
||||
<a href="#" style={{ color: "#18181b", textDecoration: "underline" }}>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "24px",
|
||||
marginBottom: 32,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<h2 style={{ fontSize: "1rem", fontWeight: 600, margin: 0 }}>
|
||||
Built-in URL
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 6,
|
||||
padding: "6px 12px",
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Edit URL
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
background: "#fafafa",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 8,
|
||||
padding: "10px 16px",
|
||||
fontSize: "0.9rem",
|
||||
color: "#71717a",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "#18181b", fontWeight: 500 }}>
|
||||
steadfast-camp-core-flow
|
||||
</span>
|
||||
.vibn.app
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<Copy size={16} color="#71717a" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style={{ fontSize: "1.1rem", fontWeight: 600, margin: "0 0 16px 0" }}>
|
||||
Custom domains
|
||||
</h2>
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
borderStyle: "dashed",
|
||||
padding: "48px 32px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 32,
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
style={{ fontSize: "1.1rem", fontWeight: 600, margin: "0 0 8px 0" }}
|
||||
>
|
||||
Want to use your domain?
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
color: "#71717a",
|
||||
textAlign: "center",
|
||||
maxWidth: 400,
|
||||
margin: "0 0 24px 0",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Custom domains are available on our Builder plan and above. Upgrade to
|
||||
continue working to this app.
|
||||
</p>
|
||||
<button
|
||||
style={{
|
||||
background: "#18181b",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
padding: "10px 24px",
|
||||
fontSize: "0.9rem",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
View Plans
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "24px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: "1rem", fontWeight: 600, margin: 0 }}>
|
||||
Email domain
|
||||
</h2>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
color: "#f97316",
|
||||
border: "1px solid #ffedd5",
|
||||
background: "#fff7ed",
|
||||
padding: "2px 6px",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
Builder+
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
fontWeight: 500,
|
||||
color: "#18181b",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
no-reply@notifications.vibn.app
|
||||
</div>
|
||||
<div style={{ fontSize: "0.85rem", color: "#71717a" }}>
|
||||
Sender Name: App
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 6,
|
||||
padding: "8px 16px",
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
color: "#71717a",
|
||||
}}
|
||||
>
|
||||
Use your custom domain
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
"use client";
|
||||
|
||||
import { Diamond } from "lucide-react";
|
||||
|
||||
export default function IntegrationsPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
position: "relative",
|
||||
height: "calc(100vh - 100px)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
Integrations
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div style={{ filter: "blur(4px)", opacity: 0.5, pointerEvents: "none" }}>
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 24 }}>
|
||||
<button
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 8,
|
||||
padding: "8px 24px",
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
My Integrations
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
background: "#f4f4f5",
|
||||
border: "none",
|
||||
color: "#71717a",
|
||||
borderRadius: 8,
|
||||
padding: "8px 24px",
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "24px",
|
||||
marginBottom: 32,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
></div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 4 }}>Stripe</div>
|
||||
<div style={{ fontSize: "0.85rem", color: "#71717a" }}>
|
||||
Sell products or subscriptions and get paid online.
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 6,
|
||||
padding: "6px 16px",
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
style={{ fontSize: "1.1rem", fontWeight: 600, margin: "0 0 16px 0" }}
|
||||
>
|
||||
Connectors
|
||||
</h2>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", marginBottom: 24 }}>
|
||||
Connect your app to popular services.
|
||||
</p>
|
||||
|
||||
<div
|
||||
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}
|
||||
>
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "24px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
background: "#f4f4f5",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div style={{ fontWeight: 600, marginBottom: 4 }}>
|
||||
Connector {i}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.85rem", color: "#71717a" }}>
|
||||
Connect with external service for app data.
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "rgba(255, 255, 255, 0.3)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 16,
|
||||
padding: "32px 48px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
boxShadow: "0 10px 25px rgba(0,0,0,0.05)",
|
||||
maxWidth: 440,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
background: "#fff7ed",
|
||||
border: "1px solid #ffedd5",
|
||||
borderRadius: 10,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<Diamond size={18} color="#f97316" />
|
||||
</div>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: 600,
|
||||
margin: "0 0 12px 0",
|
||||
}}
|
||||
>
|
||||
Unlock this feature
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
color: "#71717a",
|
||||
textAlign: "center",
|
||||
margin: "0 0 24px 0",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
This feature is only available on the Builder plan or higher.
|
||||
Upgrade to continue working without limits.
|
||||
</p>
|
||||
<button
|
||||
style={{
|
||||
background: "#f97316",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
padding: "10px 24px",
|
||||
fontSize: "0.9rem",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Upgrade
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
export default function LogsPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
Logs
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
View application and server logs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#18181b",
|
||||
borderRadius: 12,
|
||||
overflow: "hidden",
|
||||
color: "#e4e4e7",
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "12px 16px",
|
||||
borderBottom: "1px solid #3f3f46",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
background: "#27272a",
|
||||
borderRadius: 6,
|
||||
padding: "4px 10px",
|
||||
width: 300,
|
||||
}}
|
||||
>
|
||||
<Search size={14} color="#a1a1aa" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter logs..."
|
||||
style={{
|
||||
border: "none",
|
||||
outline: "none",
|
||||
background: "transparent",
|
||||
fontSize: "0.8rem",
|
||||
width: "100%",
|
||||
color: "#fff",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
fontSize: "0.85rem",
|
||||
lineHeight: 1.6,
|
||||
height: 400,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 16 }}>
|
||||
<span style={{ color: "#71717a" }}>14:32:01</span>
|
||||
<span style={{ color: "#10b981" }}>[info]</span>
|
||||
<span>Server started on port 3000</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 16 }}>
|
||||
<span style={{ color: "#71717a" }}>14:32:05</span>
|
||||
<span style={{ color: "#10b981" }}>[info]</span>
|
||||
<span>Database connected successfully</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 16 }}>
|
||||
<span style={{ color: "#71717a" }}>14:45:12</span>
|
||||
<span style={{ color: "#3b82f6" }}>[http]</span>
|
||||
<span>GET /api/users 200 OK - 45ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function MarketingPage({ params }: { params: Promise<{ workspace: string; projectId: string }> }) {
|
||||
const { workspace, projectId } = await params;
|
||||
redirect(`/${workspace}/project/${projectId}/marketing/seo`);
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
"use client";
|
||||
|
||||
import { ListFilter } from "lucide-react";
|
||||
|
||||
export default function SeoPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
SEO & GEO
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
Improve how your app appears in search results and AI answers.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<span
|
||||
style={{ fontSize: "0.9rem", color: "#18181b", fontWeight: 500 }}
|
||||
>
|
||||
Enable SEO for this app
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
width: 44,
|
||||
height: 24,
|
||||
background: "#18181b",
|
||||
borderRadius: 12,
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
background: "#fff",
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
right: 2,
|
||||
top: 2,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 24,
|
||||
marginBottom: 24,
|
||||
borderBottom: "1px solid #e4e4e7",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
borderBottom: "2px solid #18181b",
|
||||
padding: "8px 0",
|
||||
fontSize: "0.9rem",
|
||||
fontWeight: 500,
|
||||
color: "#18181b",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
padding: "8px 0",
|
||||
fontSize: "0.9rem",
|
||||
fontWeight: 500,
|
||||
color: "#71717a",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Meta tags
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
padding: "8px 0",
|
||||
fontSize: "0.9rem",
|
||||
fontWeight: 500,
|
||||
color: "#71717a",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Advanced Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "48px 32px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 32,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
background: "#fafafa",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<ListFilter size={18} color="#18181b" />
|
||||
</div>
|
||||
<h2
|
||||
style={{ fontSize: "1.1rem", fontWeight: 600, margin: "0 0 8px 0" }}
|
||||
>
|
||||
Run an SEO & GEO scan
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.9rem",
|
||||
color: "#71717a",
|
||||
textAlign: "center",
|
||||
maxWidth: 400,
|
||||
margin: "0 0 24px 0",
|
||||
}}
|
||||
>
|
||||
Scan your app for SEO basics and GEO details. Get a prioritized
|
||||
checklist to fix issues in minutes.
|
||||
</p>
|
||||
<button
|
||||
style={{
|
||||
background: "#18181b",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
padding: "10px 24px",
|
||||
fontSize: "0.9rem",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Run Scan
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "20px 24px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: "0.95rem", fontWeight: 600, marginBottom: 4 }}
|
||||
>
|
||||
AI Assistant Discovery
|
||||
</div>
|
||||
<div style={{ fontSize: "0.85rem", color: "#71717a" }}>
|
||||
Help AI search engines understand and recommend your app
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 44,
|
||||
height: 24,
|
||||
background: "#e4e4e7",
|
||||
borderRadius: 12,
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
background: "#fff",
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
left: 2,
|
||||
top: 2,
|
||||
boxShadow: "0 1px 2px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "20px 24px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: "0.95rem", fontWeight: 600, marginBottom: 4 }}
|
||||
>
|
||||
Generate robots.txt
|
||||
</div>
|
||||
<div style={{ fontSize: "0.85rem", color: "#71717a" }}>
|
||||
Off: serve your deployed public/robots.txt if shipped, otherwise
|
||||
return 404.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 44,
|
||||
height: 24,
|
||||
background: "#18181b",
|
||||
borderRadius: 12,
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
background: "#fff",
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
right: 2,
|
||||
top: 2,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "20px 24px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: "0.95rem", fontWeight: 600, marginBottom: 4 }}
|
||||
>
|
||||
Generate sitemap.xml
|
||||
</div>
|
||||
<div style={{ fontSize: "0.85rem", color: "#71717a" }}>
|
||||
Off: serve your deployed public/sitemap.xml if shipped, otherwise
|
||||
return 404.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 44,
|
||||
height: 24,
|
||||
background: "#18181b",
|
||||
borderRadius: 12,
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
background: "#fff",
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
right: 2,
|
||||
top: 2,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "20px 24px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 600 }}>
|
||||
<div
|
||||
style={{ fontSize: "0.95rem", fontWeight: 600, marginBottom: 4 }}
|
||||
>
|
||||
Auto-generate per-page breadcrumbs
|
||||
</div>
|
||||
<div
|
||||
style={{ fontSize: "0.85rem", color: "#71717a", lineHeight: 1.5 }}
|
||||
>
|
||||
Build a fresh BreadcrumbList for each route instead of using the
|
||||
same persisted list site-wide. Turn off if you hand crafted your
|
||||
breadcrumb schema and want it served verbatim.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 44,
|
||||
height: 24,
|
||||
background: "#18181b",
|
||||
borderRadius: 12,
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
background: "#fff",
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
right: 2,
|
||||
top: 2,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { Share2 } from "lucide-react";
|
||||
|
||||
export default function SocialPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
Social Content
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
Manage social sharing campaigns and meta tags.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px dashed #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "80px 32px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 32,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
background: "#fafafa",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<Share2 size={24} color="#18181b" />
|
||||
</div>
|
||||
<h2
|
||||
style={{ fontSize: "1.25rem", fontWeight: 600, margin: "0 0 12px 0" }}
|
||||
>
|
||||
Social Campaign Manager
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
color: "#71717a",
|
||||
textAlign: "center",
|
||||
maxWidth: 460,
|
||||
margin: "0 0 24px 0",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Automatically generate and schedule social media content across
|
||||
platforms based on your app's pages.
|
||||
</p>
|
||||
<button
|
||||
style={{
|
||||
background: "#18181b",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
padding: "10px 24px",
|
||||
fontSize: "0.9rem",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Connect Social Accounts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -181,9 +181,13 @@ export default function PreviewTab() {
|
||||
const path = currentPath.startsWith("/")
|
||||
? currentPath
|
||||
: `/${currentPath}`;
|
||||
setIframeSrc(`${base}${path}`);
|
||||
// Add the refreshKey as a query param so the iframe completely remounts/refetches
|
||||
// when the user hits the manual refresh button.
|
||||
const urlObj = new URL(`${base}${path}`);
|
||||
urlObj.searchParams.set("_refresh", refreshKey.toString());
|
||||
setIframeSrc(urlObj.toString());
|
||||
}
|
||||
}, [primaryRunning?.url, currentPath]);
|
||||
}, [primaryRunning?.url, currentPath, refreshKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!bridge || !iframeSrc || !iframeDomRef.current) return;
|
||||
|
||||
@@ -0,0 +1,696 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import {
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
ExternalLink,
|
||||
Globe,
|
||||
RefreshCw,
|
||||
CircleDot,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
Check,
|
||||
Terminal,
|
||||
Server,
|
||||
} from "lucide-react";
|
||||
import { useAnatomy, type Anatomy } from "@/components/project/use-anatomy";
|
||||
|
||||
/**
|
||||
* Hosting tab — user-facing: "Is my thing live? How do I reach it?"
|
||||
*
|
||||
* One endpoint = one card. Each card shows:
|
||||
* - Live URL (open in new tab)
|
||||
* - Status dot + plain-language status
|
||||
* - Redeploy button
|
||||
* - Domain(s) list
|
||||
* - Last build (time + status)
|
||||
* - Expandable recent logs
|
||||
*
|
||||
* No master-detail split — with 1-3 services the overhead isn't worth it.
|
||||
* Previews (dev server URLs) shown below in a secondary section.
|
||||
*/
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Types
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
type LiveItem = Anatomy["hosting"]["live"][number];
|
||||
type Preview = Anatomy["hosting"]["previews"][number];
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Main component
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
export default function ServicesPage() {
|
||||
const params = useParams();
|
||||
const projectId = params.projectId as string;
|
||||
const { anatomy, loading, error } = useAnatomy(projectId, { pollMs: 8000 });
|
||||
const showLoading = loading && !anatomy;
|
||||
|
||||
return (
|
||||
<div style={pageWrap}>
|
||||
{showLoading && (
|
||||
<div style={centeredMsg}>
|
||||
<Loader2
|
||||
size={16}
|
||||
className="animate-spin"
|
||||
style={{ color: INK.muted }}
|
||||
/>
|
||||
<span style={{ color: INK.muted, fontSize: "0.85rem" }}>
|
||||
Loading…
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{error && !showLoading && (
|
||||
<div style={centeredMsg}>
|
||||
<AlertCircle size={15} style={{ color: DANGER }} />
|
||||
<span style={{ fontSize: "0.85rem", color: DANGER }}>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{anatomy && (
|
||||
<>
|
||||
{/* ── Live endpoints ── */}
|
||||
<section>
|
||||
<SectionHeader title="Live" count={anatomy.hosting.live.length} />
|
||||
{anatomy.hosting.live.length === 0 ? (
|
||||
<EmptySection
|
||||
icon={<Server size={20} style={{ color: INK.muted }} />}
|
||||
title="Nothing deployed yet"
|
||||
hint="Ask the AI to deploy your app and it will appear here."
|
||||
promptSuggestion="Deploy my app to production"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: 16 }}
|
||||
>
|
||||
{anatomy.hosting.live.map((item) => (
|
||||
<LiveCard key={item.uuid} item={item} projectId={projectId} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ── Previews ── */}
|
||||
{anatomy.hosting.previews.length > 0 && (
|
||||
<section style={{ marginTop: 40 }}>
|
||||
<SectionHeader
|
||||
title="Dev Previews"
|
||||
count={anatomy.hosting.previews.length}
|
||||
/>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: 10 }}
|
||||
>
|
||||
{anatomy.hosting.previews.map((p) => (
|
||||
<PreviewRow key={p.id} preview={p} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Live card
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
function LiveCard({ item, projectId }: { item: LiveItem; projectId: string }) {
|
||||
const [deploying, setDeploying] = useState(false);
|
||||
const [logsOpen, setLogsOpen] = useState(false);
|
||||
const [logs, setLogs] = useState<string | null>(null);
|
||||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const primaryUrl = item.fqdn ? `https://${item.fqdn}` : null;
|
||||
const phase = classifyPhase(item.status);
|
||||
const { color: statusColor, label: statusLabel } = phaseDisplay(phase, item);
|
||||
|
||||
const redeploy = async () => {
|
||||
if (deploying) return;
|
||||
setDeploying(true);
|
||||
try {
|
||||
await fetch(`/api/mcp`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "apps.deploy",
|
||||
params: { uuid: item.uuid, projectId },
|
||||
}),
|
||||
});
|
||||
} finally {
|
||||
setTimeout(() => setDeploying(false), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const openLogs = async () => {
|
||||
if (!logsOpen) {
|
||||
setLogsOpen(true);
|
||||
setLogsLoading(true);
|
||||
try {
|
||||
const r = await fetch(`/api/mcp`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "apps.logs",
|
||||
params: { uuid: item.uuid, lines: 60 },
|
||||
}),
|
||||
});
|
||||
const d = await r.json();
|
||||
setLogs(
|
||||
typeof d.result === "string"
|
||||
? d.result
|
||||
: JSON.stringify(d.result ?? d.error, null, 2),
|
||||
);
|
||||
} catch {
|
||||
setLogs("Failed to load logs.");
|
||||
} finally {
|
||||
setLogsLoading(false);
|
||||
}
|
||||
} else {
|
||||
setLogsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyUrl = () => {
|
||||
if (!primaryUrl) return;
|
||||
navigator.clipboard.writeText(primaryUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={card}>
|
||||
{/* ── Card header ── */}
|
||||
<div style={cardHeader}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<CircleDot size={11} style={{ color: statusColor, flexShrink: 0 }} />
|
||||
<span style={cardTitle}>{item.name}</span>
|
||||
<span style={sourcePill(item.source)}>
|
||||
{item.source === "repo" ? "built" : "image"}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<button
|
||||
onClick={redeploy}
|
||||
disabled={deploying}
|
||||
style={actionBtn}
|
||||
title="Redeploy now"
|
||||
>
|
||||
{deploying ? (
|
||||
<Loader2 size={13} className="animate-spin" />
|
||||
) : (
|
||||
<RefreshCw size={13} />
|
||||
)}
|
||||
{deploying ? "Deploying…" : "Redeploy"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Status line ── */}
|
||||
<div style={statusLine}>
|
||||
<span style={{ color: statusColor, fontWeight: 600 }}>
|
||||
{statusLabel}
|
||||
</span>
|
||||
{item.lastBuild && (
|
||||
<span style={{ color: INK.muted }}>
|
||||
· Last build {item.lastBuild.status}{" "}
|
||||
{formatRelative(item.lastBuild.finishedAt)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Live URL ── */}
|
||||
{primaryUrl ? (
|
||||
<div style={urlRow}>
|
||||
<Globe size={13} style={{ color: "#2e7d32", flexShrink: 0 }} />
|
||||
<a href={primaryUrl} target="_blank" rel="noreferrer" style={urlLink}>
|
||||
{primaryUrl}
|
||||
</a>
|
||||
<ExternalLink size={11} style={{ color: INK.muted, flexShrink: 0 }} />
|
||||
<button onClick={copyUrl} style={iconBtn} title="Copy URL">
|
||||
{copied ? (
|
||||
<Check size={12} style={{ color: "#2e7d32" }} />
|
||||
) : (
|
||||
<Copy size={12} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={urlRow}>
|
||||
<Globe size={13} style={{ color: INK.muted, flexShrink: 0 }} />
|
||||
<span
|
||||
style={{
|
||||
color: INK.muted,
|
||||
fontSize: "0.82rem",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
No domain attached — ask the AI to add one.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Extra domains ── */}
|
||||
{item.domains.length > 1 && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 23,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{item.domains.slice(1).map((d) => (
|
||||
<a
|
||||
key={d}
|
||||
href={`https://${d}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ ...urlLink, fontSize: "0.78rem", color: INK.mid }}
|
||||
>
|
||||
{d}{" "}
|
||||
<ExternalLink
|
||||
size={10}
|
||||
style={{ display: "inline", verticalAlign: "middle" }}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Logs toggle ── */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 14,
|
||||
borderTop: `1px solid ${INK.borderSoft}`,
|
||||
paddingTop: 10,
|
||||
}}
|
||||
>
|
||||
<button onClick={openLogs} style={logsToggleBtn}>
|
||||
<Terminal size={12} />
|
||||
{logsOpen ? "Hide logs" : "Show recent logs"}
|
||||
{logsOpen ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
</button>
|
||||
|
||||
{logsOpen && (
|
||||
<div style={logsBox}>
|
||||
{logsLoading ? (
|
||||
<span style={{ color: INK.muted, fontSize: "0.8rem" }}>
|
||||
Loading…
|
||||
</span>
|
||||
) : (
|
||||
<pre style={logsPre}>{logs || "(no logs)"}</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Preview row
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
function PreviewRow({ preview }: { preview: Preview }) {
|
||||
const running = preview.state === "running";
|
||||
return (
|
||||
<div style={{ ...card, padding: "12px 16px" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<CircleDot
|
||||
size={10}
|
||||
style={{ color: running ? "#10b981" : INK.muted, flexShrink: 0 }}
|
||||
/>
|
||||
<span style={{ fontSize: "0.85rem", fontWeight: 600, color: INK.ink }}>
|
||||
{preview.name}
|
||||
</span>
|
||||
<span style={{ fontSize: "0.75rem", color: INK.mid }}>
|
||||
port {preview.port}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
color: running ? "#10b981" : INK.muted,
|
||||
fontWeight: 500,
|
||||
background: running ? "#ecfdf5" : "#f4f4f5",
|
||||
padding: "2px 8px",
|
||||
borderRadius: 12,
|
||||
marginLeft: 8,
|
||||
}}
|
||||
>
|
||||
{preview.state}
|
||||
</span>
|
||||
{preview.url && running && (
|
||||
<div
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={preview.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ ...urlLink, marginLeft: 0 }}
|
||||
>
|
||||
{preview.url.replace(/^https?:\/\//, "")}{" "}
|
||||
<ExternalLink
|
||||
size={10}
|
||||
style={{ display: "inline", verticalAlign: "middle" }}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
type Phase = "up" | "deploying" | "down" | "unknown";
|
||||
|
||||
function classifyPhase(status: string | undefined): Phase {
|
||||
const s = (status ?? "").toLowerCase();
|
||||
if (!s || s === "unknown") return "unknown";
|
||||
if (/^(running|healthy)/.test(s)) return "up";
|
||||
if (
|
||||
/^(starting|restarting|created|deploying|building|in_progress|queued|paused)/.test(
|
||||
s,
|
||||
)
|
||||
)
|
||||
return "deploying";
|
||||
if (/^(exited|dead|failed|stopped|unhealthy|error)/.test(s)) return "down";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function phaseDisplay(
|
||||
phase: Phase,
|
||||
item: LiveItem,
|
||||
): { color: string; label: string } {
|
||||
if (item.inFlightBuild)
|
||||
return {
|
||||
color: AMBER,
|
||||
label: `Deploying (${item.inFlightBuild.status ?? "in progress"})`,
|
||||
};
|
||||
switch (phase) {
|
||||
case "up":
|
||||
return { color: GREEN, label: "Live" };
|
||||
case "deploying":
|
||||
return { color: AMBER, label: "Starting…" };
|
||||
case "down":
|
||||
return { color: DANGER, label: "Down" };
|
||||
default:
|
||||
return { color: INK.muted, label: "Unknown" };
|
||||
}
|
||||
}
|
||||
|
||||
function formatRelative(iso: string | undefined) {
|
||||
if (!iso) return "";
|
||||
const ms = Date.now() - new Date(iso).getTime();
|
||||
if (Number.isNaN(ms)) return "";
|
||||
const min = Math.floor(ms / 60_000);
|
||||
if (min < 1) return "just now";
|
||||
if (min < 60) return `${min}m ago`;
|
||||
const hr = Math.floor(min / 60);
|
||||
if (hr < 24) return `${hr}h ago`;
|
||||
return `${Math.floor(hr / 24)}d ago`;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Sub-components
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
function SectionHeader({ title, count }: { title: string; count: number }) {
|
||||
return (
|
||||
<div style={sectionHeader}>
|
||||
<span style={sectionTitle}>{title}</span>
|
||||
<span style={countPill}>{count}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptySection({
|
||||
icon,
|
||||
title,
|
||||
hint,
|
||||
promptSuggestion,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
hint: string;
|
||||
promptSuggestion?: string;
|
||||
}) {
|
||||
return (
|
||||
<div style={emptyBox}>
|
||||
<div style={{ marginBottom: 10 }}>{icon}</div>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
color: INK.ink,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.82rem",
|
||||
color: INK.mid,
|
||||
marginBottom: promptSuggestion ? 14 : 0,
|
||||
}}
|
||||
>
|
||||
{hint}
|
||||
</div>
|
||||
{promptSuggestion && (
|
||||
<div style={promptChip}>
|
||||
<span
|
||||
style={{ fontSize: "0.7rem", color: INK.muted, marginRight: 6 }}
|
||||
>
|
||||
Try asking:
|
||||
</span>
|
||||
<span
|
||||
style={{ fontStyle: "italic", fontSize: "0.8rem", color: INK.mid }}
|
||||
>
|
||||
"{promptSuggestion}"
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Tokens
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
const INK = {
|
||||
ink: "#1a1a1a",
|
||||
mid: "#5f5e5a",
|
||||
muted: "#a09a90",
|
||||
border: "#e8e4dc",
|
||||
borderSoft: "#efebe1",
|
||||
cardBg: "#fff",
|
||||
fontSans: '"Outfit", "Inter", ui-sans-serif, sans-serif',
|
||||
} as const;
|
||||
const GREEN = "#10b981";
|
||||
const AMBER = "#f59e0b";
|
||||
const DANGER = "#ef4444";
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Styles
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
const pageWrap: React.CSSProperties = {
|
||||
padding: "28px 48px 64px",
|
||||
fontFamily: INK.fontSans,
|
||||
color: INK.ink,
|
||||
maxWidth: 860,
|
||||
};
|
||||
const centeredMsg: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
padding: "24px 0",
|
||||
};
|
||||
const sectionHeader: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 14,
|
||||
};
|
||||
const sectionTitle: React.CSSProperties = {
|
||||
fontSize: "0.68rem",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.12em",
|
||||
textTransform: "uppercase",
|
||||
color: INK.muted,
|
||||
};
|
||||
const countPill: React.CSSProperties = {
|
||||
fontSize: "0.7rem",
|
||||
fontWeight: 600,
|
||||
color: INK.mid,
|
||||
padding: "1px 7px",
|
||||
borderRadius: 999,
|
||||
background: "#f3eee4",
|
||||
};
|
||||
const card: React.CSSProperties = {
|
||||
background: INK.cardBg,
|
||||
border: `1px solid ${INK.border}`,
|
||||
borderRadius: 10,
|
||||
padding: "18px 20px",
|
||||
};
|
||||
const cardHeader: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
marginBottom: 6,
|
||||
};
|
||||
const cardTitle: React.CSSProperties = {
|
||||
fontSize: "0.95rem",
|
||||
fontWeight: 700,
|
||||
color: INK.ink,
|
||||
};
|
||||
const statusLine: React.CSSProperties = {
|
||||
fontSize: "0.8rem",
|
||||
color: INK.mid,
|
||||
marginBottom: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
flexWrap: "wrap",
|
||||
};
|
||||
const urlRow: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
background: "#f8f5f0",
|
||||
borderRadius: 6,
|
||||
padding: "8px 12px",
|
||||
marginBottom: 2,
|
||||
};
|
||||
const urlLink: React.CSSProperties = {
|
||||
fontSize: "0.85rem",
|
||||
color: INK.ink,
|
||||
textDecoration: "none",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
};
|
||||
const actionBtn: React.CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: "6px 12px",
|
||||
border: `1px solid ${INK.border}`,
|
||||
borderRadius: 6,
|
||||
background: "#fff",
|
||||
cursor: "pointer",
|
||||
font: "inherit",
|
||||
fontSize: "0.78rem",
|
||||
fontWeight: 600,
|
||||
color: INK.mid,
|
||||
transition: "background 0.1s, border-color 0.1s",
|
||||
};
|
||||
const iconBtn: React.CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 26,
|
||||
height: 26,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
cursor: "pointer",
|
||||
color: INK.muted,
|
||||
borderRadius: 4,
|
||||
flexShrink: 0,
|
||||
};
|
||||
const logsToggleBtn: React.CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
color: INK.mid,
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
font: "inherit",
|
||||
padding: 0,
|
||||
};
|
||||
const logsBox: React.CSSProperties = {
|
||||
marginTop: 10,
|
||||
background: "#1a1a1a",
|
||||
borderRadius: 6,
|
||||
padding: "12px 14px",
|
||||
maxHeight: 320,
|
||||
overflowY: "auto",
|
||||
};
|
||||
const logsPre: React.CSSProperties = {
|
||||
margin: 0,
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
fontSize: "0.72rem",
|
||||
color: "#d4d0c8",
|
||||
lineHeight: 1.6,
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-all",
|
||||
};
|
||||
|
||||
const emptyBox: React.CSSProperties = {
|
||||
border: `1px dashed ${INK.border}`,
|
||||
borderRadius: 10,
|
||||
padding: "36px 28px",
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
};
|
||||
const promptChip: React.CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
background: "#f3eee4",
|
||||
borderRadius: 6,
|
||||
padding: "6px 12px",
|
||||
fontSize: "0.8rem",
|
||||
};
|
||||
|
||||
function sourcePill(source: "repo" | "image"): React.CSSProperties {
|
||||
const isRepo = source === "repo";
|
||||
return {
|
||||
fontSize: "0.62rem",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.08em",
|
||||
textTransform: "uppercase",
|
||||
color: isRepo ? "#2e6d2e" : "#3b5a78",
|
||||
background: isRepo ? "#eaf3e8" : "#e9eff5",
|
||||
padding: "1px 6px",
|
||||
borderRadius: 4,
|
||||
flexShrink: 0,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
export default function AppSettingsPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
App Settings
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
General configuration for your application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<label style={{ fontSize: "0.9rem", fontWeight: 500 }}>
|
||||
App Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="CampCore"
|
||||
style={{
|
||||
padding: "10px 14px",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 8,
|
||||
fontSize: "0.9rem",
|
||||
outline: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<label style={{ fontSize: "0.9rem", fontWeight: 500 }}>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
defaultValue="A comprehensive operating system for camp and youth activity providers."
|
||||
style={{
|
||||
padding: "10px 14px",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 8,
|
||||
fontSize: "0.9rem",
|
||||
outline: "none",
|
||||
resize: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ borderTop: "1px solid #e4e4e7", margin: "12px 0" }}></div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: "1rem",
|
||||
fontWeight: 600,
|
||||
margin: "0 0 4px 0",
|
||||
color: "#ef4444",
|
||||
}}
|
||||
>
|
||||
Delete Application
|
||||
</h3>
|
||||
<p style={{ fontSize: "0.85rem", color: "#71717a", margin: 0 }}>
|
||||
Permanently delete this app and all of its data.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
background: "#fee2e2",
|
||||
color: "#ef4444",
|
||||
border: "1px solid #fca5a5",
|
||||
borderRadius: 8,
|
||||
padding: "8px 16px",
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Delete App
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
export default function AuthSettingsPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
Authentication
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
Configure how users sign in to your app.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "24px",
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: "0.95rem", fontWeight: 600, marginBottom: 4 }}
|
||||
>
|
||||
Email & Password
|
||||
</div>
|
||||
<div style={{ fontSize: "0.85rem", color: "#71717a" }}>
|
||||
Allow users to sign up with an email and password.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 44,
|
||||
height: 24,
|
||||
background: "#18181b",
|
||||
borderRadius: 12,
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
background: "#fff",
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
right: 2,
|
||||
top: 2,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ borderTop: "1px solid #e4e4e7", margin: "16px 0" }}></div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: "0.95rem", fontWeight: 600, marginBottom: 4 }}
|
||||
>
|
||||
Google OAuth
|
||||
</div>
|
||||
<div style={{ fontSize: "0.85rem", color: "#71717a" }}>
|
||||
Allow users to sign in with their Google account.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 44,
|
||||
height: 24,
|
||||
background: "#e4e4e7",
|
||||
borderRadius: 12,
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
background: "#fff",
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
left: 2,
|
||||
top: 2,
|
||||
boxShadow: "0 1px 2px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { HardDrive } from "lucide-react";
|
||||
|
||||
export default function StoragePage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
Storage
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
Manage your cloud storage buckets and assets.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px dashed #e4e4e7",
|
||||
borderRadius: 12,
|
||||
padding: "80px 32px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 32,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
background: "#fafafa",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<HardDrive size={24} color="#18181b" />
|
||||
</div>
|
||||
<h2
|
||||
style={{ fontSize: "1.25rem", fontWeight: 600, margin: "0 0 12px 0" }}
|
||||
>
|
||||
No buckets found
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
color: "#71717a",
|
||||
textAlign: "center",
|
||||
maxWidth: 460,
|
||||
margin: "0 0 24px 0",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Create an S3-compatible storage bucket to start uploading user files,
|
||||
avatars, and application assets.
|
||||
</p>
|
||||
<button
|
||||
style={{
|
||||
background: "#18181b",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
padding: "10px 24px",
|
||||
fontSize: "0.9rem",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Create Bucket
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
"use client";
|
||||
|
||||
import { Search, ChevronDown, ListFilter } from "lucide-react";
|
||||
|
||||
export default function UsersPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "32px 48px",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
color: "#18181b",
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
style={{ fontSize: "1.5rem", fontWeight: 600, margin: "0 0 4px 0" }}
|
||||
>
|
||||
Users
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.9rem", color: "#71717a", margin: 0 }}>
|
||||
Manage the app's users and their roles
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 12 }}>
|
||||
<button
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 36,
|
||||
height: 36,
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<ListFilter size={16} color="#18181b" />
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
background: "#18181b",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
padding: "0 16px",
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
height: 36,
|
||||
}}
|
||||
>
|
||||
Invite User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 24 }}>
|
||||
<button
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 8,
|
||||
padding: "8px 48px",
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
boxShadow: "0 1px 2px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
Users
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
background: "#f4f4f5",
|
||||
border: "none",
|
||||
color: "#71717a",
|
||||
borderRadius: 8,
|
||||
padding: "8px 48px",
|
||||
fontSize: "0.85rem",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Pending requests
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 12,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
borderBottom: "1px solid #e4e4e7",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: "1rem", fontWeight: 600, margin: 0 }}>
|
||||
Users
|
||||
</h2>
|
||||
<div style={{ display: "flex", gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 6,
|
||||
padding: "6px 12px",
|
||||
width: 240,
|
||||
}}
|
||||
>
|
||||
<Search size={14} color="#a1a1aa" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by Email or Name"
|
||||
style={{
|
||||
border: "none",
|
||||
outline: "none",
|
||||
background: "transparent",
|
||||
fontSize: "0.8rem",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
background: "#fff",
|
||||
border: "1px solid #e4e4e7",
|
||||
borderRadius: 6,
|
||||
padding: "6px 12px",
|
||||
fontSize: "0.8rem",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
all roles <ChevronDown size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
textAlign: "left",
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
background: "#fafafa",
|
||||
borderBottom: "1px solid #e4e4e7",
|
||||
}}
|
||||
>
|
||||
<th
|
||||
style={{
|
||||
padding: "12px 16px",
|
||||
fontWeight: 500,
|
||||
color: "#71717a",
|
||||
width: "40%",
|
||||
}}
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
padding: "12px 16px",
|
||||
fontWeight: 500,
|
||||
color: "#71717a",
|
||||
width: "20%",
|
||||
}}
|
||||
>
|
||||
Role
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
padding: "12px 16px",
|
||||
fontWeight: 500,
|
||||
color: "#71717a",
|
||||
width: "40%",
|
||||
}}
|
||||
>
|
||||
Email
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style={{ padding: "16px", borderBottom: "1px solid #e4e4e7" }}
|
||||
>
|
||||
<div style={{ fontWeight: 500, color: "#18181b" }}>
|
||||
Mark Henderson
|
||||
</div>
|
||||
<div
|
||||
style={{ color: "#71717a", fontSize: "0.8rem", marginTop: 2 }}
|
||||
>
|
||||
Owner
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "16px",
|
||||
borderBottom: "1px solid #e4e4e7",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
admin
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "16px",
|
||||
borderBottom: "1px solid #e4e4e7",
|
||||
color: "#18181b",
|
||||
}}
|
||||
>
|
||||
markhenderson1977@gmail.com
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -793,81 +793,7 @@ async function loadPreviews(projectId: string): Promise<Preview[]> {
|
||||
[projectId],
|
||||
);
|
||||
|
||||
// Filter out zombies: if a server is marked 'running' but the URL returns a 50x
|
||||
// Gateway error or times out, the process died. We mark it stopped so the
|
||||
// UI can trigger an auto-restart.
|
||||
const activePreviews: typeof rows = [];
|
||||
|
||||
await Promise.all(
|
||||
rows.map(async (r) => {
|
||||
if (r.state !== "running") {
|
||||
activePreviews.push(r);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
// We use a short timeout because we don't want to block the anatomy
|
||||
// response. A slow response doesn't mean it's dead (Next.js might
|
||||
// just be compiling) — we ONLY want to catch instant 502/503s from Traefik.
|
||||
const timeout = setTimeout(() => controller.abort(), 2000);
|
||||
const ping = await fetch(r.preview_url, {
|
||||
method: "HEAD",
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
// 502/503/504 means Traefik is up but the container isn't answering.
|
||||
// 404 means Traefik doesn't even know about the route.
|
||||
if (
|
||||
ping.status === 502 ||
|
||||
ping.status === 503 ||
|
||||
ping.status === 504 ||
|
||||
ping.status === 404
|
||||
) {
|
||||
// GRACE PERIOD: If the server was started less than 60 seconds ago,
|
||||
// Traefik might return a 502/504 simply because the Node process hasn't
|
||||
// finished booting and binding to the port yet. Do not kill it!
|
||||
const ageMs = Date.now() - new Date(r.started_at).getTime();
|
||||
if (ageMs < 60_000) {
|
||||
activePreviews.push(r); // Give it the benefit of the doubt
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`[anatomy] Preview zombie detected for ${r.preview_url} (HTTP ${ping.status}, age ${Math.round(ageMs / 1000)}s). Marking stopped.`,
|
||||
);
|
||||
await query(
|
||||
`UPDATE fs_dev_servers SET state = 'stopped' WHERE id = $1`,
|
||||
[r.id],
|
||||
).catch(() => {});
|
||||
} else {
|
||||
activePreviews.push(r);
|
||||
}
|
||||
} catch (e: any) {
|
||||
// If the fetch aborts due to our 2s timeout, the server is just slow
|
||||
// (likely doing a cold Webpack compile). DO NOT mark it as a zombie!
|
||||
// Only kill it if we get a hard DNS/network error that isn't a timeout.
|
||||
if (
|
||||
e.name === "AbortError" ||
|
||||
e.type === "aborted" ||
|
||||
e.message?.includes("timeout")
|
||||
) {
|
||||
activePreviews.push(r); // Benefit of the doubt — it's thinking
|
||||
} else {
|
||||
console.warn(
|
||||
`[anatomy] Preview zombie detected for ${r.preview_url} (${e.message}). Marking stopped.`,
|
||||
);
|
||||
await query(
|
||||
`UPDATE fs_dev_servers SET state = 'stopped' WHERE id = $1`,
|
||||
[r.id],
|
||||
).catch(() => {});
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return sortDevPreviewsFrontendFirst(activePreviews).map((r) => ({
|
||||
return sortDevPreviewsFrontendFirst(rows).map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
command: r.command ?? undefined,
|
||||
|
||||
@@ -166,63 +166,63 @@ function friendlyToolName(name: string): string {
|
||||
"request.visual.qa": "Running visual QA",
|
||||
|
||||
// Core Platform Tools
|
||||
"projects.list": "📂 Listing workspace projects",
|
||||
"projects.get": "🗒️ Retrieving project spec sheets",
|
||||
"workspace.describe": "💼 Fetching workspace details",
|
||||
"gitea.credentials": "🔑 Resolving repository Git credentials",
|
||||
"projects.list": "Listing workspace projects",
|
||||
"projects.get": "Retrieving project spec sheets",
|
||||
"workspace.describe": "Fetching workspace details",
|
||||
"gitea.credentials": "Resolving repository Git credentials",
|
||||
"shell.exec": "Running command",
|
||||
ship: "Shipping",
|
||||
"generate.media": "📸 Generating visual media assets",
|
||||
"get.design.template": "📐 Retrieving design templates",
|
||||
"apps.templates.scaffold": "🧱 Scaffolding bento-grid layouts",
|
||||
"generate.media": "Generating visual media assets",
|
||||
"get.design.template": "Retrieving design templates",
|
||||
"apps.templates.scaffold": "Scaffolding bento-grid layouts",
|
||||
|
||||
// App & Database Deployment Tools
|
||||
"apps.list": "🖥️ Listing deployed web services",
|
||||
"apps.get": "🔍 Checking application build status",
|
||||
"apps.list": "Listing deployed web services",
|
||||
"apps.get": "Checking application build status",
|
||||
"apps.create": "Creating app",
|
||||
"apps.update": "⚙️ Updating application settings",
|
||||
"apps.delete": "❌ Removing deployed application",
|
||||
"apps.update": "Updating application settings",
|
||||
"apps.delete": "Removing deployed application",
|
||||
"apps.deploy": "Deploying app",
|
||||
"apps.deployments": "📜 Fetching recent deployment history",
|
||||
"apps.envs.list": "🔒 Loading environment variables",
|
||||
"apps.envs.upsert": "🔑 Injecting environment variables",
|
||||
"apps.envs.delete": "🗑️ Removing environment variables",
|
||||
"apps.domains.list": "🌐 Checking application domain routing",
|
||||
"apps.domains.set": "🔗 Binding custom domains",
|
||||
"apps.logs": "📋 Fetching application logs",
|
||||
"apps.exec": "🐚 Running command inside container",
|
||||
"databases.list": "🛢️ Listing database clusters",
|
||||
"databases.create": "🛢️ Provisioning database service",
|
||||
"databases.get": "🔌 Retrieving database connection credentials",
|
||||
"databases.update": "⚙️ Updating database configuration",
|
||||
"databases.delete": "🗑️ Removing database service",
|
||||
"apps.deployments": "Fetching recent deployment history",
|
||||
"apps.envs.list": "Loading environment variables",
|
||||
"apps.envs.upsert": "Injecting environment variables",
|
||||
"apps.envs.delete": "Removing environment variables",
|
||||
"apps.domains.list": "Checking application domain routing",
|
||||
"apps.domains.set": "Binding custom domains",
|
||||
"apps.logs": "Fetching application logs",
|
||||
"apps.exec": "Running command inside container",
|
||||
"databases.list": "Listing database clusters",
|
||||
"databases.create": "Provisioning database service",
|
||||
"databases.get": "Retrieving database connection credentials",
|
||||
"databases.update": "Updating database configuration",
|
||||
"databases.delete": "Removing database service",
|
||||
|
||||
// Domain & Git Tools
|
||||
"domains.search": "🔎 Searching open domain names",
|
||||
"domains.list": "🌐 Listing registered domains",
|
||||
"domains.get": "📄 Retrieving domain details",
|
||||
"domains.register": "💳 Registering domain name",
|
||||
"domains.attach": "🔌 Attaching domain reverse-proxy rules",
|
||||
"gitea.repos.list": "📦 Listing Gitea repositories",
|
||||
"gitea.repo.get": "🔍 Loading Gitea repository info",
|
||||
"gitea.repo.create": "🏗️ Initializing Gitea repository",
|
||||
"gitea.file.read": "📖 Reading file from Gitea",
|
||||
"gitea.file.write": "💾 Saving file to Gitea",
|
||||
"gitea.file.delete": "🗑️ Deleting file from Gitea",
|
||||
"gitea.branches.list": "🌿 Checking repository branches",
|
||||
"gitea.branch.create": "🌱 Creating Git branch",
|
||||
"devcontainer.ensure": "🐋 Spinning up secure Alpine dev container",
|
||||
"devcontainer.status": "💓 Probing dev container liveness",
|
||||
"devcontainer.suspend": "💤 Suspending dev container",
|
||||
"domains.search": "Searching open domain names",
|
||||
"domains.list": "Listing registered domains",
|
||||
"domains.get": "Retrieving domain details",
|
||||
"domains.register": "Registering domain name",
|
||||
"domains.attach": "Attaching domain reverse-proxy rules",
|
||||
"gitea.repos.list": "Listing Gitea repositories",
|
||||
"gitea.repo.get": "Loading Gitea repository info",
|
||||
"gitea.repo.create": "Initializing Gitea repository",
|
||||
"gitea.file.read": "Reading file from Gitea",
|
||||
"gitea.file.write": "Saving file to Gitea",
|
||||
"gitea.file.delete": "Deleting file from Gitea",
|
||||
"gitea.branches.list": "Checking repository branches",
|
||||
"gitea.branch.create": "Creating Git branch",
|
||||
"devcontainer.ensure": "Spinning up secure Alpine dev container",
|
||||
"devcontainer.status": "Probing dev container liveness",
|
||||
"devcontainer.suspend": "Suspending dev container",
|
||||
|
||||
// Planning / Specs Tools
|
||||
"plan.get": "📋 Loading specifications checklist",
|
||||
"plan.vision.set": "🎯 Saving feature product specification",
|
||||
"plan.idea.add": "💡 Adding planning ideation",
|
||||
"plan.task.add": "➕ Adding task to development roadmap",
|
||||
"plan.task.edit": "✏️ Updating development roadmap task",
|
||||
"plan.task.complete": "✅ Toggling checklist milestone as completed",
|
||||
"plan.document.update": "📝 Updating specs documentation",
|
||||
"plan.get": "Loading specifications checklist",
|
||||
"plan.vision.set": "Saving feature product specification",
|
||||
"plan.idea.add": "Adding planning ideation",
|
||||
"plan.task.add": "Adding task to development roadmap",
|
||||
"plan.task.edit": "Updating development roadmap task",
|
||||
"plan.task.complete": "Toggling checklist milestone as completed",
|
||||
"plan.document.update": "Updating specs documentation",
|
||||
};
|
||||
|
||||
return map[dotted] || dotted;
|
||||
@@ -2359,25 +2359,23 @@ export function ChatPanel({
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{workspace ? (
|
||||
<Link
|
||||
href={`/${workspace}/projects`}
|
||||
title="All projects"
|
||||
style={{ flexShrink: 0, display: "flex" }}
|
||||
>
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="VIBN"
|
||||
width={26}
|
||||
height={26}
|
||||
style={{
|
||||
borderRadius: 7,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
) : null}
|
||||
<Link
|
||||
href={workspace ? `/${workspace}/projects` : "/"}
|
||||
title="All projects"
|
||||
style={{ flexShrink: 0, display: "flex" }}
|
||||
>
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="VIBN"
|
||||
width={26}
|
||||
height={26}
|
||||
style={{
|
||||
borderRadius: 7,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowThreads((v) => !v)}
|
||||
@@ -2670,25 +2668,23 @@ export function ChatPanel({
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{workspace ? (
|
||||
<Link
|
||||
href={`/${workspace}/projects`}
|
||||
title="All projects"
|
||||
style={{ flexShrink: 0, display: "flex" }}
|
||||
>
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="VIBN"
|
||||
width={26}
|
||||
height={26}
|
||||
style={{
|
||||
borderRadius: 7,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
) : null}
|
||||
<Link
|
||||
href={workspace ? `/${workspace}/projects` : "/"}
|
||||
title="All projects"
|
||||
style={{ flexShrink: 0, display: "flex" }}
|
||||
>
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="VIBN"
|
||||
width={26}
|
||||
height={26}
|
||||
style={{
|
||||
borderRadius: 7,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowThreads((v) => !v)}
|
||||
|
||||
1137
vibn-frontend/package-lock.json
generated
1137
vibn-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -53,7 +53,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"daisyui": "^5.5.1-beta.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"dotenv": "^17.4.2",
|
||||
"firebase": "^12.5.0",
|
||||
"form-data": "^4.0.5",
|
||||
"google-auth-library": "^10.5.0",
|
||||
@@ -64,7 +64,7 @@
|
||||
"next-auth": "^4.24.13",
|
||||
"next-themes": "^0.4.6",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pg": "^8.16.3",
|
||||
"pg": "^8.21.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
|
||||
Reference in New Issue
Block a user