diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/StatusResponse/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/StatusResponse/index.jsx new file mode 100644 index 0000000000..c06fb35ad1 --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/StatusResponse/index.jsx @@ -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 ( +
+
+
+ {isThinking ? ( + + ) : showCheckmark ? ( + + ) : null} +
+ + {currentThought.content} + +
+
+ {previousThoughts?.length > 0 && ( +
+ +
+ )} +
+
+ + {/* Previous thoughts dropdown */} + {previousThoughts?.length > 0 && ( +
+
+ {previousThoughts.map((thought, index) => ( +
+

+ {index + 1}/{previousThoughts.length} +

+
+ + {thought.content} + +
+
+ ))} + {/* Append current thought to the end */} +
+

+ {previousThoughts.length + 1}/{previousThoughts.length + 1} +

+
+ + {currentThought.content} + +
+
+
+
+ )} +
+
+ ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx index 9bf8eeebbe..819a6f1c26 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx @@ -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"; @@ -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 = [], @@ -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 ( + + ); + }, + [compiledHistory.length, lastMessageInfo] + ); + return (
- {history.map((props, index) => { - const isLastBotReply = - index === history.length - 1 && props.role === "assistant"; - - if (props?.type === "statusResponse" && !!props.content) { - return ; - } - - if (props.type === "rechartVisualize" && !!props.content) { - return ( - - ); - } - - if (isLastBotReply && props.animate) { - return ( - - ); - } - - return ( - - ); - })} + {compiledHistory.map((item, index) => + Array.isArray(item) ? renderStatusResponse(item, index) : item + )} {showing && ( )} @@ -253,21 +244,13 @@ export default function ChatHistory({ ); } -function StatusResponse({ props }) { - return ( -
-
-
- - {props.content} - -
-
-
- ); -} +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; @@ -286,3 +269,78 @@ function WorkspaceChatSuggestions({ suggestions = [], sendSuggestion }) {
); } + +/** + * 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( + + ); + } else if (isLastBotReply && props.animate) { + acc.push( + + ); + } else { + acc.push( + + ); + } + return acc; + }, []); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatTooltips/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatTooltips/index.jsx index 3aa5dc6ccb..ea3cbdfb1f 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatTooltips/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatTooltips/index.jsx @@ -67,6 +67,12 @@ export function ChatTooltips() { delayShow={300} className="tooltip !text-xs" /> + ); } diff --git a/frontend/src/index.css b/frontend/src/index.css index 96d01d41cb..539b586658 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -583,22 +583,6 @@ dialog::backdrop { animation: slideDown 0.3s ease-out forwards; } -@keyframes slideUp { - from { - max-height: 400px; - opacity: 1; - } - - to { - max-height: 0; - opacity: 0; - } -} - -.slide-up { - animation: slideUp 0.3s ease-out forwards; -} - .input-label { @apply text-[14px] font-bold text-white; } @@ -946,3 +930,51 @@ does not extend the close button beyond the viewport. */ .rti--container { @apply !bg-theme-settings-input-bg !text-white !placeholder-white !placeholder-opacity-60 !text-sm !rounded-lg !p-2.5; } + +@keyframes fadeUpIn { + 0% { + opacity: 0; + transform: translateY(5px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fadeUpIn { + animation: fadeUpIn 0.3s ease-out forwards; +} + +@keyframes bounce-subtle { + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-2px); + } +} + +@keyframes thoughtTransition { + 0% { + opacity: 0; + transform: translateY(10px); + } + + 30% { + opacity: 1; + transform: translateY(0); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.animate-thoughtTransition { + animation: thoughtTransition 0.5s ease-out forwards; +} diff --git a/frontend/src/utils/chat/agent.js b/frontend/src/utils/chat/agent.js index babe55605f..ad1193d304 100644 --- a/frontend/src/utils/chat/agent.js +++ b/frontend/src/utils/chat/agent.js @@ -99,7 +99,7 @@ export default function handleSocketResponse(event, setChatHistory) { sources: [], closed: true, error: null, - animate: false, + animate: data?.animate || false, pending: false, }, ]; diff --git a/frontend/src/utils/chat/index.js b/frontend/src/utils/chat/index.js index 86c1cd4208..abde54fe9c 100644 --- a/frontend/src/utils/chat/index.js +++ b/frontend/src/utils/chat/index.js @@ -17,6 +17,7 @@ export default function handleChat( sources = [], error, close, + animate = false, chatId = null, action = null, metrics = {}, @@ -34,7 +35,7 @@ export default function handleChat( sources, closed: true, error, - animate: false, + animate, pending: false, metrics, }, @@ -47,7 +48,7 @@ export default function handleChat( sources, closed: true, error, - animate: false, + animate, pending: false, metrics, }); diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 8ecdbcdc0a..517af2ea16 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -141,7 +141,10 @@ export default { }, animation: { sweep: "sweep 0.5s ease-in-out", - "pulse-glow": "pulse-glow 1.5s infinite" + "pulse-glow": "pulse-glow 1.5s infinite", + 'fade-in': 'fade-in 0.3s ease-out', + 'slide-up': 'slide-up 0.4s ease-out forwards', + 'bounce-subtle': 'bounce-subtle 2s ease-in-out infinite' }, keyframes: { sweep: { @@ -175,6 +178,18 @@ export default { boxShadow: "0 0 0 rgba(255, 255, 255, 0.0)", backgroundColor: "rgba(255, 255, 255, 0.0)" } + }, + 'fade-in': { + '0%': { opacity: '0' }, + '100%': { opacity: '1' } + }, + 'slide-up': { + '0%': { transform: 'translateY(10px)', opacity: '0' }, + '100%': { transform: 'translateY(0)', opacity: '1' } + }, + 'bounce-subtle': { + '0%, 100%': { transform: 'translateY(0)' }, + '50%': { transform: 'translateY(-2px)' } } } } diff --git a/server/utils/agents/aibitat/plugins/websocket.js b/server/utils/agents/aibitat/plugins/websocket.js index 8c8800ff3e..a253bd010e 100644 --- a/server/utils/agents/aibitat/plugins/websocket.js +++ b/server/utils/agents/aibitat/plugins/websocket.js @@ -68,7 +68,11 @@ const websocket = { aibitat.introspect = (messageText) => { if (!introspection) return; // Dump thoughts when not wanted. socket.send( - JSON.stringify({ type: "statusResponse", content: messageText }) + JSON.stringify({ + type: "statusResponse", + content: messageText, + animate: true, + }) ); }; diff --git a/server/utils/agents/ephemeral.js b/server/utils/agents/ephemeral.js index d8258eca80..035de8ffe3 100644 --- a/server/utils/agents/ephemeral.js +++ b/server/utils/agents/ephemeral.js @@ -412,6 +412,7 @@ class EphemeralEventListener extends EventEmitter { attachments: [], close: false, error: null, + animate: true, }); } @@ -423,6 +424,7 @@ class EphemeralEventListener extends EventEmitter { attachments: [], close: true, error: null, + animate: false, }); }; this.on("chunk", onChunkHandler); diff --git a/server/utils/chats/agents.js b/server/utils/chats/agents.js index cd127d07f2..26de10e8ac 100644 --- a/server/utils/chats/agents.js +++ b/server/utils/chats/agents.js @@ -33,6 +33,7 @@ async function grepAgents({ )} could not be called. Chat will be handled as default chat.`, sources: [], close: true, + animate: false, error: null, }); return; @@ -61,6 +62,7 @@ async function grepAgents({ sources: [], close: true, error: null, + animate: true, }); return true; }