Add message level 3 dots with explain, fix and view json

This commit is contained in:
akhisud3195 2025-07-21 17:57:17 +05:30
parent a1e4eddb72
commit c48c6a46ae
3 changed files with 236 additions and 76 deletions

View file

@ -12,7 +12,7 @@ import { ProfileContextBox } from "./profile-context-box";
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { FeedbackModal } from "./feedback-modal";
import { FIX_WORKFLOW_PROMPT, FIX_WORKFLOW_PROMPT_WITH_FEEDBACK } from "../copilot-prompts";
import { FIX_WORKFLOW_PROMPT, FIX_WORKFLOW_PROMPT_WITH_FEEDBACK, EXPLAIN_WORKFLOW_PROMPT_ASSISTANT, EXPLAIN_WORKFLOW_PROMPT_TOOL, EXPLAIN_WORKFLOW_PROMPT_TRANSITION } from "../copilot-prompts";
export function Chat({
chat,
@ -54,6 +54,8 @@ export function Chat({
const [showFeedbackModal, setShowFeedbackModal] = useState(false);
const [pendingFixMessage, setPendingFixMessage] = useState<string | null>(null);
const [showSuccessMessage, setShowSuccessMessage] = useState(false);
// Add state for explain (no modal needed, just direct trigger)
const [showExplainSuccess, setShowExplainSuccess] = useState(false);
// --- Scroll/auto-scroll/unread bubble logic ---
const scrollContainerRef = useRef<HTMLDivElement>(null);
@ -104,20 +106,24 @@ export function Chat({
}, [messages]);
// Handle fix functionality
const handleFix = useCallback((message: string) => {
const [pendingFixIndex, setPendingFixIndex] = useState<number | null>(null);
const handleFix = useCallback((message: string, index: number) => {
setPendingFixMessage(message);
setPendingFixIndex(index);
setShowFeedbackModal(true);
}, []);
const handleFeedbackSubmit = useCallback((feedback: string) => {
if (!pendingFixMessage) return;
if (!pendingFixMessage || pendingFixIndex === null) return;
// Create the copilot prompt
const prompt = feedback.trim()
// Create the copilot prompt with index
const prompt = feedback.trim()
? FIX_WORKFLOW_PROMPT_WITH_FEEDBACK
.replace('{index}', String(pendingFixIndex))
.replace('{chat_turn}', pendingFixMessage)
.replace('{feedback}', feedback)
: FIX_WORKFLOW_PROMPT
.replace('{index}', String(pendingFixIndex))
.replace('{chat_turn}', pendingFixMessage);
// Use the triggerCopilotChat function if available, otherwise fall back to localStorage
@ -132,7 +138,28 @@ export function Chat({
alert('Fix request submitted! Redirecting to workflow editor...');
window.location.href = `/projects/${projectId}/workflow`;
}
}, [pendingFixMessage, projectId, triggerCopilotChat]);
}, [pendingFixMessage, pendingFixIndex, projectId, triggerCopilotChat]);
// Handle explain functionality
const handleExplain = useCallback((type: 'assistant' | 'tool' | 'transition', message: string, index: number) => {
let prompt = '';
if (type === 'assistant') {
prompt = EXPLAIN_WORKFLOW_PROMPT_ASSISTANT.replace('{index}', String(index)).replace('{chat_turn}', message);
} else if (type === 'tool') {
prompt = EXPLAIN_WORKFLOW_PROMPT_TOOL.replace('{index}', String(index)).replace('{chat_turn}', message);
} else if (type === 'transition') {
prompt = EXPLAIN_WORKFLOW_PROMPT_TRANSITION.replace('{index}', String(index)).replace('{chat_turn}', message);
}
if (triggerCopilotChat) {
triggerCopilotChat(prompt);
setShowExplainSuccess(true);
setTimeout(() => setShowExplainSuccess(false), 3000);
} else {
localStorage.setItem(`project_prompt_${projectId}`, prompt);
alert('Explain request submitted! Redirecting to workflow editor...');
window.location.href = `/projects/${projectId}/workflow`;
}
}, [projectId, triggerCopilotChat]);
// collect published tool call results
const toolCallResults: Record<string, z.infer<typeof ToolMessage>> = {};
@ -345,6 +372,7 @@ export function Chat({
showDebugMessages={showDebugMessages}
showJsonMode={showJsonMode}
onFix={handleFix}
onExplain={handleExplain}
/>
{showUnreadBubble && (
<button
@ -378,6 +406,19 @@ export function Chat({
</Button>
</div>
)}
{showExplainSuccess && (
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800
rounded-lg flex gap-2 justify-between items-center">
<p className="text-blue-600 dark:text-blue-400 text-sm">Skipper will explain this for you now.</p>
<Button
size="sm"
color="primary"
onPress={() => setShowExplainSuccess(false)}
>
Dismiss
</Button>
</div>
)}
{fetchResponseError && (
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800
rounded-lg flex gap-2 justify-between items-center">

View file

@ -5,7 +5,8 @@ import z from "zod";
import { Workflow } from "@/app/lib/types/workflow_types";
import { WorkflowTool } from "@/app/lib/types/workflow_types";
import MarkdownContent from "@/app/lib/components/markdown-content";
import { ChevronRightIcon, ChevronDownIcon, ChevronUpIcon, CodeIcon, CheckCircleIcon, FileTextIcon, EyeIcon, EyeOffIcon, WrapTextIcon, ArrowRightFromLineIcon, BracesIcon, TextIcon, FlagIcon } from "lucide-react";
import { ChevronRightIcon, ChevronDownIcon, ChevronUpIcon, CodeIcon, CheckCircleIcon, FileTextIcon, EyeIcon, EyeOffIcon, WrapTextIcon, ArrowRightFromLineIcon, BracesIcon, TextIcon, FlagIcon, HelpCircleIcon, MoreHorizontal } from "lucide-react";
import { Dropdown, DropdownMenu, DropdownTrigger, DropdownItem } from "@heroui/react";
import { ProfileContextBox } from "./profile-context-box";
import { Message, ToolMessage, AssistantMessageWithToolCalls } from "@/app/lib/types/types";
@ -29,7 +30,7 @@ function UserMessage({ content }: { content: string }) {
);
}
function InternalAssistantMessage({ content, sender, latency, delta, showJsonMode = false, onFix, showDebugMessages, isFirstAssistant }: { content: string, sender: string | null | undefined, latency: number, delta: number, showJsonMode?: boolean, onFix?: (message: string) => void, showDebugMessages?: boolean, isFirstAssistant?: boolean }) {
function InternalAssistantMessage({ content, sender, latency, delta, showJsonMode = false, onFix, onExplain, showDebugMessages, isFirstAssistant, index }: { content: string, sender: string | null | undefined, latency: number, delta: number, showJsonMode?: boolean, onFix?: (message: string, index: number) => void, onExplain?: (type: 'assistant', message: string, index: number) => void, showDebugMessages?: boolean, isFirstAssistant?: boolean, index: number }) {
const isJsonContent = useMemo(() => {
try {
JSON.parse(content);
@ -86,37 +87,30 @@ function InternalAssistantMessage({ content, sender, latency, delta, showJsonMod
<div className="max-w-[85%] inline-block">
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1 flex justify-between items-center mb-2">
<span>{sender ?? 'Assistant'}</span>
{showDebugMessages && onFix && !isFirstAssistant && (
<button
onClick={() => onFix(content)}
className="flex items-center gap-1 text-xs text-orange-700 dark:text-orange-400 hover:text-orange-800 dark:hover:text-orange-300 hover:underline"
title="Fix this response"
>
<FlagIcon size={12} />
Fix
</button>
{(Boolean(showDebugMessages && typeof onFix === 'function' && !isFirstAssistant)
|| Boolean(showDebugMessages && typeof onExplain === 'function' && !isFirstAssistant)
|| Boolean(isJsonContent && hasResponseKey)) && (
<MessageActionsMenu
showFix={Boolean(showDebugMessages && typeof onFix === 'function' && !isFirstAssistant)}
showExplain={Boolean(showDebugMessages && typeof onExplain === 'function' && !isFirstAssistant)}
showJson={Boolean(isJsonContent && hasResponseKey)}
onFix={onFix ? () => onFix(content, index) : () => {}}
onExplain={onExplain ? () => onExplain('assistant', content, index) : () => {}}
onJson={() => {}}
/>
)}
</div>
<div className="bg-gray-50 dark:bg-zinc-800 px-4 py-2.5 rounded-2xl rounded-bl-lg text-sm leading-relaxed text-gray-700 dark:text-gray-200 border-none shadow-sm animate-slideUpAndFade flex flex-col items-stretch">
<div className="text-left mb-2">
{isJsonContent && hasResponseKey && (
{isJsonContent && hasResponseKey && jsonMode && (
<div className="mb-2 flex gap-4">
<button
className="flex items-center gap-1 text-xs text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-300 hover:underline self-start"
onClick={() => setJsonMode(!jsonMode)}
onClick={() => setWrapText(!wrapText)}
>
{jsonMode ? <TextIcon size={14} /> : <BracesIcon size={14} />}
{jsonMode ? 'View response text' : 'View complete JSON'}
{wrapText ? <ArrowRightFromLineIcon size={14} /> : <WrapTextIcon size={14} />}
{wrapText ? 'Overflow' : 'Wrap'}
</button>
{jsonMode && (
<button
className="flex items-center gap-1 text-xs text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-300 hover:underline self-start"
onClick={() => setWrapText(!wrapText)}
>
{wrapText ? <ArrowRightFromLineIcon size={14} /> : <WrapTextIcon size={14} />}
{wrapText ? 'Overflow' : 'Wrap'}
</button>
)}
</div>
)}
{isJsonContent && hasResponseKey && jsonMode ? (
@ -148,30 +142,35 @@ function AssistantMessage({
sender,
latency,
onFix,
onExplain,
showDebugMessages,
isFirstAssistant
isFirstAssistant,
index
}: {
content: string,
sender: string | null | undefined,
latency: number,
onFix?: (message: string) => void,
onFix?: (message: string, index: number) => void,
onExplain?: (type: 'assistant', message: string, index: number) => void,
showDebugMessages?: boolean,
isFirstAssistant?: boolean
isFirstAssistant?: boolean,
index: number
}) {
return (
<div className="self-start flex flex-col gap-1 my-5">
<div className="max-w-[85%] inline-block">
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1 flex justify-between items-center mb-2">
<span>{sender ?? 'Assistant'}</span>
{showDebugMessages && onFix && !isFirstAssistant && (
<button
onClick={() => onFix(content)}
className="flex items-center gap-1 text-xs text-orange-700 dark:text-orange-400 hover:text-orange-800 dark:hover:text-orange-300 hover:underline"
title="Fix this response"
>
<FlagIcon size={12} />
Fix
</button>
{(Boolean(showDebugMessages && typeof onFix === 'function' && !isFirstAssistant)
|| Boolean(showDebugMessages && typeof onExplain === 'function' && !isFirstAssistant)) && (
<MessageActionsMenu
showFix={Boolean(showDebugMessages && typeof onFix === 'function' && !isFirstAssistant)}
showExplain={Boolean(showDebugMessages && typeof onExplain === 'function' && !isFirstAssistant)}
showJson={false}
onFix={onFix ? () => onFix(content, index) : () => {}}
onExplain={onExplain ? () => onExplain('assistant', content, index) : () => {}}
onJson={() => {}}
/>
)}
</div>
<div className="bg-purple-50 dark:bg-purple-900/30 px-4 py-2.5
@ -216,8 +215,10 @@ function ToolCalls({
systemMessage,
delta,
onFix,
onExplain,
showDebugMessages,
isFirstAssistant
isFirstAssistant,
parentIndex
}: {
toolCalls: z.infer<typeof AssistantMessageWithToolCalls>['toolCalls'];
results: Record<string, z.infer<typeof ToolMessage>>;
@ -227,9 +228,11 @@ function ToolCalls({
workflow: z.infer<typeof Workflow>;
systemMessage: string | undefined;
delta: number;
onFix?: (message: string) => void;
onFix?: (message: string, index: number) => void;
onExplain?: (type: 'tool' | 'transition', message: string, index: number) => void;
showDebugMessages?: boolean;
isFirstAssistant?: boolean;
parentIndex: number;
}) {
return <div className="flex flex-col gap-4">
{toolCalls.map((toolCall, idx) => {
@ -241,8 +244,11 @@ function ToolCalls({
workflow={workflow}
delta={delta}
onFix={onFix}
onExplain={onExplain}
showDebugMessages={showDebugMessages}
isFirstAssistant={isFirstAssistant && idx === 0}
parentIndex={parentIndex}
toolCallIndex={idx}
/>
})}
</div>;
@ -255,17 +261,23 @@ function ToolCall({
workflow,
delta,
onFix,
onExplain,
showDebugMessages,
isFirstAssistant
isFirstAssistant,
parentIndex,
toolCallIndex
}: {
toolCall: z.infer<typeof AssistantMessageWithToolCalls>['toolCalls'][number];
result: z.infer<typeof ToolMessage> | undefined;
sender: string | null | undefined;
workflow: z.infer<typeof Workflow>;
delta: number;
onFix?: (message: string) => void;
onFix?: (message: string, index: number) => void;
onExplain?: (type: 'tool' | 'transition', message: string, index: number) => void;
showDebugMessages?: boolean;
isFirstAssistant?: boolean;
parentIndex: number;
toolCallIndex: number;
}) {
let matchingWorkflowTool: z.infer<typeof WorkflowTool> | undefined;
for (const tool of workflow.tools) {
@ -280,6 +292,10 @@ function ToolCall({
result={result}
sender={sender ?? ''}
delta={delta}
onExplain={onExplain}
showDebugMessages={showDebugMessages}
parentIndex={parentIndex}
toolCallIndex={toolCallIndex}
/>;
}
return <ClientToolCall
@ -289,18 +305,29 @@ function ToolCall({
workflow={workflow}
delta={delta}
onFix={onFix}
onExplain={onExplain}
showDebugMessages={showDebugMessages}
parentIndex={parentIndex}
toolCallIndex={toolCallIndex}
/>;
}
function TransferToAgentToolCall({
result: availableResult,
sender,
delta
delta,
onExplain,
showDebugMessages,
parentIndex,
toolCallIndex
}: {
result: z.infer<typeof ToolMessage> | undefined;
sender: string | null | undefined;
delta: number;
onExplain?: (type: 'transition', message: string, index: number) => void;
showDebugMessages?: boolean;
parentIndex: number;
toolCallIndex: number;
}) {
const typedResult = availableResult ? JSON.parse(availableResult.content) as { assistant: string } : undefined;
if (!typedResult) {
@ -318,6 +345,16 @@ function TransferToAgentToolCall({
<ChevronRightIcon size={14} className="text-gray-400 dark:text-gray-300" />
<span className="text-gray-700 dark:text-gray-200">{typedResult.assistant}</span>
<span className="ml-2">{deltaDisplay}</span>
{Boolean(showDebugMessages && typeof onExplain === 'function') && (
<MessageActionsMenu
showFix={false}
showExplain={true}
showJson={false}
onFix={() => {}}
onExplain={onExplain ? () => onExplain('transition', `From: ${sender} To: ${typedResult.assistant}`, parentIndex) : () => {}}
onJson={() => {}}
/>
)}
</div>
</div>
);
@ -330,15 +367,21 @@ function ClientToolCall({
workflow,
delta,
onFix,
showDebugMessages
onExplain,
showDebugMessages,
parentIndex,
toolCallIndex
}: {
toolCall: z.infer<typeof AssistantMessageWithToolCalls>['toolCalls'][number];
result: z.infer<typeof ToolMessage> | undefined;
sender: string | null | undefined;
workflow: z.infer<typeof Workflow>;
delta: number;
onFix?: (message: string) => void;
onFix?: (message: string, index: number) => void;
onExplain?: (type: 'tool', message: string, index: number) => void;
showDebugMessages?: boolean;
parentIndex: number;
toolCallIndex: number;
}) {
const [wrapText, setWrapText] = useState(true);
const [paramsExpanded, setParamsExpanded] = useState(false);
@ -350,19 +393,17 @@ function ClientToolCall({
if (isCompressed) {
return (
<div className="self-start flex flex-col gap-1 my-5">
{sender && (
{(Boolean(showDebugMessages && typeof onFix === 'function') || Boolean(showDebugMessages && typeof onExplain === 'function')) && (
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1 flex justify-between items-center">
<span>{sender}</span>
{showDebugMessages && onFix && (
<button
onClick={() => onFix(`Tool call: ${toolCall.function.name}`)}
className="flex items-center gap-1 text-xs text-orange-700 dark:text-orange-400 hover:text-orange-800 dark:hover:text-orange-300 hover:underline"
title="Fix this tool call"
>
<FlagIcon size={12} />
Fix
</button>
)}
<MessageActionsMenu
showFix={Boolean(showDebugMessages && typeof onFix === 'function')}
showExplain={Boolean(showDebugMessages && typeof onExplain === 'function')}
showJson={false}
onFix={onFix ? () => onFix(`Tool call: ${toolCall.function.name}`, parentIndex) : () => {}}
onExplain={onExplain ? () => onExplain('tool', `Tool call: ${toolCall.function.name}\nArguments: ${toolCall.function.arguments}`, parentIndex) : () => {}}
onJson={() => {}}
/>
</div>
)}
<div className="min-w-[85%]">
@ -425,19 +466,17 @@ function ClientToolCall({
// Expanded state: respect 85% max width, prevent overshoot
return (
<div className="self-start flex flex-col gap-1 my-5">
{sender && (
{(Boolean(showDebugMessages && typeof onFix === 'function') || Boolean(showDebugMessages && typeof onExplain === 'function')) && (
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1 flex justify-between items-center">
<span>{sender}</span>
{showDebugMessages && onFix && (
<button
onClick={() => onFix(`Tool call: ${toolCall.function.name}`)}
className="flex items-center gap-1 text-xs text-orange-700 dark:text-orange-400 hover:text-orange-800 dark:hover:text-orange-300 hover:underline"
title="Fix this tool call"
>
<FlagIcon size={12} />
Fix
</button>
)}
<MessageActionsMenu
showFix={Boolean(showDebugMessages && typeof onFix === 'function')}
showExplain={Boolean(showDebugMessages && typeof onExplain === 'function')}
showJson={false}
onFix={onFix ? () => onFix(`Tool call: ${toolCall.function.name}`, parentIndex) : () => {}}
onExplain={onExplain ? () => onExplain('tool', `Tool call: ${toolCall.function.name}\nArguments: ${toolCall.function.arguments}`, parentIndex) : () => {}}
onJson={() => {}}
/>
</div>
)}
<div className="w-full">
@ -567,6 +606,66 @@ function ExpandableContent({
</div>;
}
// MessageActionsMenu: a reusable 3-dots menu for message actions
type MessageActionsMenuProps = {
showFix: boolean;
showExplain: boolean;
showJson: boolean;
onFix: () => void;
onExplain: () => void;
onJson: () => void;
explainLabel?: string;
fixLabel?: string;
jsonLabel?: string;
disabledFix?: boolean;
disabledExplain?: boolean;
disabledJson?: boolean;
};
function MessageActionsMenu({
showFix,
showExplain,
showJson,
onFix,
onExplain,
onJson,
explainLabel = 'Explain',
fixLabel = 'Fix',
jsonLabel = 'View complete JSON',
disabledFix = false,
disabledExplain = false,
disabledJson = false,
}: MessageActionsMenuProps) {
return (
<Dropdown>
<DropdownTrigger>
<button className="p-1.5 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" aria-label="Message actions">
<MoreHorizontal size={18} />
</button>
</DropdownTrigger>
<DropdownMenu aria-label="Message actions menu">
{[
showExplain ? (
<DropdownItem key="explain" onClick={onExplain} isDisabled={disabledExplain} startContent={<HelpCircleIcon size={16} className="text-indigo-400 dark:text-indigo-300" />}>
{explainLabel}
</DropdownItem>
) : undefined,
showFix ? (
<DropdownItem key="fix" onClick={onFix} isDisabled={disabledFix} startContent={<FlagIcon size={16} className="text-orange-700 dark:text-orange-400" />}>
{fixLabel}
</DropdownItem>
) : undefined,
showJson ? (
<DropdownItem key="json" onClick={onJson} isDisabled={disabledJson} startContent={<BracesIcon size={16} className="text-slate-500 dark:text-slate-300" />}>
{jsonLabel}
</DropdownItem>
) : undefined,
].filter((el): el is React.ReactElement => Boolean(el)) as any}
</DropdownMenu>
</Dropdown>
);
}
export function Messages({
projectId,
messages,
@ -579,6 +678,7 @@ export function Messages({
showDebugMessages = true,
showJsonMode = false,
onFix,
onExplain,
}: {
projectId: string;
messages: z.infer<typeof Message>[];
@ -590,7 +690,8 @@ export function Messages({
showSystemMessage: boolean;
showDebugMessages?: boolean;
showJsonMode?: boolean;
onFix?: (message: string) => void;
onFix?: (message: string, index: number) => void;
onExplain?: (type: 'assistant' | 'tool' | 'transition', message: string, index: number) => void;
}) {
// Remove scroll/auto-scroll state and logic
// const scrollContainerRef = useRef<HTMLDivElement>(null);
@ -628,8 +729,10 @@ export function Messages({
systemMessage={systemMessage}
delta={latency}
onFix={onFix}
onExplain={onExplain}
showDebugMessages={showDebugMessages}
isFirstAssistant={isFirstAssistant}
parentIndex={index}
/>
);
}
@ -648,8 +751,10 @@ export function Messages({
delta={latency}
showJsonMode={showJsonMode}
onFix={onFix}
onExplain={onExplain}
showDebugMessages={showDebugMessages}
isFirstAssistant={isFirstAssistant}
index={index}
/>
);
}
@ -661,8 +766,10 @@ export function Messages({
sender={message.agentName ?? ''}
latency={latency}
onFix={onFix}
onExplain={onExplain}
showDebugMessages={showDebugMessages}
isFirstAssistant={isFirstAssistant}
index={index}
/>
);
}
@ -711,4 +818,8 @@ export function Messages({
{loadingAssistantResponse && <AssistantMessageLoading />}
</div>
);
}
}
// Add a utility class for icon-with-label-on-hover
const iconWithLabelClass = "group relative flex items-center gap-1 text-xs cursor-pointer hover:underline";
const iconLabelClass = "absolute left-full ml-2 px-2 py-1 rounded bg-zinc-800 text-white text-xs opacity-0 group-hover:opacity-100 pointer-events-none whitespace-nowrap z-10";

View file

@ -1,7 +1,15 @@
export const FIX_WORKFLOW_PROMPT = `There is an issue with this turn of chat: "{chat_turn}"
export const FIX_WORKFLOW_PROMPT = `There is an issue with this turn of chat (index {index}): "{chat_turn}"
Fix the issue by updating necessary agents and tools.`;
export const FIX_WORKFLOW_PROMPT_WITH_FEEDBACK = `${FIX_WORKFLOW_PROMPT}
export const FIX_WORKFLOW_PROMPT_WITH_FEEDBACK = `There is an issue with this turn of chat (index {index}): "{chat_turn}"
Here are more details: {feedback}`;
Fix the issue by updating necessary agents and tools.
Here are more details: "{feedback}"`;
export const EXPLAIN_WORKFLOW_PROMPT_ASSISTANT = `Please explain why the assistant responded with the following message (index {index}):\n"{chat_turn}"`;
export const EXPLAIN_WORKFLOW_PROMPT_TOOL = `Please explain why the following tool was called (index {index}):\n"{chat_turn}"`;
export const EXPLAIN_WORKFLOW_PROMPT_TRANSITION = `Please explain why the following agent transition occurred (index {index}):\n"{chat_turn}"`;