Fix copilot bugs

This commit is contained in:
akhisud3195 2025-04-04 11:32:31 +05:30
parent 940a86c622
commit 723aeca581
3 changed files with 196 additions and 57 deletions

View file

@ -4,12 +4,12 @@ import { useRef, useState, createContext, useContext, useCallback, forwardRef, u
import { CopilotChatContext } from "../../../lib/types/copilot_types";
import { CopilotMessage } 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 { getCopilotResponse } from "@/app/actions/copilot_actions";
import { Action as WorkflowDispatch } from "../workflow/workflow_editor";
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 { CopyIcon, CheckIcon, PlusIcon, XIcon } from "lucide-react";
@ -23,19 +23,21 @@ export function getAppliedChangeKey(messageIndex: number, actionIndex: number, f
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,
workflow,
dispatch,
chatContext = undefined,
onCopyJson,
}: {
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 }>) {
}, ref) {
const messagesEndRef = useRef<HTMLDivElement>(null);
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
const [loadingResponse, setLoadingResponse] = useState(false);
@ -55,7 +57,7 @@ const App = forwardRef(function App({
content: prompt
}]);
}
}, [projectId, messages.length, setMessages]);
}, [projectId, messages.length]);
// Reset discardContext when chatContext changes
useEffect(() => {
@ -66,14 +68,11 @@ const App = forwardRef(function App({
const effectiveContext = discardContext ? null : chatContext;
function handleUserMessage(prompt: string) {
setMessages([...messages, {
setMessages(currentMessages => [...currentMessages, {
role: 'user',
content: prompt
}]);
setResponseError(null);
// Set loading immediately after adding user message
// This ensures ComposeBox clears and disables right away
setLoadingResponse(true);
}
const handleApplyChange = useCallback((
@ -176,30 +175,31 @@ const App = forwardRef(function App({
}
}, [dispatch, appliedChanges, messages]);
// Second useEffect for copilot response
// Effect for handling copilot responses
useEffect(() => {
let ignore = false;
async function process() {
if (!messages.length) return;
const lastMessage = messages[messages.length - 1];
if (lastMessage.role !== 'user') return;
setLoadingResponse(true);
setResponseError(null);
try {
setLastRequest(null);
setLastResponse(null);
const response = await getCopilotResponse(
projectId,
messages,
workflow,
effectiveContext || null,
);
if (ignore) {
return;
}
if (ignore) return;
setLastRequest(response.rawRequest);
setLastResponse(response.rawResponse);
setMessages([...messages, response.message]);
setMessages(currentMessages => [...currentMessages, response.message]);
} catch (err) {
if (!ignore) {
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();
return () => {
ignore = true;
};
}, [
messages,
projectId,
responseError,
workflow,
effectiveContext,
setLoadingResponse,
setMessages,
setResponseError
]);
}, [messages, projectId, workflow, effectiveContext]);
// Scroll to bottom on new messages
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, loadingResponse]);
const handleCopyChat = useCallback(() => {
onCopyJson({
messages,
lastRequest,
lastResponse,
});
if (onCopyJson) {
onCopyJson({
messages,
lastRequest,
lastResponse,
});
}
}, [messages, lastRequest, lastResponse, onCopyJson]);
useImperativeHandle(ref, () => ({
@ -295,9 +278,9 @@ const App = forwardRef(function App({
</button>
</div>
</div>}
<ComposeBox
<ComposeBoxCopilot
handleUserMessage={handleUserMessage}
messages={messages as any[]}
messages={messages}
loading={loadingResponse}
disabled={loadingResponse}
/>
@ -386,3 +369,4 @@ export function Copilot({
</Panel>
);
}

View 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>
);
}

View file

@ -32,18 +32,22 @@ export function ComposeBox({
const inputRef = useRef<HTMLTextAreaElement>(null);
function handleInput() {
console.log('handleInput called');
const prompt = input.trim();
if (!prompt) {
console.log('Prompt is empty, returning');
return;
}
// Clear input before calling handleUserMessage
console.log('Clearing input');
setInput('');
if (inputRef.current) {
inputRef.current.value = '';
}
console.log('Calling handleUserMessage with prompt:', prompt);
handleUserMessage(prompt);
console.log('handleInput completed');
}
function handleInputKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
@ -60,6 +64,10 @@ export function ComposeBox({
}
}, [messages, input]);
useEffect(() => {
console.log('Input state changed to:', input);
}, [input]);
return (
<div className="relative group">
{/* Keyboard shortcut hint */}