feat: integrate custom features for smart context management
Some checks failed
CI / test (map[image:macos-latest name:macos], 1, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 2, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 3, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 4, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 1, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 2, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 3, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 4, 4) (push) Has been cancelled
CI / merge-reports (push) Has been cancelled
Some checks failed
CI / test (map[image:macos-latest name:macos], 1, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 2, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 3, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 4, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 1, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 2, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 3, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 4, 4) (push) Has been cancelled
CI / merge-reports (push) Has been cancelled
- Added a new integration script to manage custom features related to smart context. - Implemented handlers for smart context operations (get, update, clear, stats) in ipc. - Created a SmartContextStore class to manage context snippets and summaries. - Developed hooks for React to interact with smart context (useSmartContext, useUpdateSmartContext, useClearSmartContext, useSmartContextStats). - Included backup and restore functionality in the integration script. - Validated integration by checking for custom modifications and file existence.
This commit is contained in:
689
backups/backup-20251218-094212/src/pages/app-details.tsx
Normal file
689
backups/backup-20251218-094212/src/pages/app-details.tsx
Normal file
@@ -0,0 +1,689 @@
|
||||
import { useNavigate, useRouter, useSearch } from "@tanstack/react-router";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import {
|
||||
appBasePathAtom,
|
||||
appsListAtom,
|
||||
selectedAppIdAtom,
|
||||
} 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,
|
||||
MessageCircle,
|
||||
Pencil,
|
||||
Folder,
|
||||
} 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";
|
||||
import { GitHubConnector } from "@/components/GitHubConnector";
|
||||
import { SupabaseConnector } from "@/components/SupabaseConnector";
|
||||
import { showError } from "@/lib/toast";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { invalidateAppQuery } from "@/hooks/useLoadApp";
|
||||
import { useDebounce } from "@/hooks/useDebounce";
|
||||
import { useCheckName } from "@/hooks/useCheckName";
|
||||
import { AppUpgrades } from "@/components/AppUpgrades";
|
||||
import { CapacitorControls } from "@/components/CapacitorControls";
|
||||
|
||||
export default function AppDetailsPage() {
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
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);
|
||||
|
||||
const [isCopyDialogOpen, setIsCopyDialogOpen] = useState(false);
|
||||
const [newCopyAppName, setNewCopyAppName] = useState("");
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
|
||||
|
||||
const debouncedNewCopyAppName = useDebounce(newCopyAppName, 150);
|
||||
const { data: checkNameResult, isLoading: isCheckingName } = useCheckName(
|
||||
debouncedNewCopyAppName,
|
||||
);
|
||||
const nameExists = checkNameResult?.exists ?? false;
|
||||
|
||||
// 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) {
|
||||
setIsDeleteDialogOpen(false);
|
||||
showError(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);
|
||||
const errorMessage = (
|
||||
error instanceof Error ? error.message : String(error)
|
||||
).replace(/^Error invoking remote method 'rename-app': Error: /, "");
|
||||
showError(errorMessage);
|
||||
} 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);
|
||||
const errorMessage = (
|
||||
error instanceof Error ? error.message : String(error)
|
||||
).replace(/^Error invoking remote method 'rename-app': Error: /, "");
|
||||
showError(errorMessage);
|
||||
} finally {
|
||||
setIsRenamingFolder(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAppNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNewCopyAppName(e.target.value);
|
||||
};
|
||||
|
||||
const handleOpenCopyDialog = () => {
|
||||
if (selectedApp) {
|
||||
setNewCopyAppName(`${selectedApp.name}-copy`);
|
||||
setIsCopyDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const copyAppMutation = useMutation({
|
||||
mutationFn: async ({ withHistory }: { withHistory: boolean }) => {
|
||||
if (!appId || !newCopyAppName.trim()) {
|
||||
throw new Error("Invalid app ID or name for copying.");
|
||||
}
|
||||
return IpcClient.getInstance().copyApp({
|
||||
appId,
|
||||
newAppName: newCopyAppName,
|
||||
withHistory,
|
||||
});
|
||||
},
|
||||
onSuccess: async (data) => {
|
||||
const appId = data.app.id;
|
||||
setSelectedAppId(appId);
|
||||
await invalidateAppQuery(queryClient, { appId });
|
||||
await refreshApps();
|
||||
await IpcClient.getInstance().createChat(appId);
|
||||
setIsCopyDialogOpen(false);
|
||||
navigate({ to: "/app-details", search: { appId } });
|
||||
},
|
||||
onError: (error) => {
|
||||
showError(error);
|
||||
},
|
||||
});
|
||||
|
||||
if (!selectedApp) {
|
||||
return (
|
||||
<div className="relative min-h-screen p-8">
|
||||
<Button
|
||||
onClick={() => router.history.back()}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="absolute top-4 left-4 flex items-center gap-1 bg-(--background-lightest) py-5"
|
||||
>
|
||||
<ArrowLeft className="h-3 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h2 className="text-xl font-bold">App not found</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const fullAppPath = appBasePath.replace("$APP_BASE_PATH", selectedApp.path);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative min-h-screen p-4 w-full"
|
||||
data-testid="app-details-page"
|
||||
>
|
||||
<Button
|
||||
onClick={() => router.history.back()}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="absolute top-4 left-4 flex items-center gap-1 bg-(--background-lightest) py-2"
|
||||
>
|
||||
<ArrowLeft className="h-3 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<div className="w-full max-w-2xl mx-auto mt-10 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm relative">
|
||||
<div className="flex items-center mb-3">
|
||||
<h2 className="text-2xl font-bold">{selectedApp.name}</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="ml-1 p-0.5 h-auto"
|
||||
onClick={handleOpenRenameDialog}
|
||||
data-testid="app-details-rename-app-button"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Overflow Menu in top right */}
|
||||
<div className="absolute top-2 right-2">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
data-testid="app-details-more-options-button"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-40 p-2" align="end">
|
||||
<div className="flex flex-col space-y-0.5">
|
||||
<Button
|
||||
onClick={handleOpenRenameFolderDialog}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 justify-start text-xs"
|
||||
>
|
||||
Rename folder
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleOpenCopyDialog}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 justify-start text-xs"
|
||||
>
|
||||
Copy app
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsDeleteDialogOpen(true)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 justify-start text-xs"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-sm mb-4">
|
||||
<div>
|
||||
<span className="block text-gray-500 dark:text-gray-400 mb-0.5 text-xs">
|
||||
Created
|
||||
</span>
|
||||
<span>{selectedApp.createdAt.toString()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-gray-500 dark:text-gray-400 mb-0.5 text-xs">
|
||||
Last Updated
|
||||
</span>
|
||||
<span>{selectedApp.updatedAt.toString()}</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="block text-gray-500 dark:text-gray-400 mb-0.5 text-xs">
|
||||
Path
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm break-all">{fullAppPath}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="p-0.5 h-auto cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
onClick={() => {
|
||||
IpcClient.getInstance().showItemInFolder(fullAppPath);
|
||||
}}
|
||||
title="Show in folder"
|
||||
>
|
||||
<Folder className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!appId) {
|
||||
console.error("No app id found");
|
||||
return;
|
||||
}
|
||||
navigate({ to: "/chat" });
|
||||
}}
|
||||
className="cursor-pointer w-full py-5 flex justify-center items-center gap-2"
|
||||
size="lg"
|
||||
>
|
||||
Open in Chat
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="border border-gray-200 rounded-md p-4">
|
||||
<GitHubConnector appId={appId} folderName={selectedApp.path} />
|
||||
</div>
|
||||
{appId && <SupabaseConnector appId={appId} />}
|
||||
{appId && <CapacitorControls appId={appId} />}
|
||||
<AppUpgrades appId={appId} />
|
||||
</div>
|
||||
|
||||
{/* Rename Dialog */}
|
||||
<Dialog open={isRenameDialogOpen} onOpenChange={setIsRenameDialogOpen}>
|
||||
<DialogContent className="max-w-sm p-4">
|
||||
<DialogHeader className="pb-2">
|
||||
<DialogTitle>Rename App</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input
|
||||
value={newAppName}
|
||||
onChange={(e) => setNewAppName(e.target.value)}
|
||||
placeholder="Enter new app name"
|
||||
className="my-2"
|
||||
autoFocus
|
||||
/>
|
||||
<DialogFooter className="pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsRenameDialogOpen(false)}
|
||||
disabled={isRenaming}
|
||||
size="sm"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsRenameDialogOpen(false);
|
||||
setIsRenameConfirmDialogOpen(true);
|
||||
}}
|
||||
disabled={isRenaming || !newAppName.trim()}
|
||||
size="sm"
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Rename Folder Dialog */}
|
||||
<Dialog
|
||||
open={isRenameFolderDialogOpen}
|
||||
onOpenChange={setIsRenameFolderDialogOpen}
|
||||
>
|
||||
<DialogContent className="max-w-sm p-4">
|
||||
<DialogHeader className="pb-2">
|
||||
<DialogTitle>Rename app folder</DialogTitle>
|
||||
<DialogDescription className="text-xs">
|
||||
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-2"
|
||||
autoFocus
|
||||
/>
|
||||
<DialogFooter className="pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsRenameFolderDialogOpen(false)}
|
||||
disabled={isRenamingFolder}
|
||||
size="sm"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRenameFolderOnly}
|
||||
disabled={isRenamingFolder || !newFolderName.trim()}
|
||||
size="sm"
|
||||
>
|
||||
{isRenamingFolder ? (
|
||||
<>
|
||||
<svg
|
||||
className="animate-spin h-3 w-3 mr-1"
|
||||
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 className="max-w-sm p-4">
|
||||
<DialogHeader className="pb-2">
|
||||
<DialogTitle className="text-base">
|
||||
How would you like to rename "{selectedApp.name}"?
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs">
|
||||
Choose an option:
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 my-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start p-2 h-auto relative text-sm"
|
||||
onClick={() => handleRenameApp(true)}
|
||||
disabled={isRenaming}
|
||||
>
|
||||
<div className="absolute top-1 right-1">
|
||||
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-1.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300 text-[10px]">
|
||||
Recommended
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-xs">Rename app and folder</p>
|
||||
<p className="text-xs 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-2 h-auto text-sm"
|
||||
onClick={() => handleRenameApp(false)}
|
||||
disabled={isRenaming}
|
||||
>
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-xs">Rename app only</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
The folder name will remain the same.
|
||||
</p>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<DialogFooter className="pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsRenameConfirmDialogOpen(false)}
|
||||
disabled={isRenaming}
|
||||
size="sm"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Copy App Dialog */}
|
||||
{selectedApp && (
|
||||
<Dialog open={isCopyDialogOpen} onOpenChange={setIsCopyDialogOpen}>
|
||||
<DialogContent className="max-w-md p-4">
|
||||
<DialogHeader className="pb-2">
|
||||
<DialogTitle>Copy "{selectedApp.name}"</DialogTitle>
|
||||
<DialogDescription className="text-sm">
|
||||
<p>Create a copy of this app.</p>
|
||||
<p>
|
||||
Note: this does not copy over the Supabase project or GitHub
|
||||
project.
|
||||
</p>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 my-2">
|
||||
<div>
|
||||
<Label htmlFor="newAppName">New app name</Label>
|
||||
<div className="relative mt-1">
|
||||
<Input
|
||||
id="newAppName"
|
||||
value={newCopyAppName}
|
||||
onChange={handleAppNameChange}
|
||||
placeholder="Enter new app name"
|
||||
className="pr-8"
|
||||
disabled={copyAppMutation.isPending}
|
||||
/>
|
||||
{isCheckingName && (
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{nameExists && (
|
||||
<p className="text-xs text-yellow-600 dark:text-yellow-500 mt-1">
|
||||
An app with this name already exists. Please choose
|
||||
another name.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start p-2 h-auto relative text-sm"
|
||||
onClick={() =>
|
||||
copyAppMutation.mutate({ withHistory: true })
|
||||
}
|
||||
disabled={
|
||||
copyAppMutation.isPending ||
|
||||
nameExists ||
|
||||
!newCopyAppName.trim() ||
|
||||
isCheckingName
|
||||
}
|
||||
>
|
||||
{copyAppMutation.isPending &&
|
||||
copyAppMutation.variables?.withHistory === true && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
<div className="absolute top-1 right-1">
|
||||
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-1.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300 text-[10px]">
|
||||
Recommended
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-xs">
|
||||
Copy app with history
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Copies the entire app, including the Git version
|
||||
history.
|
||||
</p>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start p-2 h-auto text-sm"
|
||||
onClick={() =>
|
||||
copyAppMutation.mutate({ withHistory: false })
|
||||
}
|
||||
disabled={
|
||||
copyAppMutation.isPending ||
|
||||
nameExists ||
|
||||
!newCopyAppName.trim() ||
|
||||
isCheckingName
|
||||
}
|
||||
>
|
||||
{copyAppMutation.isPending &&
|
||||
copyAppMutation.variables?.withHistory === false && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-xs">
|
||||
Copy app without history
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Useful if the current app has a Git-related issue.
|
||||
</p>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsCopyDialogOpen(false)}
|
||||
disabled={copyAppMutation.isPending}
|
||||
size="sm"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<DialogContent className="max-w-sm p-4">
|
||||
<DialogHeader className="pb-2">
|
||||
<DialogTitle>Delete "{selectedApp.name}"?</DialogTitle>
|
||||
<DialogDescription className="text-xs">
|
||||
This action is irreversible. All app files and chat history will
|
||||
be permanently deleted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="flex justify-end gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDeleteDialogOpen(false)}
|
||||
disabled={isDeleting}
|
||||
size="sm"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDeleteApp}
|
||||
disabled={isDeleting}
|
||||
className="flex items-center gap-1"
|
||||
size="sm"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<svg
|
||||
className="animate-spin h-3 w-3 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>
|
||||
);
|
||||
}
|
||||
82
backups/backup-20251218-094212/src/pages/chat.tsx
Normal file
82
backups/backup-20251218-094212/src/pages/chat.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
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 { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
||||
import { useChats } from "@/hooks/useChats";
|
||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||
|
||||
export default function ChatPage() {
|
||||
let { id: chatId } = useSearch({ from: "/chat" });
|
||||
const navigate = useNavigate();
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const selectedAppId = useAtomValue(selectedAppIdAtom);
|
||||
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
|
||||
const { chats, loading } = useChats(selectedAppId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatId && chats.length && !loading) {
|
||||
// Not a real navigation, just a redirect, when the user navigates to /chat
|
||||
// without a chatId, we redirect to the first chat
|
||||
setSelectedAppId(chats[0].appId);
|
||||
navigate({ to: "/chat", search: { id: chats[0].id }, replace: true });
|
||||
}
|
||||
}, [chatId, chats, loading, navigate]);
|
||||
|
||||
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={chatId}
|
||||
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>
|
||||
);
|
||||
}
|
||||
309
backups/backup-20251218-094212/src/pages/home.tsx
Normal file
309
backups/backup-20251218-094212/src/pages/home.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { homeChatInputValueAtom } from "../atoms/chatAtoms";
|
||||
import { selectedAppIdAtom } 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 { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||
import { HomeChatInput } from "@/components/chat/HomeChatInput";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { PrivacyBanner } from "@/components/TelemetryBanner";
|
||||
import { INSPIRATION_PROMPTS } from "@/prompts/inspiration_prompts";
|
||||
import { useAppVersion } from "@/hooks/useAppVersion";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { ImportAppButton } from "@/components/ImportAppButton";
|
||||
import { showError } from "@/lib/toast";
|
||||
import { invalidateAppQuery } from "@/hooks/useLoadApp";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ForceCloseDialog } from "@/components/ForceCloseDialog";
|
||||
|
||||
import type { FileAttachment } from "@/ipc/ipc_types";
|
||||
import { NEON_TEMPLATE_IDS } from "@/shared/templates";
|
||||
import { neonTemplateHook } from "@/client_logic/template_hook";
|
||||
import { ProBanner } from "@/components/ProBanner";
|
||||
|
||||
// Adding an export for attachments
|
||||
export interface HomeSubmitOptions {
|
||||
attachments?: FileAttachment[];
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
|
||||
const navigate = useNavigate();
|
||||
const search = useSearch({ from: "/" });
|
||||
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
|
||||
const { refreshApps } = useLoadApps();
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [forceCloseDialogOpen, setForceCloseDialogOpen] = useState(false);
|
||||
const [performanceData, setPerformanceData] = useState<any>(undefined);
|
||||
const { streamMessage } = useStreamChat({ hasChatId: false });
|
||||
const posthog = usePostHog();
|
||||
const appVersion = useAppVersion();
|
||||
const [releaseNotesOpen, setReleaseNotesOpen] = useState(false);
|
||||
const [releaseUrl, setReleaseUrl] = useState("");
|
||||
const { theme } = useTheme();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Listen for force-close events
|
||||
useEffect(() => {
|
||||
const ipc = IpcClient.getInstance();
|
||||
const unsubscribe = ipc.onForceCloseDetected((data) => {
|
||||
setPerformanceData(data.performanceData);
|
||||
setForceCloseDialogOpen(true);
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const updateLastVersionLaunched = async () => {
|
||||
if (
|
||||
appVersion &&
|
||||
settings &&
|
||||
settings.lastShownReleaseNotesVersion !== appVersion
|
||||
) {
|
||||
const shouldShowReleaseNotes = !!settings.lastShownReleaseNotesVersion;
|
||||
await updateSettings({
|
||||
lastShownReleaseNotesVersion: appVersion,
|
||||
});
|
||||
// It feels spammy to show release notes if it's
|
||||
// the users very first time.
|
||||
if (!shouldShowReleaseNotes) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await IpcClient.getInstance().doesReleaseNoteExist({
|
||||
version: appVersion,
|
||||
});
|
||||
|
||||
if (result.exists && result.url) {
|
||||
setReleaseUrl(result.url + "?hideHeader=true&theme=" + theme);
|
||||
setReleaseNotesOpen(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"Unable to check if release note exists for: " + appVersion,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
updateLastVersionLaunched();
|
||||
}, [appVersion, settings, updateSettings, theme]);
|
||||
|
||||
// Get the appId from search params
|
||||
const appId = search.appId ? Number(search.appId) : null;
|
||||
|
||||
// State for random prompts
|
||||
const [randomPrompts, setRandomPrompts] = useState<
|
||||
typeof INSPIRATION_PROMPTS
|
||||
>([]);
|
||||
|
||||
// Function to get random prompts
|
||||
const getRandomPrompts = useCallback(() => {
|
||||
const shuffled = [...INSPIRATION_PROMPTS].sort(() => 0.5 - Math.random());
|
||||
return shuffled.slice(0, 3);
|
||||
}, []);
|
||||
|
||||
// Initialize random prompts
|
||||
useEffect(() => {
|
||||
setRandomPrompts(getRandomPrompts());
|
||||
}, [getRandomPrompts]);
|
||||
|
||||
// Redirect to app details page if appId is present
|
||||
useEffect(() => {
|
||||
if (appId) {
|
||||
navigate({ to: "/app-details", search: { appId } });
|
||||
}
|
||||
}, [appId, navigate]);
|
||||
|
||||
const handleSubmit = async (options?: HomeSubmitOptions) => {
|
||||
const attachments = options?.attachments || [];
|
||||
|
||||
if (!inputValue.trim() && attachments.length === 0) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// Create the chat and navigate
|
||||
const result = await IpcClient.getInstance().createApp({
|
||||
name: generateCuteAppName(),
|
||||
});
|
||||
if (
|
||||
settings?.selectedTemplateId &&
|
||||
NEON_TEMPLATE_IDS.has(settings.selectedTemplateId)
|
||||
) {
|
||||
await neonTemplateHook({
|
||||
appId: result.app.id,
|
||||
appName: result.app.name,
|
||||
});
|
||||
}
|
||||
|
||||
// Stream the message with attachments
|
||||
streamMessage({
|
||||
prompt: inputValue,
|
||||
chatId: result.chatId,
|
||||
attachments,
|
||||
});
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, settings?.isTestMode ? 0 : 2000),
|
||||
);
|
||||
|
||||
setInputValue("");
|
||||
setSelectedAppId(result.app.id);
|
||||
setIsPreviewOpen(false);
|
||||
await refreshApps(); // Ensure refreshApps is awaited if it's async
|
||||
await invalidateAppQuery(queryClient, { appId: result.app.id });
|
||||
posthog.capture("home:chat-submit");
|
||||
navigate({ to: "/chat", search: { id: result.chatId } });
|
||||
} catch (error) {
|
||||
console.error("Failed to create chat:", error);
|
||||
showError("Failed to create app. " + (error as any).toString());
|
||||
setIsLoading(false); // Ensure loading state is reset on error
|
||||
}
|
||||
// No finally block needed for setIsLoading(false) here if navigation happens on success
|
||||
};
|
||||
|
||||
// Loading overlay for app creation
|
||||
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">
|
||||
{/* Loading Spinner */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
// Main Home Page Content
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center max-w-3xl w-full m-auto p-8">
|
||||
<ForceCloseDialog
|
||||
isOpen={forceCloseDialogOpen}
|
||||
onClose={() => setForceCloseDialogOpen(false)}
|
||||
performanceData={performanceData}
|
||||
/>
|
||||
<SetupBanner />
|
||||
|
||||
<div className="w-full">
|
||||
<ImportAppButton />
|
||||
<HomeChatInput onSubmit={handleSubmit} />
|
||||
|
||||
<div className="flex flex-col gap-4 mt-2">
|
||||
<div className="flex flex-wrap gap-4 justify-center">
|
||||
{randomPrompts.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>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRandomPrompts(getRandomPrompts())}
|
||||
className="self-center flex items-center gap-2 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"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-700 dark:text-gray-300"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
More ideas
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<ProBanner />
|
||||
</div>
|
||||
<PrivacyBanner />
|
||||
|
||||
{/* Release Notes Dialog */}
|
||||
<Dialog open={releaseNotesOpen} onOpenChange={setReleaseNotesOpen}>
|
||||
<DialogContent className="max-w-4xl bg-(--docs-bg) pr-0 pt-4 pl-4 gap-1">
|
||||
<DialogHeader>
|
||||
<DialogTitle>What's new in v{appVersion}?</DialogTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-10 top-2 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
releaseUrl.replace("?hideHeader=true&theme=" + theme, ""),
|
||||
"_blank",
|
||||
)
|
||||
}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<div className="overflow-auto h-[70vh] flex flex-col ">
|
||||
{releaseUrl && (
|
||||
<div className="flex-1">
|
||||
<iframe
|
||||
src={releaseUrl}
|
||||
className="w-full h-full border-0 rounded-lg"
|
||||
title={`Release notes for v${appVersion}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
124
backups/backup-20251218-094212/src/pages/hub.tsx
Normal file
124
backups/backup-20251218-094212/src/pages/hub.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
import { useTemplates } from "@/hooks/useTemplates";
|
||||
import { TemplateCard } from "@/components/TemplateCard";
|
||||
import { CreateAppDialog } from "@/components/CreateAppDialog";
|
||||
import { NeonConnector } from "@/components/NeonConnector";
|
||||
|
||||
const HubPage: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const { templates, isLoading } = useTemplates();
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const selectedTemplateId = settings?.selectedTemplateId;
|
||||
|
||||
const handleTemplateSelect = (templateId: string) => {
|
||||
updateSettings({ selectedTemplateId: templateId });
|
||||
};
|
||||
|
||||
const handleCreateApp = () => {
|
||||
setIsCreateDialogOpen(true);
|
||||
};
|
||||
// Separate templates into official and community
|
||||
const officialTemplates =
|
||||
templates?.filter((template) => template.isOfficial) || [];
|
||||
const communityTemplates =
|
||||
templates?.filter((template) => !template.isOfficial) || [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen px-8 py-4">
|
||||
<div className="max-w-5xl mx-auto pb-12">
|
||||
<Button
|
||||
onClick={() => router.history.back()}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex items-center gap-2 mb-4 bg-(--background-lightest) py-5"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Go Back
|
||||
</Button>
|
||||
<header className="mb-8 text-left">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Pick your default template
|
||||
</h1>
|
||||
<p className="text-md text-gray-600 dark:text-gray-400">
|
||||
Choose a starting point for your new project.
|
||||
{isLoading && " Loading additional templates..."}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Official Templates Section */}
|
||||
{officialTemplates.length > 0 && (
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
Official templates
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{officialTemplates.map((template) => (
|
||||
<TemplateCard
|
||||
key={template.id}
|
||||
template={template}
|
||||
isSelected={template.id === selectedTemplateId}
|
||||
onSelect={handleTemplateSelect}
|
||||
onCreateApp={handleCreateApp}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Community Templates Section */}
|
||||
{communityTemplates.length > 0 && (
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
Community templates
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{communityTemplates.map((template) => (
|
||||
<TemplateCard
|
||||
key={template.id}
|
||||
template={template}
|
||||
isSelected={template.id === selectedTemplateId}
|
||||
onSelect={handleTemplateSelect}
|
||||
onCreateApp={handleCreateApp}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<BackendSection />
|
||||
</div>
|
||||
|
||||
<CreateAppDialog
|
||||
open={isCreateDialogOpen}
|
||||
onOpenChange={setIsCreateDialogOpen}
|
||||
template={templates.find((t) => t.id === settings?.selectedTemplateId)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function BackendSection() {
|
||||
return (
|
||||
<div className="">
|
||||
<header className="mb-4 text-left">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Backend Services
|
||||
</h1>
|
||||
<p className="text-md text-gray-600 dark:text-gray-400">
|
||||
Connect to backend services for your projects.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<NeonConnector />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HubPage;
|
||||
141
backups/backup-20251218-094212/src/pages/library.tsx
Normal file
141
backups/backup-20251218-094212/src/pages/library.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { usePrompts } from "@/hooks/usePrompts";
|
||||
import {
|
||||
CreatePromptDialog,
|
||||
CreateOrEditPromptDialog,
|
||||
} from "@/components/CreatePromptDialog";
|
||||
import { DeleteConfirmationDialog } from "@/components/DeleteConfirmationDialog";
|
||||
import { useDeepLink } from "@/contexts/DeepLinkContext";
|
||||
import { AddPromptDeepLinkData } from "@/ipc/deep_link_data";
|
||||
import { showInfo } from "@/lib/toast";
|
||||
|
||||
export default function LibraryPage() {
|
||||
const { prompts, isLoading, createPrompt, updatePrompt, deletePrompt } =
|
||||
usePrompts();
|
||||
const { lastDeepLink, clearLastDeepLink } = useDeepLink();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [prefillData, setPrefillData] = useState<
|
||||
| {
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const handleDeepLink = async () => {
|
||||
if (lastDeepLink?.type === "add-prompt") {
|
||||
const deepLink = lastDeepLink as AddPromptDeepLinkData;
|
||||
const payload = deepLink.payload;
|
||||
showInfo(`Prefilled prompt: ${payload.title}`);
|
||||
setPrefillData({
|
||||
title: payload.title,
|
||||
description: payload.description,
|
||||
content: payload.content,
|
||||
});
|
||||
setDialogOpen(true);
|
||||
clearLastDeepLink();
|
||||
}
|
||||
};
|
||||
handleDeepLink();
|
||||
}, [lastDeepLink?.timestamp, clearLastDeepLink]);
|
||||
|
||||
const handleDialogClose = (open: boolean) => {
|
||||
setDialogOpen(open);
|
||||
if (!open) {
|
||||
// Clear prefill data when dialog closes
|
||||
setPrefillData(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen px-8 py-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-3xl font-bold mr-4">Library: Prompts</h1>
|
||||
<CreatePromptDialog
|
||||
onCreatePrompt={createPrompt}
|
||||
prefillData={prefillData}
|
||||
isOpen={dialogOpen}
|
||||
onOpenChange={handleDialogClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div>Loading...</div>
|
||||
) : prompts.length === 0 ? (
|
||||
<div className="text-muted-foreground">
|
||||
No prompts yet. Create one to get started.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{prompts.map((p) => (
|
||||
<PromptCard
|
||||
key={p.id}
|
||||
prompt={p}
|
||||
onUpdate={updatePrompt}
|
||||
onDelete={deletePrompt}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PromptCard({
|
||||
prompt,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: {
|
||||
prompt: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
content: string;
|
||||
};
|
||||
onUpdate: (p: {
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
content: string;
|
||||
}) => Promise<void>;
|
||||
onDelete: (id: number) => Promise<void>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-testid="prompt-card"
|
||||
className="border rounded-lg p-4 bg-(--background-lightest) min-w-80"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{prompt.title}</h3>
|
||||
{prompt.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{prompt.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<CreateOrEditPromptDialog
|
||||
mode="edit"
|
||||
prompt={prompt}
|
||||
onUpdatePrompt={onUpdate}
|
||||
/>
|
||||
<DeleteConfirmationDialog
|
||||
itemName={prompt.title}
|
||||
itemType="Prompt"
|
||||
onDelete={() => onDelete(prompt.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<pre className="text-sm whitespace-pre-wrap bg-transparent border rounded p-2 max-h-48 overflow-auto">
|
||||
{prompt.content}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
337
backups/backup-20251218-094212/src/pages/settings.tsx
Normal file
337
backups/backup-20251218-094212/src/pages/settings.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
import { useEffect, 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";
|
||||
import { AutoApproveSwitch } from "@/components/AutoApproveSwitch";
|
||||
import { TelemetrySwitch } from "@/components/TelemetrySwitch";
|
||||
import { MaxChatTurnsSelector } from "@/components/MaxChatTurnsSelector";
|
||||
import { ThinkingBudgetSelector } from "@/components/ThinkingBudgetSelector";
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
import { useAppVersion } from "@/hooks/useAppVersion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { GitHubIntegration } from "@/components/GitHubIntegration";
|
||||
import { VercelIntegration } from "@/components/VercelIntegration";
|
||||
import { SupabaseIntegration } from "@/components/SupabaseIntegration";
|
||||
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { AutoFixProblemsSwitch } from "@/components/AutoFixProblemsSwitch";
|
||||
import { AutoUpdateSwitch } from "@/components/AutoUpdateSwitch";
|
||||
import { ReleaseChannelSelector } from "@/components/ReleaseChannelSelector";
|
||||
import { NeonIntegration } from "@/components/NeonIntegration";
|
||||
import { RuntimeModeSelector } from "@/components/RuntimeModeSelector";
|
||||
import { NodePathSelector } from "@/components/NodePathSelector";
|
||||
import { ToolsMcpSettings } from "@/components/settings/ToolsMcpSettings";
|
||||
import { ZoomSelector } from "@/components/ZoomSelector";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { activeSettingsSectionAtom } from "@/atoms/viewAtoms";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
const appVersion = useAppVersion();
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const router = useRouter();
|
||||
const setActiveSettingsSection = useSetAtom(activeSettingsSectionAtom);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveSettingsSection("general-settings");
|
||||
}, [setActiveSettingsSection]);
|
||||
|
||||
const handleResetEverything = async () => {
|
||||
setIsResetting(true);
|
||||
try {
|
||||
const ipcClient = IpcClient.getInstance();
|
||||
await ipcClient.resetAll();
|
||||
showSuccess("Successfully reset everything. Restart the application.");
|
||||
} 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 px-8 py-4">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Button
|
||||
onClick={() => router.history.back()}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex items-center gap-2 mb-4 bg-(--background-lightest) py-5"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Go Back
|
||||
</Button>
|
||||
<div className="flex justify-between mb-4">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Settings
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<GeneralSettings appVersion={appVersion} />
|
||||
<WorkflowSettings />
|
||||
<AISettings />
|
||||
|
||||
<div
|
||||
id="provider-settings"
|
||||
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm"
|
||||
>
|
||||
<ProviderSettingsGrid />
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div
|
||||
id="telemetry"
|
||||
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">
|
||||
Telemetry
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
<TelemetrySwitch />
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
This records anonymous usage data to improve the product.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="mr-2 font-medium">Telemetry ID:</span>
|
||||
<span className="bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded text-gray-800 dark:text-gray-200 font-mono">
|
||||
{settings ? settings.telemetryUserId : "n/a"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Integrations Section */}
|
||||
<div
|
||||
id="integrations"
|
||||
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">
|
||||
Integrations
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<GitHubIntegration />
|
||||
<VercelIntegration />
|
||||
<SupabaseIntegration />
|
||||
<NeonIntegration />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tools (MCP) */}
|
||||
<div
|
||||
id="tools-mcp"
|
||||
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">
|
||||
Tools (MCP)
|
||||
</h2>
|
||||
<ToolsMcpSettings />
|
||||
</div>
|
||||
|
||||
{/* Experiments Section */}
|
||||
<div
|
||||
id="experiments"
|
||||
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">
|
||||
Experiments
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1 mt-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="enable-native-git"
|
||||
checked={!!settings?.enableNativeGit}
|
||||
onCheckedChange={(checked) => {
|
||||
updateSettings({
|
||||
enableNativeGit: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="enable-native-git">Enable Native Git</Label>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
This doesn't require any external Git installation and offers
|
||||
a faster, native-Git performance experience.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<div
|
||||
id="danger-zone"
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export function GeneralSettings({ appVersion }: { appVersion: string | null }) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
id="general-settings"
|
||||
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">
|
||||
General Settings
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4 mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<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 className="mt-4">
|
||||
<ZoomSelector />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 mt-4">
|
||||
<AutoUpdateSwitch />
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
This will automatically update the app when new versions are
|
||||
available.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<ReleaseChannelSelector />
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<RuntimeModeSelector />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<NodePathSelector />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-sm text-gray-500 dark:text-gray-400 mt-4">
|
||||
<span className="mr-2 font-medium">App Version:</span>
|
||||
<span className="bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded text-gray-800 dark:text-gray-200 font-mono">
|
||||
{appVersion ? appVersion : "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkflowSettings() {
|
||||
return (
|
||||
<div
|
||||
id="workflow-settings"
|
||||
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">
|
||||
Workflow Settings
|
||||
</h2>
|
||||
|
||||
<div className="space-y-1">
|
||||
<AutoApproveSwitch showToast={false} />
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
This will automatically approve code changes and run them.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 mt-4">
|
||||
<AutoFixProblemsSwitch />
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
This will automatically fix TypeScript errors.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function AISettings() {
|
||||
return (
|
||||
<div
|
||||
id="ai-settings"
|
||||
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">
|
||||
AI Settings
|
||||
</h2>
|
||||
|
||||
<div className="mt-4">
|
||||
<ThinkingBudgetSelector />
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<MaxChatTurnsSelector />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user