Skip to content

Commit e86ad82

Browse files
Merge branch 'master' of github.com:Mintplex-Labs/anything-llm
2 parents 81939fe + 741ba8f commit e86ad82

File tree

7 files changed

+307
-88
lines changed

7 files changed

+307
-88
lines changed

.github/workflows/dev-build.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ concurrency:
66

77
on:
88
push:
9-
branches: ['disable-default-agent-skills'] # put your current branch to create a build. Core team only.
9+
branches: ['agent-ui-animations'] # put your current branch to create a build. Core team only.
1010
paths-ignore:
1111
- '**.md'
1212
- 'cloud-deployments/*'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React, { useState } from "react";
2+
import {
3+
CaretDown,
4+
CircleNotch,
5+
Check,
6+
CheckCircle,
7+
} from "@phosphor-icons/react";
8+
9+
export default function StatusResponse({
10+
messages = [],
11+
isThinking = false,
12+
showCheckmark = false,
13+
}) {
14+
const [isExpanded, setIsExpanded] = useState(false);
15+
const currentThought = messages[messages.length - 1];
16+
const previousThoughts = messages.slice(0, -1);
17+
18+
function handleExpandClick() {
19+
if (!previousThoughts.length > 0) return;
20+
setIsExpanded(!isExpanded);
21+
}
22+
23+
return (
24+
<div className="flex justify-center items-end w-full">
25+
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col relative">
26+
<div
27+
onClick={handleExpandClick}
28+
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`}
29+
>
30+
{isThinking ? (
31+
<CircleNotch
32+
className="w-4 h-4 text-theme-text-secondary animate-spin"
33+
aria-label="Agent is thinking..."
34+
/>
35+
) : showCheckmark ? (
36+
<CheckCircle
37+
className="w-4 h-4 text-green-400 transition-all duration-300"
38+
aria-label="Thought complete"
39+
/>
40+
) : null}
41+
<div className="flex-1 overflow-hidden">
42+
<span
43+
key={currentThought.content}
44+
className="text-xs text-theme-text-secondary font-mono inline-block w-full animate-thoughtTransition"
45+
>
46+
{currentThought.content}
47+
</span>
48+
</div>
49+
<div className="flex items-center gap-x-2">
50+
{previousThoughts?.length > 0 && (
51+
<div
52+
data-tooltip-id="expand-cot"
53+
data-tooltip-content={
54+
isExpanded ? "Hide thought chain" : "Show thought chain"
55+
}
56+
className="border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors p-1 rounded-full hover:bg-theme-sidebar-item-hover"
57+
aria-label={
58+
isExpanded ? "Hide thought chain" : "Show thought chain"
59+
}
60+
>
61+
<CaretDown
62+
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
63+
/>
64+
</div>
65+
)}
66+
</div>
67+
</div>
68+
69+
{/* Previous thoughts dropdown */}
70+
{previousThoughts?.length > 0 && (
71+
<div
72+
key={`cot-list-${currentThought.uuid}`}
73+
className={`mt-2 bg-theme-bg-chat-input backdrop-blur-sm rounded-lg overflow-hidden transition-all duration-300 border border-theme-sidebar-border ${
74+
isExpanded ? "max-h-[300px] opacity-100" : "max-h-0 opacity-0"
75+
}`}
76+
>
77+
<div className="p-2">
78+
{previousThoughts.map((thought, index) => (
79+
<div
80+
key={`cot-${thought.uuid || index}`}
81+
className="flex gap-x-2"
82+
>
83+
<p className="text-xs text-theme-text-secondary font-mono">
84+
{index + 1}/{previousThoughts.length}
85+
</p>
86+
<div
87+
className="flex items-center gap-x-3 p-2 animate-fadeUpIn"
88+
style={{ animationDelay: `${index * 50}ms` }}
89+
>
90+
<span className="text-xs text-theme-text-secondary font-mono">
91+
{thought.content}
92+
</span>
93+
</div>
94+
</div>
95+
))}
96+
{/* Append current thought to the end */}
97+
<div key={`cot-${currentThought.uuid}`} className="flex gap-x-2">
98+
<p className="text-xs text-theme-text-secondary font-mono">
99+
{previousThoughts.length + 1}/{previousThoughts.length + 1}
100+
</p>
101+
<div className="flex items-center gap-x-3 p-2 animate-fadeUpIn">
102+
<span className="text-xs text-theme-text-secondary font-mono">
103+
{currentThought.content}
104+
</span>
105+
</div>
106+
</div>
107+
</div>
108+
</div>
109+
)}
110+
</div>
111+
</div>
112+
);
113+
}

frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx

+125-67
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React, { useEffect, useRef, useState } from "react";
1+
import { useEffect, useRef, useState, useMemo, useCallback } from "react";
22
import HistoricalMessage from "./HistoricalMessage";
33
import PromptReply from "./PromptReply";
4+
import StatusResponse from "./StatusResponse";
45
import { useManageWorkspaceModal } from "../../../Modals/ManageWorkspace";
56
import ManageWorkspace from "../../../Modals/ManageWorkspace";
67
import { ArrowDown } from "@phosphor-icons/react";
@@ -12,6 +13,7 @@ import { useParams } from "react-router-dom";
1213
import paths from "@/utils/paths";
1314
import Appearance from "@/models/appearance";
1415
import useTextSize from "@/hooks/useTextSize";
16+
import { v4 } from "uuid";
1517

1618
export default function ChatHistory({
1719
history = [],
@@ -136,6 +138,42 @@ export default function ChatHistory({
136138
);
137139
};
138140

141+
const compiledHistory = useMemo(
142+
() =>
143+
buildMessages({
144+
workspace,
145+
history,
146+
regenerateAssistantMessage,
147+
saveEditedMessage,
148+
forkThread,
149+
}),
150+
[
151+
workspace,
152+
history,
153+
regenerateAssistantMessage,
154+
saveEditedMessage,
155+
forkThread,
156+
]
157+
);
158+
const lastMessageInfo = useMemo(() => getLastMessageInfo(history), [history]);
159+
const renderStatusResponse = useCallback(
160+
(item, index) => {
161+
const hasSubsequentMessages = index < compiledHistory.length - 1;
162+
return (
163+
<StatusResponse
164+
key={`status-group-${index}`}
165+
messages={item}
166+
isThinking={!hasSubsequentMessages && lastMessageInfo.isAnimating}
167+
showCheckmark={
168+
hasSubsequentMessages ||
169+
(!lastMessageInfo.isAnimating && !lastMessageInfo.isStatusResponse)
170+
}
171+
/>
172+
);
173+
},
174+
[compiledHistory.length, lastMessageInfo]
175+
);
176+
139177
if (history.length === 0 && !hasAttachments) {
140178
return (
141179
<div className="flex flex-col h-full md:mt-0 pb-44 md:pb-40 w-full justify-end items-center">
@@ -176,61 +214,14 @@ export default function ChatHistory({
176214

177215
return (
178216
<div
179-
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 ${
180-
showScrollbar ? "show-scrollbar" : "no-scroll"
181-
}`}
217+
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"}`}
182218
id="chat-history"
183219
ref={chatHistoryRef}
184220
onScroll={handleScroll}
185221
>
186-
{history.map((props, index) => {
187-
const isLastBotReply =
188-
index === history.length - 1 && props.role === "assistant";
189-
190-
if (props?.type === "statusResponse" && !!props.content) {
191-
return <StatusResponse key={props.uuid} props={props} />;
192-
}
193-
194-
if (props.type === "rechartVisualize" && !!props.content) {
195-
return (
196-
<Chartable key={props.uuid} workspace={workspace} props={props} />
197-
);
198-
}
199-
200-
if (isLastBotReply && props.animate) {
201-
return (
202-
<PromptReply
203-
key={props.uuid}
204-
uuid={props.uuid}
205-
reply={props.content}
206-
pending={props.pending}
207-
sources={props.sources}
208-
error={props.error}
209-
workspace={workspace}
210-
closed={props.closed}
211-
/>
212-
);
213-
}
214-
215-
return (
216-
<HistoricalMessage
217-
key={index}
218-
message={props.content}
219-
role={props.role}
220-
workspace={workspace}
221-
sources={props.sources}
222-
feedbackScore={props.feedbackScore}
223-
chatId={props.chatId}
224-
error={props.error}
225-
attachments={props.attachments}
226-
regenerateMessage={regenerateAssistantMessage}
227-
isLastMessage={isLastBotReply}
228-
saveEditedMessage={saveEditedMessage}
229-
forkThread={forkThread}
230-
metrics={props.metrics}
231-
/>
232-
);
233-
})}
222+
{compiledHistory.map((item, index) =>
223+
Array.isArray(item) ? renderStatusResponse(item, index) : item
224+
)}
234225
{showing && (
235226
<ManageWorkspace hideModal={hideModal} providedSlug={workspace.slug} />
236227
)}
@@ -253,21 +244,13 @@ export default function ChatHistory({
253244
);
254245
}
255246

256-
function StatusResponse({ props }) {
257-
return (
258-
<div className="flex justify-center items-end w-full">
259-
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
260-
<div className="flex gap-x-5">
261-
<span
262-
className={`text-xs inline-block p-2 rounded-lg text-white/60 font-mono whitespace-pre-line`}
263-
>
264-
{props.content}
265-
</span>
266-
</div>
267-
</div>
268-
</div>
269-
);
270-
}
247+
const getLastMessageInfo = (history) => {
248+
const lastMessage = history?.[history.length - 1] || {};
249+
return {
250+
isAnimating: lastMessage?.animate,
251+
isStatusResponse: lastMessage?.type === "statusResponse",
252+
};
253+
};
271254

272255
function WorkspaceChatSuggestions({ suggestions = [], sendSuggestion }) {
273256
if (suggestions.length === 0) return null;
@@ -286,3 +269,78 @@ function WorkspaceChatSuggestions({ suggestions = [], sendSuggestion }) {
286269
</div>
287270
);
288271
}
272+
273+
/**
274+
* Builds the history of messages for the chat.
275+
* This is mostly useful for rendering the history in a way that is easy to understand.
276+
* as well as compensating for agent thinking and other messages that are not part of the history, but
277+
* are still part of the chat.
278+
*
279+
* @param {Object} param0 - The parameters for building the messages.
280+
* @param {Array} param0.history - The history of messages.
281+
* @param {Object} param0.workspace - The workspace object.
282+
* @param {Function} param0.regenerateAssistantMessage - The function to regenerate the assistant message.
283+
* @param {Function} param0.saveEditedMessage - The function to save the edited message.
284+
* @param {Function} param0.forkThread - The function to fork the thread.
285+
* @returns {Array} The compiled history of messages.
286+
*/
287+
function buildMessages({
288+
history,
289+
workspace,
290+
regenerateAssistantMessage,
291+
saveEditedMessage,
292+
forkThread,
293+
}) {
294+
return history.reduce((acc, props, index) => {
295+
const isLastBotReply =
296+
index === history.length - 1 && props.role === "assistant";
297+
298+
if (props?.type === "statusResponse" && !!props.content) {
299+
if (acc.length > 0 && Array.isArray(acc[acc.length - 1])) {
300+
acc[acc.length - 1].push(props);
301+
} else {
302+
acc.push([props]);
303+
}
304+
return acc;
305+
}
306+
307+
if (props.type === "rechartVisualize" && !!props.content) {
308+
acc.push(
309+
<Chartable key={props.uuid} workspace={workspace} props={props} />
310+
);
311+
} else if (isLastBotReply && props.animate) {
312+
acc.push(
313+
<PromptReply
314+
key={props.uuid || v4()}
315+
uuid={props.uuid}
316+
reply={props.content}
317+
pending={props.pending}
318+
sources={props.sources}
319+
error={props.error}
320+
workspace={workspace}
321+
closed={props.closed}
322+
/>
323+
);
324+
} else {
325+
acc.push(
326+
<HistoricalMessage
327+
key={index}
328+
message={props.content}
329+
role={props.role}
330+
workspace={workspace}
331+
sources={props.sources}
332+
feedbackScore={props.feedbackScore}
333+
chatId={props.chatId}
334+
error={props.error}
335+
attachments={props.attachments}
336+
regenerateMessage={regenerateAssistantMessage}
337+
isLastMessage={isLastBotReply}
338+
saveEditedMessage={saveEditedMessage}
339+
forkThread={forkThread}
340+
metrics={props.metrics}
341+
/>
342+
);
343+
}
344+
return acc;
345+
}, []);
346+
}

0 commit comments

Comments
 (0)