@eventcatalog/core 5.6.1 → 5.7.1
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/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-LVOGLB6C.js → chunk-36XVRD36.js} +1 -1
- package/dist/{chunk-N7LDFUEB.js → chunk-AXWTR2FV.js} +1 -1
- package/dist/{chunk-QJXVAM76.js → chunk-CPF5WE3R.js} +1 -1
- package/dist/{chunk-2NALBBWH.js → chunk-GRC67WHC.js} +1 -1
- package/dist/{chunk-L63ED43X.js → chunk-HZ3XZKUT.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +596 -122
- package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +3 -2
- package/eventcatalog/src/components/ChatPanel/assistant-events.ts +37 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +21 -1
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +2 -1
- package/eventcatalog/src/components/MDX/ResourceRef/ResourceRef.astro +2 -34
- package/eventcatalog/src/features/ai/chat-api.ts +24 -5
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +75 -74
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +73 -73
- package/eventcatalog/src/features/studio/agent-choreography.ts +84 -16
- package/eventcatalog/src/features/studio/api/canvases-api.ts +3 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +330 -15
- package/eventcatalog/src/features/studio/catalog.ts +5 -3
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +14 -0
- package/eventcatalog/src/features/studio/components/NodeDetails.tsx +4 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +165 -18
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +9 -4
- package/eventcatalog/src/features/studio/pages/[id].astro +2 -0
- package/eventcatalog/src/features/studio/server/assistant-tools.ts +207 -0
- package/eventcatalog/src/features/studio/server/canvas-agent.ts +27 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +2 -14
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +4 -0
- package/eventcatalog/src/utils/node-graphs/compact-visualiser-graph.ts +14 -0
- package/eventcatalog/src/utils/node-graphs/node-resource.ts +26 -0
- package/eventcatalog/src/utils/resource-reference-icons.ts +37 -0
- package/package.json +7 -7
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { useEffect, useRef, useCallback, useState, useMemo, memo } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
X,
|
|
4
|
+
Square,
|
|
5
|
+
Trash2,
|
|
6
|
+
Sparkles,
|
|
7
|
+
Copy,
|
|
8
|
+
Check,
|
|
9
|
+
Maximize2,
|
|
10
|
+
Minimize2,
|
|
11
|
+
Wrench,
|
|
12
|
+
ChevronDown,
|
|
13
|
+
ChevronLeft,
|
|
14
|
+
ChevronRight,
|
|
15
|
+
MessageSquare,
|
|
16
|
+
LocateFixed,
|
|
17
|
+
OctagonAlert,
|
|
18
|
+
TriangleAlert,
|
|
19
|
+
Info,
|
|
20
|
+
} from 'lucide-react';
|
|
3
21
|
import { useChat } from '@ai-sdk/react';
|
|
4
22
|
import { DefaultChatTransport } from 'ai';
|
|
5
23
|
import ReactMarkdown from 'react-markdown';
|
|
@@ -10,6 +28,16 @@ import * as Dialog from '@radix-ui/react-dialog';
|
|
|
10
28
|
import * as Popover from '@radix-ui/react-popover';
|
|
11
29
|
import { buildUrl } from '@utils/url-builder';
|
|
12
30
|
import OfflineReply, { ConfigurationErrorReply, getChatError, offlineReplyText } from './OfflineReply';
|
|
31
|
+
import {
|
|
32
|
+
ASSISTANT_DRAWING_EVENT,
|
|
33
|
+
CANVAS_FOCUS_EVENT,
|
|
34
|
+
CANVAS_HOTSPOT_EVENT,
|
|
35
|
+
canvasRefIn,
|
|
36
|
+
nodesInFocusLink,
|
|
37
|
+
} from './assistant-events';
|
|
38
|
+
import { getResourceReferenceColorName } from '@utils/resource-reference-colors';
|
|
39
|
+
import { tailwind500RgbByColor } from '@utils/collection-colors';
|
|
40
|
+
import { getResourceReferenceIcon } from '@utils/resource-reference-icons';
|
|
13
41
|
|
|
14
42
|
interface ToolMetadata {
|
|
15
43
|
name: string;
|
|
@@ -23,10 +51,6 @@ const CHAT_PANEL_STYLES = `
|
|
|
23
51
|
0%, 100% { transform: scale(1); opacity: .7; }
|
|
24
52
|
50% { transform: scale(1.06); opacity: 1; }
|
|
25
53
|
}
|
|
26
|
-
@keyframes ec-chat-dot {
|
|
27
|
-
0%, 80%, 100% { transform: translateY(0); opacity: .35; }
|
|
28
|
-
40% { transform: translateY(-3px); opacity: 1; }
|
|
29
|
-
}
|
|
30
54
|
.ec-chat-surface { font-weight: 400; }
|
|
31
55
|
.ec-chat-avatar { background: rgb(var(--ec-accent) / .06); animation: ec-chat-breathe 5s ease-in-out infinite; }
|
|
32
56
|
/* The closed panel stays on the page (off screen): a running animation there still costs every frame */
|
|
@@ -38,9 +62,58 @@ const CHAT_PANEL_STYLES = `
|
|
|
38
62
|
.ec-chat-offline > :nth-child(2) { animation-delay: 100ms; }
|
|
39
63
|
.ec-chat-offline > :nth-child(3) { animation-delay: 220ms; }
|
|
40
64
|
.ec-chat-offline > :nth-child(4) { animation-delay: 340ms; }
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
/* What the assistant's doing while it works: a card with a sheen passing over it, its steps so far ticked and the
|
|
66
|
+
one it's on spinning, its words shimmering */
|
|
67
|
+
@keyframes ec-chat-sheen {
|
|
68
|
+
from { background-position: 200% 0; }
|
|
69
|
+
to { background-position: -200% 0; }
|
|
70
|
+
}
|
|
71
|
+
@keyframes ec-chat-spin { to { transform: rotate(360deg); } }
|
|
72
|
+
.ec-chat-activity {
|
|
73
|
+
position: relative;
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
border: 1px solid rgb(var(--ec-accent) / .18);
|
|
76
|
+
border-radius: 14px;
|
|
77
|
+
background: rgb(var(--ec-accent) / .03);
|
|
78
|
+
box-shadow: 0 6px 20px rgb(var(--ec-accent) / .05);
|
|
79
|
+
}
|
|
80
|
+
.ec-chat-activity::before {
|
|
81
|
+
content: '';
|
|
82
|
+
position: absolute;
|
|
83
|
+
inset: 0;
|
|
84
|
+
pointer-events: none;
|
|
85
|
+
background: linear-gradient(110deg, transparent 30%, rgb(var(--ec-accent) / .09) 50%, transparent 70%);
|
|
86
|
+
background-size: 200% 100%;
|
|
87
|
+
animation: ec-chat-sheen 2.2s linear infinite;
|
|
88
|
+
}
|
|
89
|
+
.ec-chat-activity-now {
|
|
90
|
+
background: linear-gradient(90deg, rgb(var(--ec-page-text)) 0%, rgb(var(--ec-accent)) 50%, rgb(var(--ec-page-text)) 100%);
|
|
91
|
+
background-size: 200% 100%;
|
|
92
|
+
-webkit-background-clip: text;
|
|
93
|
+
background-clip: text;
|
|
94
|
+
color: transparent;
|
|
95
|
+
animation: ec-chat-sheen 2.4s linear infinite;
|
|
96
|
+
}
|
|
97
|
+
.ec-chat-spinner {
|
|
98
|
+
width: 12px;
|
|
99
|
+
height: 12px;
|
|
100
|
+
flex: none;
|
|
101
|
+
border-radius: 50%;
|
|
102
|
+
border: 1.5px solid rgb(var(--ec-accent) / .2);
|
|
103
|
+
border-top-color: rgb(var(--ec-accent));
|
|
104
|
+
animation: ec-chat-spin .8s linear infinite;
|
|
105
|
+
}
|
|
106
|
+
/* Follow-up questions on their way: pills like theirs, a sheen passing along them */
|
|
107
|
+
.ec-chat-pending-pill {
|
|
108
|
+
height: 30px;
|
|
109
|
+
border-radius: 999px;
|
|
110
|
+
background: linear-gradient(90deg, rgb(var(--ec-accent) / .05) 25%, rgb(var(--ec-accent) / .14) 50%, rgb(var(--ec-accent) / .05) 75%);
|
|
111
|
+
background-size: 200% 100%;
|
|
112
|
+
animation: fadeInUp 300ms ease-out both, ec-chat-sheen 1.6s linear infinite;
|
|
113
|
+
}
|
|
114
|
+
.ec-chat-surface[aria-hidden='true'] .ec-chat-activity,
|
|
115
|
+
.ec-chat-surface[aria-hidden='true'] .ec-chat-activity *,
|
|
116
|
+
.ec-chat-surface[aria-hidden='true'] .ec-chat-pending-pill { animation-play-state: paused; }
|
|
44
117
|
.ec-chat-composer {
|
|
45
118
|
border-radius: 20px;
|
|
46
119
|
border-color: rgb(var(--ec-accent) / .22);
|
|
@@ -49,8 +122,9 @@ const CHAT_PANEL_STYLES = `
|
|
|
49
122
|
transition: border-color 200ms, box-shadow 200ms;
|
|
50
123
|
}
|
|
51
124
|
.ec-chat-composer:focus-within { border-color: rgb(var(--ec-accent) / .5); box-shadow: 0 0 0 3px rgb(var(--ec-accent) / .09); }
|
|
52
|
-
|
|
53
|
-
.ec-chat-composer
|
|
125
|
+
/* What's typed grows the box (up to a point, then scrolls), with what it's based on and Send in a row under it */
|
|
126
|
+
.ec-chat-composer textarea { display: block; resize: none; min-height: 58px; max-height: 200px; overflow-y: auto; padding: 14px 16px 4px; border-radius: 20px 20px 0 0; }
|
|
127
|
+
.ec-chat-composer-footer { display: flex; align-items: center; gap: 8px; padding: 2px 8px 8px 16px; }
|
|
54
128
|
@media (prefers-reduced-motion: reduce) {
|
|
55
129
|
.ec-chat-surface, .ec-chat-surface *, #eventcatalog-header, #eventcatalog-application {
|
|
56
130
|
animation: none !important;
|
|
@@ -153,6 +227,8 @@ const getGreeting = () => {
|
|
|
153
227
|
interface SuggestedQuestion {
|
|
154
228
|
label: string;
|
|
155
229
|
prompt: string;
|
|
230
|
+
/** Answered here, straight away, rather than by the model: e.g. a question back to the person */
|
|
231
|
+
reply?: string;
|
|
156
232
|
}
|
|
157
233
|
|
|
158
234
|
interface QuestionConfig {
|
|
@@ -161,6 +237,33 @@ interface QuestionConfig {
|
|
|
161
237
|
}
|
|
162
238
|
|
|
163
239
|
const suggestedQuestionsConfig: QuestionConfig[] = [
|
|
240
|
+
// A Studio canvas: the assistant draws on it
|
|
241
|
+
{
|
|
242
|
+
pattern: /\/studio\/[0-9a-f-]{36}/i,
|
|
243
|
+
questions: [
|
|
244
|
+
{
|
|
245
|
+
label: 'Create a new diagram',
|
|
246
|
+
prompt: 'Create a new diagram',
|
|
247
|
+
reply: [
|
|
248
|
+
'What would you like to diagram? Describe it in a sentence or two, for example:',
|
|
249
|
+
'',
|
|
250
|
+
'- How an order flows from checkout to shipping',
|
|
251
|
+
'- One of our domains and what it talks to',
|
|
252
|
+
"- A new service you're designing, and the events it sends and receives",
|
|
253
|
+
].join('\n'),
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
label: 'Review my architecture, highlight any issues',
|
|
257
|
+
prompt:
|
|
258
|
+
'Review the architecture on this canvas and highlight any issues: flag what you find with hotspots on the nodes they are about (critical, warning or info), and add sticky notes with your review and suggestions.',
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
label: 'How could we improve this architecture?',
|
|
262
|
+
prompt:
|
|
263
|
+
'How could we improve the architecture on this canvas? Flag the problems and risks you find with hotspots on the nodes they are about, and add sticky notes with your suggested improvements.',
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
164
267
|
// Message pages (events, commands, queries) - most specific first
|
|
165
268
|
{
|
|
166
269
|
pattern: /^\/docs\/(events|commands|queries)\/.+/,
|
|
@@ -364,6 +467,9 @@ const suggestedQuestionsConfig: QuestionConfig[] = [
|
|
|
364
467
|
},
|
|
365
468
|
];
|
|
366
469
|
|
|
470
|
+
/** The Studio canvas the page shows, if it's one: the assistant draws on it */
|
|
471
|
+
export const getCanvasId = (pathname: string) => pathname.match(/\/studio\/([0-9a-f-]{36})\/?(?:[?#]|$)/i)?.[1];
|
|
472
|
+
|
|
367
473
|
// Get suggested questions based on current URL path
|
|
368
474
|
const getSuggestedQuestions = (pathname: string): SuggestedQuestion[] => {
|
|
369
475
|
for (const config of suggestedQuestionsConfig) {
|
|
@@ -446,23 +552,29 @@ const getFollowUpSuggestions = (message: { parts?: Array<any> }): string[] => {
|
|
|
446
552
|
return [];
|
|
447
553
|
};
|
|
448
554
|
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
555
|
+
/** What the assistant's doing, in words, while a tool runs (or is being written, for the canvas's) */
|
|
556
|
+
const TOOL_ACTIVITY: Record<string, string> = {
|
|
557
|
+
getCanvas: 'Reading the canvas…',
|
|
558
|
+
addToCanvas: 'Drawing on the canvas…',
|
|
559
|
+
updateCanvasNode: 'Updating the canvas…',
|
|
560
|
+
connectCanvasNodes: 'Connecting things on the canvas…',
|
|
561
|
+
removeFromCanvas: 'Removing things from the canvas…',
|
|
562
|
+
layoutCanvas: 'Tidying the canvas…',
|
|
563
|
+
walkThrough: 'Preparing the walkthrough…',
|
|
564
|
+
showOnCanvas: 'Showing you on the canvas…',
|
|
565
|
+
addHotspot: 'Flagging hotspots…',
|
|
566
|
+
getResources: 'Searching the catalog…',
|
|
567
|
+
getResource: 'Reading the catalog…',
|
|
568
|
+
};
|
|
569
|
+
export const activityOf = (toolName: string, state?: string) =>
|
|
570
|
+
toolName === 'addToCanvas' && state === 'input-streaming'
|
|
571
|
+
? 'Planning the diagram…'
|
|
572
|
+
: (TOOL_ACTIVITY[toolName] ?? `Using ${toolName}…`);
|
|
573
|
+
|
|
574
|
+
/** The assistant is between things it writes (thinking, or using a tool), not writing text right now */
|
|
575
|
+
export const isBetweenText = (message: { parts?: Array<any> } | undefined) => {
|
|
576
|
+
const last = message?.parts?.at(-1);
|
|
577
|
+
return !last || last.type !== 'text' || last.state === 'done';
|
|
466
578
|
};
|
|
467
579
|
|
|
468
580
|
// Helper to extract completed tools from message parts (for showing after completion)
|
|
@@ -484,28 +596,218 @@ const getCompletedTools = (message: { parts?: Array<any> }): string[] => {
|
|
|
484
596
|
};
|
|
485
597
|
|
|
486
598
|
// Skeleton loading component - memoized since it never changes
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
599
|
+
export type ActivityStep = { label: string; done: boolean; count: number };
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* What the assistant has done this turn, step by step (each tool it used, in words, repeats counted), and what it's
|
|
603
|
+
* doing now: the tool it's using, else thinking. Only for a turn that's still going.
|
|
604
|
+
*/
|
|
605
|
+
export const getActivitySteps = (message: { role?: string; parts?: Array<any> } | undefined): ActivityStep[] => {
|
|
606
|
+
const steps: ActivityStep[] = [];
|
|
607
|
+
for (const part of message?.role === 'assistant' ? (message.parts ?? []) : []) {
|
|
608
|
+
if (!part.type?.startsWith('tool-')) continue;
|
|
609
|
+
const name = part.type.replace('tool-', '');
|
|
610
|
+
if (name === 'suggestFollowUpQuestions') continue;
|
|
611
|
+
const done = part.state === 'output-available' || part.state === 'output-error';
|
|
612
|
+
const label = activityOf(name, part.state).replace(/…$/, '');
|
|
613
|
+
const last = steps.at(-1);
|
|
614
|
+
if (last && last.label === label) {
|
|
615
|
+
last.count += 1;
|
|
616
|
+
last.done = done;
|
|
617
|
+
} else steps.push({ label, done, count: 1 });
|
|
618
|
+
}
|
|
619
|
+
if (!steps.length || steps.every((step) => step.done)) steps.push({ label: 'Thinking it through', done: false, count: 1 });
|
|
620
|
+
return steps;
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
/** The card the assistant's shown working in: its steps so far, ticked, and what it's on now */
|
|
624
|
+
const ActivityCard = memo(({ steps }: { steps: ActivityStep[] }) => (
|
|
625
|
+
<div role="status" aria-live="polite" className="ec-chat-message ec-chat-activity my-2 px-4 py-3">
|
|
626
|
+
<ol className="relative space-y-1.5 text-xs">
|
|
627
|
+
{steps.map((step, index) => (
|
|
628
|
+
<li key={index} className="flex items-center gap-2.5">
|
|
629
|
+
{step.done ? (
|
|
630
|
+
<Check size={12} className="flex-none text-[rgb(var(--ec-accent))]" aria-hidden="true" />
|
|
631
|
+
) : (
|
|
632
|
+
<span className="ec-chat-spinner" aria-hidden="true" />
|
|
633
|
+
)}
|
|
634
|
+
<span className={step.done ? 'text-[rgb(var(--ec-page-text-muted))]' : 'ec-chat-activity-now font-medium'}>
|
|
635
|
+
{step.label}
|
|
636
|
+
{step.done ? '' : '…'}
|
|
637
|
+
{step.count > 1 && <span className="ml-1 opacity-60">×{step.count}</span>}
|
|
638
|
+
</span>
|
|
639
|
+
</li>
|
|
640
|
+
))}
|
|
641
|
+
</ol>
|
|
495
642
|
</div>
|
|
496
643
|
));
|
|
497
|
-
|
|
644
|
+
ActivityCard.displayName = 'ActivityCard';
|
|
645
|
+
|
|
646
|
+
/** It's writing follow-up questions (what it does once it's answered) */
|
|
647
|
+
export const isSuggestingFollowUps = (message: { role?: string; parts?: Array<any> } | undefined) =>
|
|
648
|
+
message?.role === 'assistant' &&
|
|
649
|
+
!!message.parts?.some((part) => part.type === 'tool-suggestFollowUpQuestions' && part.state !== 'output-available');
|
|
650
|
+
|
|
651
|
+
/** Where the follow-up questions will be, while it writes them: pills like theirs, coming in */
|
|
652
|
+
const FollowUpsLoading = memo(() => (
|
|
653
|
+
<div role="status" aria-live="polite" className="ec-chat-message mt-1 flex flex-col items-start gap-2">
|
|
654
|
+
<span className="ec-chat-activity-now text-xs font-medium">Suggesting what to ask next…</span>
|
|
655
|
+
{['72%', '58%', '66%'].map((width, index) => (
|
|
656
|
+
<span
|
|
657
|
+
key={width}
|
|
658
|
+
aria-hidden="true"
|
|
659
|
+
className="ec-chat-pending-pill block"
|
|
660
|
+
style={{ width, animationDelay: `${index * 90}ms, ${index * 150}ms` }}
|
|
661
|
+
/>
|
|
662
|
+
))}
|
|
663
|
+
</div>
|
|
664
|
+
));
|
|
665
|
+
FollowUpsLoading.displayName = 'FollowUpsLoading';
|
|
498
666
|
|
|
499
667
|
// Memoized markdown components to prevent re-renders
|
|
668
|
+
/** Takes the person to nodes on the Studio canvas they have open */
|
|
669
|
+
const focusCanvas = (nodeIds: string[]) =>
|
|
670
|
+
window.dispatchEvent(
|
|
671
|
+
new CustomEvent(CANVAS_FOCUS_EVENT, { detail: { canvasId: getCanvasId(window.location.pathname), nodeIds } })
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
/** Takes the person to a hotspot on the Studio canvas they have open, and opens it */
|
|
675
|
+
const showHotspot = (hotspotId: string) =>
|
|
676
|
+
window.dispatchEvent(
|
|
677
|
+
new CustomEvent(CANVAS_HOTSPOT_EVENT, { detail: { canvasId: getCanvasId(window.location.pathname), hotspotId } })
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
/** How long the pointer rests on a walkthrough step before it takes them there (so moving across the steps doesn't) */
|
|
681
|
+
const FOCUS_ON_HOVER_MS = 250;
|
|
682
|
+
|
|
683
|
+
/** Something on the canvas, from the chat: clicking it, or resting the pointer on it, takes them there */
|
|
684
|
+
function CanvasLink({ go, className, children }: { go: () => void; className: string; children: React.ReactNode }) {
|
|
685
|
+
const hover = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
686
|
+
const cancel = () => {
|
|
687
|
+
if (hover.current !== null) clearTimeout(hover.current);
|
|
688
|
+
hover.current = null;
|
|
689
|
+
};
|
|
690
|
+
useEffect(() => cancel, []);
|
|
691
|
+
return (
|
|
692
|
+
<button
|
|
693
|
+
type="button"
|
|
694
|
+
onClick={() => {
|
|
695
|
+
cancel();
|
|
696
|
+
go();
|
|
697
|
+
}}
|
|
698
|
+
onMouseEnter={() => {
|
|
699
|
+
cancel();
|
|
700
|
+
hover.current = setTimeout(go, FOCUS_ON_HOVER_MS);
|
|
701
|
+
}}
|
|
702
|
+
onMouseLeave={cancel}
|
|
703
|
+
title="Show me on the canvas"
|
|
704
|
+
className={`cursor-pointer transition-colors ${className}`}
|
|
705
|
+
>
|
|
706
|
+
{children}
|
|
707
|
+
</button>
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** A step of a walkthrough on a canvas: takes them to its nodes */
|
|
712
|
+
const FocusLink = ({ nodeIds, children }: { nodeIds: string[]; children: React.ReactNode }) => (
|
|
713
|
+
<CanvasLink
|
|
714
|
+
go={() => focusCanvas(nodeIds)}
|
|
715
|
+
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-medium text-[rgb(var(--ec-accent))] bg-[rgb(var(--ec-accent)/0.08)] hover:bg-[rgb(var(--ec-accent)/0.16)]"
|
|
716
|
+
>
|
|
717
|
+
{children}
|
|
718
|
+
<LocateFixed size={12} className="flex-none opacity-70" aria-hidden="true" />
|
|
719
|
+
</CanvasLink>
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
/** Something on the canvas the assistant mentions: drawn like the docs' resource references (its type's icon and colour) */
|
|
723
|
+
const CanvasRef = ({ type, nodeId, children }: { type: string; nodeId: string; children: React.ReactNode }) => {
|
|
724
|
+
const rgb = type ? tailwind500RgbByColor[getResourceReferenceColorName(type)] : undefined;
|
|
725
|
+
const style = {
|
|
726
|
+
'--ec-resource-ref-bg': rgb ? `rgb(${rgb} / 0.12)` : 'rgb(var(--ec-accent) / 0.12)',
|
|
727
|
+
'--ec-resource-ref-color': rgb ? `rgb(${rgb})` : 'rgb(var(--ec-accent))',
|
|
728
|
+
} as React.CSSProperties;
|
|
729
|
+
return (
|
|
730
|
+
<span style={style}>
|
|
731
|
+
<CanvasLink
|
|
732
|
+
go={() => focusCanvas([nodeId])}
|
|
733
|
+
className="inline-flex items-center gap-1 px-1 -mx-0.5 rounded font-medium underline decoration-dotted decoration-2 underline-offset-[3px] text-[var(--ec-resource-ref-color)] decoration-[var(--ec-resource-ref-color)] hover:bg-[var(--ec-resource-ref-bg)]"
|
|
734
|
+
>
|
|
735
|
+
<svg
|
|
736
|
+
className="w-3.5 h-3.5 flex-shrink-0"
|
|
737
|
+
fill="none"
|
|
738
|
+
viewBox="0 0 24 24"
|
|
739
|
+
stroke="currentColor"
|
|
740
|
+
strokeWidth={1.75}
|
|
741
|
+
aria-hidden="true"
|
|
742
|
+
dangerouslySetInnerHTML={{ __html: getResourceReferenceIcon(type) }}
|
|
743
|
+
/>
|
|
744
|
+
<span>{children}</span>
|
|
745
|
+
</CanvasLink>
|
|
746
|
+
</span>
|
|
747
|
+
);
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
/** Hotspots' severities, as Studio shows them */
|
|
751
|
+
const HOTSPOT_SEVERITY = {
|
|
752
|
+
critical: { label: 'Critical', color: '#dc2626', icon: OctagonAlert },
|
|
753
|
+
warning: { label: 'Warning', color: '#d97706', icon: TriangleAlert },
|
|
754
|
+
info: { label: 'Info', color: '#2563eb', icon: Info },
|
|
755
|
+
} as const;
|
|
756
|
+
export type FlaggedHotspot = { id: string; severity: keyof typeof HOTSPOT_SEVERITY; title: string };
|
|
757
|
+
|
|
758
|
+
/** The hotspots the assistant flagged on the canvas in a message */
|
|
759
|
+
export const getFlaggedHotspots = (message: { parts?: Array<any> }): FlaggedHotspot[] =>
|
|
760
|
+
(message.parts ?? []).flatMap((part) =>
|
|
761
|
+
part.type === 'tool-addHotspot' &&
|
|
762
|
+
part.state === 'output-available' &&
|
|
763
|
+
typeof part.output?.hotspotId === 'string' &&
|
|
764
|
+
part.input?.severity in HOTSPOT_SEVERITY
|
|
765
|
+
? [{ id: part.output.hotspotId, severity: part.input.severity, title: String(part.input.title ?? '') }]
|
|
766
|
+
: []
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
/** The hotspots it flagged, listed under what it says: each takes them to it on the canvas, opened */
|
|
770
|
+
const FlaggedHotspots = memo(({ hotspots }: { hotspots: FlaggedHotspot[] }) => (
|
|
771
|
+
<div className="mt-3 w-full overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border))]">
|
|
772
|
+
<div className="px-3 py-2 text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] bg-[rgb(var(--ec-accent)/0.03)]">
|
|
773
|
+
Hotspots flagged on the canvas ({hotspots.length})
|
|
774
|
+
</div>
|
|
775
|
+
<ul>
|
|
776
|
+
{hotspots.map((hotspot) => {
|
|
777
|
+
const { label, color, icon: Icon } = HOTSPOT_SEVERITY[hotspot.severity];
|
|
778
|
+
return (
|
|
779
|
+
<li key={hotspot.id} className="border-t border-[rgb(var(--ec-page-border))]">
|
|
780
|
+
<CanvasLink
|
|
781
|
+
go={() => showHotspot(hotspot.id)}
|
|
782
|
+
className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent)/0.06)]"
|
|
783
|
+
>
|
|
784
|
+
<Icon size={14} className="flex-none" style={{ color }} aria-label={label} />
|
|
785
|
+
<span className="min-w-0 flex-1">{hotspot.title}</span>
|
|
786
|
+
<LocateFixed size={12} className="flex-none text-[rgb(var(--ec-icon-color))]" aria-hidden="true" />
|
|
787
|
+
</CanvasLink>
|
|
788
|
+
</li>
|
|
789
|
+
);
|
|
790
|
+
})}
|
|
791
|
+
</ul>
|
|
792
|
+
</div>
|
|
793
|
+
));
|
|
794
|
+
FlaggedHotspots.displayName = 'FlaggedHotspots';
|
|
795
|
+
|
|
500
796
|
const markdownComponents = {
|
|
501
|
-
a: ({ ...props }: any) =>
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
797
|
+
a: ({ node: _node, ...props }: any) => {
|
|
798
|
+
const nodeIds = nodesInFocusLink(props.href);
|
|
799
|
+
if (nodeIds) return <FocusLink nodeIds={nodeIds}>{props.children}</FocusLink>;
|
|
800
|
+
const ref = canvasRefIn(props.href);
|
|
801
|
+
if (ref) return <CanvasRef {...ref}>{props.children}</CanvasRef>;
|
|
802
|
+
return (
|
|
803
|
+
<a
|
|
804
|
+
{...props}
|
|
805
|
+
target="_blank"
|
|
806
|
+
rel="noopener noreferrer"
|
|
807
|
+
className="text-[rgb(var(--ec-accent))] hover:text-[rgb(var(--ec-accent-hover))] underline"
|
|
808
|
+
/>
|
|
809
|
+
);
|
|
810
|
+
},
|
|
509
811
|
code: ({ children, className, ...props }: any) => {
|
|
510
812
|
const isInline = !className;
|
|
511
813
|
const match = /language-(\w+)/.exec(className || '');
|
|
@@ -541,6 +843,104 @@ const markdownComponents = {
|
|
|
541
843
|
};
|
|
542
844
|
|
|
543
845
|
// Modal version with slightly different code styling
|
|
846
|
+
export type WalkthroughStep = { title: string; description: string; nodeIds: string[] };
|
|
847
|
+
export type Walkthrough = { id: string; title: string; steps: WalkthroughStep[] };
|
|
848
|
+
|
|
849
|
+
/** The walkthroughs the assistant gave in a message (walkThrough), as the canvas had them */
|
|
850
|
+
export const getWalkthroughs = (message: { parts?: Array<any> }): Walkthrough[] =>
|
|
851
|
+
(message.parts ?? []).flatMap((part) =>
|
|
852
|
+
part.type === 'tool-walkThrough' && part.state === 'output-available' && Array.isArray(part.output?.steps)
|
|
853
|
+
? [{ id: String(part.toolCallId), title: String(part.output.title ?? ''), steps: part.output.steps as WalkthroughStep[] }]
|
|
854
|
+
: []
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* A walkthrough, step by step: each step takes them to its nodes on the canvas (clicked, or the pointer resting on it),
|
|
859
|
+
* and Previous and Next go through them in turn
|
|
860
|
+
*/
|
|
861
|
+
const WalkthroughSteps = memo(({ walkthrough }: { walkthrough: Walkthrough }) => {
|
|
862
|
+
const [current, setCurrent] = useState<number | null>(null);
|
|
863
|
+
const goTo = useCallback(
|
|
864
|
+
(index: number) => {
|
|
865
|
+
const step = walkthrough.steps[index];
|
|
866
|
+
if (!step) return;
|
|
867
|
+
setCurrent(index);
|
|
868
|
+
if (step.nodeIds.length) focusCanvas(step.nodeIds);
|
|
869
|
+
},
|
|
870
|
+
[walkthrough]
|
|
871
|
+
);
|
|
872
|
+
const count = walkthrough.steps.length;
|
|
873
|
+
return (
|
|
874
|
+
<div className="mt-3 w-full overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border))]">
|
|
875
|
+
<div className="flex items-center gap-2 px-3 py-2 bg-[rgb(var(--ec-accent)/0.03)] border-b border-[rgb(var(--ec-page-border))]">
|
|
876
|
+
<span className="min-w-0 flex-1 truncate text-xs font-semibold text-[rgb(var(--ec-page-text))]">{walkthrough.title}</span>
|
|
877
|
+
<span className="flex-none text-[11px] text-[rgb(var(--ec-page-text-muted))]">
|
|
878
|
+
{current === null ? `${count} step${count === 1 ? '' : 's'}` : `Step ${current + 1} of ${count}`}
|
|
879
|
+
</span>
|
|
880
|
+
<button
|
|
881
|
+
type="button"
|
|
882
|
+
aria-label="Previous step"
|
|
883
|
+
disabled={current === null || current === 0}
|
|
884
|
+
onClick={() => goTo((current ?? 0) - 1)}
|
|
885
|
+
className="flex-none cursor-pointer rounded p-0.5 text-[rgb(var(--ec-icon-color))] hover:bg-[rgb(var(--ec-accent)/0.1)] disabled:cursor-default disabled:opacity-30"
|
|
886
|
+
>
|
|
887
|
+
<ChevronLeft size={14} />
|
|
888
|
+
</button>
|
|
889
|
+
<button
|
|
890
|
+
type="button"
|
|
891
|
+
aria-label="Next step"
|
|
892
|
+
disabled={current === count - 1}
|
|
893
|
+
onClick={() => goTo(current === null ? 0 : current + 1)}
|
|
894
|
+
className="flex-none cursor-pointer rounded p-0.5 text-[rgb(var(--ec-icon-color))] hover:bg-[rgb(var(--ec-accent)/0.1)] disabled:cursor-default disabled:opacity-30"
|
|
895
|
+
>
|
|
896
|
+
<ChevronRight size={14} />
|
|
897
|
+
</button>
|
|
898
|
+
</div>
|
|
899
|
+
<ol className="py-1">
|
|
900
|
+
{walkthrough.steps.map((step, index) => {
|
|
901
|
+
const active = index === current;
|
|
902
|
+
return (
|
|
903
|
+
<li key={index} className="relative">
|
|
904
|
+
{/* The line down to the next step */}
|
|
905
|
+
{index < count - 1 && (
|
|
906
|
+
<span aria-hidden="true" className="absolute left-[23px] top-8 bottom-0 w-px bg-[rgb(var(--ec-accent)/0.2)]" />
|
|
907
|
+
)}
|
|
908
|
+
<CanvasLink
|
|
909
|
+
go={() => goTo(index)}
|
|
910
|
+
className={`flex w-full items-start gap-3 px-3 py-2 text-left ${
|
|
911
|
+
active ? 'bg-[rgb(var(--ec-accent)/0.08)]' : 'hover:bg-[rgb(var(--ec-accent)/0.05)]'
|
|
912
|
+
}`}
|
|
913
|
+
>
|
|
914
|
+
<span
|
|
915
|
+
className={`relative z-10 flex h-[22px] w-[22px] flex-none items-center justify-center rounded-full text-[11px] font-semibold ${
|
|
916
|
+
active
|
|
917
|
+
? 'bg-[rgb(var(--ec-accent))] text-white'
|
|
918
|
+
: 'bg-[rgb(var(--ec-card-bg))] text-[rgb(var(--ec-accent))] ring-1 ring-inset ring-[rgb(var(--ec-accent)/0.35)]'
|
|
919
|
+
}`}
|
|
920
|
+
>
|
|
921
|
+
{index + 1}
|
|
922
|
+
</span>
|
|
923
|
+
<span className="min-w-0 flex-1">
|
|
924
|
+
<span className="flex items-center gap-1.5 text-xs font-medium text-[rgb(var(--ec-page-text))]">
|
|
925
|
+
{step.title}
|
|
926
|
+
{step.nodeIds.length > 0 && (
|
|
927
|
+
<LocateFixed size={11} className="flex-none text-[rgb(var(--ec-accent))] opacity-70" aria-hidden="true" />
|
|
928
|
+
)}
|
|
929
|
+
</span>
|
|
930
|
+
<span className="mt-0.5 block text-xs leading-relaxed text-[rgb(var(--ec-page-text-muted))]">
|
|
931
|
+
{step.description}
|
|
932
|
+
</span>
|
|
933
|
+
</span>
|
|
934
|
+
</CanvasLink>
|
|
935
|
+
</li>
|
|
936
|
+
);
|
|
937
|
+
})}
|
|
938
|
+
</ol>
|
|
939
|
+
</div>
|
|
940
|
+
);
|
|
941
|
+
});
|
|
942
|
+
WalkthroughSteps.displayName = 'WalkthroughSteps';
|
|
943
|
+
|
|
544
944
|
const modalMarkdownComponents = {
|
|
545
945
|
...markdownComponents,
|
|
546
946
|
code: ({ children, className, ...props }: any) => {
|
|
@@ -619,13 +1019,19 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
619
1019
|
}
|
|
620
1020
|
}, [configured, isOpen, tools.length, chatApiUrl]);
|
|
621
1021
|
|
|
622
|
-
//
|
|
1022
|
+
// Follow the current URL (pathname + search): the panel lives in the persisted header, so it stays mounted while
|
|
1023
|
+
// ClientRouter swaps pages underneath it. Reading the URL again after each swap keeps the suggested questions and
|
|
1024
|
+
// the "Based on" context on the page the person is actually looking at, not the one they opened the panel from.
|
|
623
1025
|
useEffect(() => {
|
|
624
|
-
setPathname(window.location.pathname + window.location.search);
|
|
1026
|
+
const updatePathname = () => setPathname(window.location.pathname + window.location.search);
|
|
1027
|
+
updatePathname();
|
|
1028
|
+
document.addEventListener('astro:after-swap', updatePathname);
|
|
1029
|
+
return () => document.removeEventListener('astro:after-swap', updatePathname);
|
|
625
1030
|
}, [isOpen]);
|
|
626
1031
|
|
|
627
1032
|
// Memoize suggested questions to avoid recalculating on every render
|
|
628
1033
|
const suggestedQuestions = useMemo(() => getSuggestedQuestions(pathname), [pathname]);
|
|
1034
|
+
const canvasId = useMemo(() => getCanvasId(pathname), [pathname]);
|
|
629
1035
|
|
|
630
1036
|
// Memoize page context to avoid recalculating on every render
|
|
631
1037
|
const pageContext = useMemo(() => {
|
|
@@ -694,9 +1100,31 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
694
1100
|
isWaitingForResponse ||
|
|
695
1101
|
(messages.length > 0 && (status === 'submitted' || status === 'streaming') && !assistantHasContent);
|
|
696
1102
|
const isLoading = isThinking || isStreaming;
|
|
1103
|
+
// Working, and not writing text right now (e.g. drawing on a canvas after saying what it'll do): still shown working
|
|
1104
|
+
const isWorking = isThinking || (isStreaming && isBetweenText(lastAssistantMessage));
|
|
1105
|
+
|
|
1106
|
+
// What it's done and is doing in the turn going on (the last message, once the assistant's started it)
|
|
1107
|
+
const lastMessage = messages.at(-1);
|
|
1108
|
+
const activitySteps = useMemo(() => getActivitySteps(lastMessage), [lastMessage]);
|
|
1109
|
+
const suggestingFollowUps = useMemo(() => isSuggestingFollowUps(lastMessage), [lastMessage]);
|
|
697
1110
|
|
|
698
|
-
//
|
|
699
|
-
const
|
|
1111
|
+
// The assistant showing them something on the canvas (showOnCanvas, a walkthrough's start): taken there, once
|
|
1112
|
+
const shownOnCanvas = useRef(new Set<string>());
|
|
1113
|
+
useEffect(() => {
|
|
1114
|
+
for (const part of (lastAssistantMessage?.parts ?? []) as Array<any>) {
|
|
1115
|
+
if (part.state !== 'output-available' || shownOnCanvas.current.has(part.toolCallId)) continue;
|
|
1116
|
+
// What it showed them, or a walkthrough's first step
|
|
1117
|
+
const nodeIds: string[] | undefined =
|
|
1118
|
+
part.type === 'tool-showOnCanvas'
|
|
1119
|
+
? part.output?.nodeIds
|
|
1120
|
+
: part.type === 'tool-walkThrough'
|
|
1121
|
+
? part.output?.steps?.[0]?.nodeIds
|
|
1122
|
+
: undefined;
|
|
1123
|
+
if (!nodeIds?.length) continue;
|
|
1124
|
+
shownOnCanvas.current.add(part.toolCallId);
|
|
1125
|
+
focusCanvas(nodeIds);
|
|
1126
|
+
}
|
|
1127
|
+
}, [lastAssistantMessage]);
|
|
700
1128
|
|
|
701
1129
|
// Jump the messages (in the panel and the fullscreen modal) to the latest. Instantly: a smooth scroll restarted on
|
|
702
1130
|
// every streamed token lands short of the growing answer and scrolls back and forth.
|
|
@@ -824,9 +1252,11 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
824
1252
|
return;
|
|
825
1253
|
}
|
|
826
1254
|
setIsWaitingForResponse(true);
|
|
827
|
-
|
|
1255
|
+
// The canvas follows the assistant while it draws
|
|
1256
|
+
if (canvasId) window.dispatchEvent(new CustomEvent(ASSISTANT_DRAWING_EVENT, { detail: { canvasId } }));
|
|
1257
|
+
sendMessage({ text }, canvasId ? { body: { canvasId } } : undefined);
|
|
828
1258
|
},
|
|
829
|
-
[configured, isLoading, sendMessage, setMessages]
|
|
1259
|
+
[configured, isLoading, sendMessage, setMessages, canvasId]
|
|
830
1260
|
);
|
|
831
1261
|
|
|
832
1262
|
const stopResponse = useCallback(() => {
|
|
@@ -852,6 +1282,30 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
852
1282
|
[submitMessage]
|
|
853
1283
|
);
|
|
854
1284
|
|
|
1285
|
+
// A suggested question: asked, or answered here straight away (no need to wait on the model to ask something back)
|
|
1286
|
+
const handleSuggestedQuestion = useCallback(
|
|
1287
|
+
({ prompt, reply }: SuggestedQuestion) => {
|
|
1288
|
+
if (!reply || isLoading) return handleSuggestedAction(prompt);
|
|
1289
|
+
followLatest.current = true;
|
|
1290
|
+
setMessages((previous) => [
|
|
1291
|
+
...previous,
|
|
1292
|
+
{ id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: prompt }] },
|
|
1293
|
+
{ id: crypto.randomUUID(), role: 'assistant', parts: [{ type: 'text', text: reply }] },
|
|
1294
|
+
]);
|
|
1295
|
+
(isFullscreen ? modalInputRef : inputRef).current?.focus();
|
|
1296
|
+
},
|
|
1297
|
+
[handleSuggestedAction, isLoading, isFullscreen, setMessages]
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
// The box grows with what's typed in it (up to its max-height, then scrolls), and shrinks back once it's sent
|
|
1301
|
+
useEffect(() => {
|
|
1302
|
+
for (const input of [inputRef.current, modalInputRef.current]) {
|
|
1303
|
+
if (!input) continue;
|
|
1304
|
+
input.style.height = 'auto';
|
|
1305
|
+
input.style.height = `${input.scrollHeight}px`;
|
|
1306
|
+
}
|
|
1307
|
+
}, [inputValue, isFullscreen, isOpen]);
|
|
1308
|
+
|
|
855
1309
|
// Handle input enter key
|
|
856
1310
|
const handleInputKeyDown = useCallback(
|
|
857
1311
|
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
@@ -999,7 +1453,9 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
999
1453
|
</div>
|
|
1000
1454
|
<h2 className="text-lg font-medium text-[rgb(var(--ec-page-text))] mb-1">{greeting}</h2>
|
|
1001
1455
|
<p className="text-sm font-normal text-[rgb(var(--ec-content-text))]">
|
|
1002
|
-
|
|
1456
|
+
{canvasId
|
|
1457
|
+
? "Tell me what to diagram and I'll draw it on the canvas"
|
|
1458
|
+
: "I'm here to help with your architecture"}
|
|
1003
1459
|
</p>
|
|
1004
1460
|
</div>
|
|
1005
1461
|
|
|
@@ -1008,7 +1464,7 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1008
1464
|
{suggestedQuestions.map((question, index) => (
|
|
1009
1465
|
<button
|
|
1010
1466
|
key={index}
|
|
1011
|
-
onClick={() =>
|
|
1467
|
+
onClick={() => handleSuggestedQuestion(question)}
|
|
1012
1468
|
className="ec-chat-suggestion text-left px-3 py-2 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-accent)/0.04)] hover:bg-[rgb(var(--ec-accent)/0.09)] rounded-full transition-all duration-200"
|
|
1013
1469
|
style={isOpen ? fadeInStyles.getQuestionStyle(index) : undefined}
|
|
1014
1470
|
>
|
|
@@ -1024,6 +1480,8 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1024
1480
|
const content = getMessageContent(message);
|
|
1025
1481
|
const followUpSuggestions = message.role === 'assistant' ? getFollowUpSuggestions(message) : [];
|
|
1026
1482
|
const completedTools = message.role === 'assistant' ? getCompletedTools(message) : [];
|
|
1483
|
+
const flaggedHotspots = message.role === 'assistant' ? getFlaggedHotspots(message) : [];
|
|
1484
|
+
const walkthroughs = message.role === 'assistant' ? getWalkthroughs(message) : [];
|
|
1027
1485
|
const isLastMessage = messageIndex === messages.length - 1;
|
|
1028
1486
|
return (
|
|
1029
1487
|
<div
|
|
@@ -1062,6 +1520,10 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1062
1520
|
</div>
|
|
1063
1521
|
</div>
|
|
1064
1522
|
{/* Follow-up suggestions - only show for last assistant message when not loading */}
|
|
1523
|
+
{walkthroughs.map((walkthrough) => (
|
|
1524
|
+
<WalkthroughSteps key={walkthrough.id} walkthrough={walkthrough} />
|
|
1525
|
+
))}
|
|
1526
|
+
{flaggedHotspots.length > 0 && <FlaggedHotspots hotspots={flaggedHotspots} />}
|
|
1065
1527
|
{isLastMessage && followUpSuggestions.length > 0 && !isLoading && (
|
|
1066
1528
|
<div className="flex flex-wrap gap-2 mt-3 w-full">
|
|
1067
1529
|
{followUpSuggestions.map((suggestion, index) => (
|
|
@@ -1083,9 +1545,9 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1083
1545
|
})}
|
|
1084
1546
|
|
|
1085
1547
|
{/* Skeleton loading indicator */}
|
|
1086
|
-
{
|
|
1548
|
+
{isWorking && (
|
|
1087
1549
|
<div className="w-full">
|
|
1088
|
-
|
|
1550
|
+
{suggestingFollowUps ? <FollowUpsLoading /> : <ActivityCard steps={activitySteps} />}
|
|
1089
1551
|
</div>
|
|
1090
1552
|
)}
|
|
1091
1553
|
|
|
@@ -1131,40 +1593,42 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1131
1593
|
value={inputValue}
|
|
1132
1594
|
onChange={(e) => setInputValue(e.target.value)}
|
|
1133
1595
|
onKeyDown={handleInputKeyDown}
|
|
1134
|
-
placeholder=
|
|
1596
|
+
placeholder={canvasId ? 'Describe what to diagram...' : 'Ask, search, or explain...'}
|
|
1135
1597
|
disabled={isLoading}
|
|
1136
|
-
className="w-full px-4
|
|
1598
|
+
className="w-full px-4 bg-transparent text-[rgb(var(--ec-input-text))] placeholder-[rgb(var(--ec-input-placeholder))] focus:outline-hidden text-sm disabled:opacity-50 rounded-xl"
|
|
1137
1599
|
/>
|
|
1138
|
-
<div className="
|
|
1139
|
-
<
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
?
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1600
|
+
<div className="ec-chat-composer-footer">
|
|
1601
|
+
<div className="min-w-0 flex-1 truncate text-[10px] text-[rgb(var(--ec-page-text-muted))]">
|
|
1602
|
+
<span className="mr-1.5 rounded bg-[rgb(var(--ec-accent)/0.08)] px-1 py-0.5 font-medium text-[rgb(var(--ec-accent))]">
|
|
1603
|
+
AI
|
|
1604
|
+
</span>
|
|
1605
|
+
{configured
|
|
1606
|
+
? pageContext
|
|
1607
|
+
? `Based on ${pageContext.name}`
|
|
1608
|
+
: 'Your architecture, connected'
|
|
1609
|
+
: 'Explore your catalog with AI'}
|
|
1610
|
+
</div>
|
|
1611
|
+
<div className="flex-none">
|
|
1612
|
+
{isStreaming || isSetupThinking ? (
|
|
1613
|
+
<button
|
|
1614
|
+
type="button"
|
|
1615
|
+
onClick={stopResponse}
|
|
1616
|
+
className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-colors"
|
|
1617
|
+
aria-label="Stop generating"
|
|
1618
|
+
>
|
|
1619
|
+
<Square size={14} fill="currentColor" />
|
|
1620
|
+
</button>
|
|
1621
|
+
) : (
|
|
1622
|
+
<button
|
|
1623
|
+
type="submit"
|
|
1624
|
+
disabled={!inputValue.trim() || isLoading}
|
|
1625
|
+
className="px-3 py-1.5 bg-[rgb(var(--ec-accent))] text-white text-xs font-medium rounded-lg hover:bg-[rgb(var(--ec-accent-hover))] disabled:bg-transparent disabled:text-[rgb(var(--ec-icon-color))] transition-colors"
|
|
1626
|
+
aria-label="Send message"
|
|
1627
|
+
>
|
|
1628
|
+
Send
|
|
1629
|
+
</button>
|
|
1630
|
+
)}
|
|
1631
|
+
</div>
|
|
1168
1632
|
</div>
|
|
1169
1633
|
</div>
|
|
1170
1634
|
</form>
|
|
@@ -1307,7 +1771,9 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1307
1771
|
</div>
|
|
1308
1772
|
<h2 className="text-2xl font-medium text-[rgb(var(--ec-page-text))] mb-2">{greeting}</h2>
|
|
1309
1773
|
<p className="font-normal text-[rgb(var(--ec-content-text))] text-center">
|
|
1310
|
-
|
|
1774
|
+
{canvasId
|
|
1775
|
+
? "Tell me what to diagram and I'll draw it on the canvas"
|
|
1776
|
+
: "I'm here to help with your architecture"}
|
|
1311
1777
|
</p>
|
|
1312
1778
|
</div>
|
|
1313
1779
|
|
|
@@ -1316,7 +1782,7 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1316
1782
|
{suggestedQuestions.map((question, index) => (
|
|
1317
1783
|
<button
|
|
1318
1784
|
key={index}
|
|
1319
|
-
onClick={() =>
|
|
1785
|
+
onClick={() => handleSuggestedQuestion(question)}
|
|
1320
1786
|
className="ec-chat-suggestion px-3 py-2 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-accent)/0.04)] hover:bg-[rgb(var(--ec-accent)/0.09)] rounded-full transition-all duration-200 text-left"
|
|
1321
1787
|
>
|
|
1322
1788
|
{question.label}
|
|
@@ -1331,6 +1797,8 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1331
1797
|
const content = getMessageContent(message);
|
|
1332
1798
|
const followUpSuggestions = message.role === 'assistant' ? getFollowUpSuggestions(message) : [];
|
|
1333
1799
|
const completedTools = message.role === 'assistant' ? getCompletedTools(message) : [];
|
|
1800
|
+
const flaggedHotspots = message.role === 'assistant' ? getFlaggedHotspots(message) : [];
|
|
1801
|
+
const walkthroughs = message.role === 'assistant' ? getWalkthroughs(message) : [];
|
|
1334
1802
|
const isLastMessage = messageIndex === messages.length - 1;
|
|
1335
1803
|
return (
|
|
1336
1804
|
<div
|
|
@@ -1369,6 +1837,10 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1369
1837
|
</div>
|
|
1370
1838
|
</div>
|
|
1371
1839
|
{/* Follow-up suggestions - only show for last assistant message when not loading */}
|
|
1840
|
+
{walkthroughs.map((walkthrough) => (
|
|
1841
|
+
<WalkthroughSteps key={walkthrough.id} walkthrough={walkthrough} />
|
|
1842
|
+
))}
|
|
1843
|
+
{flaggedHotspots.length > 0 && <FlaggedHotspots hotspots={flaggedHotspots} />}
|
|
1372
1844
|
{isLastMessage && followUpSuggestions.length > 0 && !isLoading && (
|
|
1373
1845
|
<div className="flex flex-wrap gap-2 mt-3 w-full">
|
|
1374
1846
|
{followUpSuggestions.map((suggestion, index) => (
|
|
@@ -1389,9 +1861,9 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1389
1861
|
);
|
|
1390
1862
|
})}
|
|
1391
1863
|
|
|
1392
|
-
{
|
|
1864
|
+
{isWorking && (
|
|
1393
1865
|
<div className="w-full max-w-md">
|
|
1394
|
-
|
|
1866
|
+
{suggestingFollowUps ? <FollowUpsLoading /> : <ActivityCard steps={activitySteps} />}
|
|
1395
1867
|
</div>
|
|
1396
1868
|
)}
|
|
1397
1869
|
|
|
@@ -1426,40 +1898,42 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1426
1898
|
value={inputValue}
|
|
1427
1899
|
onChange={(e) => setInputValue(e.target.value)}
|
|
1428
1900
|
onKeyDown={handleInputKeyDown}
|
|
1429
|
-
placeholder=
|
|
1901
|
+
placeholder={canvasId ? 'Describe what to diagram...' : 'Ask, search, or explain...'}
|
|
1430
1902
|
disabled={isLoading}
|
|
1431
|
-
className="w-full px-4
|
|
1903
|
+
className="w-full px-4 bg-transparent text-[rgb(var(--ec-input-text))] placeholder-[rgb(var(--ec-input-placeholder))] focus:outline-hidden text-sm disabled:opacity-50 rounded-xl"
|
|
1432
1904
|
/>
|
|
1433
|
-
<div className="
|
|
1434
|
-
<
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
?
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1905
|
+
<div className="ec-chat-composer-footer">
|
|
1906
|
+
<div className="min-w-0 flex-1 truncate text-[10px] text-[rgb(var(--ec-page-text-muted))]">
|
|
1907
|
+
<span className="mr-1.5 rounded bg-[rgb(var(--ec-accent)/0.08)] px-1 py-0.5 font-medium text-[rgb(var(--ec-accent))]">
|
|
1908
|
+
AI
|
|
1909
|
+
</span>
|
|
1910
|
+
{configured
|
|
1911
|
+
? pageContext
|
|
1912
|
+
? `Based on ${pageContext.name}`
|
|
1913
|
+
: 'Your architecture, connected'
|
|
1914
|
+
: 'Explore your catalog with AI'}
|
|
1915
|
+
</div>
|
|
1916
|
+
<div className="flex-none">
|
|
1917
|
+
{isStreaming || isSetupThinking ? (
|
|
1918
|
+
<button
|
|
1919
|
+
type="button"
|
|
1920
|
+
onClick={stopResponse}
|
|
1921
|
+
className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-colors"
|
|
1922
|
+
aria-label="Stop generating"
|
|
1923
|
+
>
|
|
1924
|
+
<Square size={14} fill="currentColor" />
|
|
1925
|
+
</button>
|
|
1926
|
+
) : (
|
|
1927
|
+
<button
|
|
1928
|
+
type="submit"
|
|
1929
|
+
disabled={!inputValue.trim() || isLoading}
|
|
1930
|
+
className="px-4 py-2 bg-[rgb(var(--ec-accent))] text-white text-sm font-medium rounded-lg hover:bg-[rgb(var(--ec-accent-hover))] disabled:bg-transparent disabled:text-[rgb(var(--ec-icon-color))] disabled:cursor-not-allowed transition-colors"
|
|
1931
|
+
aria-label="Send message"
|
|
1932
|
+
>
|
|
1933
|
+
Send
|
|
1934
|
+
</button>
|
|
1935
|
+
)}
|
|
1936
|
+
</div>
|
|
1463
1937
|
</div>
|
|
1464
1938
|
</div>
|
|
1465
1939
|
</form>
|