PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
extendify / src / Agent / components / messages / StatusIndicator.jsx

StatusIndicator.jsx in Extendify 3.2.2, at src/Agent/components/messages/StatusIndicator.jsx

142 lines 4.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useChatStore } from '@agent/state/chat';
2 import { useStatusStore } from '@agent/state/status';
3 import { hasRunComponent } from '@agent/workflows/abilities/components/run';
4 import { useEffect, useMemo, useState } from '@wordpress/element';
5 import { decodeEntities } from '@wordpress/html-entities';
6 import { __ } from '@wordpress/i18n';
7
8 const linesFor = (label) => ({
9 'calling-agent': __('Thinking...', 'extendify-local'),
10 'agent-working': [
11 __('Working on it...', 'extendify-local'),
12 __('Interpreting message...', 'extendify-local'),
13 __('Formulating a response...', 'extendify-local'),
14 __('Reviewing logic...', 'extendify-local'),
15 __('Checking the details...', 'extendify-local'),
16 __('Weighing the options...', 'extendify-local'),
17 __('Putting it together...', 'extendify-local'),
18 __('Still working on it...', 'extendify-local'),
19 ],
20 'workflow-tool-processing': label || __('Processing...', 'extendify-local'),
21 'tool-started': label || __('Gathering data...', 'extendify-local'),
22 'credits-exhausted': __('Usage limit reached', 'extendify-local'),
23 'credits-restored': __('Usage limit restored', 'extendify-local'),
24 });
25
26 const beat = () => 3000 + Math.random() * 2000;
27
28 // Randomized 3-5s beat so a long wait never looks stuck or syncs across sites.
29 // Kept across status changes, or 3s tool steps restart every beat before it lands.
30 const useCurrentLine = (requestId, lines) => {
31 const [tick, setTick] = useState({ requestId, count: 0, due: 0 });
32 const current = tick.requestId === requestId;
33 const count = current ? tick.count : 0;
34 const due = current ? tick.due : 0;
35
36 useEffect(() => {
37 if (!lines) return;
38 if (!due) {
39 setTick({ requestId, count, due: Date.now() + beat() });
40 return;
41 }
42 const timer = setTimeout(
43 () => setTick({ requestId, count: count + 1, due: Date.now() + beat() }),
44 Math.max(0, due - Date.now()),
45 );
46 return () => clearTimeout(timer);
47 }, [lines, requestId, count, due]);
48
49 if (!Array.isArray(lines)) return lines;
50 return lines[Math.min(count, lines.length - 1)];
51 };
52
53 export const useStatusLine = () => {
54 const leavingPage = useStatusStore((s) => s.leavingPage);
55 const last = useChatStore((s) => s.messages.at(-1));
56 const requestId = useChatStore(
57 (s) =>
58 s.messages.findLast(
59 (m) => m.type === 'message' && m.details?.role === 'user',
60 )?.id,
61 );
62 // A live picker shows its own waiting state; the status line would repeat it.
63 const awaitingPicker =
64 last?.type === 'tool' &&
65 !('result' in (last.details ?? {})) &&
66 (last.details?.id === 'acquire-image' || hasRunComponent(last.details?.id));
67 const { type, label } = useStatusStore((s) => s.statuses.at(-1)) ?? {};
68 const lines = useMemo(() => linesFor(label)[type], [label, type]);
69 const currentLine = useCurrentLine(requestId, lines);
70 const text = leavingPage
71 ? __('Refreshing the page to show your changes…', 'extendify-local')
72 : currentLine;
73
74 if (!text || awaitingPicker) return null;
75 return decodeEntities(text);
76 };
77
78 const randomWave = (startY) => {
79 const cycles = 1.5 + Math.random();
80 const offset = startY - 12;
81 const amplitude = Math.max(3.5 + Math.random() * 2.5, Math.abs(offset));
82 const rise = Math.asin(offset / amplitude);
83 const phase = Math.random() < 0.5 ? rise : Math.PI - rise;
84 const points = Array.from({ length: 31 }, (_, i) => {
85 const t = i / 30;
86 const y = 12 + amplitude * Math.sin(Math.PI * 2 * cycles * t + phase);
87 return [3 + 18 * t, y];
88 });
89 return {
90 d: `M${points.map(([x, y]) => `${x.toFixed(1)} ${y.toFixed(1)}`).join('L')}`,
91 endY: points.at(-1)[1],
92 };
93 };
94
95 export const StatusScribble = () => {
96 const [wave, setWave] = useState(() => randomWave(12));
97 return (
98 <svg
99 viewBox="0 0 24 24"
100 width="16"
101 height="16"
102 fill="none"
103 aria-hidden="true"
104 className="shrink-0 text-design-main"
105 >
106 <path
107 className="status-scribble"
108 d={wave.d}
109 onAnimationIteration={() => setWave((w) => randomWave(w.endY))}
110 stroke="currentColor"
111 strokeWidth="2"
112 strokeLinecap="round"
113 strokeLinejoin="round"
114 pathLength="1"
115 />
116 </svg>
117 );
118 };
119
120 export const StatusIndicator = () => {
121 const line = useStatusLine();
122 // Every chat message clears the statuses; unmounting then cuts the icon's loop short.
123 return (
124 <div
125 data-status-indicator
126 className={
127 line
128 ? 'flex items-center gap-1.5 px-1 pb-1.5 text-sm italic text-gray-700'
129 : 'invisible flex h-0 overflow-hidden'
130 }
131 >
132 <StatusScribble />
133 {/* Reusing the node drops the new line mid-sweep. */}
134 {line ? (
135 <span key={line} className="status-animation">
136 {line}
137 </span>
138 ) : null}
139 </div>
140 );
141 };
142