Skip to content

Commit 817de1a

Browse files
authored
feat(api,dashboard): novu copilot in the step editor fixes NV-7154 (#10439)
1 parent 7bd1a04 commit 817de1a

File tree

12 files changed

+262
-70
lines changed

12 files changed

+262
-70
lines changed

.source

apps/dashboard/src/components/ai-elements/conversation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type ConversationProps = ComponentProps<typeof StickToBottom>;
1212
export const Conversation = ({ className, ...props }: ConversationProps) => (
1313
<StickToBottom
1414
className={cn('relative flex-1 overflow-y-hidden', className)}
15-
initial="smooth"
15+
initial="instant"
1616
resize="smooth"
1717
role="log"
1818
{...props}

apps/dashboard/src/components/ai-elements/prompt-input.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ export const PromptInput = ({
608608

609609
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
610610
event.preventDefault();
611+
event.stopPropagation();
611612

612613
const form = event.currentTarget;
613614
const text = usingProvider

apps/dashboard/src/components/ai-sidekick/ai-chat-context.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export type AiChatResourceConfig = {
4545
resourceType: AiResourceTypeEnum;
4646
resourceId?: string;
4747
agentType: AiAgentTypeEnum;
48+
metadata?: Record<string, unknown>;
4849
isResourceLoading?: boolean;
4950
onRefetchResource?: () => void;
5051
onData?: (data: { type: string }) => void;
@@ -118,6 +119,7 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
118119
resourceType,
119120
resourceId,
120121
agentType,
122+
metadata,
121123
isResourceLoading = false,
122124
onRefetchResource,
123125
onData,
@@ -200,7 +202,16 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
200202
});
201203
},
202204
});
203-
const dataRef = useDataRef({ isGenerating, resourceType, resourceId, agentType, isAborted, latestChat, messages });
205+
const dataRef = useDataRef({
206+
isGenerating,
207+
resourceType,
208+
resourceId,
209+
agentType,
210+
isAborted,
211+
latestChat,
212+
messages,
213+
metadata,
214+
});
204215

205216
const { keepChanges, isPending: isKeepPending } = useKeepAiChanges();
206217
const { revertMessage, isPending: isRevertPending } = useRevertMessage();
@@ -267,7 +278,7 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
267278

