- {/* Header */}
-
-
-
- Architecture
-
-
- {architecture.productType}
-
-
-
- {architectureConfirmed && (
-
- ✓ Confirmed
+ {/* File path breadcrumb */}
+
+ {selectedPath ? (
+
+ {selectedPath.split("/").map((seg, i, arr) => (
+
+ {i > 0 && / }
+ {seg}
+
+ ))}
+
+ ) : (
+
+ Select a file to view
)}
- handleGenerate(true)}
- disabled={generating}
- style={{
- padding: "6px 14px", borderRadius: 6,
- background: "none", border: "1px solid #e0dcd4",
- fontSize: "0.72rem", color: "#8a8478", cursor: "pointer",
- fontFamily: "Outfit, sans-serif",
- }}
- >
- {generating ? "Regenerating…" : "Regenerate"}
-
+ {selectedPath && (
+
+ {lang}
+
+ )}
+
+
+ {/* Code area */}
+
+ {!selectedPath && !fileLoading && (
+
+ Select a file from the tree
+
+ )}
+ {fileLoading && (
+
+ Loading…
+
+ )}
+ {!fileLoading && fileContent !== null && (
+
+ {/* Line numbers */}
+
+ {lines.map((_, i) => (
+
+ {i + 1}
+
+ ))}
+
+ {/* Code content */}
+
+ {highlightCode(fileContent, lang)}
+
+
+ )}
-
- {/* Summary */}
-
- {architecture.summary}
-
-
- {/* Apps */}
-
Apps — monorepo/apps/
- {architecture.apps.map((app, i) =>
)}
-
- {/* Packages */}
-
Shared packages — monorepo/packages/
-
- {architecture.packages.map((pkg, i) => (
-
-
- packages/{pkg.name}
-
-
- {pkg.description}
-
-
- ))}
-
-
- {/* Infrastructure */}
- {architecture.infrastructure.length > 0 && (
- <>
-
Infrastructure
-
- {architecture.infrastructure.map((infra, i) => (
-
-
- {infra.name}
-
-
- {infra.reason}
-
-
- ))}
-
- >
- )}
-
- {/* Integrations */}
- {architecture.integrations.length > 0 && (
- <>
-
External integrations
-
- {architecture.integrations.map((intg, i) => (
-
-
- {intg.required ? "required" : "optional"}
-
-
-
{intg.name}
-
{intg.notes}
-
-
- ))}
-
- >
- )}
-
- {/* Risk notes */}
- {architecture.riskNotes.length > 0 && (
- <>
-
Architecture risks
-
- {architecture.riskNotes.map((risk, i) => (
-
- ⚠️ {risk}
-
- ))}
-
- >
- )}
-
- {/* Confirm section */}
-
- {architectureConfirmed ? (
-
-
- ✓ Architecture confirmed
-
-
- You can still regenerate or adjust the architecture before scaffolding begins. Nothing has been built yet.
-
-
- Choose your design →
-
-
- ) : (
-
-
- Does this look right?
-
-
- Review the structure above. You can regenerate if something's off, or confirm to move to design.
- You can always come back and adjust before the build starts — nothing gets scaffolded yet.
-
-
-
- {confirming ? "Confirming…" : "Confirm architecture →"}
-
- handleGenerate(true)}
- disabled={generating}
- style={{
- padding: "9px 18px", borderRadius: 7,
- background: "none", border: "1px solid #e0dcd4",
- fontSize: "0.78rem", color: "#8a8478",
- fontFamily: "Outfit, sans-serif", cursor: "pointer",
- }}
- >
- Regenerate
-
-
-
- )}
-
-
-
);
}
diff --git a/app/api/projects/[projectId]/file/route.ts b/app/api/projects/[projectId]/file/route.ts
new file mode 100644
index 0000000..03a6eef
--- /dev/null
+++ b/app/api/projects/[projectId]/file/route.ts
@@ -0,0 +1,108 @@
+/**
+ * GET /api/projects/[projectId]/file?path=apps/admin
+ *
+ * Returns directory listing or file content from the project's Gitea repo.
+ * Response for directory: { type: "dir", items: [{ name, path, type }] }
+ * Response for file: { type: "file", content: string, encoding: "utf8" | "base64" }
+ */
+import { NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth/authOptions';
+import { query } from '@/lib/db-postgres';
+
+const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com';
+const GITEA_API_TOKEN = process.env.GITEA_API_TOKEN ?? '';
+
+async function giteaGet(path: string) {
+ const res = await fetch(`${GITEA_API_URL}/api/v1${path}`, {
+ headers: { Authorization: `token ${GITEA_API_TOKEN}` },
+ next: { revalidate: 10 },
+ });
+ if (!res.ok) throw new Error(`Gitea ${res.status}: ${path}`);
+ return res.json();
+}
+
+const BINARY_EXTENSIONS = new Set([
+ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico',
+ 'woff', 'woff2', 'ttf', 'eot',
+ 'zip', 'tar', 'gz', 'pdf',
+]);
+
+function isBinary(name: string): boolean {
+ const ext = name.split('.').pop()?.toLowerCase() ?? '';
+ return BINARY_EXTENSIONS.has(ext);
+}
+
+export async function GET(
+ req: Request,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ try {
+ const { projectId } = await params;
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.email) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { searchParams } = new URL(req.url);
+ const filePath = searchParams.get('path') ?? '';
+
+ // Verify ownership + get giteaRepo
+ const rows = await query<{ data: Record
}>(
+ `SELECT p.data FROM fs_projects p
+ JOIN fs_users u ON u.id = p.user_id
+ WHERE p.id = $1 AND u.data->>'email' = $2 LIMIT 1`,
+ [projectId, session.user.email]
+ );
+ if (rows.length === 0) {
+ return NextResponse.json({ error: 'Project not found' }, { status: 404 });
+ }
+
+ const giteaRepo = rows[0].data?.giteaRepo as string | undefined;
+ if (!giteaRepo) {
+ return NextResponse.json({ error: 'No Gitea repo connected' }, { status: 404 });
+ }
+
+ const encodedPath = filePath ? encodeURIComponent(filePath).replace(/%2F/g, '/') : '';
+ const apiPath = `/repos/${giteaRepo}/contents/${encodedPath}`;
+ const data = await giteaGet(apiPath);
+
+ // Directory listing
+ if (Array.isArray(data)) {
+ const items = data
+ .map((item: { name: string; path: string; type: string; size?: number }) => ({
+ name: item.name,
+ path: item.path,
+ type: item.type, // "file" | "dir" | "symlink"
+ size: item.size,
+ }))
+ .sort((a, b) => {
+ // Dirs first
+ if (a.type === 'dir' && b.type !== 'dir') return -1;
+ if (a.type !== 'dir' && b.type === 'dir') return 1;
+ return a.name.localeCompare(b.name);
+ });
+ return NextResponse.json({ type: 'dir', items });
+ }
+
+ // Single file
+ const item = data as { name: string; content?: string; encoding?: string; size?: number };
+ if (isBinary(item.name)) {
+ return NextResponse.json({ type: 'file', content: '(binary file)', encoding: 'utf8' });
+ }
+
+ // Gitea returns base64-encoded content
+ const raw = item.content ?? '';
+ let content: string;
+ try {
+ content = Buffer.from(raw.replace(/\n/g, ''), 'base64').toString('utf8');
+ } catch {
+ content = raw;
+ }
+
+ return NextResponse.json({ type: 'file', content, encoding: 'utf8', name: item.name });
+ } catch (err) {
+ console.error('[file API]', err);
+ return NextResponse.json({ error: 'Failed to fetch file' }, { status: 500 });
+ }
+}
diff --git a/components/layout/vibn-sidebar.tsx b/components/layout/vibn-sidebar.tsx
index 357901e..8b40ec3 100644
--- a/components/layout/vibn-sidebar.tsx
+++ b/components/layout/vibn-sidebar.tsx
@@ -295,10 +295,19 @@ export function VIBNSidebar({ workspace }: VIBNSidebarProps) {
{project.productName || project.name || "Project"}
-
- {project.status === "live" ? "● Live"
- : project.status === "building" ? "● Building"
- : "● Defining"}
+
+
+
+ {project.status === "live" ? "Live"
+ : project.status === "building" ? "Building"
+ : "Defining"}
+
)}
@@ -311,12 +320,12 @@ export function VIBNSidebar({ workspace }: VIBNSidebarProps) {
key={app.name}
icon="▢"
label={app.name}
- href={project.giteaRepoUrl ? `${project.giteaRepoUrl}/src/branch/main/${app.path}` : undefined}
+ href={`${base}/build`}
collapsed={collapsed}
/>
))
) : (
-
+
)}