Load (canned) proposal from IPC
This commit is contained in:
@@ -23,6 +23,15 @@ import { useLoadApp } from "@/hooks/useLoadApp";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { useProposal } from "@/hooks/useProposal";
|
||||||
|
import { Proposal } from "@/lib/schemas";
|
||||||
|
|
||||||
|
interface ChatInputActionsProps {
|
||||||
|
proposal: Proposal;
|
||||||
|
onApprove: () => void;
|
||||||
|
onReject: () => void;
|
||||||
|
isApprovable: boolean; // Can be used to enable/disable buttons
|
||||||
|
}
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
chatId?: number;
|
chatId?: number;
|
||||||
@@ -38,6 +47,13 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
|
|||||||
const [selectedAppId] = useAtom(selectedAppIdAtom);
|
const [selectedAppId] = useAtom(selectedAppIdAtom);
|
||||||
const [showError, setShowError] = useState(true);
|
const [showError, setShowError] = useState(true);
|
||||||
|
|
||||||
|
// Use the hook to fetch the proposal
|
||||||
|
const {
|
||||||
|
proposal,
|
||||||
|
isLoading: isProposalLoading,
|
||||||
|
error: proposalError,
|
||||||
|
} = useProposal(chatId);
|
||||||
|
|
||||||
const adjustHeight = () => {
|
const adjustHeight = () => {
|
||||||
const textarea = textareaRef.current;
|
const textarea = textareaRef.current;
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
@@ -86,6 +102,16 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
|
|||||||
setShowError(false);
|
setShowError(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleApprove = () => {
|
||||||
|
console.log("Approve clicked");
|
||||||
|
// Add approve logic here
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = () => {
|
||||||
|
console.log("Reject clicked");
|
||||||
|
// Add reject logic here
|
||||||
|
};
|
||||||
|
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
return null; // Or loading state
|
return null; // Or loading state
|
||||||
}
|
}
|
||||||
@@ -105,9 +131,28 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Display loading or error state for proposal */}
|
||||||
|
{isProposalLoading && (
|
||||||
|
<div className="p-4 text-sm text-muted-foreground">
|
||||||
|
Loading proposal...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{proposalError && (
|
||||||
|
<div className="p-4 text-sm text-red-600">
|
||||||
|
Error loading proposal: {proposalError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex flex-col space-y-2 border border-border rounded-lg bg-(--background-lighter) shadow-sm">
|
<div className="flex flex-col space-y-2 border border-border rounded-lg bg-(--background-lighter) shadow-sm">
|
||||||
<ChatInputActions />
|
{/* Only render ChatInputActions if proposal is loaded */}
|
||||||
|
{proposal && (
|
||||||
|
<ChatInputActions
|
||||||
|
proposal={proposal}
|
||||||
|
onApprove={handleApprove}
|
||||||
|
onReject={handleReject}
|
||||||
|
isApprovable={!isProposalLoading && !!proposal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="flex items-start space-x-2 ">
|
<div className="flex items-start space-x-2 ">
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
@@ -151,47 +196,16 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatInputActions() {
|
// Update ChatInputActions to accept props
|
||||||
|
function ChatInputActions({
|
||||||
|
proposal,
|
||||||
|
onApprove,
|
||||||
|
onReject,
|
||||||
|
isApprovable,
|
||||||
|
}: ChatInputActionsProps) {
|
||||||
const [autoApprove, setAutoApprove] = useState(false);
|
const [autoApprove, setAutoApprove] = useState(false);
|
||||||
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
|
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
|
||||||
|
|
||||||
const handleApprove = () => {
|
|
||||||
console.log("Approve clicked");
|
|
||||||
// Add approve logic here
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReject = () => {
|
|
||||||
console.log("Reject clicked");
|
|
||||||
// Add reject logic here
|
|
||||||
};
|
|
||||||
|
|
||||||
// Placeholder data
|
|
||||||
const securityRisks = [
|
|
||||||
{
|
|
||||||
type: "warning",
|
|
||||||
title: "Potential XSS Vulnerability",
|
|
||||||
description: "User input is directly rendered without sanitization.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "danger",
|
|
||||||
title: "Hardcoded API Key",
|
|
||||||
description: "API key found in plain text in configuration file.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const filesChanged = [
|
|
||||||
{
|
|
||||||
name: "ChatInput.tsx",
|
|
||||||
path: "src/components/chat/ChatInput.tsx",
|
|
||||||
summary: "Added review actions and details section.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "api.ts",
|
|
||||||
path: "src/lib/api.ts",
|
|
||||||
summary: "Refactored API call structure.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border">
|
<div className="border-b border-border">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
@@ -206,9 +220,9 @@ function ChatInputActions() {
|
|||||||
) : (
|
) : (
|
||||||
<ChevronDown size={16} className="mr-1" />
|
<ChevronDown size={16} className="mr-1" />
|
||||||
)}
|
)}
|
||||||
Review: foo bar changes
|
Review: {proposal.title}
|
||||||
</button>
|
</button>
|
||||||
{securityRisks.length > 0 && (
|
{proposal.securityRisks.length > 0 && (
|
||||||
<span className="bg-red-100 text-red-700 text-xs font-medium px-2 py-0.5 rounded-full">
|
<span className="bg-red-100 text-red-700 text-xs font-medium px-2 py-0.5 rounded-full">
|
||||||
Security risks found
|
Security risks found
|
||||||
</span>
|
</span>
|
||||||
@@ -221,7 +235,8 @@ function ChatInputActions() {
|
|||||||
className="px-8"
|
className="px-8"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleApprove}
|
onClick={onApprove}
|
||||||
|
disabled={!isApprovable}
|
||||||
>
|
>
|
||||||
<Check size={16} className="mr-1" />
|
<Check size={16} className="mr-1" />
|
||||||
Approve
|
Approve
|
||||||
@@ -230,7 +245,8 @@ function ChatInputActions() {
|
|||||||
className="px-8"
|
className="px-8"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleReject}
|
onClick={onReject}
|
||||||
|
disabled={!isApprovable}
|
||||||
>
|
>
|
||||||
<X size={16} className="mr-1" />
|
<X size={16} className="mr-1" />
|
||||||
Reject
|
Reject
|
||||||
@@ -256,7 +272,7 @@ function ChatInputActions() {
|
|||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<h4 className="font-semibold mb-1">Security Risks</h4>
|
<h4 className="font-semibold mb-1">Security Risks</h4>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{securityRisks.map((risk, index) => (
|
{proposal.securityRisks.map((risk, index) => (
|
||||||
<li key={index} className="flex items-start space-x-2">
|
<li key={index} className="flex items-start space-x-2">
|
||||||
{risk.type === "warning" ? (
|
{risk.type === "warning" ? (
|
||||||
<AlertTriangle
|
<AlertTriangle
|
||||||
@@ -281,7 +297,7 @@ function ChatInputActions() {
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold mb-1">Files Changed</h4>
|
<h4 className="font-semibold mb-1">Files Changed</h4>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{filesChanged.map((file, index) => (
|
{proposal.filesChanged.map((file, index) => (
|
||||||
<li key={index} className="flex items-center space-x-2">
|
<li key={index} className="flex items-center space-x-2">
|
||||||
<FileText
|
<FileText
|
||||||
size={16}
|
size={16}
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export const messages = sqliteTable("messages", {
|
|||||||
.references(() => chats.id, { onDelete: "cascade" }),
|
.references(() => chats.id, { onDelete: "cascade" }),
|
||||||
role: text("role", { enum: ["user", "assistant"] }).notNull(),
|
role: text("role", { enum: ["user", "assistant"] }).notNull(),
|
||||||
content: text("content").notNull(),
|
content: text("content").notNull(),
|
||||||
|
approvalState: text("approval_state", {
|
||||||
|
enum: ["approved", "rejected", "pending"],
|
||||||
|
}),
|
||||||
createdAt: integer("created_at", { mode: "timestamp" })
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`(unixepoch())`),
|
.default(sql`(unixepoch())`),
|
||||||
|
|||||||
44
src/hooks/useProposal.ts
Normal file
44
src/hooks/useProposal.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useState, useEffect } 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);
|
||||||
|
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 () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const fetchedProposal = await IpcClient.getInstance().getProposal(
|
||||||
|
chatId
|
||||||
|
);
|
||||||
|
setProposal(fetchedProposal);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Error fetching proposal:", err);
|
||||||
|
setError(err.message || "Failed to fetch proposal");
|
||||||
|
setProposal(null); // Clear proposal on error
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchProposal();
|
||||||
|
|
||||||
|
// Cleanup function if needed (e.g., for aborting requests)
|
||||||
|
// return () => {
|
||||||
|
// // Abort logic here
|
||||||
|
// };
|
||||||
|
}, [chatId]); // Re-run effect if chatId changes
|
||||||
|
|
||||||
|
return { proposal, isLoading, error };
|
||||||
|
}
|
||||||
47
src/ipc/handlers/proposal_handlers.ts
Normal file
47
src/ipc/handlers/proposal_handlers.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { ipcMain, type IpcMainInvokeEvent } from "electron";
|
||||||
|
import type { Proposal } from "@/lib/schemas";
|
||||||
|
|
||||||
|
// Placeholder Proposal data
|
||||||
|
const placeholderProposal: Proposal = {
|
||||||
|
title: "Review: Example Refactoring (from IPC)",
|
||||||
|
securityRisks: [
|
||||||
|
{
|
||||||
|
type: "warning",
|
||||||
|
title: "Potential XSS Vulnerability",
|
||||||
|
description: "User input is directly rendered without sanitization.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "danger",
|
||||||
|
title: "Hardcoded API Key",
|
||||||
|
description: "API key found in plain text in configuration file.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
filesChanged: [
|
||||||
|
{
|
||||||
|
name: "ChatInput.tsx",
|
||||||
|
path: "src/components/chat/ChatInput.tsx",
|
||||||
|
summary: "Added review actions and details section.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "api.ts",
|
||||||
|
path: "src/lib/api.ts",
|
||||||
|
summary: "Refactored API call structure.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProposalHandler = async (
|
||||||
|
_event: IpcMainInvokeEvent,
|
||||||
|
{ chatId }: { chatId: number }
|
||||||
|
): Promise<Proposal> => {
|
||||||
|
console.log(`IPC: get-proposal called for chatId: ${chatId}`);
|
||||||
|
// Simulate async operation
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500)); // 500ms delay
|
||||||
|
return placeholderProposal;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to register proposal-related handlers
|
||||||
|
export function registerProposalHandlers() {
|
||||||
|
ipcMain.handle("get-proposal", getProposalHandler);
|
||||||
|
console.log("Registered proposal IPC handlers");
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import type {
|
|||||||
NodeSystemInfo,
|
NodeSystemInfo,
|
||||||
Version,
|
Version,
|
||||||
} from "./ipc_types";
|
} from "./ipc_types";
|
||||||
|
import type { Proposal } from "@/lib/schemas";
|
||||||
import { showError } from "@/lib/toast";
|
import { showError } from "@/lib/toast";
|
||||||
|
|
||||||
export interface ChatStreamCallbacks {
|
export interface ChatStreamCallbacks {
|
||||||
@@ -614,6 +615,18 @@ export class IpcClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get proposal details
|
||||||
|
public async getProposal(chatId: number): Promise<Proposal> {
|
||||||
|
try {
|
||||||
|
const data = await this.ipcRenderer.invoke("get-proposal", { chatId });
|
||||||
|
// Assuming the main process returns data matching the Proposal interface
|
||||||
|
return data as Proposal;
|
||||||
|
} catch (error) {
|
||||||
|
showError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Example methods for listening to events (if needed)
|
// Example methods for listening to events (if needed)
|
||||||
// public on(channel: string, func: (...args: any[]) => void): void {
|
// public on(channel: string, func: (...args: any[]) => void): void {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { registerShellHandlers } from "./handlers/shell_handler";
|
|||||||
import { registerDependencyHandlers } from "./handlers/dependency_handlers";
|
import { registerDependencyHandlers } from "./handlers/dependency_handlers";
|
||||||
import { registerGithubHandlers } from "./handlers/github_handlers";
|
import { registerGithubHandlers } from "./handlers/github_handlers";
|
||||||
import { registerNodeHandlers } from "./handlers/node_handlers";
|
import { registerNodeHandlers } from "./handlers/node_handlers";
|
||||||
|
import { registerProposalHandlers } from "./handlers/proposal_handlers";
|
||||||
|
|
||||||
export function registerIpcHandlers() {
|
export function registerIpcHandlers() {
|
||||||
// Register all IPC handlers by category
|
// Register all IPC handlers by category
|
||||||
@@ -17,4 +18,5 @@ export function registerIpcHandlers() {
|
|||||||
registerDependencyHandlers();
|
registerDependencyHandlers();
|
||||||
registerGithubHandlers();
|
registerGithubHandlers();
|
||||||
registerNodeHandlers();
|
registerNodeHandlers();
|
||||||
|
registerProposalHandlers();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,3 +97,23 @@ export const UserSettingsSchema = z.object({
|
|||||||
* Type derived from the UserSettingsSchema
|
* Type derived from the UserSettingsSchema
|
||||||
*/
|
*/
|
||||||
export type UserSettings = z.infer<typeof UserSettingsSchema>;
|
export type UserSettings = z.infer<typeof UserSettingsSchema>;
|
||||||
|
|
||||||
|
// Define interfaces for the props
|
||||||
|
export interface SecurityRisk {
|
||||||
|
type: "warning" | "danger";
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileChange {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Proposal interface
|
||||||
|
export interface Proposal {
|
||||||
|
title: string;
|
||||||
|
securityRisks: SecurityRisk[];
|
||||||
|
filesChanged: FileChange[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const validInvokeChannels = [
|
|||||||
"github:push",
|
"github:push",
|
||||||
"get-app-version",
|
"get-app-version",
|
||||||
"reload-env-path",
|
"reload-env-path",
|
||||||
|
"get-proposal",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Add valid receive channels
|
// Add valid receive channels
|
||||||
|
|||||||
Reference in New Issue
Block a user