这是indexloc提供的服务,不要输入任何密码
Skip to content

AI agent ui animation #2999

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useState } from "react";
import {
CaretDown,
CircleNotch,
Check,
CheckCircle,
} from "@phosphor-icons/react";

export default function StatusResponse({
messages = [],
isThinking = false,
showCheckmark = false,
}) {
const [isExpanded, setIsExpanded] = useState(false);
const currentThought = messages[messages.length - 1];
const previousThoughts = messages.slice(0, -1);

function handleExpandClick() {
if (!previousThoughts.length > 0) return;
setIsExpanded(!isExpanded);
}

return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col relative">
<div
onClick={handleExpandClick}
className={`${!previousThoughts?.length ? "cursor-text" : "cursor-pointer hover:bg-theme-sidebar-item-hover transition-all duration-200"} bg-theme-bg-chat-input rounded-full py-2 px-4 flex items-center gap-x-2 border border-theme-sidebar-border`}
>
{isThinking ? (
<CircleNotch
className="w-4 h-4 text-theme-text-secondary animate-spin"
aria-label="Agent is thinking..."
/>
) : showCheckmark ? (
<CheckCircle
className="w-4 h-4 text-green-400 transition-all duration-300"
aria-label="Thought complete"
/>
) : null}
<div className="flex-1 overflow-hidden">
<span
key={currentThought.content}
className="text-xs text-theme-text-secondary font-mono inline-block w-full animate-thoughtTransition"
>
{currentThought.content}
</span>
</div>
<div className="flex items-center gap-x-2">
{previousThoughts?.length > 0 && (
<div
data-tooltip-id="expand-cot"
data-tooltip-content={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
className="border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors p-1 rounded-full hover:bg-theme-sidebar-item-hover"
aria-label={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
>
<CaretDown
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
/>
</div>
)}
</div>
</div>

{/* Previous thoughts dropdown */}
{previousThoughts?.length > 0 && (
<div
key={`cot-list-${currentThought.uuid}`}
className={`mt-2 bg-theme-bg-chat-input backdrop-blur-sm rounded-lg overflow-hidden transition-all duration-300 border border-theme-sidebar-border ${
isExpanded ? "max-h-[300px] opacity-100" : "max-h-0 opacity-0"
}`}
>
<div className="p-2">
{previousThoughts.map((thought, index) => (
<div
key={`cot-${thought.uuid || index}`}
className="flex gap-x-2"
>
<p className="text-xs text-theme-text-secondary font-mono">
{index + 1}/{previousThoughts.length}
</p>
<div
className="flex items-center gap-x-3 p-2 animate-fadeUpIn"
style={{ animationDelay: `${index * 50}ms` }}
>
<span className="text-xs text-theme-text-secondary font-mono">
{thought.content}
</span>
</div>
</div>
))}
{/* Append current thought to the end */}
<div key={`cot-${currentThought.uuid}`} className="flex gap-x-2">
<p className="text-xs text-theme-text-secondary font-mono">
{previousThoughts.length + 1}/{previousThoughts.length + 1}
</p>
<div className="flex items-center gap-x-3 p-2 animate-fadeUpIn">
<span className="text-xs text-theme-text-secondary font-mono">
{currentThought.content}
</span>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, useMemo, useCallback } from "react";
import HistoricalMessage from "./HistoricalMessage";
import PromptReply from "./PromptReply";
import StatusResponse from "./StatusResponse";
import { useManageWorkspaceModal } from "../../../Modals/ManageWorkspace";
import ManageWorkspace from "../../../Modals/ManageWorkspace";
import { ArrowDown } from "@phosphor-icons/react";
Expand All @@ -12,6 +13,7 @@ import { useParams } from "react-router-dom";
import paths from "@/utils/paths";
import Appearance from "@/models/appearance";
import useTextSize from "@/hooks/useTextSize";
import { v4 } from "uuid";

export default function ChatHistory({
history = [],
Expand Down Expand Up @@ -174,63 +176,52 @@ export default function ChatHistory({
);
}

const compiledHistory = useMemo(
() =>
buildMessages({
workspace,
history,
regenerateAssistantMessage,
saveEditedMessage,
forkThread,
}),
[
workspace,
history,
regenerateAssistantMessage,
saveEditedMessage,
forkThread,
]
);
const lastMessageInfo = useMemo(() => getLastMessageInfo(history), [history]);
const renderStatusResponse = useCallback(
(item, index) => {
const hasSubsequentMessages = index < compiledHistory.length - 1;
return (
<StatusResponse
key={`status-group-${index}`}
messages={item}
isThinking={!hasSubsequentMessages && lastMessageInfo.isAnimating}
showCheckmark={
hasSubsequentMessages ||
(!lastMessageInfo.isAnimating && !lastMessageInfo.isStatusResponse)
}
/>
);
},
[compiledHistory.length, lastMessageInfo]
);

return (
<div
className={`markdown text-white/80 light:text-theme-text-primary font-light ${textSizeClass} h-full md:h-[83%] pb-[100px] pt-6 md:pt-0 md:pb-20 md:mx-0 overflow-y-scroll flex flex-col justify-start ${
showScrollbar ? "show-scrollbar" : "no-scroll"
}`}
className={`markdown text-white/80 light:text-theme-text-primary font-light ${textSizeClass} h-full md:h-[83%] pb-[100px] pt-6 md:pt-0 md:pb-20 md:mx-0 overflow-y-scroll flex flex-col justify-start ${showScrollbar ? "show-scrollbar" : "no-scroll"}`}
id="chat-history"
ref={chatHistoryRef}
onScroll={handleScroll}
>
{history.map((props, index) => {
const isLastBotReply =
index === history.length - 1 && props.role === "assistant";

if (props?.type === "statusResponse" && !!props.content) {
return <StatusResponse key={props.uuid} props={props} />;
}

if (props.type === "rechartVisualize" && !!props.content) {
return (
<Chartable key={props.uuid} workspace={workspace} props={props} />
);
}

if (isLastBotReply && props.animate) {
return (
<PromptReply
key={props.uuid}
uuid={props.uuid}
reply={props.content}
pending={props.pending}
sources={props.sources}
error={props.error}
workspace={workspace}
closed={props.closed}
/>
);
}

return (
<HistoricalMessage
key={index}
message={props.content}
role={props.role}
workspace={workspace}
sources={props.sources}
feedbackScore={props.feedbackScore}
chatId={props.chatId}
error={props.error}
attachments={props.attachments}
regenerateMessage={regenerateAssistantMessage}
isLastMessage={isLastBotReply}
saveEditedMessage={saveEditedMessage}
forkThread={forkThread}
metrics={props.metrics}
/>
);
})}
{compiledHistory.map((item, index) =>
Array.isArray(item) ? renderStatusResponse(item, index) : item
)}
{showing && (
<ManageWorkspace hideModal={hideModal} providedSlug={workspace.slug} />
)}
Expand All @@ -253,21 +244,13 @@ export default function ChatHistory({
);
}

function StatusResponse({ props }) {
return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<span
className={`text-xs inline-block p-2 rounded-lg text-white/60 font-mono whitespace-pre-line`}
>
{props.content}
</span>
</div>
</div>
</div>
);
}
const getLastMessageInfo = (history) => {
const lastMessage = history?.[history.length - 1] || {};
return {
isAnimating: lastMessage?.animate,
isStatusResponse: lastMessage?.type === "statusResponse",
};
};

function WorkspaceChatSuggestions({ suggestions = [], sendSuggestion }) {
if (suggestions.length === 0) return null;
Expand All @@ -286,3 +269,78 @@ function WorkspaceChatSuggestions({ suggestions = [], sendSuggestion }) {
</div>
);
}

/**
* Builds the history of messages for the chat.
* This is mostly useful for rendering the history in a way that is easy to understand.
* as well as compensating for agent thinking and other messages that are not part of the history, but
* are still part of the chat.
*
* @param {Object} param0 - The parameters for building the messages.
* @param {Array} param0.history - The history of messages.
* @param {Object} param0.workspace - The workspace object.
* @param {Function} param0.regenerateAssistantMessage - The function to regenerate the assistant message.
* @param {Function} param0.saveEditedMessage - The function to save the edited message.
* @param {Function} param0.forkThread - The function to fork the thread.
* @returns {Array} The compiled history of messages.
*/
function buildMessages({
history,
workspace,
regenerateAssistantMessage,
saveEditedMessage,
forkThread,
}) {
return history.reduce((acc, props, index) => {
const isLastBotReply =
index === history.length - 1 && props.role === "assistant";

if (props?.type === "statusResponse" && !!props.content) {
if (acc.length > 0 && Array.isArray(acc[acc.length - 1])) {
acc[acc.length - 1].push(props);
} else {
acc.push([props]);
}
return acc;
}

if (props.type === "rechartVisualize" && !!props.content) {
acc.push(
<Chartable key={props.uuid} workspace={workspace} props={props} />
);
} else if (isLastBotReply && props.animate) {
acc.push(
<PromptReply
key={props.uuid || v4()}
uuid={props.uuid}
reply={props.content}
pending={props.pending}
sources={props.sources}
error={props.error}
workspace={workspace}
closed={props.closed}
/>
);
} else {
acc.push(
<HistoricalMessage
key={index}
message={props.content}
role={props.role}
workspace={workspace}
sources={props.sources}
feedbackScore={props.feedbackScore}
chatId={props.chatId}
error={props.error}
attachments={props.attachments}
regenerateMessage={regenerateAssistantMessage}
isLastMessage={isLastBotReply}
saveEditedMessage={saveEditedMessage}
forkThread={forkThread}
metrics={props.metrics}
/>
);
}
return acc;
}, []);
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ export function ChatTooltips() {
delayShow={300}
className="tooltip !text-xs"
/>
<Tooltip
id="expand-cot"
place="bottom"
delayShow={300}
className="tooltip !text-xs"
/>
</>
);
}
Loading