@alpaca-editor/core 1.0.4064 → 1.0.4066
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editor/ContextMenu.js +0 -2
- package/dist/editor/ContextMenu.js.map +1 -1
- package/dist/editor/ImageEditButton.js +10 -3
- package/dist/editor/ImageEditButton.js.map +1 -1
- package/dist/editor/ai/AgentTerminal.d.ts +3 -2
- package/dist/editor/ai/AgentTerminal.js +386 -94
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/Agents.js +67 -25
- package/dist/editor/ai/Agents.js.map +1 -1
- package/dist/editor/ai/AiResponseMessage.d.ts +6 -1
- package/dist/editor/ai/AiResponseMessage.js +63 -3
- package/dist/editor/ai/AiResponseMessage.js.map +1 -1
- package/dist/editor/ai/AiTerminal.js +27 -2
- package/dist/editor/ai/AiTerminal.js.map +1 -1
- package/dist/editor/client/EditorClient.js +32 -19
- package/dist/editor/client/EditorClient.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +4 -2
- package/dist/editor/client/editContext.js.map +1 -1
- package/dist/editor/client/operations.js +9 -6
- package/dist/editor/client/operations.js.map +1 -1
- package/dist/editor/commands/componentCommands.js +57 -7
- package/dist/editor/commands/componentCommands.js.map +1 -1
- package/dist/editor/field-types/richtext/contextMenuFactory.js +0 -3
- package/dist/editor/field-types/richtext/contextMenuFactory.js.map +1 -1
- package/dist/editor/menubar/ToolbarFactory.js +5 -2
- package/dist/editor/menubar/ToolbarFactory.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/UtilityControls.js +1 -1
- package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
- package/dist/editor/page-editor-chrome/CommentHighlighting.js +6 -4
- package/dist/editor/page-editor-chrome/CommentHighlighting.js.map +1 -1
- package/dist/editor/page-editor-chrome/CommentHighlightings.js +1 -1
- package/dist/editor/page-editor-chrome/CommentHighlightings.js.map +1 -1
- package/dist/editor/page-editor-chrome/FrameMenu.js +6 -8
- package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.js +70 -4
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/reviews/Comment.js +3 -58
- package/dist/editor/reviews/Comment.js.map +1 -1
- package/dist/editor/reviews/CommentDisplayPopover.js +2 -3
- package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
- package/dist/editor/reviews/CommentEditor.js +2 -2
- package/dist/editor/reviews/CommentEditor.js.map +1 -1
- package/dist/editor/reviews/Comments.js +4 -0
- package/dist/editor/reviews/Comments.js.map +1 -1
- package/dist/editor/reviews/Reviews.js +2 -2
- package/dist/editor/reviews/Reviews.js.map +1 -1
- package/dist/editor/reviews/commentAi.d.ts +7 -0
- package/dist/editor/reviews/commentAi.js +86 -0
- package/dist/editor/reviews/commentAi.js.map +1 -0
- package/dist/editor/sidebar/ComponentTree.js +157 -49
- package/dist/editor/sidebar/ComponentTree.js.map +1 -1
- package/dist/editor/sidebar/Debug.js +1 -1
- package/dist/editor/sidebar/Debug.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +15 -4
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/editor/ContextMenu.tsx +0 -2
- package/src/editor/ImageEditButton.tsx +36 -8
- package/src/editor/ai/AgentTerminal.tsx +436 -65
- package/src/editor/ai/Agents.tsx +217 -117
- package/src/editor/ai/AiResponseMessage.tsx +106 -2
- package/src/editor/ai/AiTerminal.tsx +27 -0
- package/src/editor/client/EditorClient.tsx +41 -20
- package/src/editor/client/editContext.ts +4 -2
- package/src/editor/client/operations.ts +9 -8
- package/src/editor/commands/componentCommands.tsx +61 -13
- package/src/editor/field-types/richtext/components/EditorDropdown.css +1 -0
- package/src/editor/field-types/richtext/contextMenuFactory.tsx +0 -4
- package/src/editor/menubar/ToolbarFactory.tsx +6 -2
- package/src/editor/menubar/toolbar-sections/UtilityControls.tsx +23 -19
- package/src/editor/page-editor-chrome/CommentHighlighting.tsx +6 -4
- package/src/editor/page-editor-chrome/CommentHighlightings.tsx +3 -1
- package/src/editor/page-editor-chrome/FrameMenu.tsx +6 -8
- package/src/editor/page-viewer/PageViewerFrame.tsx +80 -4
- package/src/editor/reviews/Comment.tsx +4 -66
- package/src/editor/reviews/CommentDisplayPopover.tsx +2 -3
- package/src/editor/reviews/CommentEditor.tsx +2 -2
- package/src/editor/reviews/Comments.tsx +12 -0
- package/src/editor/reviews/Reviews.tsx +2 -0
- package/src/editor/reviews/commentAi.ts +106 -0
- package/src/editor/sidebar/ComponentTree.tsx +223 -69
- package/src/editor/sidebar/Debug.tsx +1 -1
- package/src/revision.ts +2 -2
- package/src/types.ts +1 -1
- package/styles.css +0 -5
- package/dist/editor/ai/AiPromptPopover.d.ts +0 -7
- package/dist/editor/ai/AiPromptPopover.js +0 -111
- package/dist/editor/ai/AiPromptPopover.js.map +0 -1
- package/src/editor/ai/AiPromptPopover.tsx +0 -206
package/src/editor/ai/Agents.tsx
CHANGED
|
@@ -16,9 +16,16 @@ import {
|
|
|
16
16
|
getClosedAgents,
|
|
17
17
|
closeAgent as closeAgentService,
|
|
18
18
|
deleteAgent,
|
|
19
|
+
AgentMetadata,
|
|
19
20
|
} from "../services/agentService";
|
|
20
21
|
import { AgentTerminal } from "./AgentTerminal";
|
|
21
22
|
import { useEditContext } from "../client/editContext";
|
|
23
|
+
import {
|
|
24
|
+
Tooltip,
|
|
25
|
+
TooltipContent,
|
|
26
|
+
TooltipTrigger,
|
|
27
|
+
} from "../../components/ui/tooltip";
|
|
28
|
+
import { Button } from "../../components/ui/button";
|
|
22
29
|
|
|
23
30
|
// function convertAgentMessagesToTerminalMessages(
|
|
24
31
|
// agentMessages: AgentChatMessage[],
|
|
@@ -49,13 +56,30 @@ const ACTIVE_AGENT_STORAGE_KEY = "editor.activeAgentId";
|
|
|
49
56
|
export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
|
|
50
57
|
const [agents, setAgents] = useState<Agent[]>([]);
|
|
51
58
|
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
|
|
59
|
+
const activeAgentIdRef = useRef<string | null>(null);
|
|
52
60
|
const [historyPopoverOpen, setHistoryPopoverOpen] = useState(false);
|
|
53
61
|
const [menuPopoverOpen, setMenuPopoverOpen] = useState(false);
|
|
54
62
|
const [loadingAgents, setLoadingAgents] = useState(false);
|
|
55
63
|
const [inactiveAgents, setInactiveAgents] = useState<Agent[]>([]);
|
|
64
|
+
const [historyFilter, setHistoryFilter] = useState("");
|
|
65
|
+
const [initialMetadataMap, setInitialMetadataMap] = useState<
|
|
66
|
+
Record<string, AgentMetadata | undefined>
|
|
67
|
+
>({});
|
|
56
68
|
|
|
57
69
|
const editContext = useEditContext();
|
|
58
70
|
|
|
71
|
+
// Helper function to filter agents by name
|
|
72
|
+
const getFilteredInactiveAgents = (): Agent[] => {
|
|
73
|
+
if (!historyFilter.trim()) {
|
|
74
|
+
return inactiveAgents;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const filterLower = historyFilter.toLowerCase();
|
|
78
|
+
return inactiveAgents.filter((agent) =>
|
|
79
|
+
agent.name.toLowerCase().includes(filterLower),
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
59
83
|
// Helper function to get the most recently updated agent
|
|
60
84
|
const getMostRecentAgent = (agentList: Agent[]): Agent | null => {
|
|
61
85
|
if (agentList.length === 0) return null;
|
|
@@ -69,6 +93,7 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
|
|
|
69
93
|
// Helper function to set active agent and persist to localStorage
|
|
70
94
|
const setActiveAgentIdWithStorage = (agentId: string | null) => {
|
|
71
95
|
setActiveAgentId(agentId);
|
|
96
|
+
activeAgentIdRef.current = agentId;
|
|
72
97
|
|
|
73
98
|
if (agentId) {
|
|
74
99
|
localStorage.setItem(ACTIVE_AGENT_STORAGE_KEY, agentId);
|
|
@@ -77,26 +102,43 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
|
|
|
77
102
|
}
|
|
78
103
|
};
|
|
79
104
|
|
|
80
|
-
// Initialize with a default agent if none exist
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (agents.length === 0) {
|
|
83
|
-
const defaultAgent: Agent = {
|
|
84
|
-
status: "new",
|
|
85
|
-
id: crypto.randomUUID(),
|
|
86
|
-
name: `New Agent`,
|
|
87
|
-
updatedDate: new Date().toISOString(),
|
|
88
|
-
userId: "",
|
|
89
|
-
};
|
|
90
|
-
setAgents([defaultAgent]);
|
|
91
|
-
setActiveAgentId(defaultAgent.id);
|
|
92
|
-
}
|
|
93
|
-
}, [agents.length]);
|
|
94
105
|
|
|
95
106
|
// Load agents from backend on mount
|
|
96
107
|
useEffect(() => {
|
|
97
108
|
loadAgentsFromBackend();
|
|
98
109
|
}, []);
|
|
99
110
|
|
|
111
|
+
// Listen for external requests to add a new agent (e.g., AI command)
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const handleAddNewAgent = (ev: Event) => {
|
|
114
|
+
let initialMetadata: AgentMetadata | undefined = undefined;
|
|
115
|
+
try {
|
|
116
|
+
const ce = ev as unknown as CustomEvent;
|
|
117
|
+
initialMetadata = (ce.detail && (ce.detail as any).metadata) as
|
|
118
|
+
| AgentMetadata
|
|
119
|
+
| undefined;
|
|
120
|
+
} catch {}
|
|
121
|
+
|
|
122
|
+
addAgent(initialMetadata);
|
|
123
|
+
// Ensure the prompt focuses after the agent tab mounts
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
try {
|
|
126
|
+
window.dispatchEvent(new CustomEvent("editor:focusAgentPrompt"));
|
|
127
|
+
} catch {}
|
|
128
|
+
}, 60);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
window.addEventListener(
|
|
132
|
+
"editor:addNewAgent",
|
|
133
|
+
handleAddNewAgent as EventListener,
|
|
134
|
+
);
|
|
135
|
+
return () =>
|
|
136
|
+
window.removeEventListener(
|
|
137
|
+
"editor:addNewAgent",
|
|
138
|
+
handleAddNewAgent as EventListener,
|
|
139
|
+
);
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
100
142
|
// Subscribe to websocket messages for agent-started events
|
|
101
143
|
useEffect(() => {
|
|
102
144
|
if (!editContext?.addSocketMessageListener) return;
|
|
@@ -210,7 +252,7 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
|
|
|
210
252
|
// return null;
|
|
211
253
|
// };
|
|
212
254
|
|
|
213
|
-
const addAgent = () => {
|
|
255
|
+
const addAgent = (metadata?: AgentMetadata) => {
|
|
214
256
|
const newAgent: Agent = {
|
|
215
257
|
status: "new",
|
|
216
258
|
id: crypto.randomUUID(),
|
|
@@ -220,6 +262,9 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
|
|
|
220
262
|
};
|
|
221
263
|
setAgents((prev) => [...prev, newAgent]);
|
|
222
264
|
setActiveAgentIdWithStorage(newAgent.id);
|
|
265
|
+
if (metadata) {
|
|
266
|
+
setInitialMetadataMap((prev) => ({ ...prev, [newAgent.id]: metadata }));
|
|
267
|
+
}
|
|
223
268
|
};
|
|
224
269
|
|
|
225
270
|
const closeAgent = async (agentId: string) => {
|
|
@@ -235,7 +280,7 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
|
|
|
235
280
|
const filtered = prev.filter((a) => a.id !== agentId);
|
|
236
281
|
|
|
237
282
|
// If we're closing the active terminal, switch to the most recent remaining one or clear storage
|
|
238
|
-
if (
|
|
283
|
+
if (activeAgentIdRef.current === agentId) {
|
|
239
284
|
if (filtered.length > 0) {
|
|
240
285
|
const mostRecentAgent = getMostRecentAgent(filtered);
|
|
241
286
|
setActiveAgentIdWithStorage(mostRecentAgent?.id || null);
|
|
@@ -249,10 +294,12 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
|
|
|
249
294
|
};
|
|
250
295
|
|
|
251
296
|
const closeOtherAgents = async () => {
|
|
252
|
-
if (!
|
|
297
|
+
if (!activeAgentIdRef.current) return;
|
|
253
298
|
|
|
254
299
|
// Get agents to close (all except active)
|
|
255
|
-
const agentsToClose = agents.filter(
|
|
300
|
+
const agentsToClose = agents.filter(
|
|
301
|
+
(a) => a.id !== activeAgentIdRef.current,
|
|
302
|
+
);
|
|
256
303
|
|
|
257
304
|
// Permanently close each agent in the backend
|
|
258
305
|
const closePromises = agentsToClose.map(async (agent) => {
|
|
@@ -267,7 +314,7 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
|
|
|
267
314
|
await Promise.all(closePromises);
|
|
268
315
|
|
|
269
316
|
setAgents((prev) => {
|
|
270
|
-
return prev.filter((a) => a.id ===
|
|
317
|
+
return prev.filter((a) => a.id === activeAgentIdRef.current);
|
|
271
318
|
});
|
|
272
319
|
setMenuPopoverOpen(false);
|
|
273
320
|
};
|
|
@@ -333,11 +380,22 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
|
|
|
333
380
|
);
|
|
334
381
|
}
|
|
335
382
|
|
|
383
|
+
// Empty state when no agents are open
|
|
384
|
+
if (!loadingAgents && agents.length === 0) {
|
|
385
|
+
return (
|
|
386
|
+
<div className="flex h-full items-center justify-center">
|
|
387
|
+
<Button onClick={() => addAgent()}>
|
|
388
|
+
<Plus className="mr-2 h-4 w-4" strokeWidth={1} /> Create agent
|
|
389
|
+
</Button>
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
336
394
|
return (
|
|
337
395
|
<div className="flex h-full flex-col">
|
|
338
396
|
{/* Tab Header */}
|
|
339
397
|
<div className="flex items-center border-b border-gray-200 bg-gray-50">
|
|
340
|
-
<div className="flex flex-1 overflow-x-auto">
|
|
398
|
+
<div className="scrollbar-hide flex flex-1 overflow-x-auto">
|
|
341
399
|
{agents.map((agent) => (
|
|
342
400
|
<div
|
|
343
401
|
key={agent.id}
|
|
@@ -374,121 +432,160 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
|
|
|
374
432
|
getTabsMenuItems(),
|
|
375
433
|
);
|
|
376
434
|
// Then update the active tab on the next tick to avoid interfering with positioning
|
|
377
|
-
},
|
|
435
|
+
}, 150);
|
|
378
436
|
setActiveAgentIdWithStorage(agent.id);
|
|
379
437
|
}}
|
|
380
438
|
>
|
|
381
|
-
<
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
439
|
+
<Tooltip>
|
|
440
|
+
<TooltipTrigger asChild>
|
|
441
|
+
<span className="truncate" title={agent.name}>
|
|
442
|
+
{agent.name}
|
|
443
|
+
</span>
|
|
444
|
+
</TooltipTrigger>
|
|
445
|
+
<TooltipContent>{agent.name}</TooltipContent>
|
|
446
|
+
</Tooltip>
|
|
447
|
+
<SimpleIconButton
|
|
448
|
+
onClick={(e) => {
|
|
449
|
+
e.stopPropagation();
|
|
450
|
+
closeAgent(agent.id);
|
|
451
|
+
}}
|
|
452
|
+
icon={<X className="size-2" strokeWidth={1} />}
|
|
453
|
+
label="Close"
|
|
454
|
+
className="ml-1 opacity-60 hover:opacity-100"
|
|
455
|
+
/>
|
|
393
456
|
</div>
|
|
394
457
|
))}
|
|
395
458
|
</div>
|
|
396
459
|
|
|
397
460
|
{/* History Popover */}
|
|
398
|
-
|
|
399
|
-
<
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
461
|
+
{agents.length > 0 && (
|
|
462
|
+
<div className="flex items-center px-1">
|
|
463
|
+
<Popover
|
|
464
|
+
open={historyPopoverOpen}
|
|
465
|
+
onOpenChange={(open) => {
|
|
466
|
+
setHistoryPopoverOpen(open);
|
|
467
|
+
if (!open) {
|
|
468
|
+
setHistoryFilter(""); // Clear filter when popover closes
|
|
469
|
+
}
|
|
470
|
+
}}
|
|
471
|
+
>
|
|
472
|
+
<PopoverTrigger asChild>
|
|
473
|
+
<SimpleIconButton
|
|
474
|
+
onClick={() => {}}
|
|
475
|
+
icon={<History className="size-4" strokeWidth={1} />}
|
|
476
|
+
label="Agent History"
|
|
477
|
+
className="text-gray-600 hover:text-gray-800"
|
|
478
|
+
/>
|
|
479
|
+
</PopoverTrigger>
|
|
480
|
+
<PopoverContent className="w-64 p-0" align="end">
|
|
481
|
+
<div className="border-b border-gray-100 px-3 py-2 text-xs font-medium text-gray-500">
|
|
482
|
+
Closed Agents
|
|
483
|
+
</div>
|
|
484
|
+
{inactiveAgents.length > 0 && (
|
|
485
|
+
<div className="border-b border-gray-100 px-3 py-2">
|
|
486
|
+
<input
|
|
487
|
+
type="text"
|
|
488
|
+
placeholder="Filter agents..."
|
|
489
|
+
value={historyFilter}
|
|
490
|
+
onChange={(e) => setHistoryFilter(e.target.value)}
|
|
491
|
+
className="w-full rounded border border-gray-200 px-2 py-1 text-xs focus:border-blue-500 focus:outline-none"
|
|
492
|
+
/>
|
|
419
493
|
</div>
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
494
|
+
)}
|
|
495
|
+
<div className="max-h-80 overflow-y-auto">
|
|
496
|
+
{(() => {
|
|
497
|
+
const filteredAgents = getFilteredInactiveAgents();
|
|
498
|
+
|
|
499
|
+
if (inactiveAgents.length === 0) {
|
|
500
|
+
return (
|
|
501
|
+
<div className="px-3 py-2 text-xs text-gray-500">
|
|
502
|
+
No closed agents found
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (filteredAgents.length === 0) {
|
|
508
|
+
return (
|
|
509
|
+
<div className="px-3 py-2 text-xs text-gray-500">
|
|
510
|
+
No agents match your filter
|
|
511
|
+
</div>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return filteredAgents.map((agent) => (
|
|
516
|
+
<div
|
|
517
|
+
key={agent.id}
|
|
518
|
+
className="cursor-pointer border-b border-gray-50 px-3 py-2 text-xs hover:bg-gray-50"
|
|
519
|
+
onClick={() => openAgentFromHistory(agent)}
|
|
520
|
+
>
|
|
521
|
+
<div className="flex items-center justify-between">
|
|
522
|
+
<div className="min-w-0 flex-1">
|
|
523
|
+
<div className="truncate font-medium text-gray-900">
|
|
524
|
+
{agent.name}
|
|
525
|
+
</div>
|
|
526
|
+
<div className="text-xs text-gray-400">
|
|
527
|
+
{new Date(agent.updatedDate).toLocaleString()}
|
|
528
|
+
</div>
|
|
434
529
|
</div>
|
|
530
|
+
<SimpleIconButton
|
|
531
|
+
onClick={(e) => deleteAgentFromHistory(agent.id, e)}
|
|
532
|
+
icon={<Trash className="size-3" strokeWidth={1} />}
|
|
533
|
+
label="Delete Agent"
|
|
534
|
+
className="ml-2 text-red-600 opacity-60 hover:text-red-700 hover:opacity-100"
|
|
535
|
+
/>
|
|
435
536
|
</div>
|
|
436
|
-
<SimpleIconButton
|
|
437
|
-
onClick={(e) => deleteAgentFromHistory(agent.id, e)}
|
|
438
|
-
icon={<Trash className="size-3" strokeWidth={1} />}
|
|
439
|
-
label="Delete Agent"
|
|
440
|
-
className="ml-2 text-red-600 opacity-60 hover:text-red-700 hover:opacity-100"
|
|
441
|
-
/>
|
|
442
537
|
</div>
|
|
443
|
-
|
|
444
|
-
))
|
|
445
|
-
|
|
446
|
-
</
|
|
447
|
-
</
|
|
448
|
-
</
|
|
449
|
-
|
|
538
|
+
));
|
|
539
|
+
})()}
|
|
540
|
+
</div>
|
|
541
|
+
</PopoverContent>
|
|
542
|
+
</Popover>
|
|
543
|
+
</div>
|
|
544
|
+
)}
|
|
450
545
|
|
|
451
546
|
{/* Menu Popover */}
|
|
452
|
-
|
|
453
|
-
<
|
|
454
|
-
<
|
|
455
|
-
<
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
<
|
|
464
|
-
|
|
465
|
-
item
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
547
|
+
{agents.length > 0 && (
|
|
548
|
+
<div className="flex items-center px-1">
|
|
549
|
+
<Popover open={menuPopoverOpen} onOpenChange={setMenuPopoverOpen}>
|
|
550
|
+
<PopoverTrigger asChild>
|
|
551
|
+
<SimpleIconButton
|
|
552
|
+
onClick={() => {}}
|
|
553
|
+
icon={<MoreVertical className="size-4" strokeWidth={1} />}
|
|
554
|
+
label="Menu"
|
|
555
|
+
className="text-gray-600 hover:text-gray-800"
|
|
556
|
+
/>
|
|
557
|
+
</PopoverTrigger>
|
|
558
|
+
<PopoverContent className="w-48 p-0" align="end">
|
|
559
|
+
<div className="py-1">
|
|
560
|
+
{getTabsMenuItems().map((item) =>
|
|
561
|
+
item.separator ? (
|
|
562
|
+
<div key={item.id} className="my-1 h-px bg-gray-100" />
|
|
563
|
+
) : (
|
|
564
|
+
<button
|
|
565
|
+
key={item.id}
|
|
566
|
+
onClick={async (e) => {
|
|
567
|
+
if (item.command) await item.command(e);
|
|
568
|
+
setMenuPopoverOpen(false);
|
|
569
|
+
}}
|
|
570
|
+
disabled={!!item.disabled}
|
|
571
|
+
className="w-full px-3 py-2 text-left text-xs hover:bg-gray-50 disabled:cursor-not-allowed disabled:text-gray-400"
|
|
572
|
+
>
|
|
573
|
+
{item.label}
|
|
574
|
+
</button>
|
|
575
|
+
),
|
|
576
|
+
)}
|
|
577
|
+
</div>
|
|
578
|
+
</PopoverContent>
|
|
579
|
+
</Popover>
|
|
580
|
+
</div>
|
|
581
|
+
)}
|
|
485
582
|
|
|
486
583
|
{/* Add Terminal Button */}
|
|
487
584
|
<div className="flex items-center px-1">
|
|
488
585
|
<SimpleIconButton
|
|
489
|
-
onClick={addAgent}
|
|
586
|
+
onClick={() => addAgent()}
|
|
490
587
|
icon={<Plus className="size-4" strokeWidth={1} />}
|
|
491
|
-
label="Add
|
|
588
|
+
label={agents.length === 0 ? "Create Agent" : "Add Agent"}
|
|
492
589
|
className="text-gray-600 hover:text-gray-800"
|
|
493
590
|
/>
|
|
494
591
|
</div>
|
|
@@ -509,7 +606,10 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
|
|
|
509
606
|
activeAgentId === agent.id ? "block" : "hidden",
|
|
510
607
|
)}
|
|
511
608
|
>
|
|
512
|
-
<AgentTerminal
|
|
609
|
+
<AgentTerminal
|
|
610
|
+
agentStub={agent}
|
|
611
|
+
initialMetadata={initialMetadataMap[agent.id]}
|
|
612
|
+
/>
|
|
513
613
|
</div>
|
|
514
614
|
))}
|
|
515
615
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
1
|
+
import React, { useState, useEffect, useMemo } from "react";
|
|
2
2
|
|
|
3
3
|
import { useEditContext } from "../client/editContext";
|
|
4
4
|
import { EditOperation } from "../../types";
|
|
@@ -6,17 +6,80 @@ import { Message } from "./AiTerminal";
|
|
|
6
6
|
import { ToolCallDisplay } from "./ToolCallDisplay";
|
|
7
7
|
|
|
8
8
|
import { X, Bot, Loader2 } from "lucide-react";
|
|
9
|
+
import { Button } from "../../components/ui/button";
|
|
10
|
+
|
|
11
|
+
type QuickAction = {
|
|
12
|
+
id?: string;
|
|
13
|
+
label: string;
|
|
14
|
+
prompt?: string;
|
|
15
|
+
value?: string;
|
|
16
|
+
style?: "primary" | "secondary" | "destructive" | "outline";
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function parseQuickActionsFromContent(content?: string): {
|
|
20
|
+
cleanText: string;
|
|
21
|
+
actions: QuickAction[];
|
|
22
|
+
} {
|
|
23
|
+
if (!content) return { cleanText: "", actions: [] };
|
|
24
|
+
|
|
25
|
+
// Match fenced code blocks like ```quick_action_buttons ...```
|
|
26
|
+
const fenceRegex = /```quick_action_buttons\s+([\s\S]*?)```/gi;
|
|
27
|
+
|
|
28
|
+
let cleanText = content;
|
|
29
|
+
const actions: QuickAction[] = [];
|
|
30
|
+
|
|
31
|
+
let match: RegExpExecArray | null;
|
|
32
|
+
while ((match = fenceRegex.exec(content)) !== null) {
|
|
33
|
+
const jsonText = match[1]?.trim();
|
|
34
|
+
if (!jsonText) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(jsonText);
|
|
39
|
+
const rawActions =
|
|
40
|
+
parsed?.actions || parsed?.buttons || parsed?.choices || [];
|
|
41
|
+
if (Array.isArray(rawActions)) {
|
|
42
|
+
rawActions.forEach((a) => {
|
|
43
|
+
if (!a) return;
|
|
44
|
+
const action: QuickAction = {
|
|
45
|
+
id: a.id,
|
|
46
|
+
label: a.label || a.text || String(a.value || a.prompt || ""),
|
|
47
|
+
prompt: a.prompt,
|
|
48
|
+
value: a.value,
|
|
49
|
+
style: a.style,
|
|
50
|
+
};
|
|
51
|
+
if (action.label) actions.push(action);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
} catch {}
|
|
55
|
+
// Remove this code block from the rendered text
|
|
56
|
+
cleanText = cleanText.replace(match[0], "");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { cleanText: cleanText.trim(), actions };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function simpleFormatToHtml(text: string): string {
|
|
63
|
+
return text
|
|
64
|
+
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
|
65
|
+
.replace(/\n/g, "<br/>");
|
|
66
|
+
}
|
|
9
67
|
|
|
10
68
|
export function AiResponseMessage({
|
|
11
69
|
messages,
|
|
12
70
|
finished,
|
|
13
71
|
editOperations,
|
|
14
72
|
error,
|
|
73
|
+
onQuickAction,
|
|
15
74
|
}: {
|
|
16
75
|
messages: Message[];
|
|
17
76
|
finished: boolean;
|
|
18
77
|
editOperations: EditOperation[];
|
|
19
78
|
error?: string;
|
|
79
|
+
onQuickAction?: (
|
|
80
|
+
action: { label: string; prompt?: string; value?: string },
|
|
81
|
+
message: Message,
|
|
82
|
+
) => void;
|
|
20
83
|
}) {
|
|
21
84
|
const editContext = useEditContext();
|
|
22
85
|
if (!editContext) return <></>;
|
|
@@ -108,6 +171,13 @@ export function AiResponseMessage({
|
|
|
108
171
|
? message.tool_calls
|
|
109
172
|
: preservedToolCalls[message.id] || [];
|
|
110
173
|
|
|
174
|
+
// Extract quick actions from content
|
|
175
|
+
const { cleanText, actions } = parseQuickActionsFromContent(
|
|
176
|
+
(message.content || message.formattedContent || "") as string,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const html = simpleFormatToHtml(cleanText);
|
|
180
|
+
|
|
111
181
|
return (
|
|
112
182
|
<div
|
|
113
183
|
key={filteredIndex}
|
|
@@ -116,9 +186,43 @@ export function AiResponseMessage({
|
|
|
116
186
|
<div
|
|
117
187
|
className="prose prose-sm max-w-none text-sm text-gray-700 select-text"
|
|
118
188
|
dangerouslySetInnerHTML={{
|
|
119
|
-
__html:
|
|
189
|
+
__html: html,
|
|
120
190
|
}}
|
|
121
191
|
/>
|
|
192
|
+
|
|
193
|
+
{actions.length > 0 && (
|
|
194
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
195
|
+
{actions.map((a, idx) => {
|
|
196
|
+
const variant =
|
|
197
|
+
a.style === "destructive"
|
|
198
|
+
? "destructive"
|
|
199
|
+
: a.style === "outline"
|
|
200
|
+
? "outline"
|
|
201
|
+
: a.style === "secondary"
|
|
202
|
+
? "secondary"
|
|
203
|
+
: "default"; // primary
|
|
204
|
+
return (
|
|
205
|
+
<Button
|
|
206
|
+
key={(a.id || a.label || "btn") + "-" + idx}
|
|
207
|
+
size="sm"
|
|
208
|
+
variant={variant as any}
|
|
209
|
+
onClick={() =>
|
|
210
|
+
onQuickAction?.(
|
|
211
|
+
{
|
|
212
|
+
label: a.label,
|
|
213
|
+
prompt: a.prompt,
|
|
214
|
+
value: a.value,
|
|
215
|
+
},
|
|
216
|
+
message,
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
>
|
|
220
|
+
{a.label}
|
|
221
|
+
</Button>
|
|
222
|
+
);
|
|
223
|
+
})}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
122
226
|
<ToolCallDisplay
|
|
123
227
|
toolCalls={toolCalls}
|
|
124
228
|
finished={finished}
|
|
@@ -279,6 +279,20 @@ export function AiTerminal({
|
|
|
279
279
|
messages={formattedMessages}
|
|
280
280
|
editOperations={response.editOperations}
|
|
281
281
|
finished={isFinished}
|
|
282
|
+
onQuickAction={(action) => {
|
|
283
|
+
const text = (
|
|
284
|
+
action.prompt ||
|
|
285
|
+
action.value ||
|
|
286
|
+
action.label ||
|
|
287
|
+
""
|
|
288
|
+
).trim();
|
|
289
|
+
if (!text) return;
|
|
290
|
+
// Submit this as a new command
|
|
291
|
+
commandHandler(text, (node, done) => {
|
|
292
|
+
TerminalService.emit("response", node);
|
|
293
|
+
if (done) TerminalService.emit("response", undefined);
|
|
294
|
+
});
|
|
295
|
+
}}
|
|
282
296
|
/>,
|
|
283
297
|
isFinished,
|
|
284
298
|
);
|
|
@@ -289,6 +303,19 @@ export function AiTerminal({
|
|
|
289
303
|
messages={messagesRef.current}
|
|
290
304
|
editOperations={response.editOperations}
|
|
291
305
|
finished={isFinished}
|
|
306
|
+
onQuickAction={(action) => {
|
|
307
|
+
const text = (
|
|
308
|
+
action.prompt ||
|
|
309
|
+
action.value ||
|
|
310
|
+
action.label ||
|
|
311
|
+
""
|
|
312
|
+
).trim();
|
|
313
|
+
if (!text) return;
|
|
314
|
+
commandHandler(text, (node, done) => {
|
|
315
|
+
TerminalService.emit("response", node);
|
|
316
|
+
if (done) TerminalService.emit("response", undefined);
|
|
317
|
+
});
|
|
318
|
+
}}
|
|
292
319
|
/>,
|
|
293
320
|
isFinished,
|
|
294
321
|
);
|