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