Refresh proposal & loaders for proposal action

This commit is contained in:
Will Chen
2025-04-18 11:50:50 -07:00
parent 4a158417df
commit ebf8743778
6 changed files with 322 additions and 40 deletions

View File

@@ -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}
/>
)}
<div className="flex items-start space-x-2 ">
@@ -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}
>
<Check size={16} className="mr-1" />
{isApproving ? (
<Loader2 size={16} className="mr-1 animate-spin" />
) : (
<Check size={16} className="mr-1" />
)}
Approve
</Button>
<Button
@@ -246,9 +314,13 @@ function ChatInputActions({
size="sm"
variant="outline"
onClick={onReject}
disabled={!isApprovable}
disabled={!isApprovable || isApproving || isRejecting}
>
<X size={16} className="mr-1" />
{isRejecting ? (
<Loader2 size={16} className="mr-1 animate-spin" />
) : (
<X size={16} className="mr-1" />
)}
Reject
</Button>
<div className="flex items-center space-x-1 ml-auto">

View File

@@ -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<Proposal | null>(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<ProposalResult | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(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
};
}

View File

@@ -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();

View File

@@ -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<Proposal | null> => {
): Promise<ProposalResult | null> => {
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)");
}

View File

@@ -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<Proposal> {
public async getProposal(chatId: number): Promise<ProposalResult | null> {
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 ---
}

View File

@@ -39,6 +39,8 @@ const validInvokeChannels = [
"get-app-version",
"reload-env-path",
"get-proposal",
"approve-proposal",
"reject-proposal",
] as const;
// Add valid receive channels