Skip to content

Commit fe5ab8a

Browse files
committed
improved streaming
1 parent b3a639a commit fe5ab8a

16 files changed

Lines changed: 1092 additions & 1168 deletions

File tree

apps/sim/app/api/billing/update-cost/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ const UpdateCostSchema = z.object({
1818
model: z.string().min(1, 'Model is required'),
1919
inputTokens: z.number().min(0).default(0),
2020
outputTokens: z.number().min(0).default(0),
21-
source: z.enum(['copilot', 'workspace-chat', 'mcp_copilot', 'mothership_block']).default('copilot'),
21+
source: z
22+
.enum(['copilot', 'workspace-chat', 'mcp_copilot', 'mothership_block'])
23+
.default('copilot'),
2224
})
2325

2426
/**

apps/sim/app/api/copilot/chat/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ export async function GET(req: NextRequest) {
433433
messages: copilotChats.messages,
434434
planArtifact: copilotChats.planArtifact,
435435
config: copilotChats.config,
436+
conversationId: copilotChats.conversationId,
436437
createdAt: copilotChats.createdAt,
437438
updatedAt: copilotChats.updatedAt,
438439
})
@@ -452,6 +453,7 @@ export async function GET(req: NextRequest) {
452453
messageCount: Array.isArray(chat.messages) ? chat.messages.length : 0,
453454
planArtifact: chat.planArtifact || null,
454455
config: chat.config || null,
456+
conversationId: chat.conversationId || null,
455457
createdAt: chat.createdAt,
456458
updatedAt: chat.updatedAt,
457459
}

apps/sim/app/api/mothership/chat/route.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import { db } from '@sim/db'
2+
import { copilotChats } from '@sim/db/schema'
13
import { createLogger } from '@sim/logger'
4+
import { eq } from 'drizzle-orm'
25
import { type NextRequest, NextResponse } from 'next/server'
36
import { z } from 'zod'
47
import { getSession } from '@/lib/auth'
58
import { resolveOrCreateChat } from '@/lib/copilot/chat-lifecycle'
69
import { buildCopilotRequestPayload } from '@/lib/copilot/chat-payload'
710
import { createSSEStream, SSE_RESPONSE_HEADERS } from '@/lib/copilot/chat-streaming'
11+
import type { OrchestratorResult } from '@/lib/copilot/orchestrator/types'
812
import { createRequestTracker, createUnauthorizedResponse } from '@/lib/copilot/request-helpers'
913
import { generateWorkspaceContext } from '@/lib/copilot/workspace-context'
1014

@@ -107,6 +111,13 @@ export async function POST(req: NextRequest) {
107111
: []
108112
}
109113

114+
if (actualChatId) {
115+
await db
116+
.update(copilotChats)
117+
.set({ conversationId: userMessageId })
118+
.where(eq(copilotChats.id, actualChatId))
119+
}
120+
110121
const workspaceContext = await generateWorkspaceContext(workspaceId, authenticatedUserId)
111122

112123
const requestPayload = await buildCopilotRequestPayload(
@@ -143,6 +154,41 @@ export async function POST(req: NextRequest) {
143154
goRoute: '/api/mothership',
144155
autoExecuteTools: true,
145156
interactive: false,
157+
onComplete: async (result: OrchestratorResult) => {
158+
if (!actualChatId) return
159+
160+
const userMessage = {
161+
id: userMessageId,
162+
role: 'user' as const,
163+
content: message,
164+
timestamp: new Date().toISOString(),
165+
}
166+
167+
const assistantMessage = {
168+
id: crypto.randomUUID(),
169+
role: 'assistant' as const,
170+
content: result.content,
171+
timestamp: new Date().toISOString(),
172+
}
173+
174+
const updatedMessages = [...conversationHistory, userMessage, assistantMessage]
175+
176+
try {
177+
await db
178+
.update(copilotChats)
179+
.set({
180+
messages: updatedMessages,
181+
conversationId: null,
182+
updatedAt: new Date(),
183+
})
184+
.where(eq(copilotChats.id, actualChatId))
185+
} catch (error) {
186+
logger.error(`[${tracker.requestId}] Failed to persist chat messages`, {
187+
chatId: actualChatId,
188+
error: error instanceof Error ? error.message : 'Unknown error',
189+
})
190+
}
191+
},
146192
},
147193
})
148194

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { db } from '@sim/db'
2+
import { copilotChats } from '@sim/db/schema'
3+
import { createLogger } from '@sim/logger'
4+
import { and, desc, eq, isNull } from 'drizzle-orm'
5+
import { type NextRequest, NextResponse } from 'next/server'
6+
import { z } from 'zod'
7+
import {
8+
authenticateCopilotRequestSessionOnly,
9+
createBadRequestResponse,
10+
createInternalServerErrorResponse,
11+
createUnauthorizedResponse,
12+
} from '@/lib/copilot/request-helpers'
13+
14+
const logger = createLogger('MothershipChatsAPI')
15+
16+
/**
17+
* GET /api/mothership/chats?workspaceId=xxx
18+
* Returns mothership (home) chats for the authenticated user in the given workspace.
19+
*/
20+
export async function GET(request: NextRequest) {
21+
try {
22+
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
23+
if (!isAuthenticated || !userId) {
24+
return createUnauthorizedResponse()
25+
}
26+
27+
const workspaceId = request.nextUrl.searchParams.get('workspaceId')
28+
if (!workspaceId) {
29+
return createBadRequestResponse('workspaceId is required')
30+
}
31+
32+
const chats = await db
33+
.select({
34+
id: copilotChats.id,
35+
title: copilotChats.title,
36+
updatedAt: copilotChats.updatedAt,
37+
})
38+
.from(copilotChats)
39+
.where(
40+
and(
41+
eq(copilotChats.userId, userId),
42+
eq(copilotChats.workspaceId, workspaceId),
43+
isNull(copilotChats.workflowId)
44+
)
45+
)
46+
.orderBy(desc(copilotChats.updatedAt))
47+
48+
return NextResponse.json({ success: true, data: chats })
49+
} catch (error) {
50+
logger.error('Error fetching mothership chats:', error)
51+
return createInternalServerErrorResponse('Failed to fetch chats')
52+
}
53+
}
54+
55+
const CreateChatSchema = z.object({
56+
workspaceId: z.string().min(1),
57+
})
58+
59+
/**
60+
* POST /api/mothership/chats
61+
* Creates an empty mothership chat and returns its ID.
62+
*/
63+
export async function POST(request: NextRequest) {
64+
try {
65+
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
66+
if (!isAuthenticated || !userId) {
67+
return createUnauthorizedResponse()
68+
}
69+
70+
const body = await request.json()
71+
const { workspaceId } = CreateChatSchema.parse(body)
72+
73+
const [chat] = await db
74+
.insert(copilotChats)
75+
.values({
76+
userId,
77+
workspaceId,
78+
title: null,
79+
model: 'claude-opus-4-5',
80+
messages: [],
81+
})
82+
.returning({ id: copilotChats.id })
83+
84+
return NextResponse.json({ success: true, id: chat.id })
85+
} catch (error) {
86+
if (error instanceof z.ZodError) {
87+
return createBadRequestResponse('workspaceId is required')
88+
}
89+
logger.error('Error creating mothership chat:', error)
90+
return createInternalServerErrorResponse('Failed to create chat')
91+
}
92+
}

