2025-03-07 10:47:08 -08:00
|
|
|
import "./index.css";
|
2025-03-07 11:21:49 -08:00
|
|
|
import { v4 as uuidv4 } from "uuid";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2025-03-07 10:47:08 -08:00
|
|
|
import ReactMarkdown from "react-markdown";
|
|
|
|
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
|
|
|
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
2025-03-07 11:21:49 -08:00
|
|
|
import { UIMessage, useStreamContext } from "@langchain/langgraph-sdk/react-ui";
|
|
|
|
|
import { Message } from "@langchain/langgraph-sdk";
|
|
|
|
|
import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses";
|
2025-03-07 12:43:22 -08:00
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
import { getToolResponse } from "../../utils/get-tool-response";
|
2025-03-07 13:42:37 -08:00
|
|
|
import { cn } from "@/lib/utils";
|
2025-03-07 10:47:08 -08:00
|
|
|
|
|
|
|
|
interface ProposedChangeProps {
|
|
|
|
|
toolCallId: string;
|
|
|
|
|
change: string;
|
|
|
|
|
planItem: string;
|
2025-03-07 13:42:37 -08:00
|
|
|
/**
|
|
|
|
|
* Whether or not to show the "Accept"/"Reject" buttons
|
|
|
|
|
* If true, this means the user selected the "Accept, don't ask again"
|
|
|
|
|
* button for this session.
|
|
|
|
|
*/
|
|
|
|
|
fullWriteAccess: boolean;
|
2025-03-07 10:47:08 -08:00
|
|
|
}
|
|
|
|
|
|
2025-03-07 12:43:22 -08:00
|
|
|
const ACCEPTED_CHANGE_CONTENT =
|
|
|
|
|
"User accepted the proposed change. Please continue.";
|
2025-03-07 13:42:37 -08:00
|
|
|
const REJECTED_CHANGE_CONTENT =
|
|
|
|
|
"User rejected the proposed change. Please continue.";
|
2025-03-07 12:43:22 -08:00
|
|
|
|
2025-03-07 10:47:08 -08:00
|
|
|
export default function ProposedChange(props: ProposedChangeProps) {
|
2025-03-07 11:21:49 -08:00
|
|
|
const [isAccepted, setIsAccepted] = useState(false);
|
2025-03-07 13:42:37 -08:00
|
|
|
const [isRejected, setIsRejected] = useState(false);
|
2025-03-07 11:21:49 -08:00
|
|
|
|
|
|
|
|
const thread = useStreamContext<
|
|
|
|
|
{ messages: Message[]; ui: UIMessage[] },
|
|
|
|
|
{ MetaType: { ui: UIMessage | undefined } }
|
|
|
|
|
>();
|
|
|
|
|
|
|
|
|
|
const handleReject = () => {
|
|
|
|
|
thread.submit({
|
|
|
|
|
messages: [
|
|
|
|
|
{
|
|
|
|
|
type: "tool",
|
|
|
|
|
tool_call_id: props.toolCallId,
|
|
|
|
|
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
|
2025-03-07 13:42:37 -08:00
|
|
|
name: "update_file",
|
|
|
|
|
content: REJECTED_CHANGE_CONTENT,
|
2025-03-07 11:21:49 -08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: "human",
|
2025-03-07 13:42:37 -08:00
|
|
|
content: `Rejected change.`,
|
2025-03-07 11:21:49 -08:00
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
2025-03-07 13:42:37 -08:00
|
|
|
setIsRejected(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAccept = (shouldGrantFullWriteAccess = false) => {
|
|
|
|
|
const humanMessageContent = `Accepted change. ${shouldGrantFullWriteAccess ? "Granted full write access." : ""}`;
|
|
|
|
|
thread.submit(
|
|
|
|
|
{
|
|
|
|
|
messages: [
|
|
|
|
|
{
|
|
|
|
|
type: "tool",
|
|
|
|
|
tool_call_id: props.toolCallId,
|
|
|
|
|
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
|
|
|
|
|
name: "update_file",
|
|
|
|
|
content: ACCEPTED_CHANGE_CONTENT,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: "human",
|
|
|
|
|
content: humanMessageContent,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
config: {
|
|
|
|
|
configurable: {
|
|
|
|
|
permissions: {
|
|
|
|
|
full_write_access: shouldGrantFullWriteAccess,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2025-03-07 11:21:49 -08:00
|
|
|
setIsAccepted(true);
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-07 12:43:22 -08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (typeof window === "undefined" || isAccepted) return;
|
|
|
|
|
const toolResponse = getToolResponse(props.toolCallId, thread);
|
2025-03-07 13:42:37 -08:00
|
|
|
if (toolResponse) {
|
|
|
|
|
if (toolResponse.content === ACCEPTED_CHANGE_CONTENT) {
|
|
|
|
|
setIsAccepted(true);
|
|
|
|
|
} else if (toolResponse.content === REJECTED_CHANGE_CONTENT) {
|
|
|
|
|
setIsRejected(true);
|
|
|
|
|
}
|
2025-03-07 12:43:22 -08:00
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-03-07 13:42:37 -08:00
|
|
|
if (isAccepted || isRejected) {
|
2025-03-07 11:21:49 -08:00
|
|
|
return (
|
2025-03-07 13:42:37 -08:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex flex-col gap-4 w-full max-w-4xl p-4 border-[1px] rounded-xl",
|
|
|
|
|
isAccepted ? "border-green-300" : "border-red-300",
|
|
|
|
|
)}
|
|
|
|
|
>
|
2025-03-07 11:21:49 -08:00
|
|
|
<div className="flex flex-col items-start justify-start gap-2">
|
2025-03-07 13:42:37 -08:00
|
|
|
<p className="text-lg font-medium">
|
|
|
|
|
{isAccepted ? "Accepted" : "Rejected"} Change
|
|
|
|
|
</p>
|
2025-03-07 11:21:49 -08:00
|
|
|
<p className="text-sm font-mono">{props.planItem}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<ReactMarkdown
|
|
|
|
|
children={props.change}
|
|
|
|
|
components={{
|
|
|
|
|
code(props) {
|
|
|
|
|
const { children, className, node: _node } = props;
|
|
|
|
|
const match = /language-(\w+)/.exec(className || "");
|
|
|
|
|
return match ? (
|
|
|
|
|
<SyntaxHighlighter
|
|
|
|
|
children={String(children).replace(/\n$/, "")}
|
|
|
|
|
language={match[1]}
|
|
|
|
|
style={coldarkDark}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<code className={className}>{children}</code>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-03-07 10:47:08 -08:00
|
|
|
|
|
|
|
|
return (
|
2025-03-07 11:21:49 -08:00
|
|
|
<div className="flex flex-col gap-4 w-full max-w-4xl p-4 border-[1px] rounded-xl border-slate-200">
|
|
|
|
|
<div className="flex flex-col items-start justify-start gap-2">
|
|
|
|
|
<p className="text-lg font-medium">Proposed Change</p>
|
|
|
|
|
<p className="text-sm font-mono">{props.planItem}</p>
|
|
|
|
|
</div>
|
2025-03-07 10:47:08 -08:00
|
|
|
<ReactMarkdown
|
|
|
|
|
children={props.change}
|
|
|
|
|
components={{
|
|
|
|
|
code(props) {
|
|
|
|
|
const { children, className, node: _node } = props;
|
|
|
|
|
const match = /language-(\w+)/.exec(className || "");
|
|
|
|
|
return match ? (
|
|
|
|
|
<SyntaxHighlighter
|
|
|
|
|
children={String(children).replace(/\n$/, "")}
|
|
|
|
|
language={match[1]}
|
|
|
|
|
style={coldarkDark}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<code className={className}>{children}</code>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2025-03-07 13:42:37 -08:00
|
|
|
{!props.fullWriteAccess && (
|
|
|
|
|
<div className="flex gap-2 items-center w-full">
|
|
|
|
|
<Button
|
|
|
|
|
className="cursor-pointer w-full"
|
|
|
|
|
variant="destructive"
|
|
|
|
|
onClick={handleReject}
|
|
|
|
|
>
|
|
|
|
|
Reject
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
className="cursor-pointer w-full"
|
|
|
|
|
onClick={() => handleAccept()}
|
|
|
|
|
>
|
|
|
|
|
Accept
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
2025-03-07 13:56:23 -08:00
|
|
|
className="cursor-pointer w-full bg-blue-500 hover:bg-blue-500/90"
|
2025-03-07 13:42:37 -08:00
|
|
|
onClick={() => handleAccept(true)}
|
|
|
|
|
>
|
|
|
|
|
Accept, don't ask again
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-03-07 10:47:08 -08:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|