From b197f68828c5fe6c3e4ff61e1a9e4060b80169a3 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 17 Feb 2026 15:28:23 -0800 Subject: [PATCH] v0 --- .../app/api/copilot/workspace-chat/route.ts | 131 + .../app/workspace/[workspaceId]/chat/chat.tsx | 150 + .../chat/hooks/use-workspace-chat.ts | 173 + .../workspace/[workspaceId]/chat/layout.tsx | 7 + .../app/workspace/[workspaceId]/chat/page.tsx | 26 + .../w/components/sidebar/sidebar.tsx | 8 +- apps/sim/lib/copilot/chat-lifecycle.ts | 11 +- apps/sim/lib/copilot/orchestrator/index.ts | 26 +- apps/sim/lib/copilot/workspace-prompt.ts | 66 + .../db/migrations/0155_cuddly_slapstick.sql | 4 + .../db/migrations/meta/0155_snapshot.json | 11512 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 9 +- packages/db/schema.ts | 11 +- 13 files changed, 12121 insertions(+), 13 deletions(-) create mode 100644 apps/sim/app/api/copilot/workspace-chat/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/chat/chat.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/chat/hooks/use-workspace-chat.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/chat/layout.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/chat/page.tsx create mode 100644 apps/sim/lib/copilot/workspace-prompt.ts create mode 100644 packages/db/migrations/0155_cuddly_slapstick.sql create mode 100644 packages/db/migrations/meta/0155_snapshot.json diff --git a/apps/sim/app/api/copilot/workspace-chat/route.ts b/apps/sim/app/api/copilot/workspace-chat/route.ts new file mode 100644 index 000000000..fcf6319cf --- /dev/null +++ b/apps/sim/app/api/copilot/workspace-chat/route.ts @@ -0,0 +1,131 @@ +import { db } from '@sim/db' +import { copilotChats } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { resolveOrCreateChat } from '@/lib/copilot/chat-lifecycle' +import { SIM_AGENT_VERSION } from '@/lib/copilot/constants' +import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator' +import type { SSEEvent } from '@/lib/copilot/orchestrator/types' +import { getWorkspaceChatSystemPrompt } from '@/lib/copilot/workspace-prompt' + +const logger = createLogger('WorkspaceChatAPI') + +export const dynamic = 'force-dynamic' +export const runtime = 'nodejs' +export const maxDuration = 300 + +const WorkspaceChatSchema = z.object({ + message: z.string().min(1, 'Message is required'), + workspaceId: z.string().min(1, 'workspaceId is required'), + chatId: z.string().optional(), + model: z.string().optional().default('claude-opus-4-5'), +}) + +export async function POST(req: NextRequest) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const body = await req.json() + const { message, workspaceId, chatId, model } = WorkspaceChatSchema.parse(body) + + const chatResult = await resolveOrCreateChat({ + chatId, + userId: session.user.id, + workspaceId, + model, + }) + + const requestPayload: Record = { + message, + userId: session.user.id, + model, + mode: 'agent', + headless: true, + systemPrompt: getWorkspaceChatSystemPrompt(), + messageId: crypto.randomUUID(), + version: SIM_AGENT_VERSION, + source: 'workspace-chat', + stream: true, + ...(chatResult.chatId ? { chatId: chatResult.chatId } : {}), + } + + const encoder = new TextEncoder() + const stream = new ReadableStream({ + async start(controller) { + const pushEvent = (event: Record) => { + try { + controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)) + } catch { + // Client disconnected + } + } + + if (chatResult.chatId) { + pushEvent({ type: 'chat_id', chatId: chatResult.chatId }) + } + + try { + const result = await orchestrateCopilotStream(requestPayload, { + userId: session.user.id, + workspaceId, + chatId: chatResult.chatId || undefined, + autoExecuteTools: true, + interactive: false, + onEvent: async (event: SSEEvent) => { + pushEvent(event as unknown as Record) + }, + }) + + if (chatResult.chatId && result.conversationId) { + await db + .update(copilotChats) + .set({ + updatedAt: new Date(), + conversationId: result.conversationId, + }) + .where(eq(copilotChats.id, chatResult.chatId)) + } + + pushEvent({ + type: 'done', + success: result.success, + content: result.content, + }) + } catch (error) { + logger.error('Workspace chat orchestration failed', { error }) + pushEvent({ + type: 'error', + error: error instanceof Error ? error.message : 'Chat failed', + }) + } finally { + controller.close() + } + }, + }) + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'X-Accel-Buffering': 'no', + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request', details: error.errors }, + { status: 400 } + ) + } + + logger.error('Workspace chat error', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/chat/chat.tsx new file mode 100644 index 000000000..10c11b41f --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/chat/chat.tsx @@ -0,0 +1,150 @@ +'use client' + +import { useCallback, useRef, useState } from 'react' +import { Send, Square } from 'lucide-react' +import { useParams } from 'next/navigation' +import { Button } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { useWorkspaceChat } from './hooks/use-workspace-chat' + +export function Chat() { + const { workspaceId } = useParams<{ workspaceId: string }>() + const [inputValue, setInputValue] = useState('') + const inputRef = useRef(null) + const messagesEndRef = useRef(null) + + const { messages, isSending, error, sendMessage, abortMessage } = useWorkspaceChat({ + workspaceId, + }) + + const scrollToBottom = useCallback(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, []) + + const handleSubmit = useCallback(async () => { + const trimmed = inputValue.trim() + if (!trimmed || !workspaceId) return + + setInputValue('') + await sendMessage(trimmed) + scrollToBottom() + }, [inputValue, workspaceId, sendMessage, scrollToBottom]) + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSubmit() + } + }, + [handleSubmit] + ) + + return ( +
+ {/* Header */} +
+

Chat

+
+ + {/* Messages area */} +
+ {messages.length === 0 && !isSending ? ( +
+
+

+ Ask anything about your workspace — build workflows, manage resources, get help. +

+
+
+ ) : ( +
+ {messages.map((msg) => { + const isStreamingEmpty = + isSending && msg.role === 'assistant' && !msg.content + if (isStreamingEmpty) { + return ( +
+
+ Thinking... +
+
+ ) + } + if (msg.role === 'assistant' && !msg.content) return null + return ( +
+
+

{msg.content}

+
+
+ ) + })} +
+
+ )} +
+ + {/* Error display */} + {error && ( +
+

{error}

+
+ )} + + {/* Input area */} +
+
+