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