From ebf8743778c1798ae8f5850faa38129b420055f6 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Fri, 18 Apr 2025 11:50:50 -0700 Subject: [PATCH] Refresh proposal & loaders for proposal action --- src/components/chat/ChatInput.tsx | 94 +++++++++++++++-- src/hooks/useProposal.ts | 67 ++++++++---- src/hooks/useStreamChat.ts | 4 +- src/ipc/handlers/proposal_handlers.ts | 142 ++++++++++++++++++++++++-- src/ipc/ipc_client.ts | 53 +++++++++- src/preload.ts | 2 + 6 files changed, 322 insertions(+), 40 deletions(-) diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index 72d9703..8c24a52 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -8,6 +8,7 @@ import { AlertOctagon, FileText, Check, + Loader2, } from "lucide-react"; import type React from "react"; import { useEffect, useRef, useState } from "react"; @@ -31,6 +32,8 @@ interface ChatInputActionsProps { onApprove: () => void; onReject: () => void; isApprovable: boolean; // Can be used to enable/disable buttons + isApproving: boolean; // State for approving + isRejecting: boolean; // State for rejecting } interface ChatInputProps { @@ -46,12 +49,16 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) { useStreamChat(); const [selectedAppId] = useAtom(selectedAppIdAtom); const [showError, setShowError] = useState(true); + const [isApproving, setIsApproving] = useState(false); // State for approving + const [isRejecting, setIsRejecting] = useState(false); // State for rejecting // Use the hook to fetch the proposal const { proposal, + messageId, isLoading: isProposalLoading, error: proposalError, + refreshProposal, } = useProposal(chatId); const adjustHeight = () => { @@ -102,14 +109,60 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) { setShowError(false); }; - const handleApprove = () => { - console.log("Approve clicked"); - // Add approve logic here + const handleApprove = async () => { + if (!chatId || !messageId || isApproving || isRejecting || isStreaming) + return; + console.log( + `Approving proposal for chatId: ${chatId}, messageId: ${messageId}` + ); + setIsApproving(true); + try { + const result = await IpcClient.getInstance().approveProposal({ + chatId, + messageId, + }); + if (result.success) { + console.log("Proposal approved successfully"); + // TODO: Maybe refresh proposal state or show confirmation? + } else { + console.error("Failed to approve proposal:", result.error); + setError(result.error || "Failed to approve proposal"); + } + } catch (err) { + console.error("Error approving proposal:", err); + setError((err as Error)?.message || "An error occurred while approving"); + } finally { + setIsApproving(false); + refreshProposal(); + } }; - const handleReject = () => { - console.log("Reject clicked"); - // Add reject logic here + const handleReject = async () => { + if (!chatId || !messageId || isApproving || isRejecting || isStreaming) + return; + console.log( + `Rejecting proposal for chatId: ${chatId}, messageId: ${messageId}` + ); + setIsRejecting(true); + try { + const result = await IpcClient.getInstance().rejectProposal({ + chatId, + messageId, + }); + if (result.success) { + console.log("Proposal rejected successfully"); + // TODO: Maybe refresh proposal state or show confirmation? + } else { + console.error("Failed to reject proposal:", result.error); + setError(result.error || "Failed to reject proposal"); + } + } catch (err) { + console.error("Error rejecting proposal:", err); + setError((err as Error)?.message || "An error occurred while rejecting"); + } finally { + setIsRejecting(false); + refreshProposal(); + } }; if (!settings) { @@ -150,7 +203,16 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) { proposal={proposal} onApprove={handleApprove} onReject={handleReject} - isApprovable={!isProposalLoading && !!proposal} + isApprovable={ + !isProposalLoading && + !!proposal && + !!messageId && + !isApproving && + !isRejecting && + !isStreaming + } + isApproving={isApproving} + isRejecting={isRejecting} /> )}
@@ -202,6 +264,8 @@ function ChatInputActions({ onApprove, onReject, isApprovable, + isApproving, + isRejecting, }: ChatInputActionsProps) { const [autoApprove, setAutoApprove] = useState(false); const [isDetailsVisible, setIsDetailsVisible] = useState(false); @@ -236,9 +300,13 @@ function ChatInputActions({ size="sm" variant="outline" onClick={onApprove} - disabled={!isApprovable} + disabled={!isApprovable || isApproving || isRejecting} > - + {isApproving ? ( + + ) : ( + + )} Approve
diff --git a/src/hooks/useProposal.ts b/src/hooks/useProposal.ts index 3a460a3..ec426f4 100644 --- a/src/hooks/useProposal.ts +++ b/src/hooks/useProposal.ts @@ -1,44 +1,73 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { IpcClient } from "@/ipc/ipc_client"; import type { Proposal } from "@/lib/schemas"; // Import Proposal type -export function useProposal(chatId: number | undefined) { - const [proposal, setProposal] = useState(null); +// Define the structure returned by the IPC call +interface ProposalResult { + proposal: Proposal; + messageId: number; +} + +export function useProposal(chatId?: number | undefined) { + const [proposalData, setProposalData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - useEffect(() => { - if (chatId === undefined) { - setProposal(null); - setIsLoading(false); - setError(null); - return; - } - - const fetchProposal = async () => { + const fetchProposal = useCallback( + async (innerChatId?: number) => { + chatId = chatId ?? innerChatId; + if (chatId === undefined) { + setProposalData(null); + setIsLoading(false); + setError(null); + return; + } setIsLoading(true); setError(null); + setProposalData(null); // Reset on new fetch try { - const fetchedProposal = await IpcClient.getInstance().getProposal( + // Type assertion might be needed depending on how IpcClient is typed + const result = (await IpcClient.getInstance().getProposal( chatId - ); - setProposal(fetchedProposal); + )) as ProposalResult | null; + + if (result) { + setProposalData(result); + } else { + setProposalData(null); // Explicitly set to null if IPC returns null + } } catch (err: any) { console.error("Error fetching proposal:", err); setError(err.message || "Failed to fetch proposal"); - setProposal(null); // Clear proposal on error + setProposalData(null); // Clear proposal data on error } finally { setIsLoading(false); } - }; + }, + [chatId] + ); // Depend on chatId + useEffect(() => { fetchProposal(); // Cleanup function if needed (e.g., for aborting requests) // return () => { // // Abort logic here // }; - }, [chatId]); // Re-run effect if chatId changes + }, [fetchProposal]); // Re-run effect if fetchProposal changes (due to chatId change) - return { proposal, isLoading, error }; + const refreshProposal = useCallback( + (chatId?: number) => { + fetchProposal(chatId); + }, + [fetchProposal] + ); + + return { + proposal: proposalData?.proposal ?? null, + messageId: proposalData?.messageId, + isLoading, + error, + refreshProposal, // Expose the refresh function + }; } diff --git a/src/hooks/useStreamChat.ts b/src/hooks/useStreamChat.ts index df1d476..5c7fb21 100644 --- a/src/hooks/useStreamChat.ts +++ b/src/hooks/useStreamChat.ts @@ -15,6 +15,7 @@ import { useLoadApp } from "./useLoadApp"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { useLoadVersions } from "./useLoadVersions"; import { showError } from "@/lib/toast"; +import { useProposal } from "./useProposal"; export function getRandomString() { return Math.random().toString(36).substring(2, 15); @@ -30,7 +31,7 @@ export function useStreamChat() { const { refreshApp } = useLoadApp(selectedAppId); const setStreamCount = useSetAtom(chatStreamCountAtom); const { refreshVersions } = useLoadVersions(selectedAppId); - + const { refreshProposal } = useProposal(); const streamMessage = useCallback( async ({ prompt, @@ -94,6 +95,7 @@ export function useStreamChat() { // Keep the same as below setIsStreaming(false); + refreshProposal(chatId); refreshChats(); refreshApp(); refreshVersions(); diff --git a/src/ipc/handlers/proposal_handlers.ts b/src/ipc/handlers/proposal_handlers.ts index ef3d7b2..edba535 100644 --- a/src/ipc/handlers/proposal_handlers.ts +++ b/src/ipc/handlers/proposal_handlers.ts @@ -2,12 +2,13 @@ import { ipcMain, type IpcMainInvokeEvent } from "electron"; import type { Proposal } from "@/lib/schemas"; import { db } from "../../db"; import { messages } from "../../db/schema"; -import { desc, eq, and } from "drizzle-orm"; +import { desc, eq, and, Update } from "drizzle-orm"; import path from "node:path"; // Import path for basename // Import tag parsers import { getDyadChatSummaryTag, getDyadWriteTags, + processFullResponseActions, } from "../processors/response_processor"; // Placeholder Proposal data (can be removed or kept for reference) @@ -19,6 +20,12 @@ interface ParsedProposal { files: string[]; } +// Define return type for getProposalHandler +interface ProposalResult { + proposal: Proposal; + messageId: number; +} + function isParsedProposal(obj: any): obj is ParsedProposal { return ( obj && @@ -32,7 +39,7 @@ function isParsedProposal(obj: any): obj is ParsedProposal { const getProposalHandler = async ( _event: IpcMainInvokeEvent, { chatId }: { chatId: number } -): Promise => { +): Promise => { console.log(`IPC: get-proposal called for chatId: ${chatId}`); try { @@ -41,12 +48,16 @@ const getProposalHandler = async ( where: and(eq(messages.chatId, chatId), eq(messages.role, "assistant")), orderBy: [desc(messages.createdAt)], columns: { + id: true, // Fetch the ID content: true, // Fetch the content to parse }, }); - if (latestAssistantMessage?.content) { - console.log("Found latest assistant message, parsing content..."); + if (latestAssistantMessage?.content && latestAssistantMessage.id) { + const messageId = latestAssistantMessage.id; // Get the message ID + console.log( + `Found latest assistant message (ID: ${messageId}), parsing content...` + ); const messageContent = latestAssistantMessage.content; // Parse tags directly from message content @@ -66,7 +77,7 @@ const getProposalHandler = async ( })), }; console.log("Generated proposal on the fly:", proposal); - return proposal; + return { proposal, messageId }; // Return proposal and messageId } else { console.log( "No relevant tags found in the latest assistant message content." @@ -83,8 +94,127 @@ const getProposalHandler = async ( } }; +// Handler to approve a proposal (process actions and update message) +const approveProposalHandler = async ( + _event: IpcMainInvokeEvent, + { chatId, messageId }: { chatId: number; messageId: number } +): Promise<{ success: boolean; error?: string }> => { + console.log( + `IPC: approve-proposal called for chatId: ${chatId}, messageId: ${messageId}` + ); + + try { + // 1. Fetch the specific assistant message + const messageToApprove = await db.query.messages.findFirst({ + where: and( + eq(messages.id, messageId), + eq(messages.chatId, chatId), + eq(messages.role, "assistant") + ), + columns: { + content: true, + }, + }); + + if (!messageToApprove?.content) { + console.error( + `Assistant message not found for chatId: ${chatId}, messageId: ${messageId}` + ); + return { success: false, error: "Assistant message not found." }; + } + + // 2. Process the actions defined in the message content + const chatSummary = getDyadChatSummaryTag(messageToApprove.content); + const processResult = await processFullResponseActions( + messageToApprove.content, + chatId, + { chatSummary: chatSummary ?? undefined } // Pass summary if found + ); + + if (processResult.error) { + console.error( + `Error processing actions for message ${messageId}:`, + processResult.error + ); + // Optionally: Update message state to 'error' or similar? + // For now, just return error to frontend + return { + success: false, + error: `Action processing failed: ${processResult.error}`, + }; + } + + // 3. Update the message's approval state to 'approved' + await db + .update(messages) + .set({ approvalState: "approved" }) + .where(eq(messages.id, messageId)); + + console.log(`Message ${messageId} marked as approved.`); + return { success: true }; + } catch (error) { + console.error( + `Error approving proposal for messageId ${messageId}:`, + error + ); + return { + success: false, + error: (error as Error)?.message || "Unknown error", + }; + } +}; + +// Handler to reject a proposal (just update message state) +const rejectProposalHandler = async ( + _event: IpcMainInvokeEvent, + { chatId, messageId }: { chatId: number; messageId: number } +): Promise<{ success: boolean; error?: string }> => { + console.log( + `IPC: reject-proposal called for chatId: ${chatId}, messageId: ${messageId}` + ); + + try { + // 1. Verify the message exists and is an assistant message + const messageToReject = await db.query.messages.findFirst({ + where: and( + eq(messages.id, messageId), + eq(messages.chatId, chatId), + eq(messages.role, "assistant") + ), + columns: { id: true }, // Only need to confirm existence + }); + + if (!messageToReject) { + console.error( + `Assistant message not found for chatId: ${chatId}, messageId: ${messageId}` + ); + return { success: false, error: "Assistant message not found." }; + } + + // 2. Update the message's approval state to 'rejected' + await db + .update(messages) + .set({ approvalState: "rejected" }) + .where(eq(messages.id, messageId)); + + console.log(`Message ${messageId} marked as rejected.`); + return { success: true }; + } catch (error) { + console.error( + `Error rejecting proposal for messageId ${messageId}:`, + error + ); + return { + success: false, + error: (error as Error)?.message || "Unknown error", + }; + } +}; + // Function to register proposal-related handlers export function registerProposalHandlers() { ipcMain.handle("get-proposal", getProposalHandler); - console.log("Registered proposal IPC handlers"); + ipcMain.handle("approve-proposal", approveProposalHandler); + ipcMain.handle("reject-proposal", rejectProposalHandler); + console.log("Registered proposal IPC handlers (get, approve, reject)"); } diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index dcc655b..27f61c4 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -20,6 +20,12 @@ import type { import type { Proposal } from "@/lib/schemas"; import { showError } from "@/lib/toast"; +// Define the structure returned by getProposal +interface ProposalResult { + proposal: Proposal; + messageId: number; +} + export interface ChatStreamCallbacks { onUpdate: (messages: Message[]) => void; onEnd: (response: ChatResponseEnd) => void; @@ -616,11 +622,12 @@ export class IpcClient { } // Get proposal details - public async getProposal(chatId: number): Promise { + public async getProposal(chatId: number): Promise { try { const data = await this.ipcRenderer.invoke("get-proposal", { chatId }); - // Assuming the main process returns data matching the Proposal interface - return data as Proposal; + // Assuming the main process returns data matching the ProposalResult interface + // Add a type check/guard if necessary for robustness + return data as ProposalResult | null; } catch (error) { showError(error); throw error; @@ -629,4 +636,44 @@ export class IpcClient { // Example methods for listening to events (if needed) // public on(channel: string, func: (...args: any[]) => void): void { + + // --- Proposal Management --- + public async approveProposal({ + chatId, + messageId, + }: { + chatId: number; + messageId: number; + }): Promise<{ success: boolean; error?: string }> { + try { + const result = await this.ipcRenderer.invoke("approve-proposal", { + chatId, + messageId, + }); + return result as { success: boolean; error?: string }; + } catch (error) { + showError(error); + return { success: false, error: (error as Error).message }; + } + } + + public async rejectProposal({ + chatId, + messageId, + }: { + chatId: number; + messageId: number; + }): Promise<{ success: boolean; error?: string }> { + try { + const result = await this.ipcRenderer.invoke("reject-proposal", { + chatId, + messageId, + }); + return result as { success: boolean; error?: string }; + } catch (error) { + showError(error); + return { success: false, error: (error as Error).message }; + } + } + // --- End Proposal Management --- } diff --git a/src/preload.ts b/src/preload.ts index 40b5e50..09dde42 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -39,6 +39,8 @@ const validInvokeChannels = [ "get-app-version", "reload-env-path", "get-proposal", + "approve-proposal", + "reject-proposal", ] as const; // Add valid receive channels