import type { UIMessage as MessageType } from '@ai-sdk/react' import { ArrowUpRight } from 'lucide-react' import dynamic from 'next/dynamic' import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react' import type { JSX } from 'react' import type { StreamdownProps } from 'streamdown' import { AiIconAnimation, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, Skeleton, } from 'ui' import { buildSupportAssistantPrompt } from '@/components/interfaces/Support/SupportAssistant.utils' import type { SubmittedSupportRequest } from '@/components/interfaces/Support/SupportForm.state' import { NO_PROJECT_MARKER } from '@/components/interfaces/Support/SupportForm.utils' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { useTrack } from '@/lib/telemetry/track' import { useAiAssistantState, useAiAssistantStateSnapshot, type AiAssistantState, } from '@/state/ai-assistant-state' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' type SupportAssistantPreviewChat = AiAssistantState['chatInstances'][string] const EMPTY_MESSAGES: MessageType[] = [] const Streamdown = dynamic( () => import('streamdown').then((mod) => mod.Streamdown), { ssr: false } ) interface SupportAssistantSuccessCardContentProps { request: SubmittedSupportRequest className?: string } function hasProjectScopedAssistantContext(projectRef: string | undefined) { return projectRef !== undefined && projectRef !== NO_PROJECT_MARKER } export function SupportAssistantSuccessCardContent({ request, className, }: SupportAssistantSuccessCardContentProps) { const hasAssistantContext = hasProjectScopedAssistantContext(request.projectRef) const aiAssistant = useAiAssistantStateSnapshot() const aiAssistantState = useAiAssistantState() const { openSidebar } = useSidebarManagerSnapshot() const track = useTrack() const createdChatIdRef = useRef(null) const [chatId, setChatId] = useState() const chat = chatId ? aiAssistant.chatInstances[chatId] : undefined const assistantPrompt = useMemo(() => buildSupportAssistantPrompt(request), [request]) useEffect(() => { if (!hasAssistantContext) return if (createdChatIdRef.current) return const newChatId = aiAssistant.newChat({ name: 'Support request', initialMessage: assistantPrompt, }) createdChatIdRef.current = newChatId setChatId(newChatId) }, [aiAssistant, assistantPrompt, hasAssistantContext]) const handleOpenAssistant = () => { track( 'support_assistant_follow_up_card_clicked', { ticketCategory: request.category }, { project: request.projectRef, organization: request.organizationSlug, } ) if (chatId) { // Tag the chat as a support chat on first engagement so its messages and // lifecycle sync to Front. Gated on the click (rather than on chat creation) // so chats the user never opens don't create Front conversations. const chat = aiAssistantState.chats[chatId] if (chat && !chat.supportMetadata) { chat.supportMetadata = { subject: request.subject, category: request.category, severity: request.severity, organizationSlug: request.organizationSlug, projectRef: request.projectRef, library: request.library, affectedServices: request.affectedServices, allowSupportAccess: request.allowSupportAccess, // Reuse the Front conversation created at submit so AI messages thread into it. frontConversationId: request.frontConversationId, threadRef: request.threadRef, isSupportChat: true, lifecycleStatus: 'bot_active', lastSyncedMessageCount: 0, isSyncing: false, isLifecycleSyncing: false, } // Flush any messages produced before the user engaged (the initial prompt // and any assistant reply). Subsequent turns sync via the onFinish hook. void import('@/state/ai-chat-front-sync') .then(({ syncSupportChatToFront }) => syncSupportChatToFront(chatId, aiAssistantState)) .catch(() => {}) } aiAssistantState.selectChat(chatId) } openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) } if (!hasAssistantContext) return null return ( { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault() handleOpenAssistant() } }} className={cn( 'group cursor-pointer bg-muted/50 transition-colors hover:bg-muted/50 focus-ring', className )} >
While you wait Assistant may be able to help
{chat ? ( ) : ( )}
) } function useChatMessages(chat: SupportAssistantPreviewChat | undefined) { const subscribe = useCallback( (onStoreChange: () => void) => { return chat?.['~registerMessagesCallback']?.(onStoreChange) ?? (() => {}) }, [chat] ) const getSnapshot = useCallback(() => chat?.messages ?? EMPTY_MESSAGES, [chat]) return useSyncExternalStore(subscribe, getSnapshot, getSnapshot) } function getAssistantMessageText(message: MessageType) { return ( message.parts ?.filter((part) => part.type === 'text') .map((part) => part.text) .join('') ?? '' ) } function SupportAssistantResponsePreview({ chat }: { chat: SupportAssistantPreviewChat }) { const messages = useChatMessages(chat) const latestAssistantMessage = [...messages] .reverse() .find((message) => message.role === 'assistant') if (!latestAssistantMessage) { return ( ) } const previewText = getAssistantMessageText(latestAssistantMessage) return ( {previewText} ) } function SupportAssistantPreviewMarkdown({ children }: { children: string }) { return ( {children} ) } function SupportAssistantPreviewImage({ src }: JSX.IntrinsicElements['img']) { return [Image: {src?.toString()}] } const supportAssistantPreviewMarkdownComponents: StreamdownProps['components'] = { img: SupportAssistantPreviewImage, } function SupportAssistantResponseLoadingSkeleton() { return (
) }