Skip to content

Commit 06e9a6b

Browse files
authored
feat(copilot): context (#1157)
* Copilot updates * Set/get vars * Credentials opener v1 * Progress * Checkpoint? * Context v1 * Workflow references * Add knowledge base context * Blocks * Templates * Much better pills * workflow updates * Major ui * Workflow box colors * Much i mproved ui * Improvements * Much better * Add @ icon * Welcome page * Update tool names * Matches * UPdate ordering * Good sort * Good @ handling * Update placeholder * Updates * Lint * Almost there * Wrapped up? * Lint * Builid error fix * Build fix? * Lint * Fix load vars
1 parent fed4e50 commit 06e9a6b

File tree

23 files changed

+3358
-93
lines changed

23 files changed

+3358
-93
lines changed

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

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,27 @@ const ChatMessageSchema = z.object({
3939
chatId: z.string().optional(),
4040
workflowId: z.string().min(1, 'Workflow ID is required'),
4141
mode: z.enum(['ask', 'agent']).optional().default('agent'),
42-
depth: z.number().int().min(-2).max(3).optional().default(0),
42+
depth: z.number().int().min(0).max(3).optional().default(0),
4343
prefetch: z.boolean().optional(),
4444
createNewChat: z.boolean().optional().default(false),
4545
stream: z.boolean().optional().default(true),
4646
implicitFeedback: z.string().optional(),
4747
fileAttachments: z.array(FileAttachmentSchema).optional(),
4848
provider: z.string().optional().default('openai'),
4949
conversationId: z.string().optional(),
50+
contexts: z
51+
.array(
52+
z.object({
53+
kind: z.enum(['past_chat', 'workflow', 'blocks', 'logs', 'knowledge', 'templates']),
54+
label: z.string(),
55+
chatId: z.string().optional(),
56+
workflowId: z.string().optional(),
57+
knowledgeId: z.string().optional(),
58+
blockId: z.string().optional(),
59+
templateId: z.string().optional(),
60+
})
61+
)
62+
.optional(),
5063
})
5164

5265
/**
@@ -81,7 +94,38 @@ export async function POST(req: NextRequest) {
8194
fileAttachments,
8295
provider,
8396
conversationId,
97+
contexts,
8498
} = ChatMessageSchema.parse(body)
99+
try {
100+
logger.info(`[${tracker.requestId}] Received chat POST`, {
101+
hasContexts: Array.isArray(contexts),
102+
contextsCount: Array.isArray(contexts) ? contexts.length : 0,
103+
contextsPreview: Array.isArray(contexts)
104+
? contexts.map((c: any) => ({
105+
kind: c?.kind,
106+
chatId: c?.chatId,
107+
workflowId: c?.workflowId,
108+
label: c?.label,
109+
}))
110+
: undefined,
111+
})
112+
} catch {}
113+
// Preprocess contexts server-side
114+
let agentContexts: Array<{ type: string; content: string }> = []
115+
if (Array.isArray(contexts) && contexts.length > 0) {
116+
try {
117+
const { processContextsServer } = await import('@/lib/copilot/process-contents')
118+
const processed = await processContextsServer(contexts as any, authenticatedUserId)
119+
agentContexts = processed
120+
logger.info(`[${tracker.requestId}] Contexts processed for request`, {
121+
processedCount: agentContexts.length,
122+
kinds: agentContexts.map((c) => c.type),
123+
lengthPreview: agentContexts.map((c) => c.content?.length ?? 0),
124+
})
125+
} catch (e) {
126+
logger.error(`[${tracker.requestId}] Failed to process contexts`, e)
127+
}
128+
}
85129

86130
// Consolidation mapping: map negative depths to base depth with prefetch=true
87131
let effectiveDepth: number | undefined = typeof depth === 'number' ? depth : undefined
@@ -312,8 +356,15 @@ export async function POST(req: NextRequest) {
312356
...(typeof effectiveDepth === 'number' ? { depth: effectiveDepth } : {}),
313357
...(typeof effectivePrefetch === 'boolean' ? { prefetch: effectivePrefetch } : {}),
314358
...(session?.user?.name && { userName: session.user.name }),
359+
...(agentContexts.length > 0 && { context: agentContexts }),
315360
}
316361

362+
try {
363+
logger.info(`[${tracker.requestId}] About to call Sim Agent with context`, {
364+
context: (requestPayload as any).context,
365+
})
366+
} catch {}
367+
317368
const simAgentResponse = await fetch(`${SIM_AGENT_API_URL}/api/chat-completion-streaming`, {
318369
method: 'POST',
319370
headers: {
@@ -350,6 +401,11 @@ export async function POST(req: NextRequest) {
350401
content: message,
351402
timestamp: new Date().toISOString(),
352403
...(fileAttachments && fileAttachments.length > 0 && { fileAttachments }),
404+
...(Array.isArray(contexts) && contexts.length > 0 && { contexts }),
405+
...(Array.isArray(contexts) &&
406+
contexts.length > 0 && {
407+
contentBlocks: [{ type: 'contexts', contexts: contexts as any, timestamp: Date.now() }],
408+
}),
353409
}
354410

355411
// Create a pass-through stream that captures the response
@@ -683,6 +739,11 @@ export async function POST(req: NextRequest) {
683739
content: message,
684740
timestamp: new Date().toISOString(),
685741
...(fileAttachments && fileAttachments.length > 0 && { fileAttachments }),
742+
...(Array.isArray(contexts) && contexts.length > 0 && { contexts }),
743+
...(Array.isArray(contexts) &&
744+
contexts.length > 0 && {
745+
contentBlocks: [{ type: 'contexts', contexts: contexts as any, timestamp: Date.now() }],
746+
}),
686747
}
687748

688749
const assistantMessage = {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { desc, eq } from 'drizzle-orm'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import {
4+
authenticateCopilotRequestSessionOnly,
5+
createInternalServerErrorResponse,
6+
createUnauthorizedResponse,
7+
} from '@/lib/copilot/auth'
8+
import { createLogger } from '@/lib/logs/console/logger'
9+
import { db } from '@/db'
10+
import { copilotChats } from '@/db/schema'
11+
12+
const logger = createLogger('CopilotChatsListAPI')
13+
14+
export async function GET(_req: NextRequest) {
15+
try {
16+
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
17+
if (!isAuthenticated || !userId) {
18+
return createUnauthorizedResponse()
19+
}
20+
21+
const chats = await db
22+
.select({
23+
id: copilotChats.id,
24+
title: copilotChats.title,
25+
workflowId: copilotChats.workflowId,
26+
updatedAt: copilotChats.updatedAt,
27+
})
28+
.from(copilotChats)
29+
.where(eq(copilotChats.userId, userId))
30+
.orderBy(desc(copilotChats.updatedAt))
31+
32+
logger.info(`Retrieved ${chats.length} chats for user ${userId}`)
33+
34+
return NextResponse.json({ success: true, chats })
35+
} catch (error) {
36+
logger.error('Error fetching user copilot chats:', error)
37+
return createInternalServerErrorResponse('Failed to fetch user chats')
38+
}
39+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
'use client'
22

33
import { type FC, memo, useEffect, useMemo, useState } from 'react'
4-
import { Check, Clipboard, Loader2, RotateCcw, ThumbsDown, ThumbsUp, X } from 'lucide-react'
4+
import {
5+
Blocks,
6+
Bot,
7+
Check,
8+
Clipboard,
9+
Info,
10+
LibraryBig,
11+
Loader2,
12+
RotateCcw,
13+
Shapes,
14+
ThumbsDown,
15+
ThumbsUp,
16+
Workflow,
17+
X,
18+
} from 'lucide-react'
519
import { InlineToolCall } from '@/lib/copilot/inline-tool-call'
620
import { createLogger } from '@/lib/logs/console/logger'
721
import {
@@ -31,6 +45,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
3145
const [showUpvoteSuccess, setShowUpvoteSuccess] = useState(false)
3246
const [showDownvoteSuccess, setShowDownvoteSuccess] = useState(false)
3347
const [showRestoreConfirmation, setShowRestoreConfirmation] = useState(false)
48+
const [showAllContexts, setShowAllContexts] = useState(false)
3449

3550
// Get checkpoint functionality from copilot store
3651
const {
@@ -357,6 +372,78 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
357372
</div>
358373
)}
359374

375+
{/* Context chips displayed above the message bubble, independent of inline text */}
376+
{(Array.isArray((message as any).contexts) && (message as any).contexts.length > 0) ||
377+
(Array.isArray(message.contentBlocks) &&
378+
(message.contentBlocks as any[]).some((b: any) => b?.type === 'contexts')) ? (
379+
<div className='flex items-center justify-end gap-0'>
380+
<div className='min-w-0 max-w-[80%]'>
381+
<div className='mb-1 flex flex-wrap justify-end gap-1.5'>
382+
{(() => {
383+
const direct = Array.isArray((message as any).contexts)
384+
? ((message as any).contexts as any[])
385+
: []
386+
const block = Array.isArray(message.contentBlocks)
387+
? (message.contentBlocks as any[]).find((b: any) => b?.type === 'contexts')
388+
: null
389+
const fromBlock = Array.isArray((block as any)?.contexts)
390+
? ((block as any).contexts as any[])
391+
: []
392+
const allContexts = direct.length > 0 ? direct : fromBlock
393+
const MAX_VISIBLE = 4
394+
const visible = showAllContexts
395+
? allContexts
396+
: allContexts.slice(0, MAX_VISIBLE)
397+
return (
398+
<>
399+
{visible.map((ctx: any, idx: number) => (
400+
<span
401+
key={`ctx-${idx}-${ctx?.label || ctx?.kind}`}
402+
className='inline-flex items-center gap-1 rounded-full bg-[color-mix(in_srgb,var(--brand-primary-hover-hex)_14%,transparent)] px-1.5 py-0.5 text-[11px] text-foreground'
403+
title={ctx?.label || ctx?.kind}
404+
>
405+
{ctx?.kind === 'past_chat' ? (
406+
<Bot className='h-3 w-3 text-muted-foreground' />
407+
) : ctx?.kind === 'workflow' ? (
408+
<Workflow className='h-3 w-3 text-muted-foreground' />
409+
) : ctx?.kind === 'blocks' ? (
410+
<Blocks className='h-3 w-3 text-muted-foreground' />
411+
) : ctx?.kind === 'knowledge' ? (
412+
<LibraryBig className='h-3 w-3 text-muted-foreground' />
413+
) : ctx?.kind === 'templates' ? (
414+
<Shapes className='h-3 w-3 text-muted-foreground' />
415+
) : (
416+
<Info className='h-3 w-3 text-muted-foreground' />
417+
)}
418+
<span className='max-w-[140px] truncate'>
419+
{ctx?.label || ctx?.kind}
420+
</span>
421+
</span>
422+
))}
423+
{allContexts.length > MAX_VISIBLE && (
424+
<button
425+
type='button'
426+
onClick={() => setShowAllContexts((v) => !v)}
427+
className='inline-flex items-center gap-1 rounded-full bg-[color-mix(in_srgb,var(--brand-primary-hover-hex)_10%,transparent)] px-1.5 py-0.5 text-[11px] text-foreground hover:bg-[color-mix(in_srgb,var(--brand-primary-hover-hex)_14%,transparent)]'
428+
title={
429+
showAllContexts
430+
? 'Show less'
431+
: `Show ${allContexts.length - MAX_VISIBLE} more`
432+
}
433+
>
434+
{showAllContexts
435+
? 'Show less'
436+
: `+${allContexts.length - MAX_VISIBLE} more`}
437+
</button>
438+
)}
439+
</>
440+
)
441+
})()}
442+
</div>
443+
</div>
444+
</div>
445+
) : null}
446+
360447
<div className='flex items-center justify-end gap-0'>
361448
{hasCheckpoints && (
362449
<div className='mr-1 inline-flex items-center justify-center'>
@@ -408,7 +495,39 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
408495
}}
409496
>
410497
<div className='whitespace-pre-wrap break-words font-normal text-base text-foreground leading-relaxed'>
411-
<WordWrap text={message.content} />
498+
{(() => {
499+
const text = message.content || ''
500+
const contexts: any[] = Array.isArray((message as any).contexts)
501+
? ((message as any).contexts as any[])
502+
: []
503+
const labels = contexts.map((c) => c?.label).filter(Boolean) as string[]
504+
if (!labels.length) return <WordWrap text={text} />
505+
506+
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
507+
const pattern = new RegExp(`@(${labels.map(escapeRegex).join('|')})`, 'g')
508+
509+
const nodes: React.ReactNode[] = []
510+
let lastIndex = 0
511+
let match: RegExpExecArray | null
512+
while ((match = pattern.exec(text)) !== null) {
513+
const i = match.index
514+
const before = text.slice(lastIndex, i)
515+
if (before) nodes.push(before)
516+
const mention = match[0]
517+
nodes.push(
518+
<span
519+
key={`mention-${i}-${lastIndex}`}
520+
className='rounded-[6px] bg-[color-mix(in_srgb,var(--brand-primary-hover-hex)_14%,transparent)] px-1'
521+
>
522+
{mention}
523+
</span>
524+
)
525+
lastIndex = i + mention.length
526+
}
527+
const tail = text.slice(lastIndex)
528+
if (tail) nodes.push(tail)
529+
return nodes
530+
})()}
412531
</div>
413532
</div>
414533
</div>

0 commit comments

Comments
 (0)