mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-31 19:15:17 +02:00
Add fix ctas at message levelg
This commit is contained in:
parent
a3bf3046e3
commit
9d3eccbf87
7 changed files with 261 additions and 31 deletions
|
|
@ -163,3 +163,12 @@ body {
|
||||||
.animate-float {
|
.animate-float {
|
||||||
animation: float 5s ease-in-out infinite, pulse-mascot 4s infinite;
|
animation: float 5s ease-in-out infinite, pulse-mascot 4s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Feedback modal textarea overrides */
|
||||||
|
.feedback-modal textarea,
|
||||||
|
.feedback-modal textarea:focus {
|
||||||
|
font-size: 0.75rem !important; /* Tailwind's text-xs */
|
||||||
|
box-shadow: none !important;
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #d1d5db !important; /* Tailwind's gray-300 */
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export function App({
|
||||||
isInitialState = false,
|
isInitialState = false,
|
||||||
onPanelClick,
|
onPanelClick,
|
||||||
projectTools,
|
projectTools,
|
||||||
|
triggerCopilotChat,
|
||||||
}: {
|
}: {
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|
@ -36,6 +37,7 @@ export function App({
|
||||||
isInitialState?: boolean;
|
isInitialState?: boolean;
|
||||||
onPanelClick?: () => void;
|
onPanelClick?: () => void;
|
||||||
projectTools: z.infer<typeof WorkflowTool>[];
|
projectTools: z.infer<typeof WorkflowTool>[];
|
||||||
|
triggerCopilotChat?: (message: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [counter, setCounter] = useState<number>(0);
|
const [counter, setCounter] = useState<number>(0);
|
||||||
const [testProfile, setTestProfile] = useState<WithStringId<z.infer<typeof TestProfile>> | null>(null);
|
const [testProfile, setTestProfile] = useState<WithStringId<z.infer<typeof TestProfile>> | null>(null);
|
||||||
|
|
@ -187,6 +189,7 @@ export function App({
|
||||||
onCopyClick={(fn) => { getCopyContentRef.current = fn; }}
|
onCopyClick={(fn) => { getCopyContentRef.current = fn; }}
|
||||||
showDebugMessages={showDebugMessages}
|
showDebugMessages={showDebugMessages}
|
||||||
projectTools={projectTools}
|
projectTools={projectTools}
|
||||||
|
triggerCopilotChat={triggerCopilotChat}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import { ProfileContextBox } from "./profile-context-box";
|
||||||
import { USE_TESTING_FEATURE } from "@/app/lib/feature_flags";
|
import { USE_TESTING_FEATURE } from "@/app/lib/feature_flags";
|
||||||
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { FeedbackModal } from "./feedback-modal";
|
||||||
|
import { FIX_WORKFLOW_PROMPT, FIX_WORKFLOW_PROMPT_WITH_FEEDBACK } from "../copilot-prompts";
|
||||||
|
|
||||||
export function Chat({
|
export function Chat({
|
||||||
chat,
|
chat,
|
||||||
|
|
@ -29,6 +31,7 @@ export function Chat({
|
||||||
showDebugMessages = true,
|
showDebugMessages = true,
|
||||||
showJsonMode = false,
|
showJsonMode = false,
|
||||||
projectTools,
|
projectTools,
|
||||||
|
triggerCopilotChat,
|
||||||
}: {
|
}: {
|
||||||
chat: z.infer<typeof PlaygroundChat>;
|
chat: z.infer<typeof PlaygroundChat>;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|
@ -44,6 +47,7 @@ export function Chat({
|
||||||
showDebugMessages?: boolean;
|
showDebugMessages?: boolean;
|
||||||
showJsonMode?: boolean;
|
showJsonMode?: boolean;
|
||||||
projectTools: z.infer<typeof WorkflowTool>[];
|
projectTools: z.infer<typeof WorkflowTool>[];
|
||||||
|
triggerCopilotChat?: (message: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [messages, setMessages] = useState<z.infer<typeof Message>[]>(chat.messages);
|
const [messages, setMessages] = useState<z.infer<typeof Message>[]>(chat.messages);
|
||||||
const [loadingAssistantResponse, setLoadingAssistantResponse] = useState<boolean>(false);
|
const [loadingAssistantResponse, setLoadingAssistantResponse] = useState<boolean>(false);
|
||||||
|
|
@ -53,6 +57,9 @@ export function Chat({
|
||||||
const [lastAgenticResponse, setLastAgenticResponse] = useState<unknown | null>(null);
|
const [lastAgenticResponse, setLastAgenticResponse] = useState<unknown | null>(null);
|
||||||
const [optimisticMessages, setOptimisticMessages] = useState<z.infer<typeof Message>[]>(chat.messages);
|
const [optimisticMessages, setOptimisticMessages] = useState<z.infer<typeof Message>[]>(chat.messages);
|
||||||
const [isLastInteracted, setIsLastInteracted] = useState(false);
|
const [isLastInteracted, setIsLastInteracted] = useState(false);
|
||||||
|
const [showFeedbackModal, setShowFeedbackModal] = useState(false);
|
||||||
|
const [pendingFixMessage, setPendingFixMessage] = useState<string | null>(null);
|
||||||
|
const [showSuccessMessage, setShowSuccessMessage] = useState(false);
|
||||||
|
|
||||||
// --- Scroll/auto-scroll/unread bubble logic ---
|
// --- Scroll/auto-scroll/unread bubble logic ---
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -101,6 +108,37 @@ export function Chat({
|
||||||
setOptimisticMessages(messages);
|
setOptimisticMessages(messages);
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
// Handle fix functionality
|
||||||
|
const handleFix = useCallback((message: string) => {
|
||||||
|
setPendingFixMessage(message);
|
||||||
|
setShowFeedbackModal(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFeedbackSubmit = useCallback((feedback: string) => {
|
||||||
|
if (!pendingFixMessage) return;
|
||||||
|
|
||||||
|
// Create the copilot prompt
|
||||||
|
const prompt = feedback.trim()
|
||||||
|
? FIX_WORKFLOW_PROMPT_WITH_FEEDBACK
|
||||||
|
.replace('{chat_turn}', pendingFixMessage)
|
||||||
|
.replace('{feedback}', feedback)
|
||||||
|
: FIX_WORKFLOW_PROMPT
|
||||||
|
.replace('{chat_turn}', pendingFixMessage);
|
||||||
|
|
||||||
|
// Use the triggerCopilotChat function if available, otherwise fall back to localStorage
|
||||||
|
if (triggerCopilotChat) {
|
||||||
|
triggerCopilotChat(prompt);
|
||||||
|
// Show a subtle success indication
|
||||||
|
setShowSuccessMessage(true);
|
||||||
|
setTimeout(() => setShowSuccessMessage(false), 3000);
|
||||||
|
} else {
|
||||||
|
// Fallback for standalone playground
|
||||||
|
localStorage.setItem(`project_prompt_${projectId}`, prompt);
|
||||||
|
alert('Fix request submitted! Redirecting to workflow editor...');
|
||||||
|
window.location.href = `/projects/${projectId}/workflow`;
|
||||||
|
}
|
||||||
|
}, [pendingFixMessage, projectId, triggerCopilotChat]);
|
||||||
|
|
||||||
// collect published tool call results
|
// collect published tool call results
|
||||||
const toolCallResults: Record<string, z.infer<typeof ToolMessage>> = {};
|
const toolCallResults: Record<string, z.infer<typeof ToolMessage>> = {};
|
||||||
optimisticMessages
|
optimisticMessages
|
||||||
|
|
@ -302,6 +340,7 @@ export function Chat({
|
||||||
showSystemMessage={false}
|
showSystemMessage={false}
|
||||||
showDebugMessages={showDebugMessages}
|
showDebugMessages={showDebugMessages}
|
||||||
showJsonMode={showJsonMode}
|
showJsonMode={showJsonMode}
|
||||||
|
onFix={handleFix}
|
||||||
/>
|
/>
|
||||||
{showUnreadBubble && (
|
{showUnreadBubble && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -322,6 +361,19 @@ export function Chat({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sticky bottom-0 bg-white dark:bg-zinc-900 pt-4 pb-2">
|
<div className="sticky bottom-0 bg-white dark:bg-zinc-900 pt-4 pb-2">
|
||||||
|
{showSuccessMessage && (
|
||||||
|
<div className="mb-4 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800
|
||||||
|
rounded-lg flex gap-2 justify-between items-center">
|
||||||
|
<p className="text-green-600 dark:text-green-400 text-sm">Skipper will suggest fixes for you now.</p>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
color="success"
|
||||||
|
onPress={() => setShowSuccessMessage(false)}
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{fetchResponseError && (
|
{fetchResponseError && (
|
||||||
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800
|
<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">
|
rounded-lg flex gap-2 justify-between items-center">
|
||||||
|
|
@ -354,5 +406,11 @@ export function Chat({
|
||||||
onClose={() => setBillingError(null)}
|
onClose={() => setBillingError(null)}
|
||||||
errorMessage={billingError || ''}
|
errorMessage={billingError || ''}
|
||||||
/>
|
/>
|
||||||
|
<FeedbackModal
|
||||||
|
isOpen={showFeedbackModal}
|
||||||
|
onClose={() => setShowFeedbackModal(false)}
|
||||||
|
onSubmit={handleFeedbackSubmit}
|
||||||
|
title="Fix Assistant"
|
||||||
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
'use client';
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, Textarea } from "@heroui/react";
|
||||||
|
|
||||||
|
interface FeedbackModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (feedback: string) => void;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeedbackModal({ isOpen, onClose, onSubmit, title = "Provide Feedback" }: FeedbackModalProps) {
|
||||||
|
const [feedback, setFeedback] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
onSubmit(feedback);
|
||||||
|
setFeedback("");
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setFeedback("");
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={handleCancel} size="md">
|
||||||
|
<ModalContent className="feedback-modal">
|
||||||
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
|
{title}
|
||||||
|
</ModalHeader>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 px-6 pt-1 pb-0">
|
||||||
|
Tell Skipper what needs to be fixed
|
||||||
|
</p>
|
||||||
|
<ModalBody>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Textarea
|
||||||
|
placeholder="Describe the issue..."
|
||||||
|
value={feedback}
|
||||||
|
onChange={(e) => setFeedback(e.target.value)}
|
||||||
|
minRows={3}
|
||||||
|
maxRows={6}
|
||||||
|
className="w-full !text-xs focus:ring-0 focus:shadow-none focus:border-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="bordered" onPress={handleCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onPress={handleSubmit}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import z from "zod";
|
||||||
import { Workflow } from "@/app/lib/types/workflow_types";
|
import { Workflow } from "@/app/lib/types/workflow_types";
|
||||||
import { WorkflowTool } from "@/app/lib/types/workflow_types";
|
import { WorkflowTool } from "@/app/lib/types/workflow_types";
|
||||||
import MarkdownContent from "@/app/lib/components/markdown-content";
|
import MarkdownContent from "@/app/lib/components/markdown-content";
|
||||||
import { ChevronRightIcon, ChevronDownIcon, ChevronUpIcon, CodeIcon, CheckCircleIcon, FileTextIcon, EyeIcon, EyeOffIcon, WrapTextIcon, ArrowRightFromLineIcon, BracesIcon, TextIcon } from "lucide-react";
|
import { ChevronRightIcon, ChevronDownIcon, ChevronUpIcon, CodeIcon, CheckCircleIcon, FileTextIcon, EyeIcon, EyeOffIcon, WrapTextIcon, ArrowRightFromLineIcon, BracesIcon, TextIcon, FlagIcon } from "lucide-react";
|
||||||
import { TestProfile } from "@/app/lib/types/testing_types";
|
import { TestProfile } from "@/app/lib/types/testing_types";
|
||||||
import { ProfileContextBox } from "./profile-context-box";
|
import { ProfileContextBox } from "./profile-context-box";
|
||||||
import { Message, ToolMessage, AssistantMessageWithToolCalls } from "@/app/lib/types/types";
|
import { Message, ToolMessage, AssistantMessageWithToolCalls } from "@/app/lib/types/types";
|
||||||
|
|
@ -30,7 +30,7 @@ function UserMessage({ content }: { content: string }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InternalAssistantMessage({ content, sender, latency, delta, showJsonMode = false }: { content: string, sender: string | null | undefined, latency: number, delta: number, showJsonMode?: boolean }) {
|
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 }) {
|
||||||
const isJsonContent = useMemo(() => {
|
const isJsonContent = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
JSON.parse(content);
|
JSON.parse(content);
|
||||||
|
|
@ -84,10 +84,20 @@ function InternalAssistantMessage({ content, sender, latency, delta, showJsonMod
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="self-start flex flex-col gap-1 my-5">
|
<div className="self-start flex flex-col gap-1 my-5">
|
||||||
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1">
|
|
||||||
{sender ?? 'Assistant'}
|
|
||||||
</div>
|
|
||||||
<div className="max-w-[85%] inline-block">
|
<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>
|
||||||
|
)}
|
||||||
|
</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="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">
|
<div className="text-left mb-2">
|
||||||
{isJsonContent && hasResponseKey && (
|
{isJsonContent && hasResponseKey && (
|
||||||
|
|
@ -134,13 +144,37 @@ function InternalAssistantMessage({ content, sender, latency, delta, showJsonMod
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssistantMessage({ content, sender, latency }: { content: string, sender: string | null | undefined, latency: number }) {
|
function AssistantMessage({
|
||||||
|
content,
|
||||||
|
sender,
|
||||||
|
latency,
|
||||||
|
onFix,
|
||||||
|
showDebugMessages,
|
||||||
|
isFirstAssistant
|
||||||
|
}: {
|
||||||
|
content: string,
|
||||||
|
sender: string | null | undefined,
|
||||||
|
latency: number,
|
||||||
|
onFix?: (message: string) => void,
|
||||||
|
showDebugMessages?: boolean,
|
||||||
|
isFirstAssistant?: boolean
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="self-start flex flex-col gap-1 my-5">
|
<div className="self-start flex flex-col gap-1 my-5">
|
||||||
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1">
|
|
||||||
{sender ?? 'Assistant'}
|
|
||||||
</div>
|
|
||||||
<div className="max-w-[85%] inline-block">
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="bg-purple-50 dark:bg-purple-900/30 px-4 py-2.5
|
<div className="bg-purple-50 dark:bg-purple-900/30 px-4 py-2.5
|
||||||
rounded-2xl rounded-bl-lg text-sm leading-relaxed
|
rounded-2xl rounded-bl-lg text-sm leading-relaxed
|
||||||
text-gray-800 dark:text-purple-100
|
text-gray-800 dark:text-purple-100
|
||||||
|
|
@ -182,7 +216,10 @@ function ToolCalls({
|
||||||
workflow,
|
workflow,
|
||||||
testProfile = null,
|
testProfile = null,
|
||||||
systemMessage,
|
systemMessage,
|
||||||
delta
|
delta,
|
||||||
|
onFix,
|
||||||
|
showDebugMessages,
|
||||||
|
isFirstAssistant
|
||||||
}: {
|
}: {
|
||||||
toolCalls: z.infer<typeof AssistantMessageWithToolCalls>['toolCalls'];
|
toolCalls: z.infer<typeof AssistantMessageWithToolCalls>['toolCalls'];
|
||||||
results: Record<string, z.infer<typeof ToolMessage>>;
|
results: Record<string, z.infer<typeof ToolMessage>>;
|
||||||
|
|
@ -193,9 +230,12 @@ function ToolCalls({
|
||||||
testProfile: z.infer<typeof TestProfile> | null;
|
testProfile: z.infer<typeof TestProfile> | null;
|
||||||
systemMessage: string | undefined;
|
systemMessage: string | undefined;
|
||||||
delta: number;
|
delta: number;
|
||||||
|
onFix?: (message: string) => void;
|
||||||
|
showDebugMessages?: boolean;
|
||||||
|
isFirstAssistant?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return <div className="flex flex-col gap-4">
|
return <div className="flex flex-col gap-4">
|
||||||
{toolCalls.map(toolCall => {
|
{toolCalls.map((toolCall, idx) => {
|
||||||
return <ToolCall
|
return <ToolCall
|
||||||
key={toolCall.id}
|
key={toolCall.id}
|
||||||
toolCall={toolCall}
|
toolCall={toolCall}
|
||||||
|
|
@ -203,6 +243,9 @@ function ToolCalls({
|
||||||
sender={sender}
|
sender={sender}
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
delta={delta}
|
delta={delta}
|
||||||
|
onFix={onFix}
|
||||||
|
showDebugMessages={showDebugMessages}
|
||||||
|
isFirstAssistant={isFirstAssistant && idx === 0}
|
||||||
/>
|
/>
|
||||||
})}
|
})}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
@ -213,13 +256,19 @@ function ToolCall({
|
||||||
result,
|
result,
|
||||||
sender,
|
sender,
|
||||||
workflow,
|
workflow,
|
||||||
delta
|
delta,
|
||||||
|
onFix,
|
||||||
|
showDebugMessages,
|
||||||
|
isFirstAssistant
|
||||||
}: {
|
}: {
|
||||||
toolCall: z.infer<typeof AssistantMessageWithToolCalls>['toolCalls'][number];
|
toolCall: z.infer<typeof AssistantMessageWithToolCalls>['toolCalls'][number];
|
||||||
result: z.infer<typeof ToolMessage> | undefined;
|
result: z.infer<typeof ToolMessage> | undefined;
|
||||||
sender: string | null | undefined;
|
sender: string | null | undefined;
|
||||||
workflow: z.infer<typeof Workflow>;
|
workflow: z.infer<typeof Workflow>;
|
||||||
delta: number;
|
delta: number;
|
||||||
|
onFix?: (message: string) => void;
|
||||||
|
showDebugMessages?: boolean;
|
||||||
|
isFirstAssistant?: boolean;
|
||||||
}) {
|
}) {
|
||||||
let matchingWorkflowTool: z.infer<typeof WorkflowTool> | undefined;
|
let matchingWorkflowTool: z.infer<typeof WorkflowTool> | undefined;
|
||||||
for (const tool of workflow.tools) {
|
for (const tool of workflow.tools) {
|
||||||
|
|
@ -242,6 +291,8 @@ function ToolCall({
|
||||||
sender={sender ?? ''}
|
sender={sender ?? ''}
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
delta={delta}
|
delta={delta}
|
||||||
|
onFix={onFix}
|
||||||
|
showDebugMessages={showDebugMessages}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,13 +331,17 @@ function ClientToolCall({
|
||||||
result: availableResult,
|
result: availableResult,
|
||||||
sender,
|
sender,
|
||||||
workflow,
|
workflow,
|
||||||
delta
|
delta,
|
||||||
|
onFix,
|
||||||
|
showDebugMessages
|
||||||
}: {
|
}: {
|
||||||
toolCall: z.infer<typeof AssistantMessageWithToolCalls>['toolCalls'][number];
|
toolCall: z.infer<typeof AssistantMessageWithToolCalls>['toolCalls'][number];
|
||||||
result: z.infer<typeof ToolMessage> | undefined;
|
result: z.infer<typeof ToolMessage> | undefined;
|
||||||
sender: string | null | undefined;
|
sender: string | null | undefined;
|
||||||
workflow: z.infer<typeof Workflow>;
|
workflow: z.infer<typeof Workflow>;
|
||||||
delta: number;
|
delta: number;
|
||||||
|
onFix?: (message: string) => void;
|
||||||
|
showDebugMessages?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [wrapText, setWrapText] = useState(true);
|
const [wrapText, setWrapText] = useState(true);
|
||||||
const [paramsExpanded, setParamsExpanded] = useState(false);
|
const [paramsExpanded, setParamsExpanded] = useState(false);
|
||||||
|
|
@ -299,8 +354,18 @@ function ClientToolCall({
|
||||||
return (
|
return (
|
||||||
<div className="self-start flex flex-col gap-1 my-5">
|
<div className="self-start flex flex-col gap-1 my-5">
|
||||||
{sender && (
|
{sender && (
|
||||||
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1">
|
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1 flex justify-between items-center">
|
||||||
{sender}
|
<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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="min-w-[85%]">
|
<div className="min-w-[85%]">
|
||||||
|
|
@ -309,6 +374,7 @@ function ClientToolCall({
|
||||||
bg-gray-50 dark:bg-gray-800 shadow-sm dark:shadow-gray-950/20">
|
bg-gray-50 dark:bg-gray-800 shadow-sm dark:shadow-gray-950/20">
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<div className="shrink-0 flex gap-2 items-center flex-nowrap">
|
<div className="shrink-0 flex gap-2 items-center flex-nowrap">
|
||||||
|
<div className="flex items-center gap-2 min-w-0 flex-nowrap">
|
||||||
{!availableResult && <Spinner size="sm" />}
|
{!availableResult && <Spinner size="sm" />}
|
||||||
{availableResult && <CheckCircleIcon size={16} className="text-green-500" />}
|
{availableResult && <CheckCircleIcon size={16} className="text-green-500" />}
|
||||||
<div className="flex items-center font-medium text-xs gap-2 min-w-0 flex-nowrap">
|
<div className="flex items-center font-medium text-xs gap-2 min-w-0 flex-nowrap">
|
||||||
|
|
@ -318,6 +384,7 @@ function ClientToolCall({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{hasExpandedContent && (
|
{hasExpandedContent && (
|
||||||
<div className="flex justify-start mt-2">
|
<div className="flex justify-start mt-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -362,8 +429,18 @@ function ClientToolCall({
|
||||||
return (
|
return (
|
||||||
<div className="self-start flex flex-col gap-1 my-5">
|
<div className="self-start flex flex-col gap-1 my-5">
|
||||||
{sender && (
|
{sender && (
|
||||||
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1">
|
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1 flex justify-between items-center">
|
||||||
{sender}
|
<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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|
@ -372,15 +449,17 @@ function ClientToolCall({
|
||||||
bg-gray-50 dark:bg-gray-800 shadow-sm dark:shadow-gray-950/20 w-full">
|
bg-gray-50 dark:bg-gray-800 shadow-sm dark:shadow-gray-950/20 w-full">
|
||||||
<div className="flex flex-col gap-1 w-full">
|
<div className="flex flex-col gap-1 w-full">
|
||||||
<div className="shrink-0 flex gap-2 items-center w-full flex-nowrap">
|
<div className="shrink-0 flex gap-2 items-center w-full flex-nowrap">
|
||||||
|
<div className="flex items-center gap-2 min-w-0 flex-nowrap">
|
||||||
{!availableResult && <Spinner size="sm" />}
|
{!availableResult && <Spinner size="sm" />}
|
||||||
{availableResult && <CheckCircleIcon size={16} className="text-green-500" />}
|
{availableResult && <CheckCircleIcon size={16} className="text-green-500" />}
|
||||||
<div className="flex items-center font-medium text-xs gap-2 w-full min-w-0 flex-nowrap">
|
<div className="flex items-center font-medium text-xs gap-2 min-w-0 flex-nowrap">
|
||||||
<span>Invoked Tool:</span>
|
<span>Invoked Tool:</span>
|
||||||
<span className="px-2 py-0.5 rounded-full bg-purple-50 text-purple-800 dark:bg-purple-900/30 dark:text-purple-100 text-xs align-middle truncate min-w-0 max-w-full">
|
<span className="px-2 py-0.5 rounded-full bg-purple-50 text-purple-800 dark:bg-purple-900/30 dark:text-purple-100 text-xs align-middle truncate min-w-0 max-w-full">
|
||||||
{toolCall.function.name}
|
{toolCall.function.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{hasExpandedContent && (
|
{hasExpandedContent && (
|
||||||
<div className="flex justify-start mt-2">
|
<div className="flex justify-start mt-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -503,6 +582,7 @@ export function Messages({
|
||||||
showSystemMessage,
|
showSystemMessage,
|
||||||
showDebugMessages = true,
|
showDebugMessages = true,
|
||||||
showJsonMode = false,
|
showJsonMode = false,
|
||||||
|
onFix,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
messages: z.infer<typeof Message>[];
|
messages: z.infer<typeof Message>[];
|
||||||
|
|
@ -515,6 +595,7 @@ export function Messages({
|
||||||
showSystemMessage: boolean;
|
showSystemMessage: boolean;
|
||||||
showDebugMessages?: boolean;
|
showDebugMessages?: boolean;
|
||||||
showJsonMode?: boolean;
|
showJsonMode?: boolean;
|
||||||
|
onFix?: (message: string) => void;
|
||||||
}) {
|
}) {
|
||||||
// Remove scroll/auto-scroll state and logic
|
// Remove scroll/auto-scroll state and logic
|
||||||
// const scrollContainerRef = useRef<HTMLDivElement>(null);
|
// const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -522,7 +603,11 @@ export function Messages({
|
||||||
// const [showUnreadBubble, setShowUnreadBubble] = useState(false);
|
// const [showUnreadBubble, setShowUnreadBubble] = useState(false);
|
||||||
// Remove handleScroll and useEffect for scroll
|
// Remove handleScroll and useEffect for scroll
|
||||||
|
|
||||||
|
// Find the index of the first assistant message
|
||||||
|
const firstAssistantIdx = messages.findIndex(m => m.role === 'assistant');
|
||||||
|
|
||||||
const renderMessage = (message: z.infer<typeof Message>, index: number) => {
|
const renderMessage = (message: z.infer<typeof Message>, index: number) => {
|
||||||
|
const isFirstAssistant = message.role === 'assistant' && index === firstAssistantIdx;
|
||||||
if (message.role === 'assistant') {
|
if (message.role === 'assistant') {
|
||||||
// TODO: add latency support
|
// TODO: add latency support
|
||||||
// let latency = new Date(message.createdAt).getTime() - lastUserMessageTimestamp;
|
// let latency = new Date(message.createdAt).getTime() - lastUserMessageTimestamp;
|
||||||
|
|
@ -548,6 +633,9 @@ export function Messages({
|
||||||
testProfile={testProfile}
|
testProfile={testProfile}
|
||||||
systemMessage={systemMessage}
|
systemMessage={systemMessage}
|
||||||
delta={latency}
|
delta={latency}
|
||||||
|
onFix={onFix}
|
||||||
|
showDebugMessages={showDebugMessages}
|
||||||
|
isFirstAssistant={isFirstAssistant}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -565,6 +653,9 @@ export function Messages({
|
||||||
latency={latency}
|
latency={latency}
|
||||||
delta={latency}
|
delta={latency}
|
||||||
showJsonMode={showJsonMode}
|
showJsonMode={showJsonMode}
|
||||||
|
onFix={onFix}
|
||||||
|
showDebugMessages={showDebugMessages}
|
||||||
|
isFirstAssistant={isFirstAssistant}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -575,6 +666,9 @@ export function Messages({
|
||||||
content={message.content ?? ''}
|
content={message.content ?? ''}
|
||||||
sender={message.agentName ?? ''}
|
sender={message.agentName ?? ''}
|
||||||
latency={latency}
|
latency={latency}
|
||||||
|
onFix={onFix}
|
||||||
|
showDebugMessages={showDebugMessages}
|
||||||
|
isFirstAssistant={isFirstAssistant}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const FIX_WORKFLOW_PROMPT = `There is an issue with this turn of chat: "{chat_turn}"
|
||||||
|
|
||||||
|
Fix the issue by updating necessary agents and tools.`;
|
||||||
|
|
||||||
|
export const FIX_WORKFLOW_PROMPT_WITH_FEEDBACK = `${FIX_WORKFLOW_PROMPT}
|
||||||
|
|
||||||
|
Here are more details: {feedback}`;
|
||||||
|
|
@ -1028,6 +1028,7 @@ export function WorkflowEditor({
|
||||||
isInitialState={isInitialState}
|
isInitialState={isInitialState}
|
||||||
onPanelClick={handlePlaygroundClick}
|
onPanelClick={handlePlaygroundClick}
|
||||||
projectTools={projectTools}
|
projectTools={projectTools}
|
||||||
|
triggerCopilotChat={triggerCopilotChat}
|
||||||
/>
|
/>
|
||||||
{state.present.selection?.type === "agent" && <AgentConfig
|
{state.present.selection?.type === "agent" && <AgentConfig
|
||||||
key={`agent-${state.present.workflow.agents.findIndex(agent => agent.name === state.present.selection!.name)}`}
|
key={`agent-${state.present.workflow.agents.findIndex(agent => agent.name === state.present.selection!.name)}`}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue