mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-13 17:22:37 +02:00
add copy json to copilot
This commit is contained in:
parent
8c6c3405d8
commit
7bf32d6746
7 changed files with 81 additions and 50 deletions
|
|
@ -1,5 +1,5 @@
|
|||
'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 { z } from "zod";
|
||||
import { PlaygroundChat, SimulationData, Workflow } from "@/app/lib/types";
|
||||
|
|
@ -8,6 +8,7 @@ import { Chat } from "./chat";
|
|||
import { useSearchParams } from "next/navigation";
|
||||
import { ActionButton, Pane } from "../workflow/pane";
|
||||
import { apiV1 } from "rowboat-shared";
|
||||
import { EllipsisVerticalIcon, MessageSquarePlusIcon, PlayIcon } from "lucide-react";
|
||||
|
||||
function SimulateLabel() {
|
||||
return <span>Simulate<sup className="pl-1">beta</sup></span>;
|
||||
|
|
@ -75,18 +76,14 @@ export function App({
|
|||
return <Pane title={viewSimulationMenu ? <SimulateLabel /> : "Chat"} actions={[
|
||||
<ActionButton
|
||||
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">
|
||||
<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>}
|
||||
icon={<MessageSquarePlusIcon size={16} />}
|
||||
onClick={handleNewChatButtonClick}
|
||||
>
|
||||
New chat
|
||||
</ActionButton>,
|
||||
!viewSimulationMenu && <ActionButton
|
||||
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">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 18V6l8 6-8 6Z" />
|
||||
</svg>}
|
||||
icon={<PlayIcon size={16} />}
|
||||
onClick={handleSimulateButtonClick}
|
||||
>
|
||||
Simulate
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { AgenticAPIChatRequest, convertToAgenticAPIChatMessages, convertWorkflow
|
|||
import { ComposeBox } from "./compose-box";
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { apiV1 } from "rowboat-shared";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { CopyAsJsonButton } from "./copy-as-json-button";
|
||||
|
||||
export function Chat({
|
||||
chat,
|
||||
|
|
@ -101,7 +101,7 @@ export function Chat({
|
|||
prompts,
|
||||
startAgent,
|
||||
};
|
||||
setLastAgenticRequest(request);
|
||||
setLastAgenticRequest(null);
|
||||
setLastAgenticResponse(null);
|
||||
|
||||
try {
|
||||
|
|
@ -109,7 +109,8 @@ export function Chat({
|
|||
if (ignore) {
|
||||
return;
|
||||
}
|
||||
setLastAgenticResponse(response.rawAPIResponse);
|
||||
setLastAgenticRequest(response.rawRequest);
|
||||
setLastAgenticResponse(response.rawResponse);
|
||||
setMessages([...messages, ...response.messages.map((message) => ({
|
||||
...message,
|
||||
version: 'v1' as const,
|
||||
|
|
@ -250,36 +251,15 @@ export function Chat({
|
|||
lastRequest: lastAgenticRequest,
|
||||
lastResponse: lastAgenticResponse,
|
||||
}, null, 2);
|
||||
navigator.clipboard.writeText(jsonString)
|
||||
.then(() => {
|
||||
setShowCopySuccess(true);
|
||||
setTimeout(() => {
|
||||
setShowCopySuccess(false);
|
||||
}, 1500);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy chat to clipboard:', err);
|
||||
});
|
||||
};
|
||||
navigator.clipboard.writeText(jsonString);
|
||||
}
|
||||
|
||||
function handleSystemMessageChange(message: string) {
|
||||
setSystemMessage(message);
|
||||
}
|
||||
|
||||
return <div className="relative h-full flex flex-col gap-8 pt-8 overflow-auto">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
isIconOnly
|
||||
onClick={handleCopyChat}
|
||||
className="absolute top-2 right-0"
|
||||
>
|
||||
{showCopySuccess ? (
|
||||
<CheckIcon size={16} />
|
||||
) : (
|
||||
<CopyIcon size={16} />
|
||||
)}
|
||||
</Button>
|
||||
<CopyAsJsonButton onCopy={handleCopyChat} />
|
||||
<Messages
|
||||
projectId={projectId}
|
||||
messages={messages}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { Action } from "./copilot_actions";
|
|||
import clsx from "clsx";
|
||||
import { Action as WorkflowDispatch } from "./workflow_editor";
|
||||
import MarkdownContent from "@/app/lib/components/markdown-content";
|
||||
import { CopyAsJsonButton } from "../playground/copy-as-json-button";
|
||||
|
||||
|
||||
const CopilotContext = createContext<{
|
||||
|
|
@ -181,6 +182,8 @@ function App({
|
|||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [appliedChanges, setAppliedChanges] = useState<Record<string, boolean>>({});
|
||||
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
|
||||
useEffect(() => {
|
||||
|
|
@ -328,7 +331,10 @@ function App({
|
|||
setResponseError(null);
|
||||
|
||||
try {
|
||||
const copilotMessage = await getCopilotResponse(
|
||||
setLastRequest(null);
|
||||
setLastResponse(null);
|
||||
|
||||
const response = await getCopilotResponse(
|
||||
projectId,
|
||||
messages,
|
||||
workflow,
|
||||
|
|
@ -337,7 +343,9 @@ function App({
|
|||
if (ignore) {
|
||||
return;
|
||||
}
|
||||
setMessages([...messages, copilotMessage]);
|
||||
setLastRequest(response.rawRequest);
|
||||
setLastResponse(response.rawResponse);
|
||||
setMessages([...messages, response.message]);
|
||||
} catch (err) {
|
||||
if (!ignore) {
|
||||
setResponseError(`Failed to get copilot response: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
|
|
@ -369,14 +377,24 @@ function App({
|
|||
};
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
}, [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 }}>
|
||||
<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) => {
|
||||
// Calculate if this assistant message is stale
|
||||
const isStale = m.role === 'assistant' && messages.slice(index + 1).some(
|
||||
|
|
@ -412,7 +430,7 @@ function App({
|
|||
</div>
|
||||
<div className="shrink-0">
|
||||
{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>
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@nextui-o
|
|||
import { useRef, useEffect } from "react";
|
||||
import { ActionButton, Pane } from "./pane";
|
||||
import clsx from "clsx";
|
||||
import { EllipsisVerticalIcon } from "lucide-react";
|
||||
|
||||
interface EntityListProps {
|
||||
agents: z.infer<typeof WorkflowAgent>[];
|
||||
|
|
@ -177,9 +178,7 @@ function AgentDropdown({
|
|||
return (
|
||||
<Dropdown>
|
||||
<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">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeWidth="3" d="M12 6h.01M12 12h.01M12 18h.01" />
|
||||
</svg>
|
||||
<EllipsisVerticalIcon size={16} />
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disabledKeys={[
|
||||
|
|
@ -219,9 +218,7 @@ function EntityDropdown({
|
|||
return (
|
||||
<Dropdown>
|
||||
<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">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeWidth="3" d="M12 6h.01M12 12h.01M12 18h.01" />
|
||||
</svg>
|
||||
<EllipsisVerticalIcon size={16} />
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
onAction={(key) => {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function Pane({
|
|||
{title}
|
||||
</div>
|
||||
{!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-gray-400": !fancy,
|
||||
})}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue