Refactor chat input & properly update state for proposal
This commit is contained in:
@@ -11,6 +11,7 @@ export const selectedChatIdAtom = atom<number | null>(null);
|
|||||||
|
|
||||||
export const isStreamingAtom = atom<boolean>(false);
|
export const isStreamingAtom = atom<boolean>(false);
|
||||||
export const chatInputValueAtom = atom<string>("");
|
export const chatInputValueAtom = atom<string>("");
|
||||||
|
export const homeChatInputValueAtom = atom<string>("");
|
||||||
|
|
||||||
// Atoms for chat list management
|
// Atoms for chat list management
|
||||||
export const chatsAtom = atom<ChatSummary[]>([]);
|
export const chatsAtom = atom<ChatSummary[]>([]);
|
||||||
|
|||||||
4
src/atoms/proposalAtoms.ts
Normal file
4
src/atoms/proposalAtoms.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import type { Proposal, ProposalResult } from "@/lib/schemas";
|
||||||
|
|
||||||
|
export const proposalResultAtom = atom<ProposalResult | null>(null);
|
||||||
@@ -11,12 +11,12 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { ModelPicker } from "@/components/ModelPicker";
|
import { ModelPicker } from "@/components/ModelPicker";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
import { chatInputValueAtom } from "@/atoms/chatAtoms";
|
import { chatInputValueAtom, chatMessagesAtom } from "@/atoms/chatAtoms";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom, useSetAtom } from "jotai";
|
||||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||||
import { useChats } from "@/hooks/useChats";
|
import { useChats } from "@/hooks/useChats";
|
||||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||||
@@ -26,7 +26,8 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { useProposal } from "@/hooks/useProposal";
|
import { useProposal } from "@/hooks/useProposal";
|
||||||
import { Proposal } from "@/lib/schemas";
|
import { Proposal } from "@/lib/schemas";
|
||||||
|
import type { Message } from "@/ipc/ipc_types";
|
||||||
|
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
||||||
interface ChatInputActionsProps {
|
interface ChatInputActionsProps {
|
||||||
proposal: Proposal;
|
proposal: Proposal;
|
||||||
onApprove: () => void;
|
onApprove: () => void;
|
||||||
@@ -36,12 +37,7 @@ interface ChatInputActionsProps {
|
|||||||
isRejecting: boolean; // State for rejecting
|
isRejecting: boolean; // State for rejecting
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatInputProps {
|
export function ChatInput({ chatId }: { chatId?: number }) {
|
||||||
chatId?: number;
|
|
||||||
onSubmit?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
|
|
||||||
const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
|
const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { settings, updateSettings, isAnyProviderSetup } = useSettings();
|
const { settings, updateSettings, isAnyProviderSetup } = useSettings();
|
||||||
@@ -51,6 +47,8 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
|
|||||||
const [showError, setShowError] = useState(true);
|
const [showError, setShowError] = useState(true);
|
||||||
const [isApproving, setIsApproving] = useState(false); // State for approving
|
const [isApproving, setIsApproving] = useState(false); // State for approving
|
||||||
const [isRejecting, setIsRejecting] = useState(false); // State for rejecting
|
const [isRejecting, setIsRejecting] = useState(false); // State for rejecting
|
||||||
|
const [messages, setMessages] = useAtom<Message[]>(chatMessagesAtom);
|
||||||
|
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
|
||||||
|
|
||||||
// Use the hook to fetch the proposal
|
// Use the hook to fetch the proposal
|
||||||
const {
|
const {
|
||||||
@@ -80,10 +78,19 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
|
|||||||
}
|
}
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
|
const fetchChatMessages = useCallback(async () => {
|
||||||
|
if (!chatId) {
|
||||||
|
setMessages([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chat = await IpcClient.getInstance().getChat(chatId);
|
||||||
|
setMessages(chat.messages);
|
||||||
|
}, [chatId, setMessages]);
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
submitHandler();
|
handleSubmit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,7 +103,6 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
|
|||||||
setInputValue("");
|
setInputValue("");
|
||||||
await streamMessage({ prompt: currentInput, chatId });
|
await streamMessage({ prompt: currentInput, chatId });
|
||||||
};
|
};
|
||||||
const submitHandler = onSubmit ? onSubmit : handleSubmit;
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
if (chatId) {
|
if (chatId) {
|
||||||
@@ -133,7 +139,9 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
|
|||||||
setError((err as Error)?.message || "An error occurred while approving");
|
setError((err as Error)?.message || "An error occurred while approving");
|
||||||
} finally {
|
} finally {
|
||||||
setIsApproving(false);
|
setIsApproving(false);
|
||||||
|
setIsPreviewOpen(true);
|
||||||
refreshProposal();
|
refreshProposal();
|
||||||
|
fetchChatMessages();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,6 +170,7 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsRejecting(false);
|
setIsRejecting(false);
|
||||||
refreshProposal();
|
refreshProposal();
|
||||||
|
fetchChatMessages();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,7 +245,7 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={submitHandler}
|
onClick={handleSubmit}
|
||||||
disabled={!inputValue.trim() || !isAnyProviderSetup()}
|
disabled={!inputValue.trim() || !isAnyProviderSetup()}
|
||||||
className="px-2 py-2 mt-1 mr-2 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
|
className="px-2 py-2 mt-1 mr-2 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
|
||||||
>
|
>
|
||||||
|
|||||||
85
src/components/chat/HomeChatInput.tsx
Normal file
85
src/components/chat/HomeChatInput.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { SendIcon, StopCircleIcon, X } from "lucide-react";
|
||||||
|
import type React from "react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { ModelPicker } from "@/components/ModelPicker";
|
||||||
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
|
import { homeChatInputValueAtom } from "@/atoms/chatAtoms"; // Use a different atom for home input
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||||
|
|
||||||
|
export function HomeChatInput({ onSubmit }: { onSubmit: () => void }) {
|
||||||
|
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const { settings, updateSettings, isAnyProviderSetup } = useSettings();
|
||||||
|
const { streamMessage, isStreaming, setIsStreaming } = useStreamChat(); // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
|
||||||
|
const adjustHeight = () => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = "0px";
|
||||||
|
const scrollHeight = textarea.scrollHeight;
|
||||||
|
textarea.style.height = `${scrollHeight + 4}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
adjustHeight();
|
||||||
|
}, [inputValue]);
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
return null; // Or loading state
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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 items-start space-x-2 ">
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder="Ask Dyad to build..."
|
||||||
|
className="flex-1 p-2 focus:outline-none overflow-y-auto min-h-[40px] max-h-[200px]"
|
||||||
|
style={{ resize: "none" }}
|
||||||
|
disabled={isStreaming} // Should ideally reflect if *any* stream is happening
|
||||||
|
/>
|
||||||
|
{isStreaming ? (
|
||||||
|
<button
|
||||||
|
className="px-2 py-2 mt-1 mr-2 text-(--sidebar-accent-fg) rounded-lg opacity-50 cursor-not-allowed" // Indicate disabled state
|
||||||
|
title="Cancel generation (unavailable here)"
|
||||||
|
>
|
||||||
|
<StopCircleIcon size={20} />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={onSubmit}
|
||||||
|
disabled={!inputValue.trim() || !isAnyProviderSetup()}
|
||||||
|
className="px-2 py-2 mt-1 mr-2 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
|
||||||
|
title="Start new chat"
|
||||||
|
>
|
||||||
|
<SendIcon size={20} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="px-2 pb-2">
|
||||||
|
<ModelPicker
|
||||||
|
selectedModel={settings.selectedModel}
|
||||||
|
onModelSelect={(model) =>
|
||||||
|
updateSettings({ selectedModel: model })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,30 +1,26 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
import type { Proposal } from "@/lib/schemas"; // Import Proposal type
|
import type { Proposal, ProposalResult } from "@/lib/schemas"; // Import Proposal type
|
||||||
|
import { proposalResultAtom } from "@/atoms/proposalAtoms";
|
||||||
// Define the structure returned by the IPC call
|
import { useAtom } from "jotai";
|
||||||
interface ProposalResult {
|
|
||||||
proposal: Proposal;
|
|
||||||
messageId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useProposal(chatId?: number | undefined) {
|
export function useProposal(chatId?: number | undefined) {
|
||||||
const [proposalData, setProposalData] = useState<ProposalResult | null>(null);
|
const [proposalResult, setProposalResult] = useAtom(proposalResultAtom);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchProposal = useCallback(
|
const fetchProposal = useCallback(
|
||||||
async (innerChatId?: number) => {
|
async (innerChatId?: number) => {
|
||||||
chatId = chatId ?? innerChatId;
|
chatId = chatId ?? innerChatId;
|
||||||
|
console.log("fetching proposal for chatId", chatId);
|
||||||
if (chatId === undefined) {
|
if (chatId === undefined) {
|
||||||
setProposalData(null);
|
setProposalResult(null);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setProposalData(null); // Reset on new fetch
|
setProposalResult(null); // Reset on new fetch
|
||||||
try {
|
try {
|
||||||
// Type assertion might be needed depending on how IpcClient is typed
|
// Type assertion might be needed depending on how IpcClient is typed
|
||||||
const result = (await IpcClient.getInstance().getProposal(
|
const result = (await IpcClient.getInstance().getProposal(
|
||||||
@@ -32,14 +28,14 @@ export function useProposal(chatId?: number | undefined) {
|
|||||||
)) as ProposalResult | null;
|
)) as ProposalResult | null;
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
setProposalData(result);
|
setProposalResult(result);
|
||||||
} else {
|
} else {
|
||||||
setProposalData(null); // Explicitly set to null if IPC returns null
|
setProposalResult(null); // Explicitly set to null if IPC returns null
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Error fetching proposal:", err);
|
console.error("Error fetching proposal:", err);
|
||||||
setError(err.message || "Failed to fetch proposal");
|
setError(err.message || "Failed to fetch proposal");
|
||||||
setProposalData(null); // Clear proposal data on error
|
setProposalResult(null); // Clear proposal data on error
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -64,8 +60,8 @@ export function useProposal(chatId?: number | undefined) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
proposal: proposalData?.proposal ?? null,
|
proposal: proposalResult?.proposal ?? null,
|
||||||
messageId: proposalData?.messageId,
|
messageId: proposalResult?.messageId,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
refreshProposal, // Expose the refresh function
|
refreshProposal, // Expose the refresh function
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export function useStreamChat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
console.log("streaming message - set messages", prompt);
|
|
||||||
setMessages((currentMessages: Message[]) => {
|
setMessages((currentMessages: Message[]) => {
|
||||||
if (redo) {
|
if (redo) {
|
||||||
let remainingMessages = currentMessages.slice();
|
let remainingMessages = currentMessages.slice();
|
||||||
@@ -92,10 +91,10 @@ export function useStreamChat() {
|
|||||||
if (response.updatedFiles) {
|
if (response.updatedFiles) {
|
||||||
setIsPreviewOpen(true);
|
setIsPreviewOpen(true);
|
||||||
}
|
}
|
||||||
|
refreshProposal(chatId);
|
||||||
|
|
||||||
// Keep the same as below
|
// Keep the same as below
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
refreshProposal(chatId);
|
|
||||||
refreshChats();
|
refreshChats();
|
||||||
refreshApp();
|
refreshApp();
|
||||||
refreshVersions();
|
refreshVersions();
|
||||||
|
|||||||
@@ -285,13 +285,13 @@ export function registerChatStreamHandlers() {
|
|||||||
chatId: req.chatId,
|
chatId: req.chatId,
|
||||||
updatedFiles: status.updatedFiles ?? false,
|
updatedFiles: status.updatedFiles ?? false,
|
||||||
} satisfies ChatResponseEnd);
|
} satisfies ChatResponseEnd);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
event.sender.send("chat:response:end", {
|
event.sender.send("chat:response:end", {
|
||||||
chatId: req.chatId,
|
chatId: req.chatId,
|
||||||
updatedFiles: false,
|
updatedFiles: false,
|
||||||
} satisfies ChatResponseEnd);
|
} satisfies ChatResponseEnd);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return the chat ID for backwards compatibility
|
// Return the chat ID for backwards compatibility
|
||||||
return req.chatId;
|
return req.chatId;
|
||||||
|
|||||||
@@ -17,15 +17,9 @@ import type {
|
|||||||
Message,
|
Message,
|
||||||
Version,
|
Version,
|
||||||
} from "./ipc_types";
|
} from "./ipc_types";
|
||||||
import type { Proposal } from "@/lib/schemas";
|
import type { Proposal, ProposalResult } from "@/lib/schemas";
|
||||||
import { showError } from "@/lib/toast";
|
import { showError } from "@/lib/toast";
|
||||||
|
|
||||||
// Define the structure returned by getProposal
|
|
||||||
interface ProposalResult {
|
|
||||||
proposal: Proposal;
|
|
||||||
messageId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatStreamCallbacks {
|
export interface ChatStreamCallbacks {
|
||||||
onUpdate: (messages: Message[]) => void;
|
onUpdate: (messages: Message[]) => void;
|
||||||
onEnd: (response: ChatResponseEnd) => void;
|
onEnd: (response: ChatResponseEnd) => void;
|
||||||
|
|||||||
@@ -117,3 +117,8 @@ export interface Proposal {
|
|||||||
securityRisks: SecurityRisk[];
|
securityRisks: SecurityRisk[];
|
||||||
filesChanged: FileChange[];
|
filesChanged: FileChange[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProposalResult {
|
||||||
|
proposal: Proposal;
|
||||||
|
messageId: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
import { useAtom, useSetAtom } from "jotai";
|
import { useAtom, useSetAtom } from "jotai";
|
||||||
import { chatInputValueAtom } from "../atoms/chatAtoms";
|
import { homeChatInputValueAtom } from "../atoms/chatAtoms";
|
||||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
import { generateCuteAppName } from "@/lib/utils";
|
import { generateCuteAppName } from "@/lib/utils";
|
||||||
import { useLoadApps } from "@/hooks/useLoadApps";
|
import { useLoadApps } from "@/hooks/useLoadApps";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
import { SetupBanner } from "@/components/SetupBanner";
|
import { SetupBanner } from "@/components/SetupBanner";
|
||||||
import { ChatInput } from "@/components/chat/ChatInput";
|
|
||||||
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||||
|
import { HomeChatInput } from "@/components/chat/HomeChatInput";
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
|
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const search = useSearch({ from: "/" });
|
const search = useSearch({ from: "/" });
|
||||||
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
|
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
|
||||||
@@ -87,7 +86,7 @@ export default function HomePage() {
|
|||||||
<SetupBanner />
|
<SetupBanner />
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<ChatInput onSubmit={handleSubmit} />
|
<HomeChatInput onSubmit={handleSubmit} />
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 mt-4">
|
<div className="flex flex-wrap gap-4 mt-4">
|
||||||
{[
|
{[
|
||||||
|
|||||||
Reference in New Issue
Block a user