connecting the copilot to the UI

This commit is contained in:
tusharmagar 2025-12-11 13:43:47 +05:30 committed by Ramnique Singh
parent 5e31c637f0
commit b1f6e64244
6 changed files with 785 additions and 60 deletions

View file

@ -0,0 +1,72 @@
import { cliClient, RunEvent } from '@/lib/cli-client';
import { NextRequest } from 'next/server';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
/**
* POST /api/chat
* Creates a new conversation or sends a message to existing one
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { message, runId } = body;
if (!message || typeof message !== 'string') {
return Response.json(
{ error: 'Message is required' },
{ status: 400 }
);
}
let currentRunId = runId;
// Create new run if no runId provided
if (!currentRunId) {
const run = await cliClient.createRun({
agentId: 'copilot',
});
currentRunId = run.id;
}
// Always send the message (this triggers the agent runtime)
await cliClient.sendMessage(currentRunId, message);
// Return the run ID
return Response.json({ runId: currentRunId });
} catch (error) {
console.error('Chat API error:', error);
return Response.json(
{ error: 'Failed to process message' },
{ status: 500 }
);
}
}
/**
* GET /api/chat?runId=xxx
* Get a specific run's details
*/
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const runId = searchParams.get('runId');
if (!runId) {
// List all runs
const result = await cliClient.listRuns();
return Response.json(result);
}
// Get specific run
const run = await cliClient.getRun(runId);
return Response.json(run);
} catch (error) {
console.error('Chat API error:', error);
return Response.json(
{ error: 'Failed to fetch run' },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,113 @@
import { NextRequest } from 'next/server';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
const CLI_BASE_URL = process.env.CLI_BACKEND_URL || 'http://localhost:3000';
/**
* GET /api/stream
* Proxy SSE stream from CLI backend to frontend
*/
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const customReadable = new ReadableStream({
async start(controller) {
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
let isClosed = false;
// Handle client disconnect
request.signal.addEventListener('abort', () => {
isClosed = true;
reader?.cancel();
try {
controller.close();
} catch (e) {
// Already closed, ignore
}
});
try {
// Connect to CLI backend SSE stream
const response = await fetch(`${CLI_BASE_URL}/stream`, {
headers: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
signal: request.signal, // Forward abort signal
});
if (!response.ok) {
throw new Error(`Failed to connect to backend: ${response.statusText}`);
}
reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body');
}
// Read and forward stream
while (!isClosed) {
const { done, value } = await reader.read();
if (done) {
break;
}
// Only enqueue if controller is still open
if (!isClosed) {
try {
controller.enqueue(value);
} catch (e) {
// Controller closed, stop reading
break;
}
}
}
} catch (error: any) {
// Only log non-abort errors
if (error.name !== 'AbortError') {
console.error('Stream error:', error);
}
// Try to send error message if controller is still open
if (!isClosed) {
try {
const errorMessage = `data: ${JSON.stringify({ type: 'error', error: String(error) })}\n\n`;
controller.enqueue(encoder.encode(errorMessage));
} catch (e) {
// Controller already closed, ignore
}
}
} finally {
// Clean up
if (reader) {
try {
await reader.cancel();
} catch (e) {
// Ignore cancel errors
}
}
if (!isClosed) {
try {
controller.close();
} catch (e) {
// Already closed, ignore
}
}
}
},
});
return new Response(customReadable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}