add orchestrator chat to project overview
- OrchestratorChat component with Lovable-style UI (suggestion chips, reasoning panel, tool call badges) - /api/projects/[projectId]/agent-chat proxy route to agent runner - Injects project context (repo, vision, deployment URL) into session - AGENT_RUNNER_URL wired to agents.vibnai.com Made-with: Cursor
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { OrchestratorChat } from "@/components/OrchestratorChat";
|
||||
import {
|
||||
GitBranch,
|
||||
GitCommit,
|
||||
@@ -198,6 +199,12 @@ export default function ProjectOverviewPage() {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-6 max-w-5xl space-y-6">
|
||||
|
||||
{/* ── Orchestrator Chat ── */}
|
||||
<OrchestratorChat
|
||||
projectId={projectId}
|
||||
projectName={project.productName}
|
||||
/>
|
||||
|
||||
{/* ── Header ── */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
|
||||
111
app/api/projects/[projectId]/agent-chat/route.ts
Normal file
111
app/api/projects/[projectId]/agent-chat/route.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { query } from "@/lib/db-postgres";
|
||||
|
||||
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { projectId } = await params;
|
||||
const { message } = await req.json();
|
||||
|
||||
if (!message?.trim()) {
|
||||
return NextResponse.json({ error: '"message" is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Load project context to inject into the orchestrator session
|
||||
let projectContext = "";
|
||||
try {
|
||||
const rows = await query<{ data: any }>(
|
||||
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
|
||||
[projectId]
|
||||
);
|
||||
if (rows.length > 0) {
|
||||
const p = rows[0].data;
|
||||
const lines = [
|
||||
`Project: ${p.productName ?? p.name ?? "Unnamed"}`,
|
||||
p.productVision ? `Vision: ${p.productVision}` : null,
|
||||
p.giteaRepo ? `Gitea repo: ${p.giteaRepo}` : null,
|
||||
p.coolifyAppUuid ? `Coolify app UUID: ${p.coolifyAppUuid}` : null,
|
||||
p.deploymentUrl ? `Live URL: ${p.deploymentUrl}` : null,
|
||||
p.theiaWorkspaceUrl ? `IDE: ${p.theiaWorkspaceUrl}` : null,
|
||||
].filter(Boolean);
|
||||
projectContext = lines.join("\n");
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal — orchestrator still works without extra context
|
||||
}
|
||||
|
||||
// Use projectId as the session ID so each project has its own conversation
|
||||
const sessionId = `project_${projectId}`;
|
||||
|
||||
// First message in a new session? Prepend project context
|
||||
const enrichedMessage = projectContext
|
||||
? `[Project context]\n${projectContext}\n\n[User message]\n${message}`
|
||||
: message;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${AGENT_RUNNER_URL}/orchestrator/chat`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: enrichedMessage, session_id: sessionId }),
|
||||
signal: AbortSignal.timeout(120_000), // 2 min — agents can take time
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errText = await res.text();
|
||||
return NextResponse.json(
|
||||
{ error: `Agent runner error: ${res.status} — ${errText.slice(0, 200)}` },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return NextResponse.json({
|
||||
reply: data.reply,
|
||||
reasoning: data.reasoning ?? null,
|
||||
toolCalls: data.toolCalls ?? [],
|
||||
turns: data.turns ?? 0,
|
||||
model: data.model ?? "unknown",
|
||||
sessionId,
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return NextResponse.json(
|
||||
{ error: msg.includes("fetch") ? "Agent runner is offline" : msg },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear session for this project
|
||||
export async function DELETE(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { projectId } = await params;
|
||||
const sessionId = `project_${projectId}`;
|
||||
|
||||
try {
|
||||
await fetch(`${AGENT_RUNNER_URL}/orchestrator/sessions/${sessionId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
} catch {
|
||||
// Best-effort
|
||||
}
|
||||
|
||||
return NextResponse.json({ cleared: true });
|
||||
}
|
||||
Reference in New Issue
Block a user