Spaces:
Running
Running
| /** | |
| * | |
| * Copyright 2023-2025 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 { CodeSnippet } from '@carbon/react'; | |
| import { Message, ToolCall, ToolMessage, AssistantMessage } from '@/src/types'; | |
| import Avatar from '@/src/components/avatar/Avatar'; | |
| import DocumentsViewer from '@/src/components/documents-viewer/DocumentsViewer'; | |
| import classes from './ChatLine.module.scss'; | |
| // =================================================================================== | |
| // TYPES | |
| // =================================================================================== | |
| interface ChatLineProps { | |
| messageId: string; | |
| message: Message; | |
| latestResponse?: boolean; | |
| onSelection?: Function; | |
| focused?: boolean; | |
| } | |
| // =================================================================================== | |
| // RENDER FUNCTIONS | |
| // =================================================================================== | |
| function Tool({ tool }: { tool: ToolCall }) { | |
| return ( | |
| <div className={cx(classes.message, classes.toolCall)}> | |
| <span> | |
| Tool ID: {tool.id} | |
| {tool.function.name ? <span>({tool.function.name})</span> : null} | |
| </span> | |
| {tool.function.arguments ? ( | |
| <CodeSnippet type="multi" hideCopyButton wrapText> | |
| {JSON.stringify(tool.function.arguments, null, 2)} | |
| </CodeSnippet> | |
| ) : null} | |
| </div> | |
| ); | |
| } | |
| function ToolResponse({ | |
| messageId, | |
| message, | |
| onSelection, | |
| }: { | |
| messageId: string; | |
| message: ToolMessage; | |
| onSelection?: Function; | |
| }) { | |
| // Step 1: Initialize state and necessary variables | |
| const [documentIndex, setDocumentIndex] = useState<number>(0); | |
| // Step 2: Render | |
| return ( | |
| <div className={cx(classes.message, classes.toolResponse)}> | |
| <span> | |
| Tool ID: {message.tool_id} | |
| {message.name ? <span>({message.name})</span> : null} | |
| </span> | |
| {message.type === 'documents' && Array.isArray(message.content) ? ( | |
| <DocumentsViewer | |
| key={`${messageId}__documents--${message.content.length}`} | |
| id={`${messageId}__documents`} | |
| documents={message.content} | |
| documentIndex={documentIndex} | |
| setDocumentIndex={setDocumentIndex} | |
| onSelection={onSelection} | |
| ></DocumentsViewer> | |
| ) : message.type === 'json' ? ( | |
| <CodeSnippet type="multi" hideCopyButton wrapText> | |
| {JSON.stringify(message.content, null, 2)} | |
| </CodeSnippet> | |
| ) : ( | |
| <Balancer | |
| className={cx(classes.message, classes.toolMessage)} | |
| ratio={0.2} | |
| onMouseDown={() => { | |
| if (onSelection) { | |
| onSelection( | |
| `messages[${messageId.split('--').slice(-1)[0]}].content`, | |
| ); | |
| } | |
| }} | |
| onMouseUp={() => { | |
| if (onSelection) { | |
| onSelection( | |
| `messages[${messageId.split('--').slice(-1)[0]}].content`, | |
| ); | |
| } | |
| }} | |
| > | |
| {typeof message.content === 'string' | |
| ? message.content.split('\n').map((line, i) => ( | |
| <span key={i}> | |
| {line} | |
| <br /> | |
| </span> | |
| )) | |
| : message.content} | |
| </Balancer> | |
| )} | |
| </div> | |
| ); | |
| } | |
| function AssistantResponse({ | |
| messageId, | |
| message, | |
| onSelection, | |
| }: { | |
| messageId: string; | |
| message: AssistantMessage; | |
| onSelection?: Function; | |
| }) { | |
| return ( | |
| <div className={classes.assistantResponse}> | |
| {message.content ? ( | |
| <Balancer | |
| className={cx(classes.message, classes.assistantMessage)} | |
| ratio={0.2} | |
| onMouseDown={() => { | |
| 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) => ( | |
| <span key={i}> | |
| {line} | |
| <br /> | |
| </span> | |
| ))} | |
| </Balancer> | |
| ) : null} | |
| {message.tool_calls | |
| ? message.tool_calls.map((tool, toolIdx) => { | |
| return ( | |
| <Tool key={`message-${messageId}__tool-${toolIdx}`} tool={tool} /> | |
| ); | |
| }) | |
| : null} | |
| </div> | |
| ); | |
| } | |
| // =================================================================================== | |
| // MAIN FUNCTIONS | |
| // =================================================================================== | |
| export default function ChatLine({ | |
| messageId, | |
| message, | |
| latestResponse, | |
| onSelection, | |
| focused, | |
| }: ChatLineProps) { | |
| // Step 1: Initialize state and necessary variables | |
| const anchorRef = useRef<HTMLDivElement>(null); | |
| // Step 2: Run effects | |
| // Step 2.a: Scroll into view | |
| useEffect(() => { | |
| if (anchorRef.current && focused) { | |
| anchorRef.current.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: message.role === 'user' ? 'start' : 'center', | |
| inline: 'center', | |
| }); | |
| } | |
| }, [focused, message.role]); | |
| // Step 3: Render | |
| // Step 3.a: Return "null" if message is undefined | |
| if (!message) { | |
| return null; | |
| } | |
| // Step 3.b: Render chat line | |
| return ( | |
| <div | |
| ref={anchorRef} | |
| className={cx(classes.line, { | |
| [classes.assistantLine]: message.role === 'assistant', | |
| [classes.latestResponse]: latestResponse, | |
| })} | |
| > | |
| <Avatar role={message.role} /> | |
| <div | |
| className={cx( | |
| classes.baloon, | |
| message.role === 'assistant' | |
| ? //@ts-ignore | |
| message.tool_calls | |
| ? classes.toolCallBaloon | |
| : classes.assistantBaloon | |
| : message.role === 'tool' | |
| ? classes.toolResponseBaloon | |
| : null, | |
| )} | |
| > | |
| {message.role === 'system' || | |
| message.role === 'developer' || | |
| message.role === 'user' ? ( | |
| <Balancer | |
| className={cx(classes.message)} | |
| ratio={0.2} | |
| onMouseDown={() => { | |
| 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) => ( | |
| <span key={i}> | |
| {line} | |
| <br /> | |
| </span> | |
| ))} | |
| </Balancer> | |
| ) : message.role === 'tool' ? ( | |
| //@ts-ignore | |
| <ToolResponse messageId={messageId} message={message} /> | |
| ) : ( | |
| <AssistantResponse | |
| messageId={messageId} | |
| //@ts-ignore | |
| message={message} | |
| onSelection={onSelection} | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |