PluginProbe
Extendify / 3.1.3
Extendify v3.1.3
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.1.3, at src/Agent/components/ChatMessages.jsx

168 lines 5.6 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 { StatusIndicator } from '@agent/components/messages/StatusIndicator';
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 = messages.at(-1)?.details?.role === 'user';
34
35 useEffect(() => {
36 if (!containerRef.current || !open) return;
37 if (!isFreshPageLoad.current) return;
38 isFreshPageLoad.current = false;
39 // Scroll to the bottom of the chat container on load
40 const c = containerRef.current;
41 const last = c.querySelector(
42 '#extendify-agent-chat-scroll-area > :last-child',
43 );
44 let id2;
45 const id = requestAnimationFrame(() => {
46 id2 = requestAnimationFrame(() => {
47 last?.scrollIntoView({ behavior: 'auto', block: 'start' });
48 setReady(true);
49 });
50 });
51 return () => {
52 cancelAnimationFrame(id);
53 cancelAnimationFrame(id2);
54 isFreshPageLoad.current = true;
55 setReady(false);
56 };
57 }, [open]);
58
59 // Handles scrolling to the top of the last user message
60 // TODO: if the user sends in a long message, maybe we scroll to the bottom
61 // of the message offset by 2-3 lines
62 useEffect(() => {
63 if (!containerRef.current) return;
64 if (!isUserMessage) return;
65 const c = containerRef.current;
66 const messages = c.querySelectorAll('[data-agent-message-role="user"]');
67 const last = messages[messages.length - 1];
68 if (!last || messages.length < 2) return;
69 const scrollArea = c.querySelector('#extendify-agent-chat-scroll-area');
70 const lastRect = last.getBoundingClientRect();
71 const innerHeight = Array.from(scrollArea.children).reduce(
72 (sum, child) => sum + child.offsetHeight,
73 0,
74 );
75 const minHeight = innerHeight + c.clientHeight - lastRect.height;
76 scrollArea.style.minHeight = `${minHeight}px`;
77 last.scrollIntoView({ behavior: 'smooth', block: 'start' });
78 }, [isUserMessage, messages]);
79
80 // Handles the scroll down button visibility
81 useLayoutEffect(() => {
82 const c = containerRef.current;
83 if (!c) return;
84 const last = c.querySelector(
85 '#extendify-agent-chat-scroll-area > :last-child',
86 );
87 if (!last) return;
88
89 const areWeAtBottom = c.scrollTop + c.clientHeight >= c.scrollHeight - 4;
90 if (areWeAtBottom) return setCanScrollDown(false);
91
92 const observer = new IntersectionObserver(
93 ([entry]) => {
94 const last = c.querySelector(
95 '#extendify-agent-chat-scroll-area > :last-child',
96 );
97 if (!last) return setCanScrollDown(false);
98 const areWeAtBottom =
99 c.scrollTop + c.clientHeight >= c.scrollHeight - 4;
100 if (areWeAtBottom) return setCanScrollDown(false);
101 setCanScrollDown(!entry.isIntersecting);
102 },
103 { root: c, threshold: 0.1 },
104 );
105
106 observer.observe(last);
107 return () => observer.disconnect();
108 }, [messages]);
109
110 return (
111 <div
112 ref={containerRef}
113 style={{ overscrollBehavior: 'contain' }}
114 className="relative grow overflow-y-auto overflow-x-hidden p-1 pb-0 text-sm text-gray-900 md:p-2 scheme-light"
115 >
116 <div
117 id="extendify-agent-chat-scroll-area"
118 className={ready ? '' : 'invisible pointer-events-none'}
119 >
120 {messages.map((message) => {
121 const freshLoad = isFreshPageLoad.current;
122 if (message.details?.role === 'user') {
123 return <UserMessage key={message.id} message={message} />;
124 }
125 if (message.details?.role === 'assistant') {
126 return (
127 <AgentMessage
128 key={message.id}
129 animate={!freshLoad}
130 message={message}
131 />
132 );
133 }
134 if (message.type === 'workflow') {
135 return <WorkflowMessage key={message.id} message={message} />;
136 }
137 if (message.type === 'workflow-component') {
138 return <WorkflowComponent key={message.id} message={message} />;
139 }
140 return null;
141 })}
142 <StatusIndicator />
143 {!workflow?.needsRedirect?.() &&
144 whenFinishedToolProps?.id &&
145 whenFinishedComponent ? (
146 <ScrollIntoViewOnce>
147 {createElement(whenFinishedComponent, whenFinishedToolProps)}
148 </ScrollIntoViewOnce>
149 ) : null}
150 {workflow?.needsRedirect?.() ? <workflow.redirectComponent /> : null}
151 </div>
152 <ScrollDownButton
153 canScrollDown={canScrollDown}
154 onClick={() => {
155 if (!containerRef.current) return;
156 const c = containerRef.current;
157 // Scroll the last message into view
158 const last = c.querySelector(
159 '#extendify-agent-chat-scroll-area > :last-child',
160 );
161 if (!last) return;
162 last.scrollIntoView({ behavior: 'smooth', block: 'start' });
163 }}
164 />
165 </div>
166 );
167 };
168