268279
const handleSendMessage = useCallback(
269280
async (message: string) => {
270-
const { resourceType, resourceId, agentType, latestChat, messages } = dataRef.current;
281+
const { resourceType, resourceId, agentType, latestChat, messages, metadata } = dataRef.current;
271282
const isLastUserMessage = messages.length > 0 && messages[messages.length - 1].role === AiMessageRoleEnum.USER;
272283

273284
const messageToSend = message.trim();
@@ -280,12 +291,17 @@ export function AiChatProvider({ children, config }: { children: React.ReactNode
280291
resourceType,
281292
agentType,
282293
});
283-
sendPrompt({ chatId: newChat._id, prompt: messageToSend });
294+
sendPrompt({ chatId: newChat._id, prompt: messageToSend, metadata: { ...metadata } });
284295
} else if (isLastUserMessage) {
285296
const lastUserMessage = messages.filter((m) => m.role === AiMessageRoleEnum.USER).pop();
286-
sendPrompt({ messageId: lastUserMessage?.id, chatId: latestChat._id, prompt: messageToSend });
297+
sendPrompt({
298+
messageId: lastUserMessage?.id,
299+
chatId: latestChat._id,
300+
prompt: messageToSend,
301+
metadata: { ...metadata },
302+
});
287303
} else if (messageToSend) {
288-
sendPrompt({ chatId: latestChat._id, prompt: messageToSend });
304+
sendPrompt({ chatId: latestChat._id, prompt: messageToSend, metadata: { ...metadata } });
289305
}
290306

291307
track(TelemetryEvent.COPILOT_MESSAGE_SENT, {

apps/dashboard/src/components/ai-sidekick/chat-body.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ChatStatus, UIMessage } from 'ai';
2-
import { useMemo } from 'react';
2+
import { FormEvent, useMemo } from 'react';
33
import { Conversation, ConversationContent, ConversationScrollButton } from '../ai-elements/conversation';
44
import { Message } from '../ai-elements/message';
55
import {
@@ -106,7 +106,10 @@ export const ChatBody = ({
106106
const isSubmitGuard = !inputText.trim() || isGenerating || isSubmitDisabled;
107107
const isSubmitButtonDisabled = (!inputText.trim() && !isGenerating) || isSubmitDisabled;
108108

109-
const onSubmitHandler = (message: PromptInputMessage) => {
109+
const onSubmitHandler = (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => {
110+
event.preventDefault();
111+
event.stopPropagation();
112+
110113
if (isSubmitGuard) return;
111114

112115
onSubmit(message.text);
@@ -193,6 +196,15 @@ export const ChatBody = ({
193196
<PromptInputTextarea
194197
onChange={(event) => onInputChange(event.target.value)}
195198
value={inputText}
199+
onKeyDown={(e) => {
200+
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
201+
e.preventDefault();
202+
203+
if (!isSubmitGuard) {
204+
onSubmit(inputText);
205+
}
206+
}
207+
}}
196208
placeholder="Ask for changes… eg: Make the workflow high severity.."
197209
/>
198210
</PromptInputBody>

apps/dashboard/src/components/ai-sidekick/chat-chain-of-thought.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import {
1111
RiEdit2Line,
1212
RiLoader3Line,
1313
} from 'react-icons/ri';
14+
import { useNavigate } from 'react-router-dom';
15+
import { useEnvironment } from '@/context/environment/hooks';
1416
import { STEP_TYPE_TO_COLOR } from '@/utils/color';
1517
import { StepTypeEnum } from '@/utils/enums';
18+
import { buildRoute, ROUTES } from '@/utils/routes';
1619
import { cn } from '@/utils/ui';
1720
import { ChainOfThought, ChainOfThoughtContent, ChainOfThoughtStep } from '../ai-elements/chain-of-thought';
1821
import { Shimmer } from '../ai-elements/shimmer';
@@ -22,6 +25,7 @@ import { Badge } from '../primitives/badge';
2225
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../primitives/collapsible';
2326
import { Skeleton } from '../primitives/skeleton';
2427
import { Tag } from '../primitives/tag';
28+
import { useWorkflow } from '../workflow-editor/workflow-provider';
2529
import { StyledMessageResponse } from './chat-message-response';
2630
import { isCancelledToolCall, unwrapToolResult } from './message-utils';
2731

@@ -175,11 +179,40 @@ function WorkflowStepItem({
175179
isStreaming: boolean;
176180
action: 'add' | 'edit' | 'remove';
177181
}) {
182+
const navigate = useNavigate();
183+
const { workflow } = useWorkflow();
184+
const { currentEnvironment } = useEnvironment();
178185
const showStreaming = isStreaming || !output;
179186
const stepType = (output?.type ?? StepTypeEnum.IN_APP) as StepTypeEnum;
180187
const Icon = STEP_TYPE_TO_ICON[stepType] ?? STEP_TYPE_TO_ICON[StepTypeEnum.IN_APP];
181188
const color = STEP_TYPE_TO_COLOR[stepType] ?? STEP_TYPE_TO_COLOR[StepTypeEnum.IN_APP];
182189

190+
const matchedStep = useMemo(
191+
() => workflow?.steps.find((s) => s.stepId === output?.stepId),
192+
[workflow?.steps, output?.stepId]
193+
);
194+
const isClickable = !!matchedStep && action !== 'remove';
195+
const routeStepType = (matchedStep?.type ?? stepType) as StepTypeEnum;
196+
197+
const handleClick = () => {
198+
if (!isClickable || !matchedStep) return;
199+
200+
const baseParams = {
201+
environmentSlug: currentEnvironment?.slug ?? '',
202+
workflowSlug: workflow?.slug ?? '',
203+
};
204+
205+
const stepRoute =
206+
routeStepType === StepTypeEnum.DELAY ||
207+
routeStepType === StepTypeEnum.DIGEST ||
208+
routeStepType === StepTypeEnum.THROTTLE
209+
? ROUTES.EDIT_STEP
210+
: ROUTES.EDIT_STEP_TEMPLATE;
211+
212+
const absolutePath = `${buildRoute(ROUTES.EDIT_WORKFLOW, baseParams)}/${buildRoute(stepRoute, { stepSlug: matchedStep.slug })}`;
213+
navigate(absolutePath);
214+
};
215+
183216
return (
184217
<AnimatePresence mode="wait">
185218
{showStreaming ? (
@@ -201,10 +234,11 @@ function WorkflowStepItem({
201234
initial={{ opacity: 0 }}
202235
animate={{ opacity: 1 }}
203236
transition={stepTransition}
204-
className={cn(stepItemBaseClasses, 'bg-bg-weak')}
237+
className={cn(stepItemBaseClasses, 'bg-bg-weak', isClickable && 'cursor-pointer hover:bg-bg-weak/80')}
238+
onClick={isClickable ? handleClick : undefined}
205239
>
206240
<div
207-
className="flex size-5 items-center justify-center border opacity-40 rounded-full"
241+
className="flex size-5 min-w-5 items-center justify-center border opacity-40 rounded-full"
208242
style={{ borderColor: `hsl(var(--${color}))`, color: `hsl(var(--${color}))` }}
209243
>
210244
<Icon className="size-3" />

apps/dashboard/src/components/ai-sidekick/novu-copilot-panel.tsx

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Badge } from '../primitives/badge';
33
import { useAiChat } from './ai-chat-context';
44
import { ChatBody, ChatBodySkeleton } from './chat-body';
55

6-
export function NovuCopilotPanel() {
6+
export function NovuCopilotPanel({ hideHeader }: { hideHeader?: boolean }) {
77
const {
88
hasNoChatHistory,
99
messages,
@@ -27,27 +27,29 @@ export function NovuCopilotPanel() {
2727

2828
return (
2929
<div className="flex h-full w-full min-w-0 flex-col overflow-hidden bg-white">
30-
<div className="flex shrink-0 items-center justify-between gap-3 border-b px-3 py-2">
31-
<div className="flex items-center gap-0.5 rounded px-0.5 py-1">
32-
<div className="flex size-5 items-center justify-center">
33-
<BroomSparkle className="size-3" isAnimating={isGenerating} />
30+
{!hideHeader && (
31+
<div className="flex shrink-0 items-center justify-between gap-3 border-b px-3 py-2">
32+
<div className="flex items-center gap-0.5 rounded px-0.5 py-1">
33+
<div className="flex size-5 items-center justify-center">
34+
<BroomSparkle className="size-3" isAnimating={isGenerating} />
35+
</div>
36+
<span
37+
className="text-label-sm font-medium"
38+
style={{
39+
background: 'linear-gradient(90deg, #939292 0%, #646464 100%)',
40+
WebkitBackgroundClip: 'text',
41+
WebkitTextFillColor: 'transparent',
42+
backgroundClip: 'text',
43+
}}
44+
>
45+
Novu Copilot
46+
</span>
47+
<Badge variant="lighter" color="gray" className="ml-1">
48+
BETA
49+
</Badge>
3450
</div>
35-
<span
36-
className="text-label-sm font-medium"
37-
style={{
38-
background: 'linear-gradient(90deg, #939292 0%, #646464 100%)',
39-
WebkitBackgroundClip: 'text',
40-
WebkitTextFillColor: 'transparent',
41-
backgroundClip: 'text',
42-
}}
43-
>
44-
Novu Copilot
45-
</span>
46-
<Badge variant="lighter" color="gray" className="ml-1">
47-
BETA
48-
</Badge>
4951
</div>
50-
</div>
52+
)}
5153
{isLoading ? (
5254
<ChatBodySkeleton />
5355
) : (

apps/dashboard/src/components/command-palette/command-palette-provider.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
1+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
22
import { useEscapeKeyManager } from '@/context/escape-key-manager/hooks';
33
import { EscapeKeyManagerPriority } from '@/context/escape-key-manager/priority';
44
import { useTelemetry } from '@/hooks/use-telemetry';
@@ -56,12 +56,15 @@ export function CommandPaletteProvider({ children }: { children: React.ReactNode
5656
};
5757
}, [toggleCommandPalette]);
5858

59-
const value = {
60-
isOpen,
61-
openCommandPalette,
62-
closeCommandPalette,
63-
toggleCommandPalette,
64-
};
59+
const value = useMemo(
60+
() => ({
61+
isOpen,
62+
openCommandPalette,
63+
closeCommandPalette,
64+
toggleCommandPalette,
65+
}),
66+
[isOpen, openCommandPalette, closeCommandPalette, toggleCommandPalette]
67+
);
6568

6669
return <CommandPaletteContext.Provider value={value}>{children}</CommandPaletteContext.Provider>;
6770
}

0 commit comments

Comments
 (0)