Initial open-source release
This commit is contained in:
454
src/pages/app-details.tsx
Normal file
454
src/pages/app-details.tsx
Normal file
@@ -0,0 +1,454 @@
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { appBasePathAtom, appsListAtom } from "@/atoms/appAtoms";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import { useLoadApps } from "@/hooks/useLoadApps";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ArrowLeft,
|
||||
MoreVertical,
|
||||
ArrowRight,
|
||||
MessageCircle,
|
||||
Pencil,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export default function AppDetailsPage() {
|
||||
const navigate = useNavigate();
|
||||
const search = useSearch({ from: "/app-details" as const });
|
||||
const [appsList] = useAtom(appsListAtom);
|
||||
const { refreshApps } = useLoadApps();
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||
const [isRenameConfirmDialogOpen, setIsRenameConfirmDialogOpen] =
|
||||
useState(false);
|
||||
const [newAppName, setNewAppName] = useState("");
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [isRenameFolderDialogOpen, setIsRenameFolderDialogOpen] =
|
||||
useState(false);
|
||||
const [newFolderName, setNewFolderName] = useState("");
|
||||
const [isRenamingFolder, setIsRenamingFolder] = useState(false);
|
||||
const appBasePath = useAtomValue(appBasePathAtom);
|
||||
|
||||
// Get the appId from search params and find the corresponding app
|
||||
const appId = search.appId ? Number(search.appId) : null;
|
||||
const selectedApp = appId ? appsList.find((app) => app.id === appId) : null;
|
||||
|
||||
const handleDeleteApp = async () => {
|
||||
if (!appId) return;
|
||||
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await IpcClient.getInstance().deleteApp(appId);
|
||||
setIsDeleteDialogOpen(false);
|
||||
await refreshApps();
|
||||
navigate({ to: "/", search: {} });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete app:", error);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenRenameDialog = () => {
|
||||
if (selectedApp) {
|
||||
setNewAppName(selectedApp.name);
|
||||
setIsRenameDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenRenameFolderDialog = () => {
|
||||
if (selectedApp) {
|
||||
setNewFolderName(selectedApp.path.split("/").pop() || selectedApp.path);
|
||||
setIsRenameFolderDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameApp = async (renameFolder: boolean) => {
|
||||
if (!appId || !selectedApp || !newAppName.trim()) return;
|
||||
|
||||
try {
|
||||
setIsRenaming(true);
|
||||
|
||||
// Determine the new path based on user's choice
|
||||
const appPath = renameFolder ? newAppName : selectedApp.path;
|
||||
|
||||
await IpcClient.getInstance().renameApp({
|
||||
appId,
|
||||
appName: newAppName,
|
||||
appPath,
|
||||
});
|
||||
|
||||
setIsRenameDialogOpen(false);
|
||||
setIsRenameConfirmDialogOpen(false);
|
||||
await refreshApps();
|
||||
} catch (error) {
|
||||
console.error("Failed to rename app:", error);
|
||||
alert(
|
||||
`Error renaming app: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
} finally {
|
||||
setIsRenaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameFolderOnly = async () => {
|
||||
if (!appId || !selectedApp || !newFolderName.trim()) return;
|
||||
|
||||
try {
|
||||
setIsRenamingFolder(true);
|
||||
|
||||
await IpcClient.getInstance().renameApp({
|
||||
appId,
|
||||
appName: selectedApp.name, // Keep the app name the same
|
||||
appPath: newFolderName, // Change only the folder path
|
||||
});
|
||||
|
||||
setIsRenameFolderDialogOpen(false);
|
||||
await refreshApps();
|
||||
} catch (error) {
|
||||
console.error("Failed to rename folder:", error);
|
||||
alert(
|
||||
`Error renaming folder: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
} finally {
|
||||
setIsRenamingFolder(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedApp) {
|
||||
return (
|
||||
<div className="relative min-h-screen p-8">
|
||||
<Button
|
||||
onClick={() => navigate({ to: "/", search: {} })}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="absolute top-4 left-4 flex items-center gap-2 bg-(--background-lightest) py-5"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Go Back
|
||||
</Button>
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h2 className="text-2xl font-bold mb-4">App not found</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen p-8 w-full">
|
||||
<Button
|
||||
onClick={() => navigate({ to: "/", search: {} })}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="absolute top-4 left-4 flex items-center gap-2 bg-(--background-lightest) py-5"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Go Back
|
||||
</Button>
|
||||
|
||||
<div className="w-full max-w-2xl mx-auto mt-16 p-8 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md relative">
|
||||
<div className="flex items-center mb-6">
|
||||
<h2 className="text-3xl font-bold">{selectedApp.name}</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="ml-2 p-1 h-auto"
|
||||
onClick={handleOpenRenameDialog}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Overflow Menu in top right */}
|
||||
<div className="absolute top-4 right-4">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48" align="end">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Button onClick={handleOpenRenameFolderDialog} variant="ghost">
|
||||
Rename folder
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsDeleteDialogOpen(true)}
|
||||
variant="ghost"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 text-base mb-8">
|
||||
<div>
|
||||
<span className="block text-gray-500 dark:text-gray-400 mb-1 text-base">
|
||||
Created
|
||||
</span>
|
||||
<span>{new Date().toLocaleString()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-gray-500 dark:text-gray-400 mb-1 text-base">
|
||||
Last Updated
|
||||
</span>
|
||||
<span>{new Date().toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="block text-gray-500 dark:text-gray-400 mb-1 text-base">
|
||||
Path
|
||||
</span>
|
||||
<span>
|
||||
{appBasePath.replace("$APP_BASE_PATH", selectedApp.path)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex gap-4">
|
||||
<Button
|
||||
onClick={() =>
|
||||
appId && navigate({ to: "/chat", search: { id: appId } })
|
||||
}
|
||||
className="cursor-pointer w-full py-6 flex justify-center items-center gap-2 text-lg"
|
||||
size="lg"
|
||||
>
|
||||
Open in Chat
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Rename Dialog */}
|
||||
<Dialog open={isRenameDialogOpen} onOpenChange={setIsRenameDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rename App</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
value={newAppName}
|
||||
onChange={(e) => setNewAppName(e.target.value)}
|
||||
placeholder="Enter new app name"
|
||||
className="my-4"
|
||||
autoFocus
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsRenameDialogOpen(false)}
|
||||
disabled={isRenaming}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsRenameDialogOpen(false);
|
||||
setIsRenameConfirmDialogOpen(true);
|
||||
}}
|
||||
disabled={isRenaming || !newAppName.trim()}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Rename Folder Dialog */}
|
||||
<Dialog
|
||||
open={isRenameFolderDialogOpen}
|
||||
onOpenChange={setIsRenameFolderDialogOpen}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rename app folder</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will change only the folder name, not the app name.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
value={newFolderName}
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
placeholder="Enter new folder name"
|
||||
className="my-4"
|
||||
autoFocus
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsRenameFolderDialogOpen(false)}
|
||||
disabled={isRenamingFolder}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRenameFolderOnly}
|
||||
disabled={isRenamingFolder || !newFolderName.trim()}
|
||||
>
|
||||
{isRenamingFolder ? (
|
||||
<>
|
||||
<svg
|
||||
className="animate-spin h-4 w-4 mr-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Renaming...
|
||||
</>
|
||||
) : (
|
||||
"Rename Folder"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Rename Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={isRenameConfirmDialogOpen}
|
||||
onOpenChange={setIsRenameConfirmDialogOpen}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
How would you like to rename "{selectedApp.name}"?
|
||||
</DialogTitle>
|
||||
<DialogDescription>Choose an option:</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 my-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start p-4 h-auto relative"
|
||||
onClick={() => handleRenameApp(true)}
|
||||
disabled={isRenaming}
|
||||
>
|
||||
<div className="absolute top-2 right-2">
|
||||
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
|
||||
Recommended
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-medium">Rename app and folder</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Renames the folder to match the new app name.
|
||||
</p>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start p-4 h-auto"
|
||||
onClick={() => handleRenameApp(false)}
|
||||
disabled={isRenaming}
|
||||
>
|
||||
<div className="text-left">
|
||||
<p className="font-medium">Rename app only</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
The folder name will remain the same.
|
||||
</p>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsRenameConfirmDialogOpen(false)}
|
||||
disabled={isRenaming}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete "{selectedApp.name}"?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action is irreversible. All app files and chat history will
|
||||
be permanently deleted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="flex justify-end gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDeleteDialogOpen(false)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDeleteApp}
|
||||
disabled={isDeleting}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<svg
|
||||
className="animate-spin h-4 w-4 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
"Delete App"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
src/pages/chat.tsx
Normal file
67
src/pages/chat.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import {
|
||||
PanelGroup,
|
||||
Panel,
|
||||
PanelResizeHandle,
|
||||
type ImperativePanelHandle,
|
||||
} from "react-resizable-panels";
|
||||
import { ChatPanel } from "../components/ChatPanel";
|
||||
import { PreviewPanel } from "../components/preview_panel/PreviewPanel";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAtom } from "jotai";
|
||||
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
||||
|
||||
export default function ChatPage() {
|
||||
const { id } = useSearch({ from: "/chat" });
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreviewOpen) {
|
||||
ref.current?.expand();
|
||||
} else {
|
||||
ref.current?.collapse();
|
||||
}
|
||||
}, [isPreviewOpen]);
|
||||
const ref = useRef<ImperativePanelHandle>(null);
|
||||
|
||||
return (
|
||||
<PanelGroup autoSaveId="persistence" direction="horizontal">
|
||||
<Panel id="chat-panel" minSize={30}>
|
||||
<div className="h-full w-full">
|
||||
<ChatPanel
|
||||
chatId={id}
|
||||
isPreviewOpen={isPreviewOpen}
|
||||
onTogglePreview={() => {
|
||||
setIsPreviewOpen(!isPreviewOpen);
|
||||
if (isPreviewOpen) {
|
||||
ref.current?.collapse();
|
||||
} else {
|
||||
ref.current?.expand();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<>
|
||||
<PanelResizeHandle
|
||||
onDragging={(e) => setIsResizing(e)}
|
||||
className="w-1 bg-gray-200 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors cursor-col-resize"
|
||||
/>
|
||||
<Panel
|
||||
collapsible
|
||||
ref={ref}
|
||||
id="preview-panel"
|
||||
minSize={20}
|
||||
className={cn(
|
||||
!isResizing && "transition-all duration-100 ease-in-out"
|
||||
)}
|
||||
>
|
||||
<PreviewPanel />
|
||||
</Panel>
|
||||
</>
|
||||
</PanelGroup>
|
||||
);
|
||||
}
|
||||
183
src/pages/home.tsx
Normal file
183
src/pages/home.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { chatInputValueAtom } from "../atoms/chatAtoms";
|
||||
import { selectedAppIdAtom, appsListAtom } from "@/atoms/appAtoms";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import { generateCuteAppName } from "@/lib/utils";
|
||||
import { useLoadApps } from "@/hooks/useLoadApps";
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
import { SetupBanner } from "@/components/SetupBanner";
|
||||
import { ChatInput } from "@/components/chat/ChatInput";
|
||||
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||
|
||||
export default function HomePage() {
|
||||
const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
|
||||
const navigate = useNavigate();
|
||||
const search = useSearch({ from: "/" });
|
||||
const [appsList] = useAtom(appsListAtom);
|
||||
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
|
||||
const { refreshApps } = useLoadApps();
|
||||
const { isAnyProviderSetup } = useSettings();
|
||||
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { streamMessage } = useStreamChat();
|
||||
|
||||
// Get the appId from search params
|
||||
const appId = search.appId ? Number(search.appId) : null;
|
||||
|
||||
// Redirect to app details page if appId is present
|
||||
useEffect(() => {
|
||||
if (appId) {
|
||||
navigate({ to: "/app-details", search: { appId } });
|
||||
}
|
||||
}, [appId, navigate]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!inputValue.trim()) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// Create the chat and navigate
|
||||
const result = await IpcClient.getInstance().createApp({
|
||||
name: generateCuteAppName(),
|
||||
path: "./apps/foo",
|
||||
});
|
||||
|
||||
// Add a 2-second timeout *after* the streamMessage call
|
||||
// This makes the loading UI feel less janky.
|
||||
streamMessage({ prompt: inputValue, chatId: result.chatId });
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
setInputValue("");
|
||||
setSelectedAppId(result.app.id);
|
||||
setIsPreviewOpen(false);
|
||||
refreshApps();
|
||||
navigate({ to: "/chat", search: { id: result.chatId } });
|
||||
} catch (error) {
|
||||
console.error("Failed to create chat:", error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Loading overlay
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center max-w-3xl m-auto p-8">
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<div className="relative w-24 h-24 mb-8">
|
||||
<div className="absolute top-0 left-0 w-full h-full border-8 border-gray-200 dark:border-gray-700 rounded-full"></div>
|
||||
<div className="absolute top-0 left-0 w-full h-full border-8 border-t-(--primary) rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-2 text-gray-800 dark:text-gray-200">
|
||||
Building your app
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-center max-w-md mb-8">
|
||||
We're setting up your app with AI magic. <br />
|
||||
This might take a moment...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center max-w-3xl m-auto p-8">
|
||||
<h1 className="text-6xl font-bold mb-12 bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-600 dark:from-gray-100 dark:to-gray-400 tracking-tight">
|
||||
Build your dream app
|
||||
</h1>
|
||||
|
||||
{!isAnyProviderSetup() && <SetupBanner />}
|
||||
|
||||
<div className="w-full">
|
||||
<ChatInput onSubmit={handleSubmit} />
|
||||
|
||||
<div className="flex flex-wrap gap-4 mt-4">
|
||||
{[
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
label: "TODO list app",
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1h2a1 1 0 001-1v-7m-6 0a1 1 0 00-1 1v3"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
label: "Landing Page",
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
label: "Sign Up Form",
|
||||
},
|
||||
].map((item, index) => (
|
||||
<button
|
||||
type="button"
|
||||
key={index}
|
||||
onClick={() => setInputValue(`Build me a ${item.label}`)}
|
||||
className="flex items-center gap-3 px-4 py-2 rounded-xl border border-gray-200
|
||||
bg-white/50 backdrop-blur-sm
|
||||
transition-all duration-200
|
||||
hover:bg-white hover:shadow-md hover:border-gray-300
|
||||
active:scale-[0.98]
|
||||
dark:bg-gray-800/50 dark:border-gray-700
|
||||
dark:hover:bg-gray-800 dark:hover:border-gray-600"
|
||||
>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{item.icon}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
121
src/pages/settings.tsx
Normal file
121
src/pages/settings.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
import { ProviderSettingsGrid } from "@/components/ProviderSettings";
|
||||
import ConfirmationDialog from "@/components/ConfirmationDialog";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import { showSuccess, showError } from "@/lib/toast";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
const handleResetEverything = async () => {
|
||||
setIsResetting(true);
|
||||
try {
|
||||
const ipcClient = IpcClient.getInstance();
|
||||
const result = await ipcClient.resetAll();
|
||||
if (result.success) {
|
||||
showSuccess("Successfully reset everything. Restart the application.");
|
||||
} else {
|
||||
showError(result.message || "Failed to reset everything.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error resetting:", error);
|
||||
showError(
|
||||
error instanceof Error ? error.message : "An unknown error occurred"
|
||||
);
|
||||
} finally {
|
||||
setIsResetting(false);
|
||||
setIsResetDialogOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-8">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-8 text-gray-900 dark:text-white">
|
||||
Settings
|
||||
</h1>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
Appearance
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Theme
|
||||
</label>
|
||||
|
||||
<div className="relative bg-gray-100 dark:bg-gray-700 rounded-lg p-1 flex">
|
||||
{(["system", "light", "dark"] as const).map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
onClick={() => setTheme(option)}
|
||||
className={`
|
||||
px-4 py-1.5 text-sm font-medium rounded-md
|
||||
transition-all duration-200
|
||||
${
|
||||
theme === option
|
||||
? "bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{option.charAt(0).toUpperCase() + option.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||
<ProviderSettingsGrid configuredProviders={[]} />
|
||||
</div>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-red-200 dark:border-red-800">
|
||||
<h2 className="text-lg font-medium text-red-600 dark:text-red-400 mb-4">
|
||||
Danger Zone
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between flex-col sm:flex-row sm:items-center gap-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
Reset Everything
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
This will delete all your apps, chats, and settings. This
|
||||
action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsResetDialogOpen(true)}
|
||||
disabled={isResetting}
|
||||
className="rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isResetting ? "Resetting..." : "Reset Everything"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmationDialog
|
||||
isOpen={isResetDialogOpen}
|
||||
title="Reset Everything"
|
||||
message="Are you sure you want to reset everything? This will delete all your apps, chats, and settings. This action cannot be undone."
|
||||
confirmText="Reset Everything"
|
||||
cancelText="Cancel"
|
||||
onConfirm={handleResetEverything}
|
||||
onCancel={() => setIsResetDialogOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user