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/app/home/page.js
6408 views
1
"use client";
2
3
import { Button, Loading } from "@carbon/react";
4
import { useState, useContext, useRef, useCallback } from "react";
5
import { fetchEventSource } from "@microsoft/fetch-event-source";
6
import { DeploymentContext } from "../../contexts/deployment-context";
7
import _get from "lodash/get";
8
9
import QAPanel from "../../components/QAPanel/QAPanel";
10
import { MESSAGE_ROLE, MESSAGE_STATUS } from "@/utils/constants";
11
12
export default function LandingPage() {
13
const deployment = useContext(DeploymentContext);
14
const [messages, setMessages] = useState([]);
15
const [isGenerating, setIsGenerating] = useState(false);
16
const shouldAutoScrollRef = useRef(true);
17
const autoScrollIntersectorRef = useRef(null);
18
const controllerRef = useRef(null);
19
20
// useChatAutoScrollDetector(autoScrollIntersectorRef, shouldAutoScrollRef);
21
22
const _getResponse = async (messages, reply, updateFn) => {
23
const payload = {
24
messages,
25
};
26
27
return new Promise((resolve, reject) => {
28
let content = "";
29
30
reply.plan = {
31
steps: [
32
{
33
state: "thinking",
34
},
35
],
36
};
37
updateFn();
38
39
fetchEventSource("/api/generate", {
40
method: "POST",
41
headers: {
42
"Content-Type": "application/json",
43
Accept: "text/event-stream",
44
},
45
signal: controllerRef.current.signal,
46
body: JSON.stringify(payload),
47
openWhenHidden: true,
48
async onopen(response) {
49
if (response.status !== 200) {
50
reject(await response.json());
51
}
52
},
53
onmessage(event) {
54
if (!event.data) {
55
return;
56
}
57
let parsedData = null;
58
59
try {
60
parsedData = JSON.parse(event.data);
61
} catch (err) {
62
return reject(err);
63
}
64
65
if (parsedData.errors && Array.isArray(parsedData.errors) && parsedData.errors[0]) {
66
const error = parsedData.errors[0];
67
const errorData = error.messageId ? error : "Unknown generate error";
68
reject(errorData);
69
}
70
71
let newContent = "";
72
const message = _get(parsedData.choices[0], "message");
73
const delta = _get(parsedData.choices[0], "delta");
74
if (message) {
75
// Support old format
76
if (message.tool_calls || message.role === MESSAGE_ROLE.TOOL) {
77
newContent = _processToolMessage(message);
78
} else {
79
newContent = _processDeltaLegacy(content, message.delta);
80
}
81
} else if (delta) {
82
if (delta.tool_calls || delta.role === MESSAGE_ROLE.TOOL) {
83
newContent = _processToolMessage(delta);
84
} else {
85
newContent = _processDelta(content, delta);
86
}
87
} else {
88
newContent = null;
89
}
90
91
if (newContent) {
92
// const codeBlockCounts = newContent.match(/```/gu);
93
// const inCodeBlock = Boolean(codeBlockCounts && (codeBlockCounts.length % 2 !== 0));
94
95
content = newContent;
96
reply.content = content;
97
updateFn();
98
// answer.inCodeBlock = inCodeBlock;
99
}
100
},
101
onclose() {
102
resolve(content);
103
},
104
onerror(error) {
105
reject(error);
106
},
107
});
108
109
const _processToolMessage = (message) => {
110
if (message.tool_calls) {
111
// Tool start
112
const id = message.tool_calls[0].id;
113
const toolStart = message.tool_calls[0].function;
114
const toolName = toolStart.name;
115
const toolArguments = toolStart.arguments;
116
let currentStep = reply.plan.steps[reply.plan.steps.length - 1];
117
118
if (currentStep.state !== "thinking") {
119
currentStep = {
120
state: "thinking",
121
};
122
reply.plan.steps.push(currentStep);
123
}
124
currentStep.state = "started";
125
currentStep.tool_name = toolName;
126
currentStep.tool_input = toolArguments;
127
currentStep.id = id;
128
currentStep.definition = toolName; // We should really show input
129
updateFn();
130
} else if (message.role === "tool") {
131
// Tool end
132
const id = message.tool_call_id;
133
for (const step of reply.plan.steps) {
134
if (id === step.id && message.name === step.tool_name) {
135
// Found it - update
136
step.state = "finished";
137
step.evidence = message.content;
138
step.success = true;
139
updateFn();
140
break;
141
}
142
}
143
}
144
return null;
145
};
146
147
const _processDeltaLegacy = (oldContent, delta) => {
148
if (!delta) {
149
return null;
150
}
151
return `${oldContent}${delta}`;
152
};
153
154
const _processDelta = (oldContent, delta) => {
155
if (!delta) {
156
return null;
157
}
158
return `${oldContent}${delta.content}`;
159
};
160
});
161
};
162
163
const _ensureLastVisible = () => {
164
setTimeout(() => {
165
const element = document.getElementById("last-answer");
166
const isEmptyAnswer = messages[0] && !messages[0].content;
167
if (element && (shouldAutoScrollRef.current || isEmptyAnswer)) {
168
element.scrollIntoView(false);
169
// To reduce flicker as we replace rendered markdown over and over.
170
element.style.minHeight = `${element.offsetHeight}px`;
171
}
172
}, 20);
173
};
174
175
const _updateLast = (messages) => {
176
const newMessages = [...messages];
177
setMessages(newMessages);
178
_ensureLastVisible();
179
};
180
181
const _onInput = async (input) => {
182
setIsGenerating(true);
183
let newMessages = [...messages];
184
controllerRef.current = new AbortController();
185
const message = {
186
role: MESSAGE_ROLE.USER,
187
content: input,
188
status: MESSAGE_STATUS.READY,
189
timestamp: Date.now(),
190
};
191
newMessages.unshift(message);
192
const reply = {
193
role: MESSAGE_ROLE.ASSISTANT,
194
content: "",
195
status: MESSAGE_STATUS.LOADING,
196
timestamp: Date.now(),
197
};
198
newMessages.unshift(reply);
199
setMessages(newMessages);
200
201
try {
202
const response = await _getResponse(
203
[...newMessages].reverse().slice(0, -1),
204
reply,
205
_updateLast.bind(null, newMessages)
206
);
207
reply.status = MESSAGE_STATUS.READY;
208
reply.content = response;
209
reply.timestamp = Date.now();
210
const newMessages2 = [...newMessages];
211
newMessages2[0] = reply;
212
setMessages(newMessages2);
213
setIsGenerating(false);
214
} catch (err) {
215
console.error(err);
216
}
217
};
218
219
const _onAbort = async () => {
220
controllerRef.current.abort();
221
setIsGenerating(false);
222
setMessages((prevMessages) => {
223
const newMessages = [...prevMessages];
224
newMessages[0].aborted = true;
225
newMessages[0].status = MESSAGE_STATUS.READY;
226
return newMessages;
227
});
228
controllerRef.current = null;
229
};
230
231
const handleNewChat = useCallback(() => {
232
setMessages([]);
233
controllerRef.current?.abort();
234
setIsGenerating(false);
235
controllerRef.current = null;
236
}, []);
237
238
return (
239
<div className="landing-page__container">
240
{!deployment && <Loading />}
241
{deployment && (
242
<div className="landing-page__commandPanel">
243
<Button onClick={handleNewChat}>New chat</Button>
244
</div>
245
)}
246
{deployment && (
247
<QAPanel
248
messages={messages}
249
onInput={_onInput}
250
onAbort={_onAbort}
251
intersectorRef={autoScrollIntersectorRef}
252
isRunning={isGenerating}
253
/>
254
)}
255
</div>
256
);
257
}
258
259