@eventcatalog/core 5.7.0 → 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-22KZ4TJF.js → chunk-36XVRD36.js} +1 -1
- package/dist/{chunk-FMWQ2YYN.js → chunk-AXWTR2FV.js} +1 -1
- package/dist/{chunk-BE5PTWNC.js → chunk-CPF5WE3R.js} +1 -1
- package/dist/{chunk-4CV7VC4K.js → chunk-GRC67WHC.js} +1 -1
- package/dist/{chunk-MOPKQMQP.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 +589 -120
- package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +3 -2
- package/eventcatalog/src/components/ChatPanel/assistant-events.ts +37 -0
- 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 +62 -61
- 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/StudioDesigner.tsx +91 -10
- 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/utils/resource-reference-icons.ts +37 -0
- package/package.json +4 -4
|
@@ -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) => {
|
|
@@ -631,6 +1031,7 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
631
1031
|
|
|
632
1032
|
// Memoize suggested questions to avoid recalculating on every render
|
|
633
1033
|
const suggestedQuestions = useMemo(() => getSuggestedQuestions(pathname), [pathname]);
|
|
1034
|
+
const canvasId = useMemo(() => getCanvasId(pathname), [pathname]);
|
|
634
1035
|
|
|
635
1036
|
// Memoize page context to avoid recalculating on every render
|
|
636
1037
|
const pageContext = useMemo(() => {
|
|
@@ -699,9 +1100,31 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
699
1100
|
isWaitingForResponse ||
|
|
700
1101
|
(messages.length > 0 && (status === 'submitted' || status === 'streaming') && !assistantHasContent);
|
|
701
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]);
|
|
702
1110
|
|
|
703
|
-
//
|
|
704
|
-
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]);
|
|
705
1128
|
|
|
706
1129
|
// Jump the messages (in the panel and the fullscreen modal) to the latest. Instantly: a smooth scroll restarted on
|
|
707
1130
|
// every streamed token lands short of the growing answer and scrolls back and forth.
|
|
@@ -829,9 +1252,11 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
829
1252
|
return;
|
|
830
1253
|
}
|
|
831
1254
|
setIsWaitingForResponse(true);
|
|
832
|
-
|
|
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);
|
|
833
1258
|
},
|
|
834
|
-
[configured, isLoading, sendMessage, setMessages]
|
|
1259
|
+
[configured, isLoading, sendMessage, setMessages, canvasId]
|
|
835
1260
|
);
|
|
836
1261
|
|
|
837
1262
|
const stopResponse = useCallback(() => {
|
|
@@ -857,6 +1282,30 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
857
1282
|
[submitMessage]
|
|
858
1283
|
);
|
|
859
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
|
+
|
|
860
1309
|
// Handle input enter key
|
|
861
1310
|
const handleInputKeyDown = useCallback(
|
|
862
1311
|
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
@@ -1004,7 +1453,9 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1004
1453
|
</div>
|
|
1005
1454
|
<h2 className="text-lg font-medium text-[rgb(var(--ec-page-text))] mb-1">{greeting}</h2>
|
|
1006
1455
|
<p className="text-sm font-normal text-[rgb(var(--ec-content-text))]">
|
|
1007
|
-
|
|
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"}
|
|
1008
1459
|
</p>
|
|
1009
1460
|
</div>
|
|
1010
1461
|
|
|
@@ -1013,7 +1464,7 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1013
1464
|
{suggestedQuestions.map((question, index) => (
|
|
1014
1465
|
<button
|
|
1015
1466
|
key={index}
|
|
1016
|
-
onClick={() =>
|
|
1467
|
+
onClick={() => handleSuggestedQuestion(question)}
|
|
1017
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"
|
|
1018
1469
|
style={isOpen ? fadeInStyles.getQuestionStyle(index) : undefined}
|
|
1019
1470
|
>
|
|
@@ -1029,6 +1480,8 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1029
1480
|
const content = getMessageContent(message);
|
|
1030
1481
|
const followUpSuggestions = message.role === 'assistant' ? getFollowUpSuggestions(message) : [];
|
|
1031
1482
|
const completedTools = message.role === 'assistant' ? getCompletedTools(message) : [];
|
|
1483
|
+
const flaggedHotspots = message.role === 'assistant' ? getFlaggedHotspots(message) : [];
|
|
1484
|
+
const walkthroughs = message.role === 'assistant' ? getWalkthroughs(message) : [];
|
|
1032
1485
|
const isLastMessage = messageIndex === messages.length - 1;
|
|
1033
1486
|
return (
|
|
1034
1487
|
<div
|
|
@@ -1067,6 +1520,10 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1067
1520
|
</div>
|
|
1068
1521
|
</div>
|
|
1069
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} />}
|
|
1070
1527
|
{isLastMessage && followUpSuggestions.length > 0 && !isLoading && (
|
|
1071
1528
|
<div className="flex flex-wrap gap-2 mt-3 w-full">
|
|
1072
1529
|
{followUpSuggestions.map((suggestion, index) => (
|
|
@@ -1088,9 +1545,9 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1088
1545
|
})}
|
|
1089
1546
|
|
|
1090
1547
|
{/* Skeleton loading indicator */}
|
|
1091
|
-
{
|
|
1548
|
+
{isWorking && (
|
|
1092
1549
|
<div className="w-full">
|
|
1093
|
-
|
|
1550
|
+
{suggestingFollowUps ? <FollowUpsLoading /> : <ActivityCard steps={activitySteps} />}
|
|
1094
1551
|
</div>
|
|
1095
1552
|
)}
|
|
1096
1553
|
|
|
@@ -1136,40 +1593,42 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1136
1593
|
value={inputValue}
|
|
1137
1594
|
onChange={(e) => setInputValue(e.target.value)}
|
|
1138
1595
|
onKeyDown={handleInputKeyDown}
|
|
1139
|
-
placeholder=
|
|
1596
|
+
placeholder={canvasId ? 'Describe what to diagram...' : 'Ask, search, or explain...'}
|
|
1140
1597
|
disabled={isLoading}
|
|
1141
|
-
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"
|
|
1142
1599
|
/>
|
|
1143
|
-
<div className="
|
|
1144
|
-
<
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
?
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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>
|
|
1173
1632
|
</div>
|
|
1174
1633
|
</div>
|
|
1175
1634
|
</form>
|
|
@@ -1312,7 +1771,9 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1312
1771
|
</div>
|
|
1313
1772
|
<h2 className="text-2xl font-medium text-[rgb(var(--ec-page-text))] mb-2">{greeting}</h2>
|
|
1314
1773
|
<p className="font-normal text-[rgb(var(--ec-content-text))] text-center">
|
|
1315
|
-
|
|
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"}
|
|
1316
1777
|
</p>
|
|
1317
1778
|
</div>
|
|
1318
1779
|
|
|
@@ -1321,7 +1782,7 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1321
1782
|
{suggestedQuestions.map((question, index) => (
|
|
1322
1783
|
<button
|
|
1323
1784
|
key={index}
|
|
1324
|
-
onClick={() =>
|
|
1785
|
+
onClick={() => handleSuggestedQuestion(question)}
|
|
1325
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"
|
|
1326
1787
|
>
|
|
1327
1788
|
{question.label}
|
|
@@ -1336,6 +1797,8 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1336
1797
|
const content = getMessageContent(message);
|
|
1337
1798
|
const followUpSuggestions = message.role === 'assistant' ? getFollowUpSuggestions(message) : [];
|
|
1338
1799
|
const completedTools = message.role === 'assistant' ? getCompletedTools(message) : [];
|
|
1800
|
+
const flaggedHotspots = message.role === 'assistant' ? getFlaggedHotspots(message) : [];
|
|
1801
|
+
const walkthroughs = message.role === 'assistant' ? getWalkthroughs(message) : [];
|
|
1339
1802
|
const isLastMessage = messageIndex === messages.length - 1;
|
|
1340
1803
|
return (
|
|
1341
1804
|
<div
|
|
@@ -1374,6 +1837,10 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1374
1837
|
</div>
|
|
1375
1838
|
</div>
|
|
1376
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} />}
|
|
1377
1844
|
{isLastMessage && followUpSuggestions.length > 0 && !isLoading && (
|
|
1378
1845
|
<div className="flex flex-wrap gap-2 mt-3 w-full">
|
|
1379
1846
|
{followUpSuggestions.map((suggestion, index) => (
|
|
@@ -1394,9 +1861,9 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1394
1861
|
);
|
|
1395
1862
|
})}
|
|
1396
1863
|
|
|
1397
|
-
{
|
|
1864
|
+
{isWorking && (
|
|
1398
1865
|
<div className="w-full max-w-md">
|
|
1399
|
-
|
|
1866
|
+
{suggestingFollowUps ? <FollowUpsLoading /> : <ActivityCard steps={activitySteps} />}
|
|
1400
1867
|
</div>
|
|
1401
1868
|
)}
|
|
1402
1869
|
|
|
@@ -1431,40 +1898,42 @@ const ChatPanel = ({ isOpen, onClose, configured = false }: ChatPanelProps) => {
|
|
|
1431
1898
|
value={inputValue}
|
|
1432
1899
|
onChange={(e) => setInputValue(e.target.value)}
|
|
1433
1900
|
onKeyDown={handleInputKeyDown}
|
|
1434
|
-
placeholder=
|
|
1901
|
+
placeholder={canvasId ? 'Describe what to diagram...' : 'Ask, search, or explain...'}
|
|
1435
1902
|
disabled={isLoading}
|
|
1436
|
-
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"
|
|
1437
1904
|
/>
|
|
1438
|
-
<div className="
|
|
1439
|
-
<
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
?
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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>
|
|
1468
1937
|
</div>
|
|
1469
1938
|
</div>
|
|
1470
1939
|
</form>
|