From c48c6a46ae644913f5875cb0975a945f2166d4f6 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Mon, 21 Jul 2025 17:57:17 +0530 Subject: [PATCH] Add message level 3 dots with explain, fix and view json --- .../playground/components/chat.tsx | 53 +++- .../playground/components/messages.tsx | 245 +++++++++++++----- .../[projectId]/playground/copilot-prompts.ts | 14 +- 3 files changed, 236 insertions(+), 76 deletions(-) diff --git a/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx b/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx index 18f8910e..cdd600c1 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx @@ -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(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(null); @@ -104,20 +106,24 @@ export function Chat({ }, [messages]); // Handle fix functionality - const handleFix = useCallback((message: string) => { + const [pendingFixIndex, setPendingFixIndex] = useState(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> = {}; @@ -345,6 +372,7 @@ export function Chat({ showDebugMessages={showDebugMessages} showJsonMode={showJsonMode} onFix={handleFix} + onExplain={handleExplain} /> {showUnreadBubble && ( + + )} {fetchResponseError && (
diff --git a/apps/rowboat/app/projects/[projectId]/playground/components/messages.tsx b/apps/rowboat/app/projects/[projectId]/playground/components/messages.tsx index 793a6c7e..bcbedef4 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/components/messages.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/components/messages.tsx @@ -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
{sender ?? 'Assistant'} - {showDebugMessages && onFix && !isFirstAssistant && ( - + {(Boolean(showDebugMessages && typeof onFix === 'function' && !isFirstAssistant) + || Boolean(showDebugMessages && typeof onExplain === 'function' && !isFirstAssistant) + || Boolean(isJsonContent && hasResponseKey)) && ( + onFix(content, index) : () => {}} + onExplain={onExplain ? () => onExplain('assistant', content, index) : () => {}} + onJson={() => {}} + /> )}
- {isJsonContent && hasResponseKey && ( + {isJsonContent && hasResponseKey && jsonMode && (
- {jsonMode && ( - - )}
)} {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 (
{sender ?? 'Assistant'} - {showDebugMessages && onFix && !isFirstAssistant && ( - + {(Boolean(showDebugMessages && typeof onFix === 'function' && !isFirstAssistant) + || Boolean(showDebugMessages && typeof onExplain === 'function' && !isFirstAssistant)) && ( + onFix(content, index) : () => {}} + onExplain={onExplain ? () => onExplain('assistant', content, index) : () => {}} + onJson={() => {}} + /> )}
{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} /> })}
; @@ -255,17 +261,23 @@ function ToolCall({ workflow, delta, onFix, + onExplain, showDebugMessages, - isFirstAssistant + isFirstAssistant, + parentIndex, + toolCallIndex }: { toolCall: z.infer['toolCalls'][number]; result: z.infer | undefined; sender: string | null | undefined; workflow: z.infer; 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 | 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 ; } function TransferToAgentToolCall({ result: availableResult, sender, - delta + delta, + onExplain, + showDebugMessages, + parentIndex, + toolCallIndex }: { result: z.infer | 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({ {typedResult.assistant} {deltaDisplay} + {Boolean(showDebugMessages && typeof onExplain === 'function') && ( + {}} + onExplain={onExplain ? () => onExplain('transition', `From: ${sender} To: ${typedResult.assistant}`, parentIndex) : () => {}} + onJson={() => {}} + /> + )}
); @@ -330,15 +367,21 @@ function ClientToolCall({ workflow, delta, onFix, - showDebugMessages + onExplain, + showDebugMessages, + parentIndex, + toolCallIndex }: { toolCall: z.infer['toolCalls'][number]; result: z.infer | undefined; sender: string | null | undefined; workflow: z.infer; 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 (
- {sender && ( + {(Boolean(showDebugMessages && typeof onFix === 'function') || Boolean(showDebugMessages && typeof onExplain === 'function')) && (
{sender} - {showDebugMessages && onFix && ( - - )} + onFix(`Tool call: ${toolCall.function.name}`, parentIndex) : () => {}} + onExplain={onExplain ? () => onExplain('tool', `Tool call: ${toolCall.function.name}\nArguments: ${toolCall.function.arguments}`, parentIndex) : () => {}} + onJson={() => {}} + />
)}
@@ -425,19 +466,17 @@ function ClientToolCall({ // Expanded state: respect 85% max width, prevent overshoot return (
- {sender && ( + {(Boolean(showDebugMessages && typeof onFix === 'function') || Boolean(showDebugMessages && typeof onExplain === 'function')) && (
{sender} - {showDebugMessages && onFix && ( - - )} + onFix(`Tool call: ${toolCall.function.name}`, parentIndex) : () => {}} + onExplain={onExplain ? () => onExplain('tool', `Tool call: ${toolCall.function.name}\nArguments: ${toolCall.function.arguments}`, parentIndex) : () => {}} + onJson={() => {}} + />
)}
@@ -567,6 +606,66 @@ function ExpandableContent({
; } +// 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 ( + + + + + + {[ + showExplain ? ( + }> + {explainLabel} + + ) : undefined, + showFix ? ( + }> + {fixLabel} + + ) : undefined, + showJson ? ( + }> + {jsonLabel} + + ) : undefined, + ].filter((el): el is React.ReactElement => Boolean(el)) as any} + + + ); +} + export function Messages({ projectId, messages, @@ -579,6 +678,7 @@ export function Messages({ showDebugMessages = true, showJsonMode = false, onFix, + onExplain, }: { projectId: string; messages: z.infer[]; @@ -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(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 && }
); -} \ No newline at end of file +} + +// 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"; \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/playground/copilot-prompts.ts b/apps/rowboat/app/projects/[projectId]/playground/copilot-prompts.ts index 37d47294..90b9217e 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/copilot-prompts.ts +++ b/apps/rowboat/app/projects/[projectId]/playground/copilot-prompts.ts @@ -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}"`;