/** * * Copyright 2023-present InspectorRAGet Team * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **/ import cx from 'classnames'; import Balancer from 'react-wrap-balancer'; import { useState, useEffect, useRef } from 'react'; import { Modal, Button, DefinitionTooltip } from '@carbon/react'; import { CheckmarkFilled, WarningFilled, ErrorFilled, Restart, } from '@carbon/icons-react'; import { ToolCallCard, ToolResponseCard, } from '@/src/components/tools/ToolCards'; import { Message, MessageRetry, ToolMessage, AssistantMessage, } from '@/src/types'; import Avatar from '@/src/components/chat/Avatar'; import DocumentsViewer from '@/src/components/chat/DocumentsViewer'; import classes from './ChatLine.module.scss'; // =================================================================================== // TYPES // =================================================================================== interface ChatLineProps { messageId: string; message: Message; latestResponse?: boolean; onSelection?: Function; focused?: boolean; selected?: boolean; } // =================================================================================== // RENDER FUNCTIONS // =================================================================================== const STATUS_CONFIG = { pass: { icon: CheckmarkFilled, label: 'Pass', badgeClass: classes.statusBadgePass, }, warn: { icon: WarningFilled, label: 'Warn', badgeClass: classes.statusBadgeWarn, }, fail: { icon: ErrorFilled, label: 'Fail', badgeClass: classes.statusBadgeFail, }, } as const; function RetriesModal({ retries, open, onClose, }: { retries: MessageRetry[]; open: boolean; onClose: () => void; }) { return (
{retries.map((retry, idx) => (
Attempt {idx + 1} {retry.error && (
Error {retry.error}
)} {retry.content &&

{retry.content}

} {retry.tool_calls && retry.tool_calls.map((call, callIdx) => ( ))}
))}
); } function BalloonFooter({ status, statusDefinition, retries, }: { status?: string; statusDefinition?: string; retries?: MessageRetry[]; }) { const [retriesOpen, setRetriesOpen] = useState(false); const hasStatus = status && status in STATUS_CONFIG; const hasRetries = retries && retries.length > 0; if (!hasStatus && !hasRetries) return null; const config = hasStatus ? STATUS_CONFIG[status as keyof typeof STATUS_CONFIG] : null; return ( <>
{config && (statusDefinition ? ( {config.label} ) : ( {config.label} ))} {hasRetries && ( )}
{hasRetries && ( setRetriesOpen(false)} /> )} ); } function ToolResponseContent({ messageId, message, onSelection, }: { messageId: string; message: ToolMessage; onSelection?: Function; }) { const [documentIndex, setDocumentIndex] = useState(0); if (message.type === 'documents' && Array.isArray(message.content)) { return (
); } const contentStr = message.type === 'json' || typeof message.content !== 'string' ? JSON.stringify(message.content, null, 2) : (message.content as string); return (
); } function AssistantResponse({ messageId, message, onSelection, }: { messageId: string; message: AssistantMessage; onSelection?: Function; }) { return (
{message.content ? ( { if (onSelection) { onSelection( `messages[${messageId.split('--').slice(-1)[0]}].content`, ); } }} onMouseUp={() => { if (onSelection) { onSelection( `messages[${messageId.split('--').slice(-1)[0]}].content`, ); } }} > {message.content.split('\n').map((line, i) => ( {line}
))}
) : null} {message.tool_calls ? message.tool_calls.map((tool, toolIdx) => { // Collapse if there are more than 2 calls, or if the single call's args are long const defaultExpanded = message.tool_calls!.length <= 2 && JSON.stringify(tool.arguments).length <= 120; return (
); }) : null}
); } // =================================================================================== // MAIN FUNCTIONS // =================================================================================== export default function ChatLine({ messageId, message, latestResponse, onSelection, focused, selected, }: ChatLineProps) { const anchorRef = useRef(null); useEffect(() => { if (anchorRef.current && focused) { anchorRef.current.scrollIntoView({ behavior: 'smooth', block: message.role === 'user' ? 'start' : 'center', inline: 'center', }); } }, [focused, message.role]); if (!message) { return null; } const status = message.metadata?.status as string | undefined; const statusDefinition = message.metadata?.statusDefinition as | string | undefined; const retries = message.retries; return (
{message.role === 'system' || message.role === 'developer' || message.role === 'user' ? ( { if (onSelection) { onSelection( `messages[${messageId.split('--').slice(-1)[0]}].text`, ); } }} onMouseUp={() => { if (onSelection) { onSelection( `messages[${messageId.split('--').slice(-1)[0]}].text`, ); } }} > {message.content.split('\n').map((line, i) => ( {line}
))}
) : message.role === 'tool' ? ( ) : ( )}
); }