Landing-page Frontend Input save state in local storage & auto-create after registration

This commit is contained in:
Adam Cohen Hillel 2025-04-19 22:36:52 +01:00
parent 3c63663a47
commit 8ced0959a8
3 changed files with 293 additions and 8 deletions

View File

@ -1,17 +1,48 @@
"use client";
import React, { useState, Suspense } from 'react';
import React, { useState, Suspense, useEffect } from 'react';
import { Skeleton } from "@/components/ui/skeleton";
import { useRouter } from 'next/navigation';
import { ChatInput } from '@/components/thread/chat-input';
import { createProject, addUserMessage, startAgent, createThread } from "@/lib/api";
import { generateThreadName } from "@/lib/actions/threads";
// Constant for localStorage key to ensure consistency
const PENDING_PROMPT_KEY = 'pendingAgentPrompt';
function DashboardContent() {
const [inputValue, setInputValue] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [autoSubmit, setAutoSubmit] = useState(false);
const router = useRouter();
// Check for pending prompt in localStorage on mount
useEffect(() => {
// Use a small delay to ensure we're fully mounted
const timer = setTimeout(() => {
const pendingPrompt = localStorage.getItem(PENDING_PROMPT_KEY);
if (pendingPrompt) {
setInputValue(pendingPrompt);
setAutoSubmit(true); // Flag to auto-submit after mounting
}
}, 200);
return () => clearTimeout(timer);
}, []);
// Auto-submit the form if we have a pending prompt
useEffect(() => {
if (autoSubmit && inputValue && !isSubmitting) {
const timer = setTimeout(() => {
handleSubmit(inputValue);
setAutoSubmit(false);
}, 500);
return () => clearTimeout(timer);
}
}, [autoSubmit, inputValue, isSubmitting]);
const handleSubmit = async (message: string, options?: { model_name?: string; enable_thinking?: boolean }) => {
if (!message.trim() || isSubmitting) return;
@ -40,6 +71,9 @@ function DashboardContent() {
stream: true
});
// If successful, clear the pending prompt
localStorage.removeItem(PENDING_PROMPT_KEY);
// 5. Navigate to the new agent's thread page
router.push(`/dashboard/agents/${thread.thread_id}`);
} catch (error) {

View File

@ -74,7 +74,11 @@ export default function GoogleSignIn({ returnUrl }: GoogleSignInProps) {
});
if (error) throw error;
window.location.href = returnUrl || "/dashboard";
// Add a small delay before redirecting to ensure localStorage is properly saved
setTimeout(() => {
window.location.href = returnUrl || "/dashboard";
}, 100);
} catch (error) {
console.error('Error signing in with Google:', error);
setIsLoading(false);

View File

@ -1,21 +1,51 @@
"use client"
import { HeroVideoSection } from "@/components/home/sections/hero-video-section";
import { siteConfig } from "@/lib/home";
import { ArrowRight, Github } from "lucide-react";
import { ArrowRight, Github, X, AlertCircle } from "lucide-react";
import { FlickeringGrid } from "@/components/home/ui/flickering-grid";
import { useMediaQuery } from "@/hooks/use-media-query";
import { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef, FormEvent } from "react";
import { useScroll } from "motion/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useAuth } from "@/components/AuthProvider";
import { createProject, createThread, addUserMessage, startAgent } from "@/lib/api";
import { generateThreadName } from "@/lib/actions/threads";
import GoogleSignIn from "@/components/GoogleSignIn";
import { Input } from "@/components/ui/input";
import { SubmitButton } from "@/components/ui/submit-button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogOverlay
} from "@/components/ui/dialog";
// Custom dialog overlay with blur effect
const BlurredDialogOverlay = () => (
<DialogOverlay className="bg-background/40 backdrop-blur-md" />
);
// Constant for localStorage key to ensure consistency
const PENDING_PROMPT_KEY = 'pendingAgentPrompt';
export function HeroSection() {
const { hero } = siteConfig;
const tablet = useMediaQuery("(max-width: 1024px)");
const [mounted, setMounted] = useState(false);
const [isScrolling, setIsScrolling] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
const { scrollY } = useScroll();
const [inputValue, setInputValue] = useState("");
const router = useRouter();
const { user, isLoading } = useAuth();
// Auth dialog state
const [authDialogOpen, setAuthDialogOpen] = useState(false);
const [authError, setAuthError] = useState<string | null>(null);
useEffect(() => {
setMounted(true);
@ -45,6 +75,108 @@ export function HeroSection() {
};
}, [scrollY]);
// Store the input value when auth dialog opens
useEffect(() => {
if (authDialogOpen && inputValue.trim()) {
localStorage.setItem(PENDING_PROMPT_KEY, inputValue.trim());
}
}, [authDialogOpen, inputValue]);
// Close dialog and redirect when user authenticates
useEffect(() => {
if (authDialogOpen && user && !isLoading) {
setAuthDialogOpen(false);
router.push('/dashboard');
}
}, [user, isLoading, authDialogOpen, router]);
// Create an agent with the provided prompt
const createAgentWithPrompt = async () => {
if (!inputValue.trim() || isSubmitting) return;
setIsSubmitting(true);
try {
// Generate a name for the project using GPT
const projectName = await generateThreadName(inputValue);
// 1. Create a new project with the GPT-generated name
const newAgent = await createProject({
name: projectName,
description: "",
});
// 2. Create a new thread for this project
const thread = await createThread(newAgent.id);
// 3. Add the user message to the thread
await addUserMessage(thread.thread_id, inputValue.trim());
// 4. Start the agent with the thread ID
await startAgent(thread.thread_id, {
stream: true
});
// 5. Navigate to the new agent's thread page
router.push(`/dashboard/agents/${thread.thread_id}`);
} catch (error) {
console.error("Error creating agent:", error);
setIsSubmitting(false);
}
};
// Handle form submission
const handleSubmit = async (e?: FormEvent) => {
if (e) {
e.preventDefault();
e.stopPropagation(); // Stop event propagation to prevent dialog closing
}
if (!inputValue.trim() || isSubmitting) return;
// If user is not logged in, save prompt and show auth dialog
if (!user && !isLoading) {
// Save prompt to localStorage BEFORE showing the dialog
localStorage.setItem(PENDING_PROMPT_KEY, inputValue.trim());
setAuthDialogOpen(true);
return;
}
// User is logged in, create the agent
createAgentWithPrompt();
};
// Handle Enter key press
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault(); // Prevent default form submission
e.stopPropagation(); // Stop event propagation
handleSubmit();
}
};
// Handle auth form submission
const handleSignIn = async (prevState: any, formData: FormData) => {
setAuthError(null);
try {
// Implement sign in logic here
const email = formData.get("email") as string;
const password = formData.get("password") as string;
// Add the returnUrl to the form data for proper redirection
formData.append("returnUrl", "/dashboard");
// Call your authentication function here
// Return any error state
return { message: "Invalid credentials" };
} catch (error) {
console.error("Sign in error:", error);
setAuthError(error instanceof Error ? error.message : "An error occurred");
return { message: "An error occurred during sign in" };
}
};
return (
<section id="hero" className="w-full relative overflow-hidden">
<div className="relative flex flex-col items-center w-full px-6">
@ -122,7 +254,7 @@ export function HeroSection() {
</p>
</div>
<div className="flex items-center w-full max-w-xl gap-2 flex-wrap justify-center">
<div className="w-full relative">
<form className="w-full relative" onSubmit={handleSubmit}>
{/* ChatGPT-like input with glow effect */}
<div className="relative z-10">
<div className="flex items-center rounded-full border border-border bg-background/80 backdrop-blur px-4 shadow-lg transition-all duration-200 hover:border-secondary/50 focus-within:border-secondary/50 focus-within:shadow-[0_0_15px_rgba(var(--secondary),0.3)]">
@ -130,30 +262,145 @@ export function HeroSection() {
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={hero.inputPlaceholder}
className="flex-1 h-12 md:h-14 rounded-full px-2 bg-transparent focus:outline-none text-sm md:text-base py-2"
disabled={isSubmitting}
/>
<button
type="submit"
className={`rounded-full p-2 md:p-3 transition-all duration-200 ${
inputValue.trim()
? "bg-secondary text-secondary-foreground hover:bg-secondary/80"
: "bg-muted text-muted-foreground"
}`}
disabled={!inputValue.trim()}
disabled={!inputValue.trim() || isSubmitting}
aria-label="Submit"
>
<ArrowRight className="size-4 md:size-5" />
{isSubmitting ? (
<div className="h-4 md:h-5 w-4 md:w-5 border-2 border-secondary-foreground border-t-transparent rounded-full animate-spin" />
) : (
<ArrowRight className="size-4 md:size-5" />
)}
</button>
</div>
</div>
{/* Subtle glow effect */}
<div className="absolute -bottom-4 inset-x-0 h-6 bg-secondary/20 blur-xl rounded-full -z-10 opacity-70"></div>
</div>
</form>
</div>
</div>
</div>
<div className="mb-10 max-w-4xl mx-auto">
<HeroVideoSection />
</div>
{/* Auth Dialog */}
<Dialog open={authDialogOpen} onOpenChange={setAuthDialogOpen}>
<BlurredDialogOverlay />
<DialogContent className="sm:max-w-md rounded-xl bg-[#F3F4F6] dark:bg-[#F9FAFB]/[0.02] border border-border">
<DialogHeader>
<div className="flex items-center justify-between">
<DialogTitle className="text-xl font-medium">Sign in to continue</DialogTitle>
<button
onClick={() => setAuthDialogOpen(false)}
className="rounded-full p-1 hover:bg-muted transition-colors"
>
<X className="h-4 w-4 text-muted-foreground" />
</button>
</div>
<DialogDescription className="text-muted-foreground">
Sign in or create an account to talk with Suna
</DialogDescription>
</DialogHeader>
{/* Auth error message */}
{authError && (
<div className="mb-4 p-3 rounded-lg flex items-center gap-3 bg-secondary/10 border border-secondary/20 text-secondary">
<AlertCircle className="h-5 w-5 flex-shrink-0 text-secondary" />
<span className="text-sm font-medium">{authError}</span>
</div>
)}
{/* Google Sign In */}
<div className="w-full">
<GoogleSignIn returnUrl="/dashboard" />
</div>
{/* Divider */}
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-[#F3F4F6] dark:bg-[#F9FAFB]/[0.02] text-muted-foreground">
or continue with email
</span>
</div>
</div>
{/* Sign in form */}
<form className="space-y-4">
<div>
<Input
id="email"
name="email"
type="email"
placeholder="Email address"
className="h-12 rounded-full bg-background border-border"
required
/>
</div>
<div>
<Input
id="password"
name="password"
type="password"
placeholder="Password"
className="h-12 rounded-full bg-background border-border"
required
/>
</div>
<div className="space-y-4 pt-4">
<SubmitButton
formAction={handleSignIn}
className="w-full h-12 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 transition-all shadow-md"
pendingText="Signing in..."
>
Sign in
</SubmitButton>
<Link
href={`/auth?mode=signup&returnUrl=${encodeURIComponent("/dashboard")}`}
className="flex h-12 items-center justify-center w-full text-center rounded-full border border-border bg-background hover:bg-accent/20 transition-all"
onClick={() => setAuthDialogOpen(false)}
>
Create new account
</Link>
</div>
<div className="text-center pt-2">
<Link
href={`/auth?returnUrl=${encodeURIComponent("/dashboard")}`}
className="text-sm text-primary hover:underline"
onClick={() => setAuthDialogOpen(false)}
>
More sign in options
</Link>
</div>
</form>
<div className="mt-4 text-center text-xs text-muted-foreground">
By continuing, you agree to our{' '}
<Link href="/terms" className="text-primary hover:underline">
Terms of Service
</Link>{' '}
and{' '}<Link href="/privacy" className="text-primary hover:underline">Privacy Policy</Link>
</div>
</DialogContent>
</Dialog>
</section>
);
}