apps/sim/app/workspace/[workspaceId]/home/home.tsx

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,59 @@
22

33
import { useCallback, useState } from 'react'
44
import { Loader2 } from 'lucide-react'
5-
import { useParams } from 'next/navigation'
5+
import { useParams, useRouter } from 'next/navigation'
6+
import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
67
import { MessageContent, UserInput } from './components'
78
import { useChat } from './hooks'
89

9-
export function Home() {
10+
interface HomeProps {
11+
chatId?: string
12+
streamId?: string
13+
}
14+
15+
export function Home({ chatId, streamId }: HomeProps = {}) {
1016
const { workspaceId } = useParams<{ workspaceId: string }>()
17+
const router = useRouter()
1118
const [inputValue, setInputValue] = useState('')
12-
const { messages, isSending, error, sendMessage, stopGeneration, chatBottomRef } =
13-
useChat(workspaceId)
19+
const { messages, isSending, sendMessage, stopGeneration, chatBottomRef } = useChat(
20+
workspaceId,
21+
chatId,
22+
streamId
23+
)
1424

15-
const handleSubmit = useCallback(() => {
25+
const handleSubmit = useCallback(async () => {
1626
const trimmed = inputValue.trim()
1727
if (!trimmed) return
1828
setInputValue('')
19-
sendMessage(trimmed)
20-
}, [inputValue, sendMessage])
29+
30+
if (chatId) {
31+
sendMessage(trimmed)
32+
return
33+
}
34+
35+
const userMessageId = crypto.randomUUID()
36+
37+
try {
38+
const response = await fetch(MOTHERSHIP_CHAT_API_PATH, {
39+
method: 'POST',
40+
headers: { 'Content-Type': 'application/json' },
41+
body: JSON.stringify({
42+
message: trimmed,
43+
workspaceId,
44+
userMessageId,
45+
createNewChat: true,
46+
}),
47+
})
48+
49+
if (!response.ok) throw new Error('Failed to start task')
50+
response.body?.cancel()
51+
router.push(
52+
`/workspace/${workspaceId}/task/new?sid=${userMessageId}&m=${encodeURIComponent(trimmed)}`
53+
)
54+
} catch {
55+
setInputValue(trimmed)
56+
}
57+
}, [inputValue, chatId, sendMessage, workspaceId, router])
2158

2259
const hasMessages = messages.length > 0
2360

@@ -85,12 +122,6 @@ export function Home() {
85122
</div>
86123
</div>
87124

88-
{error && (
89-
<div className='px-[24px] pb-[8px]'>
90-
<p className='text-[12px] text-red-500'>{error}</p>
91-
</div>
92-
)}
93-
94125
<div className='flex-shrink-0 border-[var(--border)] border-t px-[24px] py-[16px]'>
95126
<UserInput
96127
value={inputValue}

0 commit comments

Comments
 (0)