180 lines
6.6 KiB
TypeScript
180 lines
6.6 KiB
TypeScript
import { useNavigate, useSearch } from "@tanstack/react-router";
|
|
import { useAtom, useSetAtom } from "jotai";
|
|
import { chatInputValueAtom } 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 { 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 setSelectedAppId = useSetAtom(selectedAppIdAtom);
|
|
const { refreshApps } = useLoadApps();
|
|
const { settings, 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(),
|
|
});
|
|
|
|
// Stream the message
|
|
streamMessage({ prompt: inputValue, chatId: result.chatId });
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
|
|
setInputValue("");
|
|
setSelectedAppId(result.app.id);
|
|
setIsPreviewOpen(false);
|
|
await refreshApps(); // Ensure refreshApps is awaited if it's async
|
|
navigate({ to: "/chat", search: { id: result.chatId } });
|
|
} catch (error) {
|
|
console.error("Failed to create chat:", error);
|
|
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 m-auto p-8">
|
|
<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>
|
|
);
|
|
}
|