Files
sim/apps/sim/lib/copilot/api.ts
Siddharth Ganesan 8361931cdf fix(copilot): fix copilot bugs (#2855)
* Fix edit workflow returning bad state

* Fix block id edit, slash commands at end, thinking tag resolution, add continue button

* Clean up autosend and continue options and enable mention menu

* Cleanup

* Fix thinking tags

* Fix thinking text

* Fix get block options text

* Fix bugs

* Fix redeploy

* Fix loading indicators

* User input expansion

* Normalize copilot subblock ids

* Fix handlecancelcheckpoint
2026-01-16 13:57:55 -08:00

187 lines
4.4 KiB
TypeScript

import { createLogger } from '@sim/logger'
import type { CopilotMode, CopilotModelId, CopilotTransportMode } from '@/lib/copilot/models'
const logger = createLogger('CopilotAPI')
/**
* Citation interface for documentation references
*/
export interface Citation {
id: number
title: string
url: string
similarity?: number
}
/**
* Message interface for copilot conversations
*/
export interface CopilotMessage {
id: string
role: 'user' | 'assistant' | 'system'
content: string
timestamp: string
citations?: Citation[]
}
/**
* Chat config stored in database
*/
export interface CopilotChatConfig {
mode?: CopilotMode
model?: CopilotModelId
}
/**
* Chat interface for copilot conversations
*/
export interface CopilotChat {
id: string
title: string | null
model: string
messages: CopilotMessage[]
messageCount: number
planArtifact: string | null
config: CopilotChatConfig | null
createdAt: Date
updatedAt: Date
}
/**
* File attachment interface for message requests
*/
export interface MessageFileAttachment {
id: string
key: string
filename: string
media_type: string
size: number
}
/**
* Request interface for sending messages
*/
export interface SendMessageRequest {
message: string
userMessageId?: string // ID from frontend for the user message
chatId?: string
workflowId?: string
mode?: CopilotMode | CopilotTransportMode
model?: CopilotModelId
prefetch?: boolean
createNewChat?: boolean
stream?: boolean
implicitFeedback?: string
fileAttachments?: MessageFileAttachment[]
abortSignal?: AbortSignal
contexts?: Array<{
kind: string
label?: string
chatId?: string
workflowId?: string
executionId?: string
}>
commands?: string[]
}
/**
* Base API response interface
*/
export interface ApiResponse {
success: boolean
error?: string
status?: number
}
/**
* Streaming response interface
*/
export interface StreamingResponse extends ApiResponse {
stream?: ReadableStream
}
/**
* Handle API errors and return user-friendly error messages
*/
async function handleApiError(response: Response, defaultMessage: string): Promise<string> {
try {
const data = await response.json()
return (data && (data.error || data.message)) || defaultMessage
} catch {
return `${defaultMessage} (${response.status})`
}
}
/**
* Send a streaming message to the copilot chat API
* This is the main API endpoint that handles all chat operations
*/
export async function sendStreamingMessage(
request: SendMessageRequest
): Promise<StreamingResponse> {
try {
const { abortSignal, ...requestBody } = request
try {
const preview = Array.isArray((requestBody as any).contexts)
? (requestBody as any).contexts.map((c: any) => ({
kind: c?.kind,
chatId: c?.chatId,
workflowId: c?.workflowId,
label: c?.label,
}))
: undefined
logger.info('Preparing to send streaming message', {
hasContexts: Array.isArray((requestBody as any).contexts),
contextsCount: Array.isArray((requestBody as any).contexts)
? (requestBody as any).contexts.length
: 0,
contextsPreview: preview,
})
} catch {}
const response = await fetch('/api/copilot/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...requestBody, stream: true }),
signal: abortSignal,
credentials: 'include', // Include cookies for session authentication
})
if (!response.ok) {
const errorMessage = await handleApiError(response, 'Failed to send streaming message')
return {
success: false,
error: errorMessage,
status: response.status,
}
}
if (!response.body) {
return {
success: false,
error: 'No response body received',
status: 500,
}
}
return {
success: true,
stream: response.body,
}
} catch (error) {
// Handle AbortError gracefully - this is expected when user aborts
if (error instanceof Error && error.name === 'AbortError') {
logger.info('Streaming message was aborted by user')
return {
success: false,
error: 'Request was aborted',
}
}
logger.error('Failed to send streaming message:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}