Path: blob/master/cloud/ai-service-apps/nextjs-carbon-react-ui/src/components/ChatItem/ChatItem.js
6408 views
"use client";12import { memo, useContext, useRef } from "react";3import * as Showdown from "showdown";4import cx from "classnames";5import { Transition } from "react-transition-group";6import { moderate01 } from "@carbon/motion";7import { Loading, Accordion, AccordionItem, DefinitionTooltip, CodeSnippet } from "@carbon/react";8import { CheckmarkFilled, CheckmarkFilledError } from "@carbon/react/icons";9import { DeploymentContext } from "../../contexts/deployment-context";10import { Avatar } from "../Avatar/Avatar";11import { MESSAGE_ROLE, MESSAGE_STATUS } from "@/utils/constants";12import { useAppContext } from "@/contexts/app-context";13import { getUserInitials, mapUserColor } from "@/utils/user-util";1415const MESSAGE_ANIMATION_DURATION = parseInt(moderate01, 10);1617const converter = new Showdown.Converter({18tables: true,19simplifiedAutoLink: true,20strikethrough: true,21tasklists: true,22smoothLivePreview: true,23backslashEscapesHTMLTags: true,24openLinksInNewWindow: true,25emoji: true,26metadata: true,27});2829converter.setFlavor("github");30converter.setOption("smoothLivePreview", true);3132const renderMarkdownToHTML = (markdown) => {33// This is ONLY safe because the output HTML34// is shown to the same user, and because you35// trust this Markdown parser to not have bugs.36const renderedHTML = converter.makeHtml(markdown);37return { __html: renderedHTML };38};3940const AnswerContent = ({ content, aborted }) => {41if (typeof content === "string") {42let markdown = content.trim();43const triangle = aborted ? " :small_red_triangle:" : "";44markdown += triangle;4546const markup = renderMarkdownToHTML(markdown);4748return <div className="preview-content-light" dangerouslySetInnerHTML={markup} />;49}5051return content;52};5354const ChatItem = memo(55function ChatItem({ message, last }) {56const deployment = useContext(DeploymentContext);57const { userData } = useAppContext();58const nodeRef = useRef(null);5960const _cleanToolInput = (input) => {61try {62if (input && input.includes('"__arg1"')) {63// plain string input - extract64const inputObject = JSON.parse(input);65return inputObject.__arg1;66}67} catch (err) {68console.error(err.message);69}70return input;71};7273const _renderStepBody = (step) => (74<div className="qa-panel__stepBody">75{step.tool_name && (76<div className="qa-panel__stepLabel">77<span>Tool name:</span>78<span>{step.tool_name}</span>79</div>80)}81{step.tool_input && (82<div className="qa-panel__stepLabel">83<span>Tool input:</span>84<span className="qa-panel__longValue">{_cleanToolInput(step.tool_input)}</span>85</div>86)}87{step.evidence && (88<CodeSnippet className="qa-panel__codeSnippet" type="multi" wrapText hideCopyButton>89{step.evidence}90</CodeSnippet>91)}92</div>93);9495const _renderSteps = (steps) => (96<Accordion align="start" size="sm" className="qa-panel__executionStatusDetails">97{steps.map((step, idx) => (98<AccordionItem99key={`agent-plan:${step.id}`}100className="qa-panel__executionStatusStep"101disabled={!step.tool_name}102title={103<>104<span className="qa-panel__stepIndex">{`${idx + 1}:`}</span>105{step.state === "thinking" && (106<span className="qa-panel__stepTitle">Thinking...</span>107)}108{step.state !== "thinking" && (109<span className="qa-panel__stepTitle">110{_cleanToolInput(step.tool_input) || step.definition}111</span>112)}113{step.state !== "thinking" && (114<span115className={cx("qa-panel__stepNumber", {116["started"]: step.state === "started",117["finished"]: step.state === "finished",118["loading"]: !step.tool_name,119["success"]: step.success,120})}121>122{step.state === "finished" && step.success && <CheckmarkFilled size="16" />}123{step.state === "finished" && !step.success && (124<CheckmarkFilledError size="16" />125)}126{step.state === "started" && <Loading small withOverlay={false} />}127</span>128)}129</>130}131>132{step.tool_name && _renderStepBody(step)}133</AccordionItem>134))}135<div className="qa-panel__lastStep">136<DefinitionTooltip137align="top"138definition="An AI agent analyzes a prompt, searches for information using selected tools, and generates a response."139>140Steps created by Agent141</DefinitionTooltip>142</div>143</Accordion>144);145146const _renderReasoning = () => (147<details className="qa-panel__reasoningSection">148<summary>How did I get this answer?</summary>149{_renderSteps(message.plan.steps)}150</details>151);152153let reasoning = null;154155if (156last &&157message.status === MESSAGE_STATUS.READY &&158!message.aborted &&159message.plan &&160message.plan.steps161) {162reasoning = _renderReasoning();163}164165const isEmpty = message.status === MESSAGE_STATUS.LOADING && !message.content;166const itemParentClass = cx("qa-panel__itemParent", {167["isInitial"]: message.initial,168});169let emptyContent = null;170171if (isEmpty) {172if (message.plan && message.plan.steps) {173emptyContent = _renderSteps(message.plan.steps);174} else {175emptyContent = <Loading withOverlay={false} small />;176}177}178179const itemId = last ? "last-answer" : message.timestamp;180181return (182<Transition in appear nodeRef={nodeRef} timeout={MESSAGE_ANIMATION_DURATION}>183{(state) => (184<div ref={nodeRef} className={itemParentClass}>185<div186id={itemId}187className={cx("qa-panel__chatItem", [`status-${state}`], {188initial: message.initial,189})}190>191{message.role === MESSAGE_ROLE.ASSISTANT && deployment && (192<Avatar icon={deployment.avatar_icon} color={deployment.avatar_color} chat />193)}194{message.role === MESSAGE_ROLE.USER && (195<div196className="qa-panel__userAvatar"197style={{ backgroundColor: mapUserColor(userData.name) }}198>199{message.role === MESSAGE_ROLE.USER && getUserInitials(userData)}200</div>201)}202<div className="qa-panel__itemContent">203{emptyContent}204{message.status === MESSAGE_STATUS.READY &&205message.role === MESSAGE_ROLE.USER &&206message.content}207{!isEmpty && message.role === MESSAGE_ROLE.ASSISTANT && (208<AnswerContent content={message.content} aborted={message.aborted} />209)}210{reasoning}211</div>212</div>213</div>214)}215</Transition>216);217},218(oldProps, newProps) => {219// Once message is ready, there is no need to re-render it.220return (221oldProps.message.status === MESSAGE_STATUS.READY &&222newProps.message.status === MESSAGE_STATUS.READY &&223oldProps.message.aborted !== newProps.message.aborted224);225}226);227228export default ChatItem;229230231