add copy json to copilot

This commit is contained in:
ramnique 2025-01-23 11:39:27 +05:30
parent 8c6c3405d8
commit 7bf32d6746
7 changed files with 81 additions and 50 deletions

View file

@ -462,7 +462,8 @@ export async function getAssistantResponse(
): Promise<{ ): Promise<{
messages: z.infer<typeof apiV1.ChatMessage>[], messages: z.infer<typeof apiV1.ChatMessage>[],
state: unknown, state: unknown,
rawAPIResponse: unknown, rawRequest: unknown,
rawResponse: unknown,
}> { }> {
await projectAuthCheck(projectId); await projectAuthCheck(projectId);
@ -470,7 +471,8 @@ export async function getAssistantResponse(
return { return {
messages: convertFromAgenticAPIChatMessages(response.messages), messages: convertFromAgenticAPIChatMessages(response.messages),
state: response.state, state: response.state,
rawAPIResponse: response.rawAPIResponse, rawRequest: request,
rawResponse: response.rawAPIResponse,
}; };
} }
@ -479,7 +481,11 @@ export async function getCopilotResponse(
messages: z.infer<typeof CopilotMessage>[], messages: z.infer<typeof CopilotMessage>[],
current_workflow_config: z.infer<typeof Workflow>, current_workflow_config: z.infer<typeof Workflow>,
context: z.infer<typeof CopilotChatContext> | null, context: z.infer<typeof CopilotChatContext> | null,
): Promise<z.infer<typeof CopilotAssistantMessage>> { ): Promise<{
message: z.infer<typeof CopilotAssistantMessage>,
rawRequest: unknown,
rawResponse: unknown,
}> {
await projectAuthCheck(projectId); await projectAuthCheck(projectId);
// prepare request // prepare request
@ -515,7 +521,12 @@ export async function getCopilotResponse(
role: 'assistant', role: 'assistant',
content: json.response.replace(/^```json\n/, '').replace(/\n```$/, ''), content: json.response.replace(/^```json\n/, '').replace(/\n```$/, ''),
}); });
return msg as z.infer<typeof CopilotAssistantMessage>;
return {
message: msg as z.infer<typeof CopilotAssistantMessage>,
rawRequest: request,
rawResponse: json,
};
} }
export async function suggestToolResponse(toolId: string, projectId: string, messages: z.infer<typeof apiV1.ChatMessage>[]): Promise<string> { export async function suggestToolResponse(toolId: string, projectId: string, messages: z.infer<typeof apiV1.ChatMessage>[]): Promise<string> {

View file

@ -1,5 +1,5 @@
'use client'; 'use client';
import { Spinner } from "@nextui-org/react"; import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner } from "@nextui-org/react";
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo } from "react";
import { z } from "zod"; import { z } from "zod";
import { PlaygroundChat, SimulationData, Workflow } from "@/app/lib/types"; import { PlaygroundChat, SimulationData, Workflow } from "@/app/lib/types";
@ -8,6 +8,7 @@ import { Chat } from "./chat";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { ActionButton, Pane } from "../workflow/pane"; import { ActionButton, Pane } from "../workflow/pane";
import { apiV1 } from "rowboat-shared"; import { apiV1 } from "rowboat-shared";
import { EllipsisVerticalIcon, MessageSquarePlusIcon, PlayIcon } from "lucide-react";
function SimulateLabel() { function SimulateLabel() {
return <span>Simulate<sup className="pl-1">beta</sup></span>; return <span>Simulate<sup className="pl-1">beta</sup></span>;
@ -75,18 +76,14 @@ export function App({
return <Pane title={viewSimulationMenu ? <SimulateLabel /> : "Chat"} actions={[ return <Pane title={viewSimulationMenu ? <SimulateLabel /> : "Chat"} actions={[
<ActionButton <ActionButton
key="new-chat" key="new-chat"
icon={<svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> icon={<MessageSquarePlusIcon size={16} />}
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 10.5h.01m-4.01 0h.01M8 10.5h.01M5 5h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-6.6a1 1 0 0 0-.69.275l-2.866 2.723A.5.5 0 0 1 8 18.635V17a1 1 0 0 0-1-1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Z" />
</svg>}
onClick={handleNewChatButtonClick} onClick={handleNewChatButtonClick}
> >
New chat New chat
</ActionButton>, </ActionButton>,
!viewSimulationMenu && <ActionButton !viewSimulationMenu && <ActionButton
key="simulate" key="simulate"
icon={<svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> icon={<PlayIcon size={16} />}
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 18V6l8 6-8 6Z" />
</svg>}
onClick={handleSimulateButtonClick} onClick={handleSimulateButtonClick}
> >
Simulate Simulate

View file

@ -7,7 +7,7 @@ import { AgenticAPIChatRequest, convertToAgenticAPIChatMessages, convertWorkflow
import { ComposeBox } from "./compose-box"; import { ComposeBox } from "./compose-box";
import { Button } from "@nextui-org/react"; import { Button } from "@nextui-org/react";
import { apiV1 } from "rowboat-shared"; import { apiV1 } from "rowboat-shared";
import { CheckIcon, CopyIcon } from "lucide-react"; import { CopyAsJsonButton } from "./copy-as-json-button";
export function Chat({ export function Chat({
chat, chat,
@ -101,7 +101,7 @@ export function Chat({
prompts, prompts,
startAgent, startAgent,
}; };
setLastAgenticRequest(request); setLastAgenticRequest(null);
setLastAgenticResponse(null); setLastAgenticResponse(null);
try { try {
@ -109,7 +109,8 @@ export function Chat({
if (ignore) { if (ignore) {
return; return;
} }
setLastAgenticResponse(response.rawAPIResponse); setLastAgenticRequest(response.rawRequest);
setLastAgenticResponse(response.rawResponse);
setMessages([...messages, ...response.messages.map((message) => ({ setMessages([...messages, ...response.messages.map((message) => ({
...message, ...message,
version: 'v1' as const, version: 'v1' as const,
@ -250,36 +251,15 @@ export function Chat({
lastRequest: lastAgenticRequest, lastRequest: lastAgenticRequest,
lastResponse: lastAgenticResponse, lastResponse: lastAgenticResponse,
}, null, 2); }, null, 2);
navigator.clipboard.writeText(jsonString) navigator.clipboard.writeText(jsonString);
.then(() => { }
setShowCopySuccess(true);
setTimeout(() => {
setShowCopySuccess(false);
}, 1500);
})
.catch(err => {
console.error('Failed to copy chat to clipboard:', err);
});
};
function handleSystemMessageChange(message: string) { function handleSystemMessageChange(message: string) {
setSystemMessage(message); setSystemMessage(message);
} }
return <div className="relative h-full flex flex-col gap-8 pt-8 overflow-auto"> return <div className="relative h-full flex flex-col gap-8 pt-8 overflow-auto">
<Button <CopyAsJsonButton onCopy={handleCopyChat} />
size="sm"
variant="bordered"
isIconOnly
onClick={handleCopyChat}
className="absolute top-2 right-0"
>
{showCopySuccess ? (
<CheckIcon size={16} />
) : (
<CopyIcon size={16} />
)}
</Button>
<Messages <Messages
projectId={projectId} projectId={projectId}
messages={messages} messages={messages}

View file

@ -0,0 +1,28 @@
import { CheckIcon, CopyIcon } from "lucide-react";
import { useState } from "react";
export function CopyAsJsonButton({ onCopy }: { onCopy: () => void }) {
const [showCopySuccess, setShowCopySuccess] = useState(false);
const handleCopyChat = () => {
onCopy();
setShowCopySuccess(true);
setTimeout(() => {
setShowCopySuccess(false);
}, 500);
};
return <button
onClick={handleCopyChat}
className="absolute top-0 right-0 text-gray-300 hover:text-gray-700 flex items-center gap-1 group"
>
{showCopySuccess ? (
<CheckIcon size={16} />
) : (
<CopyIcon size={16} />
)}
<div className="text-xs hidden group-hover:block">
{showCopySuccess ? 'Copied' : 'Copy as JSON'}
</div>
</button>
}

View file

@ -9,6 +9,7 @@ import { Action } from "./copilot_actions";
import clsx from "clsx"; import clsx from "clsx";
import { Action as WorkflowDispatch } from "./workflow_editor"; import { Action as WorkflowDispatch } from "./workflow_editor";
import MarkdownContent from "@/app/lib/components/markdown-content"; import MarkdownContent from "@/app/lib/components/markdown-content";
import { CopyAsJsonButton } from "../playground/copy-as-json-button";
const CopilotContext = createContext<{ const CopilotContext = createContext<{
@ -181,6 +182,8 @@ function App({
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const [appliedChanges, setAppliedChanges] = useState<Record<string, boolean>>({}); const [appliedChanges, setAppliedChanges] = useState<Record<string, boolean>>({});
const [discardContext, setDiscardContext] = useState(false); const [discardContext, setDiscardContext] = useState(false);
const [lastRequest, setLastRequest] = useState<unknown | null>(null);
const [lastResponse, setLastResponse] = useState<unknown | null>(null);
// Cycle through loading messages until reaching the last one // Cycle through loading messages until reaching the last one
useEffect(() => { useEffect(() => {
@ -328,7 +331,10 @@ function App({
setResponseError(null); setResponseError(null);
try { try {
const copilotMessage = await getCopilotResponse( setLastRequest(null);
setLastResponse(null);
const response = await getCopilotResponse(
projectId, projectId,
messages, messages,
workflow, workflow,
@ -337,7 +343,9 @@ function App({
if (ignore) { if (ignore) {
return; return;
} }
setMessages([...messages, copilotMessage]); setLastRequest(response.rawRequest);
setLastResponse(response.rawResponse);
setMessages([...messages, response.message]);
} catch (err) { } catch (err) {
if (!ignore) { if (!ignore) {
setResponseError(`Failed to get copilot response: ${err instanceof Error ? err.message : 'Unknown error'}`); setResponseError(`Failed to get copilot response: ${err instanceof Error ? err.message : 'Unknown error'}`);
@ -369,14 +377,24 @@ function App({
}; };
}, [messages, projectId, responseError, workflow, effectiveContext]); }, [messages, projectId, responseError, workflow, effectiveContext]);
function handleCopyChat() {
const jsonString = JSON.stringify({
messages: messages,
lastRequest: lastRequest,
lastResponse: lastResponse,
}, null, 2);
navigator.clipboard.writeText(jsonString);
}
// scroll to bottom on new messages // scroll to bottom on new messages
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}, [messages, loadingResponse]); }, [messages, loadingResponse]);
return <div className="h-full flex flex-col"> return <div className="h-full flex flex-col relative">
<CopilotContext.Provider value={{ workflow, handleApplyChange, appliedChanges }}> <CopilotContext.Provider value={{ workflow, handleApplyChange, appliedChanges }}>
<div className="grow flex flex-col gap-2 overflow-auto px-1"> <CopyAsJsonButton onCopy={handleCopyChat} />
<div className="grow flex flex-col gap-2 overflow-auto px-1 mt-6">
{messages.map((m, index) => { {messages.map((m, index) => {
// Calculate if this assistant message is stale // Calculate if this assistant message is stale
const isStale = m.role === 'assistant' && messages.slice(index + 1).some( const isStale = m.role === 'assistant' && messages.slice(index + 1).some(
@ -412,7 +430,7 @@ function App({
</div> </div>
<div className="shrink-0"> <div className="shrink-0">
{responseError && ( {responseError && (
<div className="max-w-[768px] mx-auto mb-4 p-2 bg-red-50 border border-red-200 rounded-lg flex gap-2 justify-between items-center"> <div className="max-w-[768px] mx-auto mb-4 p-2 bg-red-50 border border-red-200 rounded-lg flex gap-2 justify-between items-center text-sm">
<p className="text-red-600">{responseError}</p> <p className="text-red-600">{responseError}</p>
<Button <Button
size="sm" size="sm"

View file

@ -4,6 +4,7 @@ import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@nextui-o
import { useRef, useEffect } from "react"; import { useRef, useEffect } from "react";
import { ActionButton, Pane } from "./pane"; import { ActionButton, Pane } from "./pane";
import clsx from "clsx"; import clsx from "clsx";
import { EllipsisVerticalIcon } from "lucide-react";
interface EntityListProps { interface EntityListProps {
agents: z.infer<typeof WorkflowAgent>[]; agents: z.infer<typeof WorkflowAgent>[];
@ -177,9 +178,7 @@ function AgentDropdown({
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<svg className="w-4 h-4 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <EllipsisVerticalIcon size={16} />
<path stroke="currentColor" strokeLinecap="round" strokeWidth="3" d="M12 6h.01M12 12h.01M12 18h.01" />
</svg>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu <DropdownMenu
disabledKeys={[ disabledKeys={[
@ -219,9 +218,7 @@ function EntityDropdown({
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<svg className="w-4 h-4 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <EllipsisVerticalIcon size={16} />
<path stroke="currentColor" strokeLinecap="round" strokeWidth="3" d="M12 6h.01M12 12h.01M12 18h.01" />
</svg>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu <DropdownMenu
onAction={(key) => { onAction={(key) => {

View file

@ -23,7 +23,7 @@ export function Pane({
{title} {title}
</div> </div>
{!actions && <div className="w-4 h-4" />} {!actions && <div className="w-4 h-4" />}
{actions && <div className={clsx("rounded-md hover:text-gray-800 px-2 text-sm flex items-center gap-1", { {actions && <div className={clsx("rounded-md hover:text-gray-800 px-2 text-sm flex items-center gap-2", {
"text-blue-600": fancy, "text-blue-600": fancy,
"text-gray-400": !fancy, "text-gray-400": !fancy,
})}> })}>