mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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
This commit is contained in:
committed by
GitHub
parent
fed4e507cc
commit
06e9a6b302
@@ -39,7 +39,7 @@ const ChatMessageSchema = z.object({
|
||||
chatId: z.string().optional(),
|
||||
workflowId: z.string().min(1, 'Workflow ID is required'),
|
||||
mode: z.enum(['ask', 'agent']).optional().default('agent'),
|
||||
depth: z.number().int().min(-2).max(3).optional().default(0),
|
||||
depth: z.number().int().min(0).max(3).optional().default(0),
|
||||
prefetch: z.boolean().optional(),
|
||||
createNewChat: z.boolean().optional().default(false),
|
||||
stream: z.boolean().optional().default(true),
|
||||
@@ -47,6 +47,19 @@ const ChatMessageSchema = z.object({
|
||||
fileAttachments: z.array(FileAttachmentSchema).optional(),
|
||||
provider: z.string().optional().default('openai'),
|
||||
conversationId: z.string().optional(),
|
||||
contexts: z
|
||||
.array(
|
||||
z.object({
|
||||
kind: z.enum(['past_chat', 'workflow', 'blocks', 'logs', 'knowledge', 'templates']),
|
||||
label: z.string(),
|
||||
chatId: z.string().optional(),
|
||||
workflowId: z.string().optional(),
|
||||
knowledgeId: z.string().optional(),
|
||||
blockId: z.string().optional(),
|
||||
templateId: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -81,7 +94,38 @@ export async function POST(req: NextRequest) {
|
||||
fileAttachments,
|
||||
provider,
|
||||
conversationId,
|
||||
contexts,
|
||||
} = ChatMessageSchema.parse(body)
|
||||
try {
|
||||
logger.info(`[${tracker.requestId}] Received chat POST`, {
|
||||
hasContexts: Array.isArray(contexts),
|
||||
contextsCount: Array.isArray(contexts) ? contexts.length : 0,
|
||||
contextsPreview: Array.isArray(contexts)
|
||||
? contexts.map((c: any) => ({
|
||||
kind: c?.kind,
|
||||
chatId: c?.chatId,
|
||||
workflowId: c?.workflowId,
|
||||
label: c?.label,
|
||||
}))
|
||||
: undefined,
|
||||
})
|
||||
} catch {}
|
||||
// Preprocess contexts server-side
|
||||
let agentContexts: Array<{ type: string; content: string }> = []
|
||||
if (Array.isArray(contexts) && contexts.length > 0) {
|
||||
try {
|
||||
const { processContextsServer } = await import('@/lib/copilot/process-contents')
|
||||
const processed = await processContextsServer(contexts as any, authenticatedUserId)
|
||||
agentContexts = processed
|
||||
logger.info(`[${tracker.requestId}] Contexts processed for request`, {
|
||||
processedCount: agentContexts.length,
|
||||
kinds: agentContexts.map((c) => c.type),
|
||||
lengthPreview: agentContexts.map((c) => c.content?.length ?? 0),
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(`[${tracker.requestId}] Failed to process contexts`, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Consolidation mapping: map negative depths to base depth with prefetch=true
|
||||
let effectiveDepth: number | undefined = typeof depth === 'number' ? depth : undefined
|
||||
@@ -312,8 +356,15 @@ export async function POST(req: NextRequest) {
|
||||
...(typeof effectiveDepth === 'number' ? { depth: effectiveDepth } : {}),
|
||||
...(typeof effectivePrefetch === 'boolean' ? { prefetch: effectivePrefetch } : {}),
|
||||
...(session?.user?.name && { userName: session.user.name }),
|
||||
...(agentContexts.length > 0 && { context: agentContexts }),
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`[${tracker.requestId}] About to call Sim Agent with context`, {
|
||||
context: (requestPayload as any).context,
|
||||
})
|
||||
} catch {}
|
||||
|
||||
const simAgentResponse = await fetch(`${SIM_AGENT_API_URL}/api/chat-completion-streaming`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -350,6 +401,11 @@ export async function POST(req: NextRequest) {
|
||||
content: message,
|
||||
timestamp: new Date().toISOString(),
|
||||
...(fileAttachments && fileAttachments.length > 0 && { fileAttachments }),
|
||||
...(Array.isArray(contexts) && contexts.length > 0 && { contexts }),
|
||||
...(Array.isArray(contexts) &&
|
||||
contexts.length > 0 && {
|
||||
contentBlocks: [{ type: 'contexts', contexts: contexts as any, timestamp: Date.now() }],
|
||||
}),
|
||||
}
|
||||
|
||||
// Create a pass-through stream that captures the response
|
||||
@@ -683,6 +739,11 @@ export async function POST(req: NextRequest) {
|
||||
content: message,
|
||||
timestamp: new Date().toISOString(),
|
||||
...(fileAttachments && fileAttachments.length > 0 && { fileAttachments }),
|
||||
...(Array.isArray(contexts) && contexts.length > 0 && { contexts }),
|
||||
...(Array.isArray(contexts) &&
|
||||
contexts.length > 0 && {
|
||||
contentBlocks: [{ type: 'contexts', contexts: contexts as any, timestamp: Date.now() }],
|
||||
}),
|
||||
}
|
||||
|
||||
const assistantMessage = {
|
||||
|
||||
39
apps/sim/app/api/copilot/chats/route.ts
Normal file
39
apps/sim/app/api/copilot/chats/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { desc, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import {
|
||||
authenticateCopilotRequestSessionOnly,
|
||||
createInternalServerErrorResponse,
|
||||
createUnauthorizedResponse,
|
||||
} from '@/lib/copilot/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { db } from '@/db'
|
||||
import { copilotChats } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('CopilotChatsListAPI')
|
||||
|
||||
export async function GET(_req: NextRequest) {
|
||||
try {
|
||||
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
|
||||
if (!isAuthenticated || !userId) {
|
||||
return createUnauthorizedResponse()
|
||||
}
|
||||
|
||||
const chats = await db
|
||||
.select({
|
||||
id: copilotChats.id,
|
||||
title: copilotChats.title,
|
||||
workflowId: copilotChats.workflowId,
|
||||
updatedAt: copilotChats.updatedAt,
|
||||
})
|
||||
.from(copilotChats)
|
||||
.where(eq(copilotChats.userId, userId))
|
||||
.orderBy(desc(copilotChats.updatedAt))
|
||||
|
||||
logger.info(`Retrieved ${chats.length} chats for user ${userId}`)
|
||||
|
||||
return NextResponse.json({ success: true, chats })
|
||||
} catch (error) {
|
||||
logger.error('Error fetching user copilot chats:', error)
|
||||
return createInternalServerErrorResponse('Failed to fetch user chats')
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { type FC, memo, useEffect, useMemo, useState } from 'react'
|
||||
import { Check, Clipboard, Loader2, RotateCcw, ThumbsDown, ThumbsUp, X } from 'lucide-react'
|
||||
import {
|
||||
Blocks,
|
||||
Bot,
|
||||
Check,
|
||||
Clipboard,
|
||||
Info,
|
||||
LibraryBig,
|
||||
Loader2,
|
||||
RotateCcw,
|
||||
Shapes,
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
Workflow,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import { InlineToolCall } from '@/lib/copilot/inline-tool-call'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
@@ -31,6 +45,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
||||
const [showUpvoteSuccess, setShowUpvoteSuccess] = useState(false)
|
||||
const [showDownvoteSuccess, setShowDownvoteSuccess] = useState(false)
|
||||
const [showRestoreConfirmation, setShowRestoreConfirmation] = useState(false)
|
||||
const [showAllContexts, setShowAllContexts] = useState(false)
|
||||
|
||||
// Get checkpoint functionality from copilot store
|
||||
const {
|
||||
@@ -357,6 +372,78 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Context chips displayed above the message bubble, independent of inline text */}
|
||||
{(Array.isArray((message as any).contexts) && (message as any).contexts.length > 0) ||
|
||||
(Array.isArray(message.contentBlocks) &&
|
||||
(message.contentBlocks as any[]).some((b: any) => b?.type === 'contexts')) ? (
|
||||
<div className='flex items-center justify-end gap-0'>
|
||||
<div className='min-w-0 max-w-[80%]'>
|
||||
<div className='mb-1 flex flex-wrap justify-end gap-1.5'>
|
||||
{(() => {
|
||||
const direct = Array.isArray((message as any).contexts)
|
||||
? ((message as any).contexts as any[])
|
||||
: []
|
||||
const block = Array.isArray(message.contentBlocks)
|
||||
? (message.contentBlocks as any[]).find((b: any) => b?.type === 'contexts')
|
||||
: null
|
||||
const fromBlock = Array.isArray((block as any)?.contexts)
|
||||
? ((block as any).contexts as any[])
|
||||
: []
|
||||
const allContexts = direct.length > 0 ? direct : fromBlock
|
||||
const MAX_VISIBLE = 4
|
||||
const visible = showAllContexts
|
||||
? allContexts
|
||||
: allContexts.slice(0, MAX_VISIBLE)
|
||||
return (
|
||||
<>
|
||||
{visible.map((ctx: any, idx: number) => (
|
||||
<span
|
||||
key={`ctx-${idx}-${ctx?.label || ctx?.kind}`}
|
||||
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'
|
||||
title={ctx?.label || ctx?.kind}
|
||||
>
|
||||
{ctx?.kind === 'past_chat' ? (
|
||||
<Bot className='h-3 w-3 text-muted-foreground' />
|
||||
) : ctx?.kind === 'workflow' ? (
|
||||
<Workflow className='h-3 w-3 text-muted-foreground' />
|
||||
) : ctx?.kind === 'blocks' ? (
|
||||
<Blocks className='h-3 w-3 text-muted-foreground' />
|
||||
) : ctx?.kind === 'knowledge' ? (
|
||||
<LibraryBig className='h-3 w-3 text-muted-foreground' />
|
||||
) : ctx?.kind === 'templates' ? (
|
||||
<Shapes className='h-3 w-3 text-muted-foreground' />
|
||||
) : (
|
||||
<Info className='h-3 w-3 text-muted-foreground' />
|
||||
)}
|
||||
<span className='max-w-[140px] truncate'>
|
||||
{ctx?.label || ctx?.kind}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
{allContexts.length > MAX_VISIBLE && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowAllContexts((v) => !v)}
|
||||
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)]'
|
||||
title={
|
||||
showAllContexts
|
||||
? 'Show less'
|
||||
: `Show ${allContexts.length - MAX_VISIBLE} more`
|
||||
}
|
||||
>
|
||||
{showAllContexts
|
||||
? 'Show less'
|
||||
: `+${allContexts.length - MAX_VISIBLE} more`}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className='flex items-center justify-end gap-0'>
|
||||
{hasCheckpoints && (
|
||||
<div className='mr-1 inline-flex items-center justify-center'>
|
||||
@@ -408,7 +495,39 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
||||
}}
|
||||
>
|
||||
<div className='whitespace-pre-wrap break-words font-normal text-base text-foreground leading-relaxed'>
|
||||
<WordWrap text={message.content} />
|
||||
{(() => {
|
||||
const text = message.content || ''
|
||||
const contexts: any[] = Array.isArray((message as any).contexts)
|
||||
? ((message as any).contexts as any[])
|
||||
: []
|
||||
const labels = contexts.map((c) => c?.label).filter(Boolean) as string[]
|
||||
if (!labels.length) return <WordWrap text={text} />
|
||||
|
||||
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const pattern = new RegExp(`@(${labels.map(escapeRegex).join('|')})`, 'g')
|
||||
|
||||
const nodes: React.ReactNode[] = []
|
||||
let lastIndex = 0
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
const i = match.index
|
||||
const before = text.slice(lastIndex, i)
|
||||
if (before) nodes.push(before)
|
||||
const mention = match[0]
|
||||
nodes.push(
|
||||
<span
|
||||
key={`mention-${i}-${lastIndex}`}
|
||||
className='rounded-[6px] bg-[color-mix(in_srgb,var(--brand-primary-hover-hex)_14%,transparent)] px-1'
|
||||
>
|
||||
{mention}
|
||||
</span>
|
||||
)
|
||||
lastIndex = i + mention.length
|
||||
}
|
||||
const tail = text.slice(lastIndex)
|
||||
if (tail) nodes.push(tail)
|
||||
return nodes
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Bot } from 'lucide-react'
|
||||
import { Blocks, Bot, LibraryBig, Workflow } from 'lucide-react'
|
||||
|
||||
interface CopilotWelcomeProps {
|
||||
onQuestionClick?: (question: string) => void
|
||||
@@ -8,49 +8,90 @@ interface CopilotWelcomeProps {
|
||||
}
|
||||
|
||||
export function CopilotWelcome({ onQuestionClick, mode = 'ask' }: CopilotWelcomeProps) {
|
||||
const askQuestions = [
|
||||
'How do I create a workflow?',
|
||||
'What tools are available?',
|
||||
'What does my workflow do?',
|
||||
]
|
||||
|
||||
const agentQuestions = [
|
||||
'Help me build a workflow',
|
||||
'Help me optimize my workflow',
|
||||
'Help me debug my workflow',
|
||||
]
|
||||
|
||||
const exampleQuestions = mode === 'ask' ? askQuestions : agentQuestions
|
||||
|
||||
const handleQuestionClick = (question: string) => {
|
||||
onQuestionClick?.(question)
|
||||
}
|
||||
|
||||
const subtitle =
|
||||
mode === 'ask'
|
||||
? 'Ask about workflows, tools, or how to get started'
|
||||
: 'Build, edit, and optimize workflows'
|
||||
|
||||
const capabilities =
|
||||
mode === 'agent'
|
||||
? [
|
||||
{
|
||||
title: 'Build & edit workflows',
|
||||
question: 'Help me build a workflow',
|
||||
Icon: Workflow,
|
||||
},
|
||||
{
|
||||
title: 'Optimize workflows',
|
||||
question: 'Help me optimize my workflow',
|
||||
Icon: Blocks,
|
||||
},
|
||||
{
|
||||
title: 'Debug workflows',
|
||||
question: 'Help me debug my workflow',
|
||||
Icon: LibraryBig,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: 'Understand my workflow',
|
||||
question: 'What does my workflow do?',
|
||||
Icon: Workflow,
|
||||
},
|
||||
{
|
||||
title: 'Discover tools',
|
||||
question: 'What tools are available?',
|
||||
Icon: Blocks,
|
||||
},
|
||||
{
|
||||
title: 'Get started',
|
||||
question: 'How do I create a workflow?',
|
||||
Icon: LibraryBig,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col items-center justify-center px-4 py-10'>
|
||||
<div className='space-y-6 text-center'>
|
||||
<Bot className='mx-auto h-12 w-12 text-muted-foreground' />
|
||||
<div className='space-y-2'>
|
||||
<h3 className='font-medium text-lg'>How can I help you today?</h3>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
{mode === 'ask'
|
||||
? 'Ask me anything about your workflows, available tools, or how to get started.'
|
||||
: 'I can help you build, edit, and optimize workflows. What would you like to do?'}
|
||||
</p>
|
||||
<div className='relative h-full w-full overflow-hidden px-4 pt-8 pb-6'>
|
||||
<div className='relative mx-auto w-full max-w-xl'>
|
||||
{/* Header */}
|
||||
<div className='flex flex-col items-center text-center'>
|
||||
<Bot className='h-12 w-12 text-[var(--brand-primary-hover-hex)]' strokeWidth={1.5} />
|
||||
<h3 className='mt-2 font-medium text-foreground text-lg sm:text-xl'>{subtitle}</h3>
|
||||
</div>
|
||||
<div className='mx-auto max-w-sm space-y-3'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>Try asking:</div>
|
||||
<div className='flex flex-wrap justify-center gap-2'>
|
||||
{exampleQuestions.map((question, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className='inline-flex cursor-pointer items-center rounded-full bg-muted/60 px-3 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:scale-105 hover:bg-muted hover:text-foreground active:scale-95'
|
||||
onClick={() => handleQuestionClick(question)}
|
||||
>
|
||||
{question}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Unified capability cards */}
|
||||
<div className='mt-7 space-y-2.5'>
|
||||
{capabilities.map(({ title, question, Icon }, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
type='button'
|
||||
onClick={() => handleQuestionClick(question)}
|
||||
className='w-full rounded-[10px] border bg-background/60 p-3 text-left transition-colors hover:bg-muted focus:outline-none focus:ring-2 focus:ring-[var(--brand-primary-hover-hex)]/30'
|
||||
>
|
||||
<div className='flex items-start gap-2'>
|
||||
<div className='mt-0.5 flex h-6 w-6 items-center justify-center rounded bg-[color-mix(in_srgb,var(--brand-primary-hover-hex)_16%,transparent)] text-[var(--brand-primary-hover-hex)]'>
|
||||
<Icon className='h-3.5 w-3.5' />
|
||||
</div>
|
||||
<div>
|
||||
<div className='font-medium text-xs'>{title}</div>
|
||||
<p className='mt-1 text-[11px] text-muted-foreground'>{question}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tips */}
|
||||
<div className='mt-6 text-center text-[11px] text-muted-foreground'>
|
||||
<p>
|
||||
Tip: Use <span className='font-medium text-foreground'>@</span> to reference chats,
|
||||
workflows, knowledge, blocks, or templates
|
||||
</p>
|
||||
<p className='mt-1.5'>Shift+Enter for newline</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -346,7 +346,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
|
||||
|
||||
// Handle message submission
|
||||
const handleSubmit = useCallback(
|
||||
async (query: string, fileAttachments?: MessageFileAttachment[]) => {
|
||||
async (query: string, fileAttachments?: MessageFileAttachment[], contexts?: any[]) => {
|
||||
if (!query || isSendingMessage || !activeWorkflowId) return
|
||||
|
||||
// Clear todos when sending a new message
|
||||
@@ -356,7 +356,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
|
||||
}
|
||||
|
||||
try {
|
||||
await sendMessage(query, { stream: true, fileAttachments })
|
||||
await sendMessage(query, { stream: true, fileAttachments, contexts })
|
||||
logger.info(
|
||||
'Sent message:',
|
||||
query,
|
||||
|
||||
@@ -16,6 +16,7 @@ const logger = createLogger('Credentials')
|
||||
|
||||
interface CredentialsProps {
|
||||
onOpenChange?: (open: boolean) => void
|
||||
registerCloseHandler?: (handler: (open: boolean) => void) => void
|
||||
}
|
||||
|
||||
interface ServiceInfo extends OAuthServiceConfig {
|
||||
@@ -24,7 +25,7 @@ interface ServiceInfo extends OAuthServiceConfig {
|
||||
accounts?: { id: string; name: string }[]
|
||||
}
|
||||
|
||||
export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
export function Credentials({ onOpenChange, registerCloseHandler }: CredentialsProps) {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { data: session } = useSession()
|
||||
@@ -39,6 +40,8 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
const [_pendingScopes, setPendingScopes] = useState<string[]>([])
|
||||
const [authSuccess, setAuthSuccess] = useState(false)
|
||||
const [showActionRequired, setShowActionRequired] = useState(false)
|
||||
const prevConnectedIdsRef = useRef<Set<string>>(new Set())
|
||||
const connectionAddedRef = useRef<boolean>(false)
|
||||
|
||||
// Define available services from our standardized OAuth providers
|
||||
const defineServices = (): ServiceInfo[] => {
|
||||
@@ -186,6 +189,43 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
// Track when a new connection is added compared to previous render
|
||||
useEffect(() => {
|
||||
try {
|
||||
const currentConnected = new Set<string>()
|
||||
services.forEach((svc) => {
|
||||
if (svc.isConnected) currentConnected.add(svc.id)
|
||||
})
|
||||
// Detect new connections by comparing to previous connected set
|
||||
for (const id of currentConnected) {
|
||||
if (!prevConnectedIdsRef.current.has(id)) {
|
||||
connectionAddedRef.current = true
|
||||
break
|
||||
}
|
||||
}
|
||||
prevConnectedIdsRef.current = currentConnected
|
||||
} catch {}
|
||||
}, [services])
|
||||
|
||||
// On mount, register a close handler so the parent modal can delegate close events here
|
||||
useEffect(() => {
|
||||
if (!registerCloseHandler) return
|
||||
const handle = (open: boolean) => {
|
||||
if (open) return
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('oauth-integration-closed', {
|
||||
detail: { success: connectionAddedRef.current === true },
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch {}
|
||||
onOpenChange?.(open)
|
||||
}
|
||||
registerCloseHandler(handle)
|
||||
}, [registerCloseHandler, onOpenChange])
|
||||
|
||||
// Handle connect button click
|
||||
const handleConnect = async (service: ServiceInfo) => {
|
||||
try {
|
||||
|
||||
@@ -48,6 +48,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
const { activeOrganization } = useOrganizationStore()
|
||||
const hasLoadedInitialData = useRef(false)
|
||||
const environmentCloseHandler = useRef<((open: boolean) => void) | null>(null)
|
||||
const credentialsCloseHandler = useRef<((open: boolean) => void) | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function loadAllSettings() {
|
||||
@@ -100,6 +101,8 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
const handleDialogOpenChange = (newOpen: boolean) => {
|
||||
if (!newOpen && activeSection === 'environment' && environmentCloseHandler.current) {
|
||||
environmentCloseHandler.current(newOpen)
|
||||
} else if (!newOpen && activeSection === 'credentials' && credentialsCloseHandler.current) {
|
||||
credentialsCloseHandler.current(newOpen)
|
||||
} else {
|
||||
onOpenChange(newOpen)
|
||||
}
|
||||
@@ -139,7 +142,12 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
<Account onOpenChange={onOpenChange} />
|
||||
</div>
|
||||
<div className={cn('h-full', activeSection === 'credentials' ? 'block' : 'hidden')}>
|
||||
<Credentials onOpenChange={onOpenChange} />
|
||||
<Credentials
|
||||
onOpenChange={onOpenChange}
|
||||
registerCloseHandler={(handler) => {
|
||||
credentialsCloseHandler.current = handler
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn('h-full', activeSection === 'apikeys' ? 'block' : 'hidden')}>
|
||||
<ApiKeys onOpenChange={onOpenChange} />
|
||||
|
||||
@@ -76,14 +76,140 @@ export function ToolCallExecution({ toolCall, isCompact = false }: ToolCallProps
|
||||
{toolCall.parameters &&
|
||||
Object.keys(toolCall.parameters).length > 0 &&
|
||||
(toolCall.name === 'make_api_request' ||
|
||||
toolCall.name === 'set_environment_variables') && (
|
||||
<div className='min-w-0 max-w-full rounded bg-amber-100 p-2 dark:bg-amber-900'>
|
||||
<div className='mb-1 font-medium text-amber-800 text-xs dark:text-amber-200'>
|
||||
Parameters:
|
||||
</div>
|
||||
<div className='min-w-0 max-w-full break-all font-mono text-amber-700 text-xs dark:text-amber-300'>
|
||||
{JSON.stringify(toolCall.parameters, null, 2)}
|
||||
</div>
|
||||
toolCall.name === 'set_environment_variables' ||
|
||||
toolCall.name === 'set_global_workflow_variables') && (
|
||||
<div className='min-w-0 max-w-full rounded border border-amber-200 bg-amber-50 p-2 dark:border-amber-800 dark:bg-amber-950'>
|
||||
{toolCall.name === 'make_api_request' ? (
|
||||
<div className='w-full overflow-hidden rounded border border-muted bg-card'>
|
||||
<div className='grid grid-cols-2 gap-0 border-muted/60 border-b bg-muted/40 px-2 py-1.5'>
|
||||
<div className='font-medium text-[10px] text-muted-foreground uppercase tracking-wide'>
|
||||
Method
|
||||
</div>
|
||||
<div className='font-medium text-[10px] text-muted-foreground uppercase tracking-wide'>
|
||||
Endpoint
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-[auto_1fr] items-center gap-2 px-2 py-2'>
|
||||
<div>
|
||||
<span className='inline-flex rounded bg-muted px-1.5 py-0.5 font-mono text-muted-foreground text-xs'>
|
||||
{String((toolCall.parameters as any).method || '').toUpperCase() ||
|
||||
'GET'}
|
||||
</span>
|
||||
</div>
|
||||
<div className='min-w-0'>
|
||||
<span
|
||||
className='block overflow-x-auto whitespace-nowrap font-mono text-foreground text-xs'
|
||||
title={String((toolCall.parameters as any).url || '')}
|
||||
>
|
||||
{String((toolCall.parameters as any).url || '') || 'URL not provided'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{toolCall.name === 'set_environment_variables'
|
||||
? (() => {
|
||||
const variables =
|
||||
(toolCall.parameters as any).variables &&
|
||||
typeof (toolCall.parameters as any).variables === 'object'
|
||||
? (toolCall.parameters as any).variables
|
||||
: {}
|
||||
const entries = Object.entries(variables)
|
||||
return (
|
||||
<div className='w-full overflow-hidden rounded border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950'>
|
||||
<div className='grid grid-cols-2 gap-0 border-amber-200/60 border-b px-2 py-1.5 dark:border-amber-800/60'>
|
||||
<div className='font-medium text-[10px] text-amber-700 uppercase tracking-wide dark:text-amber-300'>
|
||||
Name
|
||||
</div>
|
||||
<div className='font-medium text-[10px] text-amber-700 uppercase tracking-wide dark:text-amber-300'>
|
||||
Value
|
||||
</div>
|
||||
</div>
|
||||
{entries.length === 0 ? (
|
||||
<div className='px-2 py-2 text-muted-foreground text-xs'>
|
||||
No variables provided
|
||||
</div>
|
||||
) : (
|
||||
<div className='divide-y divide-amber-200 dark:divide-amber-800'>
|
||||
{entries.map(([k, v]) => (
|
||||
<div
|
||||
key={k}
|
||||
className='grid grid-cols-[auto_1fr] items-center gap-2 px-2 py-1.5'
|
||||
>
|
||||
<div className='truncate font-medium text-amber-800 text-xs dark:text-amber-200'>
|
||||
{k}
|
||||
</div>
|
||||
<div className='min-w-0'>
|
||||
<span className='block overflow-x-auto whitespace-nowrap font-mono text-amber-700 text-xs dark:text-amber-300'>
|
||||
{String(v)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()
|
||||
: null}
|
||||
|
||||
{toolCall.name === 'set_global_workflow_variables'
|
||||
? (() => {
|
||||
const ops = Array.isArray((toolCall.parameters as any).operations)
|
||||
? ((toolCall.parameters as any).operations as any[])
|
||||
: []
|
||||
return (
|
||||
<div className='w-full overflow-hidden rounded border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950'>
|
||||
<div className='grid grid-cols-3 gap-0 border-amber-200/60 border-b px-2 py-1.5 dark:border-amber-800/60'>
|
||||
<div className='font-medium text-[10px] text-amber-700 uppercase tracking-wide dark:text-amber-300'>
|
||||
Name
|
||||
</div>
|
||||
<div className='font-medium text-[10px] text-amber-700 uppercase tracking-wide dark:text-amber-300'>
|
||||
Type
|
||||
</div>
|
||||
<div className='font-medium text-[10px] text-amber-700 uppercase tracking-wide dark:text-amber-300'>
|
||||
Value
|
||||
</div>
|
||||
</div>
|
||||
{ops.length === 0 ? (
|
||||
<div className='px-2 py-2 text-muted-foreground text-xs'>
|
||||
No operations provided
|
||||
</div>
|
||||
) : (
|
||||
<div className='divide-y divide-amber-200 dark:divide-amber-800'>
|
||||
{ops.map((op, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className='grid grid-cols-3 items-center gap-0 px-2 py-1.5'
|
||||
>
|
||||
<div className='min-w-0'>
|
||||
<span className='truncate text-amber-800 text-xs dark:text-amber-200'>
|
||||
{String(op.name || '')}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className='rounded border px-1 py-0.5 text-[10px] text-muted-foreground'>
|
||||
{String(op.type || '')}
|
||||
</span>
|
||||
</div>
|
||||
<div className='min-w-0'>
|
||||
{op.value !== undefined ? (
|
||||
<span className='block overflow-x-auto whitespace-nowrap font-mono text-amber-700 text-xs dark:text-amber-300'>
|
||||
{String(op.value)}
|
||||
</span>
|
||||
) : (
|
||||
<span className='text-muted-foreground text-xs'>—</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()
|
||||
: null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -57,13 +57,14 @@ export interface SendMessageRequest {
|
||||
chatId?: string
|
||||
workflowId?: string
|
||||
mode?: 'ask' | 'agent'
|
||||
depth?: -2 | -1 | 0 | 1 | 2 | 3
|
||||
depth?: 0 | 1 | 2 | 3
|
||||
prefetch?: boolean
|
||||
createNewChat?: boolean
|
||||
stream?: boolean
|
||||
implicitFeedback?: string
|
||||
fileAttachments?: MessageFileAttachment[]
|
||||
abortSignal?: AbortSignal
|
||||
contexts?: Array<{ kind: string; label?: string; chatId?: string; workflowId?: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,6 +104,23 @@ export async function sendStreamingMessage(
|
||||
): 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' },
|
||||
|
||||
@@ -255,11 +255,15 @@ export function InlineToolCall({
|
||||
|
||||
const isExpandablePending =
|
||||
toolCall.state === 'pending' &&
|
||||
(toolCall.name === 'make_api_request' || toolCall.name === 'set_environment_variables')
|
||||
(toolCall.name === 'make_api_request' ||
|
||||
toolCall.name === 'set_environment_variables' ||
|
||||
toolCall.name === 'set_global_workflow_variables')
|
||||
|
||||
const [expanded, setExpanded] = useState(isExpandablePending)
|
||||
const isExpandableTool =
|
||||
toolCall.name === 'make_api_request' || toolCall.name === 'set_environment_variables'
|
||||
toolCall.name === 'make_api_request' ||
|
||||
toolCall.name === 'set_environment_variables' ||
|
||||
toolCall.name === 'set_global_workflow_variables'
|
||||
|
||||
const showButtons = shouldShowRunSkipButtons(toolCall)
|
||||
const showMoveToBackground =
|
||||
@@ -291,11 +295,30 @@ export function InlineToolCall({
|
||||
const url = params.url || ''
|
||||
const method = (params.method || '').toUpperCase()
|
||||
return (
|
||||
<div className='mt-0.5 flex items-center gap-2'>
|
||||
<span className='truncate text-foreground text-xs' title={url}>
|
||||
{method ? `${method} ` : ''}
|
||||
{url || 'URL not provided'}
|
||||
</span>
|
||||
<div className='mt-0.5 w-full overflow-hidden rounded border border-muted bg-card'>
|
||||
<div className='grid grid-cols-2 gap-0 border-muted/60 border-b bg-muted/40 px-2 py-1.5'>
|
||||
<div className='font-medium text-[10px] text-muted-foreground uppercase tracking-wide'>
|
||||
Method
|
||||
</div>
|
||||
<div className='font-medium text-[10px] text-muted-foreground uppercase tracking-wide'>
|
||||
Endpoint
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-[auto_1fr] items-center gap-2 px-2 py-2'>
|
||||
<div>
|
||||
<span className='inline-flex rounded bg-muted px-1.5 py-0.5 font-mono text-muted-foreground text-xs'>
|
||||
{method || 'GET'}
|
||||
</span>
|
||||
</div>
|
||||
<div className='min-w-0'>
|
||||
<span
|
||||
className='block overflow-x-auto whitespace-nowrap font-mono text-foreground text-xs'
|
||||
title={url}
|
||||
>
|
||||
{url || 'URL not provided'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -305,16 +328,77 @@ export function InlineToolCall({
|
||||
params.variables && typeof params.variables === 'object' ? params.variables : {}
|
||||
const entries = Object.entries(variables)
|
||||
return (
|
||||
<div className='mt-0.5'>
|
||||
<div className='mt-0.5 w-full overflow-hidden rounded border border-muted bg-card'>
|
||||
<div className='grid grid-cols-2 gap-0 border-muted/60 border-b bg-muted/40 px-2 py-1.5'>
|
||||
<div className='font-medium text-[10px] text-muted-foreground uppercase tracking-wide'>
|
||||
Name
|
||||
</div>
|
||||
<div className='font-medium text-[10px] text-muted-foreground uppercase tracking-wide'>
|
||||
Value
|
||||
</div>
|
||||
</div>
|
||||
{entries.length === 0 ? (
|
||||
<span className='text-muted-foreground text-xs'>No variables provided</span>
|
||||
<div className='px-2 py-2 text-muted-foreground text-xs'>No variables provided</div>
|
||||
) : (
|
||||
<div className='space-y-0.5'>
|
||||
<div className='divide-y divide-muted/60'>
|
||||
{entries.map(([k, v]) => (
|
||||
<div key={k} className='flex items-center gap-0.5'>
|
||||
<span className='font-medium text-muted-foreground text-xs'>{k}</span>
|
||||
<span className='mx-1 font-medium text-muted-foreground text-xs'>:</span>
|
||||
<span className='truncate font-medium text-foreground text-xs'>{String(v)}</span>
|
||||
<div key={k} className='grid grid-cols-[auto_1fr] items-center gap-2 px-2 py-1.5'>
|
||||
<div className='truncate font-medium text-amber-800 text-xs dark:text-amber-200'>
|
||||
{k}
|
||||
</div>
|
||||
<div className='min-w-0'>
|
||||
<span className='block overflow-x-auto whitespace-nowrap font-mono text-amber-700 text-xs dark:text-amber-300'>
|
||||
{String(v)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (toolCall.name === 'set_global_workflow_variables') {
|
||||
const ops = Array.isArray(params.operations) ? (params.operations as any[]) : []
|
||||
return (
|
||||
<div className='mt-0.5 w-full overflow-hidden rounded border border-muted bg-card'>
|
||||
<div className='grid grid-cols-3 gap-0 border-muted/60 border-b bg-muted/40 px-2 py-1.5'>
|
||||
<div className='font-medium text-[10px] text-muted-foreground uppercase tracking-wide'>
|
||||
Name
|
||||
</div>
|
||||
<div className='font-medium text-[10px] text-muted-foreground uppercase tracking-wide'>
|
||||
Type
|
||||
</div>
|
||||
<div className='font-medium text-[10px] text-muted-foreground uppercase tracking-wide'>
|
||||
Value
|
||||
</div>
|
||||
</div>
|
||||
{ops.length === 0 ? (
|
||||
<div className='px-2 py-2 text-muted-foreground text-xs'>No operations provided</div>
|
||||
) : (
|
||||
<div className='divide-y divide-amber-200 dark:divide-amber-800'>
|
||||
{ops.map((op, idx) => (
|
||||
<div key={idx} className='grid grid-cols-3 items-center gap-0 px-2 py-1.5'>
|
||||
<div className='min-w-0'>
|
||||
<span className='truncate text-amber-800 text-xs dark:text-amber-200'>
|
||||
{String(op.name || '')}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className='rounded border px-1 py-0.5 text-[10px] text-muted-foreground'>
|
||||
{String(op.type || '')}
|
||||
</span>
|
||||
</div>
|
||||
<div className='min-w-0'>
|
||||
{op.value !== undefined ? (
|
||||
<span className='block overflow-x-auto whitespace-nowrap font-mono text-amber-700 text-xs dark:text-amber-300'>
|
||||
{String(op.value)}
|
||||
</span>
|
||||
) : (
|
||||
<span className='text-muted-foreground text-xs'>—</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
358
apps/sim/lib/copilot/process-contents.ts
Normal file
358
apps/sim/lib/copilot/process-contents.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import { and, eq, isNull } from 'drizzle-orm'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
import { db } from '@/db'
|
||||
import { copilotChats, document, knowledgeBase, templates } from '@/db/schema'
|
||||
import type { ChatContext } from '@/stores/copilot/types'
|
||||
|
||||
export type AgentContextType =
|
||||
| 'past_chat'
|
||||
| 'workflow'
|
||||
| 'blocks'
|
||||
| 'logs'
|
||||
| 'knowledge'
|
||||
| 'templates'
|
||||
|
||||
export interface AgentContext {
|
||||
type: AgentContextType
|
||||
tag: string
|
||||
content: string
|
||||
}
|
||||
|
||||
const logger = createLogger('ProcessContents')
|
||||
|
||||
export async function processContexts(
|
||||
contexts: ChatContext[] | undefined
|
||||
): Promise<AgentContext[]> {
|
||||
if (!Array.isArray(contexts) || contexts.length === 0) return []
|
||||
const tasks = contexts.map(async (ctx) => {
|
||||
try {
|
||||
if (ctx.kind === 'past_chat') {
|
||||
return await processPastChatViaApi(ctx.chatId, ctx.label ? `@${ctx.label}` : '@')
|
||||
}
|
||||
if (ctx.kind === 'workflow' && ctx.workflowId) {
|
||||
return await processWorkflowFromDb(ctx.workflowId, ctx.label ? `@${ctx.label}` : '@')
|
||||
}
|
||||
if (ctx.kind === 'knowledge' && (ctx as any).knowledgeId) {
|
||||
return await processKnowledgeFromDb(
|
||||
(ctx as any).knowledgeId,
|
||||
ctx.label ? `@${ctx.label}` : '@'
|
||||
)
|
||||
}
|
||||
if (ctx.kind === 'blocks' && (ctx as any).blockId) {
|
||||
return await processBlockMetadata((ctx as any).blockId, ctx.label ? `@${ctx.label}` : '@')
|
||||
}
|
||||
if (ctx.kind === 'templates' && (ctx as any).templateId) {
|
||||
return await processTemplateFromDb(
|
||||
(ctx as any).templateId,
|
||||
ctx.label ? `@${ctx.label}` : '@'
|
||||
)
|
||||
}
|
||||
// Other kinds can be added here: workflow, blocks, logs, knowledge, templates
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error('Failed processing context', { ctx, error })
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.all(tasks)
|
||||
return results.filter((r): r is AgentContext => !!r)
|
||||
}
|
||||
|
||||
// Server-side variant (recommended for use in API routes)
|
||||
export async function processContextsServer(
|
||||
contexts: ChatContext[] | undefined,
|
||||
userId: string
|
||||
): Promise<AgentContext[]> {
|
||||
if (!Array.isArray(contexts) || contexts.length === 0) return []
|
||||
const tasks = contexts.map(async (ctx) => {
|
||||
try {
|
||||
if (ctx.kind === 'past_chat' && ctx.chatId) {
|
||||
return await processPastChatFromDb(ctx.chatId, userId, ctx.label ? `@${ctx.label}` : '@')
|
||||
}
|
||||
if (ctx.kind === 'workflow' && ctx.workflowId) {
|
||||
return await processWorkflowFromDb(ctx.workflowId, ctx.label ? `@${ctx.label}` : '@')
|
||||
}
|
||||
if (ctx.kind === 'knowledge' && (ctx as any).knowledgeId) {
|
||||
return await processKnowledgeFromDb(
|
||||
(ctx as any).knowledgeId,
|
||||
ctx.label ? `@${ctx.label}` : '@'
|
||||
)
|
||||
}
|
||||
if (ctx.kind === 'blocks' && (ctx as any).blockId) {
|
||||
return await processBlockMetadata((ctx as any).blockId, ctx.label ? `@${ctx.label}` : '@')
|
||||
}
|
||||
if (ctx.kind === 'templates' && (ctx as any).templateId) {
|
||||
return await processTemplateFromDb(
|
||||
(ctx as any).templateId,
|
||||
ctx.label ? `@${ctx.label}` : '@'
|
||||
)
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error('Failed processing context (server)', { ctx, error })
|
||||
return null
|
||||
}
|
||||
})
|
||||
const results = await Promise.all(tasks)
|
||||
const filtered = results.filter(
|
||||
(r): r is AgentContext => !!r && typeof r.content === 'string' && r.content.trim().length > 0
|
||||
)
|
||||
logger.info('Processed contexts (server)', {
|
||||
totalRequested: contexts.length,
|
||||
totalProcessed: filtered.length,
|
||||
kinds: Array.from(filtered.reduce((s, r) => s.add(r.type), new Set<string>())),
|
||||
})
|
||||
return filtered
|
||||
}
|
||||
|
||||
async function processPastChatFromDb(
|
||||
chatId: string,
|
||||
userId: string,
|
||||
tag: string
|
||||
): Promise<AgentContext | null> {
|
||||
try {
|
||||
const rows = await db
|
||||
.select({ messages: copilotChats.messages })
|
||||
.from(copilotChats)
|
||||
.where(and(eq(copilotChats.id, chatId), eq(copilotChats.userId, userId)))
|
||||
.limit(1)
|
||||
const messages = Array.isArray(rows?.[0]?.messages) ? (rows[0] as any).messages : []
|
||||
const content = messages
|
||||
.map((m: any) => {
|
||||
const role = m.role || 'user'
|
||||
let text = ''
|
||||
if (Array.isArray(m.contentBlocks) && m.contentBlocks.length > 0) {
|
||||
text = m.contentBlocks
|
||||
.filter((b: any) => b?.type === 'text')
|
||||
.map((b: any) => String(b.content || ''))
|
||||
.join('')
|
||||
.trim()
|
||||
}
|
||||
if (!text && typeof m.content === 'string') text = m.content
|
||||
return `${role}: ${text}`.trim()
|
||||
})
|
||||
.filter((s: string) => s.length > 0)
|
||||
.join('\n')
|
||||
logger.info('Processed past_chat context from DB', {
|
||||
chatId,
|
||||
length: content.length,
|
||||
lines: content ? content.split('\n').length : 0,
|
||||
})
|
||||
return { type: 'past_chat', tag, content }
|
||||
} catch (error) {
|
||||
logger.error('Error processing past chat from db', { chatId, error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function processWorkflowFromDb(
|
||||
workflowId: string,
|
||||
tag: string
|
||||
): Promise<AgentContext | null> {
|
||||
try {
|
||||
const normalized = await loadWorkflowFromNormalizedTables(workflowId)
|
||||
if (!normalized) {
|
||||
logger.warn('No normalized workflow data found', { workflowId })
|
||||
return null
|
||||
}
|
||||
const workflowState = {
|
||||
blocks: normalized.blocks || {},
|
||||
edges: normalized.edges || [],
|
||||
loops: normalized.loops || {},
|
||||
parallels: normalized.parallels || {},
|
||||
}
|
||||
// Match get-user-workflow format: just the workflow state JSON
|
||||
const content = JSON.stringify(workflowState, null, 2)
|
||||
logger.info('Processed workflow context', {
|
||||
workflowId,
|
||||
blocks: Object.keys(workflowState.blocks || {}).length,
|
||||
edges: workflowState.edges.length,
|
||||
})
|
||||
return { type: 'workflow', tag, content }
|
||||
} catch (error) {
|
||||
logger.error('Error processing workflow context', { workflowId, error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function processPastChat(chatId: string, tagOverride?: string): Promise<AgentContext | null> {
|
||||
try {
|
||||
const resp = await fetch(`/api/copilot/chat/${encodeURIComponent(chatId)}`)
|
||||
if (!resp.ok) {
|
||||
logger.error('Failed to fetch past chat', { chatId, status: resp.status })
|
||||
return null
|
||||
}
|
||||
const data = await resp.json()
|
||||
const messages = Array.isArray(data?.chat?.messages) ? data.chat.messages : []
|
||||
const content = messages
|
||||
.map((m: any) => {
|
||||
const role = m.role || 'user'
|
||||
// Prefer contentBlocks text if present (joins text blocks), else use content
|
||||
let text = ''
|
||||
if (Array.isArray(m.contentBlocks) && m.contentBlocks.length > 0) {
|
||||
text = m.contentBlocks
|
||||
.filter((b: any) => b?.type === 'text')
|
||||
.map((b: any) => String(b.content || ''))
|
||||
.join('')
|
||||
.trim()
|
||||
}
|
||||
if (!text && typeof m.content === 'string') text = m.content
|
||||
return `${role}: ${text}`.trim()
|
||||
})
|
||||
.filter((s: string) => s.length > 0)
|
||||
.join('\n')
|
||||
logger.info('Processed past_chat context via API', { chatId, length: content.length })
|
||||
|
||||
return { type: 'past_chat', tag: tagOverride || '@', content }
|
||||
} catch (error) {
|
||||
logger.error('Error processing past chat', { chatId, error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Back-compat alias; used by processContexts above
|
||||
async function processPastChatViaApi(chatId: string, tag?: string) {
|
||||
return processPastChat(chatId, tag)
|
||||
}
|
||||
|
||||
async function processKnowledgeFromDb(
|
||||
knowledgeBaseId: string,
|
||||
tag: string
|
||||
): Promise<AgentContext | null> {
|
||||
try {
|
||||
// Load KB metadata
|
||||
const kbRows = await db
|
||||
.select({
|
||||
id: knowledgeBase.id,
|
||||
name: knowledgeBase.name,
|
||||
updatedAt: knowledgeBase.updatedAt,
|
||||
})
|
||||
.from(knowledgeBase)
|
||||
.where(and(eq(knowledgeBase.id, knowledgeBaseId), isNull(knowledgeBase.deletedAt)))
|
||||
.limit(1)
|
||||
const kb = kbRows?.[0]
|
||||
if (!kb) return null
|
||||
|
||||
// Load up to 20 recent doc filenames
|
||||
const docRows = await db
|
||||
.select({ filename: document.filename })
|
||||
.from(document)
|
||||
.where(and(eq(document.knowledgeBaseId, knowledgeBaseId), isNull(document.deletedAt)))
|
||||
.limit(20)
|
||||
|
||||
const sampleDocuments = docRows.map((d: any) => d.filename).filter(Boolean)
|
||||
// We don't have total via this quick select; fallback to sample count
|
||||
const summary = {
|
||||
id: kb.id,
|
||||
name: kb.name,
|
||||
docCount: sampleDocuments.length,
|
||||
sampleDocuments,
|
||||
}
|
||||
const content = JSON.stringify(summary)
|
||||
return { type: 'knowledge', tag, content }
|
||||
} catch (error) {
|
||||
logger.error('Error processing knowledge context (db)', { knowledgeBaseId, error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function processBlockMetadata(blockId: string, tag: string): Promise<AgentContext | null> {
|
||||
try {
|
||||
// Reuse registry to match get_blocks_metadata tool result
|
||||
const { registry: blockRegistry } = await import('@/blocks/registry')
|
||||
const { tools: toolsRegistry } = await import('@/tools/registry')
|
||||
const SPECIAL_BLOCKS_METADATA: Record<string, any> = {}
|
||||
|
||||
let metadata: any = {}
|
||||
if ((SPECIAL_BLOCKS_METADATA as any)[blockId]) {
|
||||
metadata = { ...(SPECIAL_BLOCKS_METADATA as any)[blockId] }
|
||||
metadata.tools = metadata.tools?.access || []
|
||||
} else {
|
||||
const blockConfig: any = (blockRegistry as any)[blockId]
|
||||
if (!blockConfig) {
|
||||
return null
|
||||
}
|
||||
metadata = {
|
||||
id: blockId,
|
||||
name: blockConfig.name || blockId,
|
||||
description: blockConfig.description || '',
|
||||
longDescription: blockConfig.longDescription,
|
||||
category: blockConfig.category,
|
||||
bgColor: blockConfig.bgColor,
|
||||
inputs: blockConfig.inputs || {},
|
||||
outputs: blockConfig.outputs || {},
|
||||
tools: blockConfig.tools?.access || [],
|
||||
hideFromToolbar: blockConfig.hideFromToolbar,
|
||||
}
|
||||
if (blockConfig.subBlocks && Array.isArray(blockConfig.subBlocks)) {
|
||||
metadata.subBlocks = (blockConfig.subBlocks as any[]).map((sb: any) => ({
|
||||
id: sb.id,
|
||||
name: sb.name,
|
||||
type: sb.type,
|
||||
description: sb.description,
|
||||
default: sb.default,
|
||||
options: Array.isArray(sb.options) ? sb.options : [],
|
||||
}))
|
||||
} else {
|
||||
metadata.subBlocks = []
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(metadata.tools) && metadata.tools.length > 0) {
|
||||
metadata.toolDetails = {}
|
||||
for (const toolId of metadata.tools) {
|
||||
const tool = (toolsRegistry as any)[toolId]
|
||||
if (tool) {
|
||||
metadata.toolDetails[toolId] = { name: tool.name, description: tool.description }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = JSON.stringify({ metadata })
|
||||
return { type: 'blocks', tag, content }
|
||||
} catch (error) {
|
||||
logger.error('Error processing block metadata', { blockId, error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function processTemplateFromDb(
|
||||
templateId: string,
|
||||
tag: string
|
||||
): Promise<AgentContext | null> {
|
||||
try {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: templates.id,
|
||||
name: templates.name,
|
||||
description: templates.description,
|
||||
category: templates.category,
|
||||
author: templates.author,
|
||||
stars: templates.stars,
|
||||
state: templates.state,
|
||||
})
|
||||
.from(templates)
|
||||
.where(eq(templates.id, templateId))
|
||||
.limit(1)
|
||||
const t = rows?.[0]
|
||||
if (!t) return null
|
||||
const workflowState = (t as any).state || {}
|
||||
// Match get-user-workflow format: just the workflow state JSON
|
||||
const summary = {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
description: t.description || '',
|
||||
category: t.category,
|
||||
author: t.author,
|
||||
stars: t.stars || 0,
|
||||
workflow: workflowState,
|
||||
}
|
||||
const content = JSON.stringify(summary)
|
||||
return { type: 'templates', tag, content }
|
||||
} catch (error) {
|
||||
logger.error('Error processing template context (db)', { templateId, error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,14 @@ export const ToolIds = z.enum([
|
||||
'list_gdrive_files',
|
||||
'read_gdrive_file',
|
||||
'reason',
|
||||
// New tools
|
||||
'list_user_workflows',
|
||||
'get_workflow_from_name',
|
||||
// New variable tools
|
||||
'get_global_workflow_variables',
|
||||
'set_global_workflow_variables',
|
||||
// New
|
||||
'oauth_request_access',
|
||||
])
|
||||
export type ToolId = z.infer<typeof ToolIds>
|
||||
|
||||
@@ -45,6 +53,23 @@ const NumberOptional = z.number().optional()
|
||||
// Tool argument schemas (per SSE examples provided)
|
||||
export const ToolArgSchemas = {
|
||||
get_user_workflow: z.object({}),
|
||||
// New tools
|
||||
list_user_workflows: z.object({}),
|
||||
get_workflow_from_name: z.object({ workflow_name: z.string() }),
|
||||
// New variable tools
|
||||
get_global_workflow_variables: z.object({}),
|
||||
set_global_workflow_variables: z.object({
|
||||
operations: z.array(
|
||||
z.object({
|
||||
operation: z.enum(['add', 'delete', 'edit']),
|
||||
name: z.string(),
|
||||
type: z.enum(['plain', 'number', 'boolean', 'array', 'object']).optional(),
|
||||
value: z.string().optional(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
// New
|
||||
oauth_request_access: z.object({}),
|
||||
|
||||
build_workflow: z.object({
|
||||
yamlContent: z.string(),
|
||||
@@ -152,6 +177,21 @@ function toolCallSSEFor<TName extends ToolId, TArgs extends z.ZodTypeAny>(
|
||||
|
||||
export const ToolSSESchemas = {
|
||||
get_user_workflow: toolCallSSEFor('get_user_workflow', ToolArgSchemas.get_user_workflow),
|
||||
// New tools
|
||||
list_user_workflows: toolCallSSEFor('list_user_workflows', ToolArgSchemas.list_user_workflows),
|
||||
get_workflow_from_name: toolCallSSEFor(
|
||||
'get_workflow_from_name',
|
||||
ToolArgSchemas.get_workflow_from_name
|
||||
),
|
||||
// New variable tools
|
||||
get_global_workflow_variables: toolCallSSEFor(
|
||||
'get_global_workflow_variables',
|
||||
ToolArgSchemas.get_global_workflow_variables
|
||||
),
|
||||
set_global_workflow_variables: toolCallSSEFor(
|
||||
'set_global_workflow_variables',
|
||||
ToolArgSchemas.set_global_workflow_variables
|
||||
),
|
||||
build_workflow: toolCallSSEFor('build_workflow', ToolArgSchemas.build_workflow),
|
||||
edit_workflow: toolCallSSEFor('edit_workflow', ToolArgSchemas.edit_workflow),
|
||||
run_workflow: toolCallSSEFor('run_workflow', ToolArgSchemas.run_workflow),
|
||||
@@ -187,11 +227,13 @@ export const ToolSSESchemas = {
|
||||
),
|
||||
gdrive_request_access: toolCallSSEFor(
|
||||
'gdrive_request_access',
|
||||
ToolArgSchemas.gdrive_request_access
|
||||
ToolArgSchemas.gdrive_request_access as any
|
||||
),
|
||||
list_gdrive_files: toolCallSSEFor('list_gdrive_files', ToolArgSchemas.list_gdrive_files),
|
||||
read_gdrive_file: toolCallSSEFor('read_gdrive_file', ToolArgSchemas.read_gdrive_file),
|
||||
reason: toolCallSSEFor('reason', ToolArgSchemas.reason),
|
||||
// New
|
||||
oauth_request_access: toolCallSSEFor('oauth_request_access', ToolArgSchemas.oauth_request_access),
|
||||
} as const
|
||||
export type ToolSSESchemaMap = typeof ToolSSESchemas
|
||||
|
||||
@@ -225,6 +267,25 @@ const ExecutionEntry = z.object({
|
||||
|
||||
export const ToolResultSchemas = {
|
||||
get_user_workflow: z.object({ yamlContent: z.string() }).or(z.string()),
|
||||
// New tools
|
||||
list_user_workflows: z.object({ workflow_names: z.array(z.string()) }),
|
||||
get_workflow_from_name: z
|
||||
.object({ yamlContent: z.string() })
|
||||
.or(z.object({ userWorkflow: z.string() }))
|
||||
.or(z.string()),
|
||||
// New variable tools
|
||||
get_global_workflow_variables: z
|
||||
.object({ variables: z.record(z.any()) })
|
||||
.or(z.array(z.object({ name: z.string(), value: z.any() }))),
|
||||
set_global_workflow_variables: z
|
||||
.object({ variables: z.record(z.any()) })
|
||||
.or(z.object({ message: z.any().optional(), data: z.any().optional() })),
|
||||
// New
|
||||
oauth_request_access: z.object({
|
||||
granted: z.boolean().optional(),
|
||||
message: z.string().optional(),
|
||||
}),
|
||||
|
||||
build_workflow: BuildOrEditWorkflowResult,
|
||||
edit_workflow: BuildOrEditWorkflowResult,
|
||||
run_workflow: z.object({
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { CheckCircle, Loader2, MinusCircle, PlugZap, X, XCircle } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
|
||||
export class OAuthRequestAccessClientTool extends BaseClientTool {
|
||||
static readonly id = 'oauth_request_access'
|
||||
|
||||
private cleanupListener?: () => void
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(toolCallId, OAuthRequestAccessClientTool.id, OAuthRequestAccessClientTool.metadata)
|
||||
}
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: { text: 'Requesting integration access', icon: Loader2 },
|
||||
[ClientToolCallState.pending]: { text: 'Requesting integration access', icon: Loader2 },
|
||||
[ClientToolCallState.executing]: { text: 'Requesting integration access', icon: Loader2 },
|
||||
[ClientToolCallState.rejected]: { text: 'Skipped integration access', icon: MinusCircle },
|
||||
[ClientToolCallState.success]: { text: 'Integration connected', icon: CheckCircle },
|
||||
[ClientToolCallState.error]: { text: 'Failed to request integration access', icon: X },
|
||||
[ClientToolCallState.aborted]: { text: 'Aborted integration access request', icon: XCircle },
|
||||
},
|
||||
interrupt: {
|
||||
accept: { text: 'Connect', icon: PlugZap },
|
||||
reject: { text: 'Skip', icon: MinusCircle },
|
||||
},
|
||||
}
|
||||
|
||||
async handleAccept(): Promise<void> {
|
||||
try {
|
||||
// Move to executing (we're waiting for the user to connect an integration)
|
||||
this.setState(ClientToolCallState.executing)
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// Listen for modal close; complete success on connection, otherwise mark skipped/rejected
|
||||
const onClosed = async (evt: Event) => {
|
||||
try {
|
||||
const detail = (evt as CustomEvent).detail as { success?: boolean }
|
||||
if (detail?.success) {
|
||||
await this.markToolComplete(200, { granted: true })
|
||||
this.setState(ClientToolCallState.success)
|
||||
} else {
|
||||
await this.markToolComplete(200, 'Tool execution was skipped by the user')
|
||||
this.setState(ClientToolCallState.rejected)
|
||||
}
|
||||
} finally {
|
||||
if (this.cleanupListener) this.cleanupListener()
|
||||
this.cleanupListener = undefined
|
||||
}
|
||||
}
|
||||
window.addEventListener(
|
||||
'oauth-integration-closed',
|
||||
onClosed as EventListener,
|
||||
{
|
||||
once: true,
|
||||
} as any
|
||||
)
|
||||
this.cleanupListener = () =>
|
||||
window.removeEventListener('oauth-integration-closed', onClosed as EventListener)
|
||||
|
||||
window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'credentials' } }))
|
||||
}
|
||||
} catch (e) {
|
||||
this.setState(ClientToolCallState.error)
|
||||
await this.markToolComplete(500, 'Failed to open integrations settings')
|
||||
}
|
||||
}
|
||||
|
||||
async handleReject(): Promise<void> {
|
||||
await super.handleReject()
|
||||
this.setState(ClientToolCallState.rejected)
|
||||
if (this.cleanupListener) this.cleanupListener()
|
||||
this.cleanupListener = undefined
|
||||
}
|
||||
|
||||
async completeAfterConnection(): Promise<void> {
|
||||
await this.markToolComplete(200, { granted: true })
|
||||
this.setState(ClientToolCallState.success)
|
||||
if (this.cleanupListener) this.cleanupListener()
|
||||
this.cleanupListener = undefined
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
await this.handleAccept()
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,10 @@ export class SetEnvironmentVariablesClientTool extends BaseClientTool {
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: {
|
||||
text: 'Setting environment variables',
|
||||
text: 'Preparing to set environment variables',
|
||||
icon: Loader2,
|
||||
},
|
||||
[ClientToolCallState.pending]: { text: 'Setting environment variables', icon: Loader2 },
|
||||
[ClientToolCallState.pending]: { text: 'Set environment variables?', icon: Settings2 },
|
||||
[ClientToolCallState.executing]: { text: 'Setting environment variables', icon: Loader2 },
|
||||
[ClientToolCallState.success]: { text: 'Set environment variables', icon: Settings2 },
|
||||
[ClientToolCallState.error]: { text: 'Failed to set environment variables', icon: X },
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { List, Loader2, X, XCircle } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('GetGlobalWorkflowVariablesClientTool')
|
||||
|
||||
export class GetGlobalWorkflowVariablesClientTool extends BaseClientTool {
|
||||
static readonly id = 'get_global_workflow_variables'
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(
|
||||
toolCallId,
|
||||
GetGlobalWorkflowVariablesClientTool.id,
|
||||
GetGlobalWorkflowVariablesClientTool.metadata
|
||||
)
|
||||
}
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: { text: 'Fetching workflow variables', icon: Loader2 },
|
||||
[ClientToolCallState.pending]: { text: 'Fetching workflow variables', icon: List },
|
||||
[ClientToolCallState.executing]: { text: 'Fetching workflow variables', icon: Loader2 },
|
||||
[ClientToolCallState.aborted]: { text: 'Aborted fetching variables', icon: XCircle },
|
||||
[ClientToolCallState.success]: { text: 'Workflow variables retrieved', icon: List },
|
||||
[ClientToolCallState.error]: { text: 'Failed to fetch variables', icon: X },
|
||||
[ClientToolCallState.rejected]: { text: 'Skipped fetching variables', icon: XCircle },
|
||||
},
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
try {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
const { activeWorkflowId } = useWorkflowRegistry.getState()
|
||||
if (!activeWorkflowId) {
|
||||
await this.markToolComplete(400, 'No active workflow found')
|
||||
this.setState(ClientToolCallState.error)
|
||||
return
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/workflows/${activeWorkflowId}/variables`, { method: 'GET' })
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '')
|
||||
await this.markToolComplete(res.status, text || 'Failed to fetch workflow variables')
|
||||
this.setState(ClientToolCallState.error)
|
||||
return
|
||||
}
|
||||
const json = await res.json()
|
||||
const varsRecord = (json?.data as Record<string, any>) || {}
|
||||
// Convert to name/value pairs for clarity
|
||||
const variables = Object.values(varsRecord).map((v: any) => ({
|
||||
name: String(v?.name || ''),
|
||||
value: (v as any)?.value,
|
||||
}))
|
||||
logger.info('Fetched workflow variables', { count: variables.length })
|
||||
await this.markToolComplete(200, `Found ${variables.length} variable(s)`, { variables })
|
||||
this.setState(ClientToolCallState.success)
|
||||
} catch (error: any) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
await this.markToolComplete(500, message || 'Failed to fetch workflow variables')
|
||||
this.setState(ClientToolCallState.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { FileText, Loader2, X, XCircle } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('GetWorkflowFromNameClientTool')
|
||||
|
||||
interface GetWorkflowFromNameArgs {
|
||||
workflow_name: string
|
||||
}
|
||||
|
||||
export class GetWorkflowFromNameClientTool extends BaseClientTool {
|
||||
static readonly id = 'get_workflow_from_name'
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(toolCallId, GetWorkflowFromNameClientTool.id, GetWorkflowFromNameClientTool.metadata)
|
||||
}
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: { text: 'Retrieving workflow', icon: Loader2 },
|
||||
[ClientToolCallState.pending]: { text: 'Retrieving workflow', icon: FileText },
|
||||
[ClientToolCallState.executing]: { text: 'Retrieving workflow', icon: Loader2 },
|
||||
[ClientToolCallState.aborted]: { text: 'Aborted retrieving workflow', icon: XCircle },
|
||||
[ClientToolCallState.success]: { text: 'Retrieved workflow', icon: FileText },
|
||||
[ClientToolCallState.error]: { text: 'Failed to retrieve workflow', icon: X },
|
||||
[ClientToolCallState.rejected]: { text: 'Skipped retrieving workflow', icon: XCircle },
|
||||
},
|
||||
}
|
||||
|
||||
async execute(args?: GetWorkflowFromNameArgs): Promise<void> {
|
||||
try {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
|
||||
const workflowName = args?.workflow_name?.trim()
|
||||
if (!workflowName) {
|
||||
await this.markToolComplete(400, 'workflow_name is required')
|
||||
this.setState(ClientToolCallState.error)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to find by name from registry first to get ID
|
||||
const registry = useWorkflowRegistry.getState()
|
||||
const match = Object.values((registry as any).workflows || {}).find(
|
||||
(w: any) =>
|
||||
String(w?.name || '')
|
||||
.trim()
|
||||
.toLowerCase() === workflowName.toLowerCase()
|
||||
) as any
|
||||
|
||||
if (!match?.id) {
|
||||
await this.markToolComplete(404, `Workflow not found: ${workflowName}`)
|
||||
this.setState(ClientToolCallState.error)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch full workflow from API route (normalized tables)
|
||||
const res = await fetch(`/api/workflows/${encodeURIComponent(match.id)}`, { method: 'GET' })
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '')
|
||||
await this.markToolComplete(res.status, text || 'Failed to fetch workflow by name')
|
||||
this.setState(ClientToolCallState.error)
|
||||
return
|
||||
}
|
||||
|
||||
const json = await res.json()
|
||||
const wf = json?.data
|
||||
if (!wf?.state?.blocks) {
|
||||
await this.markToolComplete(422, 'Workflow state is empty or invalid')
|
||||
this.setState(ClientToolCallState.error)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert state to the same string format as get_user_workflow
|
||||
const workflowState = {
|
||||
blocks: wf.state.blocks || {},
|
||||
edges: wf.state.edges || [],
|
||||
loops: wf.state.loops || {},
|
||||
parallels: wf.state.parallels || {},
|
||||
}
|
||||
const userWorkflow = JSON.stringify(workflowState, null, 2)
|
||||
|
||||
await this.markToolComplete(200, `Retrieved workflow ${workflowName}`, { userWorkflow })
|
||||
this.setState(ClientToolCallState.success)
|
||||
} catch (error: any) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
await this.markToolComplete(500, message || 'Failed to retrieve workflow by name')
|
||||
this.setState(ClientToolCallState.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ListChecks, Loader2, X, XCircle } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('ListUserWorkflowsClientTool')
|
||||
|
||||
export class ListUserWorkflowsClientTool extends BaseClientTool {
|
||||
static readonly id = 'list_user_workflows'
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(toolCallId, ListUserWorkflowsClientTool.id, ListUserWorkflowsClientTool.metadata)
|
||||
}
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: { text: 'Listing your workflows', icon: Loader2 },
|
||||
[ClientToolCallState.pending]: { text: 'Listing your workflows', icon: ListChecks },
|
||||
[ClientToolCallState.executing]: { text: 'Listing your workflows', icon: Loader2 },
|
||||
[ClientToolCallState.aborted]: { text: 'Aborted listing workflows', icon: XCircle },
|
||||
[ClientToolCallState.success]: { text: 'Listed your workflows', icon: ListChecks },
|
||||
[ClientToolCallState.error]: { text: 'Failed to list workflows', icon: X },
|
||||
[ClientToolCallState.rejected]: { text: 'Skipped listing workflows', icon: XCircle },
|
||||
},
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
try {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
|
||||
const res = await fetch('/api/workflows/sync', { method: 'GET' })
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '')
|
||||
await this.markToolComplete(res.status, text || 'Failed to fetch workflows')
|
||||
this.setState(ClientToolCallState.error)
|
||||
return
|
||||
}
|
||||
|
||||
const json = await res.json()
|
||||
const workflows = Array.isArray(json?.data) ? json.data : []
|
||||
const names = workflows
|
||||
.map((w: any) => (typeof w?.name === 'string' ? w.name : null))
|
||||
.filter((n: string | null) => !!n)
|
||||
|
||||
logger.info('Found workflows', { count: names.length })
|
||||
|
||||
await this.markToolComplete(200, `Found ${names.length} workflow(s)`, {
|
||||
workflow_names: names,
|
||||
})
|
||||
this.setState(ClientToolCallState.success)
|
||||
} catch (error: any) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
await this.markToolComplete(500, message || 'Failed to list workflows')
|
||||
this.setState(ClientToolCallState.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export class RunWorkflowClientTool extends BaseClientTool {
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: { text: 'Preparing to run your workflow', icon: Play },
|
||||
[ClientToolCallState.generating]: { text: 'Preparing to run your workflow', icon: Loader2 },
|
||||
[ClientToolCallState.pending]: { text: 'Run this workflow?', icon: Play },
|
||||
[ClientToolCallState.executing]: { text: 'Running your workflow', icon: Loader2 },
|
||||
[ClientToolCallState.success]: { text: 'Workflow executed', icon: Play },
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Loader2, Settings2, X, XCircle } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
interface OperationItem {
|
||||
operation: 'add' | 'edit' | 'delete'
|
||||
name: string
|
||||
type?: 'plain' | 'number' | 'boolean' | 'array' | 'object'
|
||||
value?: string
|
||||
}
|
||||
|
||||
interface SetGlobalVarsArgs {
|
||||
operations: OperationItem[]
|
||||
workflowId?: string
|
||||
}
|
||||
|
||||
export class SetGlobalWorkflowVariablesClientTool extends BaseClientTool {
|
||||
static readonly id = 'set_global_workflow_variables'
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(
|
||||
toolCallId,
|
||||
SetGlobalWorkflowVariablesClientTool.id,
|
||||
SetGlobalWorkflowVariablesClientTool.metadata
|
||||
)
|
||||
}
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: {
|
||||
text: 'Preparing to set workflow variables',
|
||||
icon: Loader2,
|
||||
},
|
||||
[ClientToolCallState.pending]: { text: 'Set workflow variables?', icon: Settings2 },
|
||||
[ClientToolCallState.executing]: { text: 'Setting workflow variables', icon: Loader2 },
|
||||
[ClientToolCallState.success]: { text: 'Workflow variables updated', icon: Settings2 },
|
||||
[ClientToolCallState.error]: { text: 'Failed to set workflow variables', icon: X },
|
||||
[ClientToolCallState.aborted]: { text: 'Aborted setting variables', icon: XCircle },
|
||||
[ClientToolCallState.rejected]: { text: 'Skipped setting variables', icon: XCircle },
|
||||
},
|
||||
interrupt: {
|
||||
accept: { text: 'Apply', icon: Settings2 },
|
||||
reject: { text: 'Skip', icon: XCircle },
|
||||
},
|
||||
}
|
||||
|
||||
async handleReject(): Promise<void> {
|
||||
await super.handleReject()
|
||||
this.setState(ClientToolCallState.rejected)
|
||||
}
|
||||
|
||||
async handleAccept(args?: SetGlobalVarsArgs): Promise<void> {
|
||||
const logger = createLogger('SetGlobalWorkflowVariablesClientTool')
|
||||
try {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
const payload: SetGlobalVarsArgs = { ...(args || { operations: [] }) }
|
||||
if (!payload.workflowId) {
|
||||
const { activeWorkflowId } = useWorkflowRegistry.getState()
|
||||
if (activeWorkflowId) payload.workflowId = activeWorkflowId
|
||||
}
|
||||
if (!payload.workflowId) {
|
||||
throw new Error('No active workflow found')
|
||||
}
|
||||
|
||||
// Fetch current variables so we can construct full array payload
|
||||
const getRes = await fetch(`/api/workflows/${payload.workflowId}/variables`, {
|
||||
method: 'GET',
|
||||
})
|
||||
if (!getRes.ok) {
|
||||
const txt = await getRes.text().catch(() => '')
|
||||
throw new Error(txt || 'Failed to load current variables')
|
||||
}
|
||||
const currentJson = await getRes.json()
|
||||
const currentVarsRecord = (currentJson?.data as Record<string, any>) || {}
|
||||
|
||||
// Helper to convert string -> typed value
|
||||
function coerceValue(
|
||||
value: string | undefined,
|
||||
type?: 'plain' | 'number' | 'boolean' | 'array' | 'object'
|
||||
) {
|
||||
if (value === undefined) return value
|
||||
const t = type || 'plain'
|
||||
try {
|
||||
if (t === 'number') {
|
||||
const n = Number(value)
|
||||
if (Number.isNaN(n)) return value
|
||||
return n
|
||||
}
|
||||
if (t === 'boolean') {
|
||||
const v = String(value).trim().toLowerCase()
|
||||
if (v === 'true') return true
|
||||
if (v === 'false') return false
|
||||
return value
|
||||
}
|
||||
if (t === 'array' || t === 'object') {
|
||||
const parsed = JSON.parse(value)
|
||||
if (t === 'array' && Array.isArray(parsed)) return parsed
|
||||
if (t === 'object' && parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
||||
return parsed
|
||||
return value
|
||||
}
|
||||
} catch {}
|
||||
return value
|
||||
}
|
||||
|
||||
// Build mutable map by variable name
|
||||
const byName: Record<string, any> = {}
|
||||
Object.values(currentVarsRecord).forEach((v: any) => {
|
||||
if (v && typeof v === 'object' && v.id && v.name) byName[String(v.name)] = v
|
||||
})
|
||||
|
||||
// Apply operations in order
|
||||
for (const op of payload.operations || []) {
|
||||
const key = String(op.name)
|
||||
const nextType = (op.type as any) || byName[key]?.type || 'plain'
|
||||
if (op.operation === 'delete') {
|
||||
delete byName[key]
|
||||
continue
|
||||
}
|
||||
const typedValue = coerceValue(op.value, nextType)
|
||||
if (op.operation === 'add') {
|
||||
byName[key] = {
|
||||
id: crypto.randomUUID(),
|
||||
workflowId: payload.workflowId,
|
||||
name: key,
|
||||
type: nextType,
|
||||
value: typedValue,
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (op.operation === 'edit') {
|
||||
if (!byName[key]) {
|
||||
// If editing a non-existent variable, create it
|
||||
byName[key] = {
|
||||
id: crypto.randomUUID(),
|
||||
workflowId: payload.workflowId,
|
||||
name: key,
|
||||
type: nextType,
|
||||
value: typedValue,
|
||||
}
|
||||
} else {
|
||||
byName[key] = {
|
||||
...byName[key],
|
||||
type: nextType,
|
||||
...(op.value !== undefined ? { value: typedValue } : {}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const variablesArray = Object.values(byName)
|
||||
|
||||
// POST full variables array to persist
|
||||
const res = await fetch(`/api/workflows/${payload.workflowId}/variables`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ variables: variablesArray }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => '')
|
||||
throw new Error(txt || `Failed to update variables (${res.status})`)
|
||||
}
|
||||
|
||||
try {
|
||||
const { activeWorkflowId } = useWorkflowRegistry.getState()
|
||||
if (activeWorkflowId) {
|
||||
// Fetch the updated variables from the API
|
||||
const refreshRes = await fetch(`/api/workflows/${activeWorkflowId}/variables`, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (refreshRes.ok) {
|
||||
const refreshJson = await refreshRes.json()
|
||||
const updatedVarsRecord = (refreshJson?.data as Record<string, any>) || {}
|
||||
|
||||
// Update the variables store with the fresh data
|
||||
useVariablesStore.setState((state) => {
|
||||
// Remove old variables for this workflow
|
||||
const withoutWorkflow = Object.fromEntries(
|
||||
Object.entries(state.variables).filter(([, v]) => v.workflowId !== activeWorkflowId)
|
||||
)
|
||||
// Add the updated variables
|
||||
return {
|
||||
variables: { ...withoutWorkflow, ...updatedVarsRecord },
|
||||
}
|
||||
})
|
||||
|
||||
logger.info('Refreshed variables in store', { workflowId: activeWorkflowId })
|
||||
}
|
||||
}
|
||||
} catch (refreshError) {
|
||||
logger.warn('Failed to refresh variables in store', { error: refreshError })
|
||||
}
|
||||
|
||||
await this.markToolComplete(200, 'Workflow variables updated', { variables: byName })
|
||||
this.setState(ClientToolCallState.success)
|
||||
} catch (e: any) {
|
||||
const message = e instanceof Error ? e.message : String(e)
|
||||
this.setState(ClientToolCallState.error)
|
||||
await this.markToolComplete(500, message || 'Failed to set workflow variables')
|
||||
}
|
||||
}
|
||||
|
||||
async execute(args?: SetGlobalVarsArgs): Promise<void> {
|
||||
await this.handleAccept(args)
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export async function generateChatTitle(message: string): Promise<string | null>
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'Generate a very short title (3-5 words max) for a chat that starts with this message. The title should be concise and descriptive.',
|
||||
'Generate a very short title (3-5 words max) for a chat that starts with this message. The title should be concise and descriptive. Do not wrap the title in quotes.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { CheckoffTodoClientTool } from '@/lib/copilot/tools/client/other/checkoff-todo'
|
||||
import { MakeApiRequestClientTool } from '@/lib/copilot/tools/client/other/make-api-request'
|
||||
import { MarkTodoInProgressClientTool } from '@/lib/copilot/tools/client/other/mark-todo-in-progress'
|
||||
import { OAuthRequestAccessClientTool } from '@/lib/copilot/tools/client/other/oauth-request-access'
|
||||
import { PlanClientTool } from '@/lib/copilot/tools/client/other/plan'
|
||||
import { SearchDocumentationClientTool } from '@/lib/copilot/tools/client/other/search-documentation'
|
||||
import { SearchOnlineClientTool } from '@/lib/copilot/tools/client/other/search-online'
|
||||
@@ -30,11 +31,16 @@ import { GetOAuthCredentialsClientTool } from '@/lib/copilot/tools/client/user/g
|
||||
import { SetEnvironmentVariablesClientTool } from '@/lib/copilot/tools/client/user/set-environment-variables'
|
||||
import { BuildWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/build-workflow'
|
||||
import { EditWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/edit-workflow'
|
||||
import { GetGlobalWorkflowVariablesClientTool } from '@/lib/copilot/tools/client/workflow/get-global-workflow-variables'
|
||||
import { GetUserWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/get-user-workflow'
|
||||
import { GetWorkflowConsoleClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-console'
|
||||
import { GetWorkflowFromNameClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-from-name'
|
||||
import { ListUserWorkflowsClientTool } from '@/lib/copilot/tools/client/workflow/list-user-workflows'
|
||||
import { RunWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/run-workflow'
|
||||
import { SetGlobalWorkflowVariablesClientTool } from '@/lib/copilot/tools/client/workflow/set-global-workflow-variables'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
ChatContext,
|
||||
CopilotMessage,
|
||||
CopilotStore,
|
||||
CopilotToolCall,
|
||||
@@ -72,9 +78,14 @@ const CLIENT_TOOL_INSTANTIATORS: Record<string, (id: string) => any> = {
|
||||
checkoff_todo: (id) => new CheckoffTodoClientTool(id),
|
||||
mark_todo_in_progress: (id) => new MarkTodoInProgressClientTool(id),
|
||||
gdrive_request_access: (id) => new GDriveRequestAccessClientTool(id),
|
||||
oauth_request_access: (id) => new OAuthRequestAccessClientTool(id),
|
||||
edit_workflow: (id) => new EditWorkflowClientTool(id),
|
||||
build_workflow: (id) => new BuildWorkflowClientTool(id),
|
||||
get_user_workflow: (id) => new GetUserWorkflowClientTool(id),
|
||||
list_user_workflows: (id) => new ListUserWorkflowsClientTool(id),
|
||||
get_workflow_from_name: (id) => new GetWorkflowFromNameClientTool(id),
|
||||
get_global_workflow_variables: (id) => new GetGlobalWorkflowVariablesClientTool(id),
|
||||
set_global_workflow_variables: (id) => new SetGlobalWorkflowVariablesClientTool(id),
|
||||
}
|
||||
|
||||
// Read-only static metadata for class-based tools (no instances)
|
||||
@@ -98,6 +109,11 @@ export const CLASS_TOOL_METADATA: Record<string, BaseClientToolMetadata | undefi
|
||||
edit_workflow: (EditWorkflowClientTool as any)?.metadata,
|
||||
build_workflow: (BuildWorkflowClientTool as any)?.metadata,
|
||||
get_user_workflow: (GetUserWorkflowClientTool as any)?.metadata,
|
||||
list_user_workflows: (ListUserWorkflowsClientTool as any)?.metadata,
|
||||
get_workflow_from_name: (GetWorkflowFromNameClientTool as any)?.metadata,
|
||||
get_global_workflow_variables: (GetGlobalWorkflowVariablesClientTool as any)?.metadata,
|
||||
set_global_workflow_variables: (SetGlobalWorkflowVariablesClientTool as any)?.metadata,
|
||||
oauth_request_access: (OAuthRequestAccessClientTool as any)?.metadata,
|
||||
}
|
||||
|
||||
function ensureClientToolInstance(toolName: string | undefined, toolCallId: string | undefined) {
|
||||
@@ -250,7 +266,19 @@ function abortAllInProgressTools(set: any, get: () => CopilotStore) {
|
||||
function normalizeMessagesForUI(messages: CopilotMessage[]): CopilotMessage[] {
|
||||
try {
|
||||
return messages.map((message) => {
|
||||
if (message.role !== 'assistant') return message
|
||||
if (message.role !== 'assistant') {
|
||||
// For user messages (and others), restore contexts from a saved contexts block
|
||||
if (Array.isArray(message.contentBlocks) && message.contentBlocks.length > 0) {
|
||||
const ctxBlock = (message.contentBlocks as any[]).find((b: any) => b?.type === 'contexts')
|
||||
if (ctxBlock && Array.isArray((ctxBlock as any).contexts)) {
|
||||
return {
|
||||
...message,
|
||||
contexts: (ctxBlock as any).contexts,
|
||||
}
|
||||
}
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// Use existing contentBlocks ordering if present; otherwise only render text content
|
||||
const blocks: any[] = Array.isArray(message.contentBlocks)
|
||||
@@ -393,7 +421,8 @@ class StringBuilder {
|
||||
// Helpers
|
||||
function createUserMessage(
|
||||
content: string,
|
||||
fileAttachments?: MessageFileAttachment[]
|
||||
fileAttachments?: MessageFileAttachment[],
|
||||
contexts?: ChatContext[]
|
||||
): CopilotMessage {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
@@ -401,6 +430,13 @@ function createUserMessage(
|
||||
content,
|
||||
timestamp: new Date().toISOString(),
|
||||
...(fileAttachments && fileAttachments.length > 0 && { fileAttachments }),
|
||||
...(contexts && contexts.length > 0 && { contexts }),
|
||||
...(contexts &&
|
||||
contexts.length > 0 && {
|
||||
contentBlocks: [
|
||||
{ type: 'contexts', contexts: contexts as any, timestamp: Date.now() },
|
||||
] as any,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,6 +501,10 @@ function validateMessagesForLLM(messages: CopilotMessage[]): any[] {
|
||||
msg.fileAttachments.length > 0 && {
|
||||
fileAttachments: msg.fileAttachments,
|
||||
}),
|
||||
...((msg as any).contexts &&
|
||||
Array.isArray((msg as any).contexts) && {
|
||||
contexts: (msg as any).contexts,
|
||||
}),
|
||||
}
|
||||
})
|
||||
.filter((m) => {
|
||||
@@ -1439,16 +1479,21 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
// Send a message (streaming only)
|
||||
sendMessage: async (message: string, options = {}) => {
|
||||
const { workflowId, currentChat, mode, revertState } = get()
|
||||
const { stream = true, fileAttachments } = options as {
|
||||
const {
|
||||
stream = true,
|
||||
fileAttachments,
|
||||
contexts,
|
||||
} = options as {
|
||||
stream?: boolean
|
||||
fileAttachments?: MessageFileAttachment[]
|
||||
contexts?: ChatContext[]
|
||||
}
|
||||
if (!workflowId) return
|
||||
|
||||
const abortController = new AbortController()
|
||||
set({ isSendingMessage: true, error: null, abortController })
|
||||
|
||||
const userMessage = createUserMessage(message, fileAttachments)
|
||||
const userMessage = createUserMessage(message, fileAttachments, contexts)
|
||||
const streamingMessage = createStreamingMessage()
|
||||
|
||||
let newMessages: CopilotMessage[]
|
||||
@@ -1473,6 +1518,22 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
}
|
||||
|
||||
try {
|
||||
// Debug: log contexts presence before sending
|
||||
try {
|
||||
logger.info('sendMessage: preparing request', {
|
||||
hasContexts: Array.isArray(contexts),
|
||||
contextsCount: Array.isArray(contexts) ? contexts.length : 0,
|
||||
contextsPreview: Array.isArray(contexts)
|
||||
? contexts.map((c: any) => ({
|
||||
kind: c?.kind,
|
||||
chatId: (c as any)?.chatId,
|
||||
workflowId: (c as any)?.workflowId,
|
||||
label: (c as any)?.label,
|
||||
}))
|
||||
: undefined,
|
||||
})
|
||||
} catch {}
|
||||
|
||||
const result = await sendStreamingMessage({
|
||||
message,
|
||||
userMessageId: userMessage.id,
|
||||
@@ -1484,6 +1545,7 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
createNewChat: !currentChat,
|
||||
stream,
|
||||
fileAttachments,
|
||||
contexts,
|
||||
abortSignal: abortController.signal,
|
||||
})
|
||||
|
||||
|
||||
@@ -35,10 +35,21 @@ export interface CopilotMessage {
|
||||
startTime?: number
|
||||
}
|
||||
| { type: 'tool_call'; toolCall: CopilotToolCall; timestamp: number }
|
||||
| { type: 'contexts'; contexts: ChatContext[]; timestamp: number }
|
||||
>
|
||||
fileAttachments?: MessageFileAttachment[]
|
||||
contexts?: ChatContext[]
|
||||
}
|
||||
|
||||
// Contexts attached to a user message
|
||||
export type ChatContext =
|
||||
| { kind: 'past_chat'; chatId: string; label: string }
|
||||
| { kind: 'workflow'; workflowId: string; label: string }
|
||||
| { kind: 'blocks'; blockIds: string[]; label: string }
|
||||
| { kind: 'logs'; label: string }
|
||||
| { kind: 'knowledge'; knowledgeId?: string; label: string }
|
||||
| { kind: 'templates'; templateId?: string; label: string }
|
||||
|
||||
export interface CopilotChat {
|
||||
id: string
|
||||
title: string | null
|
||||
@@ -107,7 +118,11 @@ export interface CopilotActions {
|
||||
|
||||
sendMessage: (
|
||||
message: string,
|
||||
options?: { stream?: boolean; fileAttachments?: MessageFileAttachment[] }
|
||||
options?: {
|
||||
stream?: boolean
|
||||
fileAttachments?: MessageFileAttachment[]
|
||||
contexts?: ChatContext[]
|
||||
}
|
||||
) => Promise<void>
|
||||
abortMessage: () => void
|
||||
sendImplicitFeedback: (
|
||||
|
||||
Reference in New Issue
Block a user