PluginProbe
Extendify / 3.0.4
Extendify v3.0.4
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / components / ChatMessages.jsx

ChatMessages.jsx in Extendify 3.0.4, at src/Agent/components/ChatMessages.jsx

187 lines 6.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { AgentMessage } from '@agent/components/messages/AgentMessage';
2 import { StatusMessage } from '@agent/components/messages/StatusMessage';
3 import { UserMessage } from '@agent/components/messages/UserMessage';
4 import { WorkflowComponent } from '@agent/components/messages/WorkflowComponent';
5 import { WorkflowMessage } from '@agent/components/messages/WorkflowMessage';
6 import { ScrollDownButton } from '@agent/components/ScrollDownButton';
7 import { ScrollIntoViewOnce } from '@agent/components/ScrollIntoViewOnce';
8 import { useWhenFinishedToolProps } from '@agent/hooks/useWhenFinishedToolProps';
9 import { useChatStore } from '@agent/state/chat';
10 import { useGlobalStore } from '@agent/state/global';
11 import { useWorkflowStore } from '@agent/state/workflows';
12 import {
13 createElement,
14 useEffect,
15 useLayoutEffect,
16 useRef,
17 useState,
18 } from '@wordpress/element';
19
20 export const ChatMessages = () => {
21 const { open } = useGlobalStore();
22 const { messages } = useChatStore();
23 const { getWorkflow } = useWorkflowStore();
24 const workflow = getWorkflow();
25 const whenFinishedToolProps = useWhenFinishedToolProps();
26 const whenFinishedComponent = workflow?.whenFinished?.component;
27 const [canScrollDown, setCanScrollDown] = useState(false);
28 const containerRef = useRef(null);
29 const isFreshPageLoad = useRef(true);
30 const [ready, setReady] = useState(false);
31
32 // If last message is a user message, move it to the top
33 const isUserMessage =
34 messages.filter(({ type }) => type !== 'status').at(-1)?.details?.role ===
35 'user';
36
37 useEffect(() => {
38 if (!containerRef.current || !open) return;
39 if (!isFreshPageLoad.current) return;
40 isFreshPageLoad.current = false;
41 // Scroll to the bottom of the chat container on load
42 const c = containerRef.current;
43 const last = c.querySelector(
44 '#extendify-agent-chat-scroll-area > :last-child',
45 );
46 let id2;
47 const id = requestAnimationFrame(() => {
48 id2 = requestAnimationFrame(() => {
49 last?.scrollIntoView({ behavior: 'auto', block: 'start' });
50 setReady(true);
51 });
52 });
53 return () => {
54 cancelAnimationFrame(id);
55 cancelAnimationFrame(id2);
56 isFreshPageLoad.current = true;
57 setReady(false);
58 };
59 }, [open]);
60
61 // Handles scrolling to the top of the last user message
62 // TODO: if the user sends in a long message, maybe we scroll to the bottom
63 // of the message offset by 2-3 lines
64 useEffect(() => {
65 if (!containerRef.current) return;
66 if (!isUserMessage) return;
67 const c = containerRef.current;
68 const messages = c.querySelectorAll('[data-agent-message-role="user"]');
69 const last = messages[messages.length - 1];
70 if (!last || messages.length < 2) return;
71 const scrollArea = c.querySelector('#extendify-agent-chat-scroll-area');
72 const lastRect = last.getBoundingClientRect();
73 const innerHeight = Array.from(scrollArea.children).reduce(
74 (sum, child) => sum + child.offsetHeight,
75 0,
76 );
77 const minHeight = innerHeight + c.clientHeight - lastRect.height;
78 scrollArea.style.minHeight = `${minHeight}px`;
79 last.scrollIntoView({ behavior: 'smooth', block: 'start' });
80 }, [isUserMessage, messages]);
81
82 // Handles the scroll down button visibility
83 useLayoutEffect(() => {
84 const c = containerRef.current;
85 if (!c) return;
86 const last = c.querySelector(
87 '#extendify-agent-chat-scroll-area > :last-child',
88 );
89 if (!last) return;
90
91 const areWeAtBottom = c.scrollTop + c.clientHeight >= c.scrollHeight - 4;
92 if (areWeAtBottom) return setCanScrollDown(false);
93
94 const observer = new IntersectionObserver(
95 ([entry]) => {
96 const last = c.querySelector(
97 '#extendify-agent-chat-scroll-area > :last-child',
98 );
99 if (!last) return setCanScrollDown(false);
100 const areWeAtBottom =
101 c.scrollTop + c.clientHeight >= c.scrollHeight - 4;
102 if (areWeAtBottom) return setCanScrollDown(false);
103 setCanScrollDown(!entry.isIntersecting);
104 },
105 { root: c, threshold: 0.1 },
106 );
107
108 observer.observe(last);
109 return () => observer.disconnect();
110 }, [messages]);
111
112 return (
113 <div
114 ref={containerRef}
115 style={{ overscrollBehavior: 'contain' }}
116 className="relative grow overflow-y-auto overflow-x-hidden p-1 pb-0 text-sm text-gray-900 md:p-2"
117 >
118 <div
119 id="extendify-agent-chat-scroll-area"
120 className={ready ? '' : 'invisible pointer-events-none'}
121 >
122 {messages.map((message) => {
123 const isLastMessage = messages.at(-1)?.id === message.id;
124 const freshLoad = isFreshPageLoad.current;
125 if (message.details?.role === 'user') {
126 return <UserMessage key={message.id} message={message} />;
127 }
128 if (message.details?.role === 'assistant') {
129 return (
130 <AgentMessage
131 key={message.id}
132 animate={!freshLoad}
133 message={message}
134 />
135 );
136 }
137 if (message.type === 'workflow') {
138 return <WorkflowMessage key={message.id} message={message} />;
139 }
140 if (message.type === 'workflow-component') {
141 return <WorkflowComponent key={message.id} message={message} />;
142 }
143 if (
144 message.type === 'status' &&
145 // Only show the status if it's last, or a workflow-tool-completed message
146 (isLastMessage ||
147 ['workflow-tool-completed', 'workflow-canceled'].includes(
148 message.details?.type,
149 ))
150 ) {
151 const isError = message.details?.type === 'error';
152 return (
153 <StatusMessage
154 animate={!isError}
155 key={message.id}
156 status={message}
157 />
158 );
159 }
160 return null;
161 })}
162 {!workflow?.needsRedirect?.() &&
163 whenFinishedToolProps?.id &&
164 whenFinishedComponent ? (
165 <ScrollIntoViewOnce>
166 {createElement(whenFinishedComponent, whenFinishedToolProps)}
167 </ScrollIntoViewOnce>
168 ) : null}
169 {workflow?.needsRedirect?.() ? <workflow.redirectComponent /> : null}
170 </div>
171 <ScrollDownButton
172 canScrollDown={canScrollDown}
173 onClick={() => {
174 if (!containerRef.current) return;
175 const c = containerRef.current;
176 // Scroll the last message into view
177 const last = c.querySelector(
178 '#extendify-agent-chat-scroll-area > :last-child',
179 );
180 if (!last) return;
181 last.scrollIntoView({ behavior: 'smooth', block: 'start' });
182 }}
183 />
184 </div>
185 );
186 };
187