mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-15 21:11:08 +02:00
Fix copilot bugs
This commit is contained in:
parent
940a86c622
commit
723aeca581
3 changed files with 196 additions and 57 deletions
|
|
@ -4,12 +4,12 @@ import { useRef, useState, createContext, useContext, useCallback, forwardRef, u
|
||||||
import { CopilotChatContext } from "../../../lib/types/copilot_types";
|
import { CopilotChatContext } from "../../../lib/types/copilot_types";
|
||||||
import { CopilotMessage } from "../../../lib/types/copilot_types";
|
import { CopilotMessage } from "../../../lib/types/copilot_types";
|
||||||
import { CopilotAssistantMessageActionPart } from "../../../lib/types/copilot_types";
|
import { CopilotAssistantMessageActionPart } from "../../../lib/types/copilot_types";
|
||||||
import { Workflow } from "../../../lib/types/workflow_types";
|
import { Workflow } from "@/app/lib/types/workflow_types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getCopilotResponse } from "@/app/actions/copilot_actions";
|
import { getCopilotResponse } from "@/app/actions/copilot_actions";
|
||||||
import { Action as WorkflowDispatch } from "../workflow/workflow_editor";
|
import { Action as WorkflowDispatch } from "../workflow/workflow_editor";
|
||||||
import { Panel } from "@/components/common/panel-common";
|
import { Panel } from "@/components/common/panel-common";
|
||||||
import { ComposeBox } from "@/components/common/compose-box";
|
import { ComposeBoxCopilot } from "@/components/common/compose-box-copilot";
|
||||||
import { Messages } from "./components/messages";
|
import { Messages } from "./components/messages";
|
||||||
import { CopyIcon, CheckIcon, PlusIcon, XIcon } from "lucide-react";
|
import { CopyIcon, CheckIcon, PlusIcon, XIcon } from "lucide-react";
|
||||||
|
|
||||||
|
|
@ -23,19 +23,21 @@ export function getAppliedChangeKey(messageIndex: number, actionIndex: number, f
|
||||||
return `${messageIndex}-${actionIndex}-${field}`;
|
return `${messageIndex}-${actionIndex}-${field}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = forwardRef(function App({
|
interface AppProps {
|
||||||
|
projectId: string;
|
||||||
|
workflow: z.infer<typeof Workflow>;
|
||||||
|
dispatch: (action: any) => void;
|
||||||
|
chatContext?: any;
|
||||||
|
onCopyJson?: (data: { messages: any[], lastRequest: any, lastResponse: any }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
|
||||||
projectId,
|
projectId,
|
||||||
workflow,
|
workflow,
|
||||||
dispatch,
|
dispatch,
|
||||||
chatContext = undefined,
|
chatContext = undefined,
|
||||||
onCopyJson,
|
onCopyJson,
|
||||||
}: {
|
}, ref) {
|
||||||
projectId: string;
|
|
||||||
workflow: z.infer<typeof Workflow>;
|
|
||||||
dispatch: (action: WorkflowDispatch) => void;
|
|
||||||
chatContext?: z.infer<typeof CopilotChatContext>;
|
|
||||||
onCopyJson: (data: { messages: any[], lastRequest: any, lastResponse: any }) => void;
|
|
||||||
}, ref: Ref<{ handleCopyChat: () => void }>) {
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
|
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
|
||||||
const [loadingResponse, setLoadingResponse] = useState(false);
|
const [loadingResponse, setLoadingResponse] = useState(false);
|
||||||
|
|
@ -55,7 +57,7 @@ const App = forwardRef(function App({
|
||||||
content: prompt
|
content: prompt
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
}, [projectId, messages.length, setMessages]);
|
}, [projectId, messages.length]);
|
||||||
|
|
||||||
// Reset discardContext when chatContext changes
|
// Reset discardContext when chatContext changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -66,14 +68,11 @@ const App = forwardRef(function App({
|
||||||
const effectiveContext = discardContext ? null : chatContext;
|
const effectiveContext = discardContext ? null : chatContext;
|
||||||
|
|
||||||
function handleUserMessage(prompt: string) {
|
function handleUserMessage(prompt: string) {
|
||||||
setMessages([...messages, {
|
setMessages(currentMessages => [...currentMessages, {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: prompt
|
content: prompt
|
||||||
}]);
|
}]);
|
||||||
setResponseError(null);
|
setResponseError(null);
|
||||||
// Set loading immediately after adding user message
|
|
||||||
// This ensures ComposeBox clears and disables right away
|
|
||||||
setLoadingResponse(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleApplyChange = useCallback((
|
const handleApplyChange = useCallback((
|
||||||
|
|
@ -176,30 +175,31 @@ const App = forwardRef(function App({
|
||||||
}
|
}
|
||||||
}, [dispatch, appliedChanges, messages]);
|
}, [dispatch, appliedChanges, messages]);
|
||||||
|
|
||||||
// Second useEffect for copilot response
|
// Effect for handling copilot responses
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let ignore = false;
|
let ignore = false;
|
||||||
|
|
||||||
async function process() {
|
async function process() {
|
||||||
|
if (!messages.length) return;
|
||||||
|
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
if (lastMessage.role !== 'user') return;
|
||||||
|
|
||||||
setLoadingResponse(true);
|
setLoadingResponse(true);
|
||||||
setResponseError(null);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLastRequest(null);
|
|
||||||
setLastResponse(null);
|
|
||||||
|
|
||||||
const response = await getCopilotResponse(
|
const response = await getCopilotResponse(
|
||||||
projectId,
|
projectId,
|
||||||
messages,
|
messages,
|
||||||
workflow,
|
workflow,
|
||||||
effectiveContext || null,
|
effectiveContext || null,
|
||||||
);
|
);
|
||||||
if (ignore) {
|
|
||||||
return;
|
if (ignore) return;
|
||||||
}
|
|
||||||
setLastRequest(response.rawRequest);
|
setLastRequest(response.rawRequest);
|
||||||
setLastResponse(response.rawResponse);
|
setLastResponse(response.rawResponse);
|
||||||
setMessages([...messages, response.message]);
|
setMessages(currentMessages => [...currentMessages, 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'}`);
|
||||||
|
|
@ -211,43 +211,26 @@ const App = forwardRef(function App({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no messages, return
|
|
||||||
if (messages.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if last message is not from role user
|
|
||||||
// or tool, return
|
|
||||||
const last = messages[messages.length - 1];
|
|
||||||
if (responseError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (last.role !== 'user') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
process();
|
process();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ignore = true;
|
ignore = true;
|
||||||
};
|
};
|
||||||
}, [
|
}, [messages, projectId, workflow, effectiveContext]);
|
||||||
messages,
|
|
||||||
projectId,
|
// Scroll to bottom on new messages
|
||||||
responseError,
|
useEffect(() => {
|
||||||
workflow,
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
effectiveContext,
|
}, [messages, loadingResponse]);
|
||||||
setLoadingResponse,
|
|
||||||
setMessages,
|
|
||||||
setResponseError
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleCopyChat = useCallback(() => {
|
const handleCopyChat = useCallback(() => {
|
||||||
onCopyJson({
|
if (onCopyJson) {
|
||||||
messages,
|
onCopyJson({
|
||||||
lastRequest,
|
messages,
|
||||||
lastResponse,
|
lastRequest,
|
||||||
});
|
lastResponse,
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [messages, lastRequest, lastResponse, onCopyJson]);
|
}, [messages, lastRequest, lastResponse, onCopyJson]);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
|
|
@ -295,9 +278,9 @@ const App = forwardRef(function App({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
<ComposeBox
|
<ComposeBoxCopilot
|
||||||
handleUserMessage={handleUserMessage}
|
handleUserMessage={handleUserMessage}
|
||||||
messages={messages as any[]}
|
messages={messages}
|
||||||
loading={loadingResponse}
|
loading={loadingResponse}
|
||||||
disabled={loadingResponse}
|
disabled={loadingResponse}
|
||||||
/>
|
/>
|
||||||
|
|
@ -386,3 +369,4 @@ export function Copilot({
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
147
apps/rowboat/components/common/compose-box-copilot.tsx
Normal file
147
apps/rowboat/components/common/compose-box-copilot.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button, Spinner } from "@heroui/react";
|
||||||
|
import { useRef, useState, useEffect } from "react";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
|
// Add a type to support both message formats
|
||||||
|
type FlexibleMessage = {
|
||||||
|
role: 'user' | 'assistant' | 'system' | 'tool';
|
||||||
|
content: string | any;
|
||||||
|
version?: string;
|
||||||
|
chatId?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
// Add any other optional fields that might be needed
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ComposeBoxCopilot({
|
||||||
|
minRows=3,
|
||||||
|
disabled=false,
|
||||||
|
loading=false,
|
||||||
|
handleUserMessage,
|
||||||
|
messages,
|
||||||
|
}: {
|
||||||
|
minRows?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
handleUserMessage: (prompt: string) => void;
|
||||||
|
messages: FlexibleMessage[]; // Use the flexible message type
|
||||||
|
}) {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
function handleInput() {
|
||||||
|
const prompt = input.trim();
|
||||||
|
if (!prompt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInput('');
|
||||||
|
handleUserMessage(prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInputKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
handleInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// focus on the input field only when there is at least one message
|
||||||
|
useEffect(() => {
|
||||||
|
if (messages.length > 0) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative group">
|
||||||
|
{/* Keyboard shortcut hint */}
|
||||||
|
<div className="absolute -top-6 right-0 text-xs text-gray-500 dark:text-gray-400 opacity-0
|
||||||
|
group-hover:opacity-100 transition-opacity">
|
||||||
|
Press ⌘ + Enter to send
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Outer container with padding */}
|
||||||
|
<div className="rounded-2xl border-[1.5px] border-gray-200 dark:border-[#2a2d31] p-3 relative
|
||||||
|
bg-white dark:bg-[#1e2023] flex items-end gap-2">
|
||||||
|
{/* Textarea */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<Textarea
|
||||||
|
ref={inputRef}
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
onBlur={() => setIsFocused(false)}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
placeholder="Type a message..."
|
||||||
|
autoResize={true}
|
||||||
|
maxHeight={120}
|
||||||
|
className={`
|
||||||
|
!min-h-0
|
||||||
|
!border-0 !shadow-none !ring-0
|
||||||
|
bg-transparent
|
||||||
|
resize-none
|
||||||
|
overflow-y-auto
|
||||||
|
[&::-webkit-scrollbar]:w-1
|
||||||
|
[&::-webkit-scrollbar-track]:bg-transparent
|
||||||
|
[&::-webkit-scrollbar-thumb]:bg-gray-300
|
||||||
|
[&::-webkit-scrollbar-thumb]:dark:bg-[#2a2d31]
|
||||||
|
[&::-webkit-scrollbar-thumb]:rounded-full
|
||||||
|
placeholder:text-gray-500 dark:placeholder:text-gray-400
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Send button */}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
disabled={disabled || loading || !input.trim()}
|
||||||
|
onPress={handleInput}
|
||||||
|
className={`
|
||||||
|
transition-all duration-200
|
||||||
|
${input.trim()
|
||||||
|
? 'bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:hover:bg-indigo-800/60 dark:text-indigo-300'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500'
|
||||||
|
}
|
||||||
|
scale-100 hover:scale-105 active:scale-95
|
||||||
|
disabled:opacity-50 disabled:scale-95
|
||||||
|
hover:shadow-md dark:hover:shadow-indigo-950/10
|
||||||
|
mb-0.5
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Spinner size="sm" color={input.trim() ? "primary" : "default"} />
|
||||||
|
) : (
|
||||||
|
<SendIcon
|
||||||
|
size={16}
|
||||||
|
className={`transform transition-transform ${isFocused ? 'translate-x-0.5' : ''}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom SendIcon component for better visual alignment
|
||||||
|
function SendIcon({ size, className }: { size: number, className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path d="M22 2L11 13" />
|
||||||
|
<path d="M22 2L15 22L11 13L2 9L22 2Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -32,18 +32,22 @@ export function ComposeBox({
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
function handleInput() {
|
function handleInput() {
|
||||||
|
console.log('handleInput called');
|
||||||
const prompt = input.trim();
|
const prompt = input.trim();
|
||||||
if (!prompt) {
|
if (!prompt) {
|
||||||
|
console.log('Prompt is empty, returning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear input before calling handleUserMessage
|
console.log('Clearing input');
|
||||||
setInput('');
|
setInput('');
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
inputRef.current.value = '';
|
inputRef.current.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Calling handleUserMessage with prompt:', prompt);
|
||||||
handleUserMessage(prompt);
|
handleUserMessage(prompt);
|
||||||
|
console.log('handleInput completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInputKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
|
function handleInputKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||||
|
|
@ -60,6 +64,10 @@ export function ComposeBox({
|
||||||
}
|
}
|
||||||
}, [messages, input]);
|
}, [messages, input]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Input state changed to:', input);
|
||||||
|
}, [input]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
{/* Keyboard shortcut hint */}
|
{/* Keyboard shortcut hint */}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue