mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 16:36:22 +02:00
connecting the copilot to the UI
This commit is contained in:
parent
5e31c637f0
commit
b1f6e64244
6 changed files with 785 additions and 60 deletions
72
apps/rowboatx/app/api/chat/route.ts
Normal file
72
apps/rowboatx/app/api/chat/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
113
apps/rowboatx/app/api/stream/route.ts
Normal file
113
apps/rowboatx/app/api/stream/route.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue