Refresh proposal & loaders for proposal action
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
|
||||
@@ -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 ---
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ const validInvokeChannels = [
|
||||
"get-app-version",
|
||||
"reload-env-path",
|
||||
"get-proposal",
|
||||
"approve-proposal",
|
||||
"reject-proposal",
|
||||
] as const;
|
||||
|
||||
// Add valid receive channels
|
||||
|
||||
Reference in New Issue
Block a user