From a17296e48789997aa2ba584fe82271672f8bc870 Mon Sep 17 00:00:00 2001 From: ankitmukhia Date: Thu, 28 Aug 2025 06:58:47 +0530 Subject: [PATCH 1/4] shadcn comp added --- frontend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/package.json b/frontend/package.json index c07aa556..f7085d61 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", From 85647c48f3395ae152a5a24f6ce13b9c9bdfb863 Mon Sep 17 00:00:00 2001 From: ankitmukhia Date: Thu, 4 Sep 2025 04:19:28 +0530 Subject: [PATCH 2/4] Resolved conflict --- frontend/app/(app)/layout.tsx | 25 +- frontend/app/_components/Email.tsx | 133 ++++---- frontend/app/_components/Otp.tsx | 339 ++++++++++++--------- frontend/app/layout.tsx | 25 +- frontend/components/nav-setting.tsx | 86 ++++++ frontend/components/ui/input-otp.tsx | 2 +- frontend/components/ui/model-selector.tsx | 110 ++++--- frontend/components/ui/popover.tsx | 48 +++ frontend/components/ui/select.tsx | 13 +- frontend/components/ui/tabs-suggestion.tsx | 111 +++---- frontend/components/ui/theme-toggler.tsx | 55 ++-- frontend/components/ui/ui-input.tsx | 332 ++++++++++---------- frontend/components/ui/ui-structure.tsx | 128 +++++--- frontend/components/ui/upgrade-cta.tsx | 74 +++-- frontend/models/utils.tsx | 8 +- frontend/styles/globals.css | 10 +- 16 files changed, 896 insertions(+), 603 deletions(-) create mode 100644 frontend/components/nav-setting.tsx create mode 100644 frontend/components/ui/popover.tsx diff --git a/frontend/app/(app)/layout.tsx b/frontend/app/(app)/layout.tsx index 27cb8dc3..8ee2beed 100644 --- a/frontend/app/(app)/layout.tsx +++ b/frontend/app/(app)/layout.tsx @@ -1,15 +1,25 @@ -import { SidebarToggle } from "@/app/_components/sidebar-toggle"; -import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +"use client"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; +import { Separator } from "@/components/ui/separator"; import { SelectTheme } from "@/components/ui/theme-toggler"; import { UIStructure } from "@/components/ui/ui-structure"; import { ThemeProvider } from "@/components/theme-provider"; import { UpgradeCTA } from "@/components/ui/upgrade-cta"; +import { useUser } from "@/hooks/useUser"; + +import Link from "next/link"; export default function ChatLayout({ children, }: { children: React.ReactNode; }) { + const { user, isLoading: isUserLoading } = useUser(); + return ( <> @@ -23,9 +33,18 @@ export default function ChatLayout({
- + + {/* */}
+ {!isUserLoading && !user && ( + + Login + + )}
diff --git a/frontend/app/_components/Email.tsx b/frontend/app/_components/Email.tsx index fa9a5d8b..4f3a5a44 100644 --- a/frontend/app/_components/Email.tsx +++ b/frontend/app/_components/Email.tsx @@ -1,12 +1,15 @@ "use client"; import { toast } from "sonner"; -import { ArrowLeft } from "lucide-react"; import { Button } from "@/components/ui/button"; -import Link from "next/link"; import { Input } from "@/components/ui/input"; import { BACKEND_URL } from "@/lib/utils"; -import { useRouter } from "next/navigation"; import { useState } from "react"; +import Link from "next/link"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; const isEmailValid = (email: string) => { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); @@ -21,80 +24,82 @@ export function Email({ setStep: (step: string) => void; email: string; }) { - const router = useRouter(); const [sendingRequest, setSendingRequest] = useState(false); - const handleSendOTP = async () => { + const handleAuth = (e: React.FormEvent) => { + e.preventDefault(); setSendingRequest(true); - - try { - const response = await fetch(`${BACKEND_URL}/auth/initiate_signin`, { - method: "POST", - body: JSON.stringify({ email }), - headers: { - "Content-Type": "application/json", - }, - }); - - if (response.status === 200) { - setStep("otp"); - toast.success("OTP sent to email"); - } else { + fetch(`${BACKEND_URL}/auth/initiate_signin`, { + method: "POST", + body: JSON.stringify({ email }), + headers: { + "Content-Type": "application/json", + }, + }) + .then((res) => { + if (res.status === 200) { + setStep("otp"); + toast.success("OTP sent to email"); + } else { + toast.error("Failed to send OTP, please retry after a few minutes"); + } + }) + .catch((err) => { + console.error(err); toast.error("Failed to send OTP, please retry after a few minutes"); - } - } catch (err) { - console.error(err); - toast.error("Failed to send OTP, please retry after a few minutes"); - } finally { - setSendingRequest(false); - } + }) + .finally(() => { + setSendingRequest(false); + }); }; + return ( -
-
- {/* */} -
-
-

- Welcome to 1ai +
+
+

+ Welcome to 1ai

-
- setEmail(e.target.value)} - placeholder="Email" - onKeyDown={(e) => { - if (e.key === "Enter" && isEmailValid(email) && !sendingRequest) { - handleSendOTP(); - } - }} - /> +
+
+ setEmail(e.target.value)} + placeholder="Email" + /> +
-
-
- By continuing, you agree to our{" "} - - Terms - {" "} - and{" "} - - Privacy Policy - + +
+ + +
+ + Terms + {" "} + and + + Privacy Policy + +
+
+ +

By continuing, you agree

+
+
diff --git a/frontend/app/_components/Otp.tsx b/frontend/app/_components/Otp.tsx index 85d19702..ab929b14 100644 --- a/frontend/app/_components/Otp.tsx +++ b/frontend/app/_components/Otp.tsx @@ -1,161 +1,198 @@ "use client"; +import { useRef, useEffect } from "react"; import { Button } from "@/components/ui/button"; import Link from "next/link"; -import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ui/input-otp"; +import { Input } from "@/components/ui/input"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot, +} from "@/components/ui/input-otp"; import { useState } from "react"; import { BACKEND_URL } from "@/lib/utils"; import { toast } from "sonner"; -import { Mail } from "lucide-react"; export function Otp({ email }: { email: string }) { - const [otp, setOtp] = useState(""); - const [isResending, setIsResending] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleResend = async () => { - setIsResending(true); - try { - const response = await fetch(`${BACKEND_URL}/auth/initiate_signin`, { - method: "POST", - body: JSON.stringify({ email }), - headers: { - "Content-Type": "application/json", - }, - }); - - const data = await response.json(); - - if (response.ok) { - toast("New verification code sent to your email"); - } else { - toast(data.message || "Failed to resend code"); - } - } catch (error) { - toast("Failed to resend code. Please try again."); - } finally { - setIsResending(false); - } - }; - - const handleLogin = async (otpValue?: string) => { - const currentOtp = otpValue || otp; - console.log("handleLogin called with:", { currentOtp, isSubmitting, length: currentOtp.length }); - if (currentOtp.length !== 6 || isSubmitting) return; - - setIsSubmitting(true); - try { - const response = await fetch(`${BACKEND_URL}/auth/signin`, { - method: "POST", - body: JSON.stringify({ email, otp: currentOtp }), - headers: { - "Content-Type": "application/json", - }, - }); - - const data = await response.json(); - - if (response.status === 401) { - toast(data.message); - } - - if (response.status === 429) { - toast(data.message); - } - - if (response.status === 200) { - localStorage.setItem("token", data.token); - window.location.href = "/"; - } else if (response.status !== 401 && response.status !== 429) { - toast(data.message || "An unexpected error occurred"); - } - } catch (error) { - console.error("Some error occured ", error); - toast("An error occurred. Please try again."); - } finally { - setIsSubmitting(false); - } - }; - - return ( -
-
-
- {/* Email Icon */} -
- -
- - {/* Title and Description */} -
-

- Check your email -

-

- Enter the verification code sent to{" "} - {email} -

-
- - {/* OTP Input */} -
- { - setOtp(value); - // Auto-submit when OTP is complete - if (value.length === 6) { - console.log("Auto-submitting OTP:", value); - setTimeout(() => handleLogin(value), 50); - } - }} - onKeyDown={(e) => { - if (e.key === "Enter" && otp.length === 6) { - handleLogin(otp); - } - }} - className="flex justify-center" - > - - - - - - - - - - - - -
- - {/* Resend Link */} -
-

- Didn't get a code?{" "} - -

-
- - {/* Verify Button - Always visible, disabled when incomplete */} - -
-
-
- ); + const initialRef = useRef(null); + const [otp, setOtp] = useState(""); + const [isResending, setIsResending] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleResend = async () => { + setIsResending(true); + try { + const response = await fetch(`${BACKEND_URL}/auth/initiate_signin`, { + method: "POST", + body: JSON.stringify({ email }), + headers: { + "Content-Type": "application/json", + }, + }); + + const data = await response.json(); + + if (response.ok) { + toast("New verification code sent to your email"); + } else { + toast(data.message || "Failed to resend code"); + } + } catch (error) { + toast("Failed to resend code. Please try again."); + } finally { + setIsResending(false); + } + }; + + useEffect(() => { + if (initialRef.current) { + initialRef.current.focus(); + } + }, []); + + const handleLogin = async () => { + const currentOtp = otp; + console.log("handleLogin called with:", { currentOtp, isSubmitting, length: currentOtp.length }); + if (currentOtp.length !== 6 || isSubmitting) return; + + setIsSubmitting(true); + + try { + const response = await fetch(`${BACKEND_URL}/auth/signin`, { + method: "POST", + body: JSON.stringify({ email, otp: currentOtp }), + headers: { + "Content-Type": "application/json", + }, + }); + + const data = await response.json(); + + if (response.status === 401) { + toast(data.message); + } + + if (response.status === 429) { + toast(data.message); + } + + if (response.status === 200) { + localStorage.setItem("token", data.token); + window.location.href = "/"; + } else if (response.status !== 401 && response.status !== 429) { + toast(data.message || "An unexpected error occurred"); + } + } catch (error) { + console.error("Some error occured ", error); + toast("An error occurred. Please try again."); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+

+ Welcome to 1ai +

+
+
+ + { + setOtp(value); + // Auto-submit when OTP is complete + if (value.length === 6) { + console.log("Auto-submitting OTP:", value); + setTimeout(() => handleLogin(value), 50); + } + }} + > + + + + + + + + + + +
+ + {/* Resend Link */} +
+

+ Didn't get a code?{" "} + +

+
+ +
+ + +
+ + Terms + {" "} + and + + Privacy Policy + +
+
+ +

By continuing, you agree

+
+
+
+
+
+ ); } diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index caab366b..0c846fcd 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -2,24 +2,31 @@ import "@/styles/globals.css"; import type { Metadata } from "next"; import { Toaster } from "sonner"; import { siteConfig } from "@/config/site"; -import { Plus_Jakarta_Sans } from "next/font/google"; import { Providers } from "./providers"; +import { Inter, Syne } from "next/font/google"; export const metadata: Metadata = siteConfig; -const font = Plus_Jakarta_Sans({ +const inter = Inter({ subsets: ["latin"], - variable: "--font-jakarta", + variable: "--font-inter", + weight: ["200", "400", "500"], +}); + +const syne = Syne({ + subsets: ["latin"], + variable: "--font-syne", + weight: ["700"], }); export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( - - - {children} - - - + + + {children} + + + ); } diff --git a/frontend/components/nav-setting.tsx b/frontend/components/nav-setting.tsx new file mode 100644 index 00000000..c9d364a8 --- /dev/null +++ b/frontend/components/nav-setting.tsx @@ -0,0 +1,86 @@ +"use client" + +import { + Settings, + LogOut, +} from "lucide-react" +import { ShieldSlashIcon, ScrollIcon, ArrowClockwiseIcon } from "@phosphor-icons/react" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" +import Link from "next/link" + +export function NavSetting() { + return ( + + + + + +
+ + Settings +
+
+
+ + +
+
+ Settings +
+
+
+ + + window.open("/terms", "_target")}> + + Terms + + window.open("/privacy", "_target")}> + + Privacy + + window.open("/refund", "_target")}> + + + Refund + + + + + { + e.preventDefault(); + localStorage.removeItem("token"); + window.location.reload(); + }}> + + Log out + +
+
+
+
+ ) +} + diff --git a/frontend/components/ui/input-otp.tsx b/frontend/components/ui/input-otp.tsx index 6f7f55da..87e2d032 100644 --- a/frontend/components/ui/input-otp.tsx +++ b/frontend/components/ui/input-otp.tsx @@ -41,7 +41,7 @@ const InputOTPSlot = React.forwardRef< ref={ref} className={cn( "relative flex h-9 w-9 items-center justify-center border border-input text-sm shadow-sm transition-all rounded-xl bg-background", - isActive && "z-10 ring-1 ring-ring", + isActive && "z-10 ring-1 ring-orange-400", className )} {...props} diff --git a/frontend/components/ui/model-selector.tsx b/frontend/components/ui/model-selector.tsx index 3449a9b3..3aba179e 100644 --- a/frontend/components/ui/model-selector.tsx +++ b/frontend/components/ui/model-selector.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from "react"; import { DEFAULT_MODEL_ID, MODELS, getModelById } from "@/models/constants"; import type { Model } from "@/models/types"; import { getModelProviderIcon } from "@/models/utils"; +import { cn } from "@/lib/utils"; import { Select, SelectContent, @@ -43,7 +44,7 @@ export function ModelSelector({ showIcons = true, }: ModelSelectorProps) { const [selectedModel, setSelectedModel] = useState( - value ?? DEFAULT_MODEL_ID + value ?? DEFAULT_MODEL_ID, ); const { userCredits } = useCredits(); @@ -65,59 +66,61 @@ export function ModelSelector({ const getModelStatusIcon = (model: Model) => { if (model.isAvailable === false) { return ( - - - + + +
- - -

- This model is not available in 1ai API and will fall back to - GPT-3.5 -

-
- - +
+
+ +

+ This model is not available in 1ai API and will fall back to + GPT-3.5 +

+
+
); } else if (model.name.includes("Experimental")) { return ( - - - + + +
- - -

This model is experimental and may not be reliable on 1ai

-
- - +
+
+ +

This model is experimental and may not be reliable on 1ai

+
+
); } else if (model.name.includes("Beta")) { return ( - - - - - - -

This model is in beta and may not be fully reliable

-
-
-
- ); - } - - return ( - - +
+ +
-

This model is fully supported by 1ai API

+

This model is in beta and may not be fully reliable

-
+ ); + } + + return ( + + +
+ +
+
+ +

+ This model is not available in 1ai API and will fall back to GPT-3.5 +

+
+
); }; @@ -127,7 +130,7 @@ export function ModelSelector({ onValueChange={handleValueChange} disabled={disabled} > - + {selectedModelObj && (
@@ -137,7 +140,8 @@ export function ModelSelector({ )} - + + {/* Free Models Section */} @@ -148,7 +152,12 @@ export function ModelSelector({ key={model.id} value={model.id} disabled={model.isAvailable === false} - className={model.isAvailable === false ? "opacity-60" : ""} + className={cn( + `rounded-lg focus:bg-orange-300/20 focus:text-orange-400 dark:focus:text-orange-100 dark:focus:bg-orange-100/10 data-[state=checked]:bg-orange-300/20 dark:data-[state=checked]:bg-orange-100/10 [&_svg:not([class*='text-'])]:text-orange-400 dark:[&_svg:not([class*='text-'])]:text-orange-100`, + { + "opacity-60": model.isAvailable === false, + }, + )} >
{showIcons && getModelProviderIcon(model)} @@ -163,15 +172,17 @@ export function ModelSelector({
- - Premium Models + + + Premium Models +
{!userCredits?.isPremium && ( - ))} -
+
+ {/* Tab Navigation */} +
+ {tabs.map((tab) => ( + { + setActivePopover(open ? tab.id : null) + }} + > + + + - {/* Tab Content */} -
- {activeTabData?.content.map((item, index) => ( -
{ - if (suggestedInput || suggestedInput === "") { - setSuggestedInput(item); - } - }} - className="flex items-start gap-2 border-t border-secondary/40 py-1 first:border-none cursor-pointer" - > - -
- ))} -
-
-
+ +
+

{tab.label}

+ {tab.content.map((item, index) => ( +
{ + if (setSuggestedInput) { + setSuggestedInput(item) + } + }} + className="flex items-start gap-2 border-t border-secondary/40 py-1 first:border-none" + > + +
+ ))} +
+
+ + ))} +
+
); }; diff --git a/frontend/components/ui/theme-toggler.tsx b/frontend/components/ui/theme-toggler.tsx index 7e202050..865228e4 100644 --- a/frontend/components/ui/theme-toggler.tsx +++ b/frontend/components/ui/theme-toggler.tsx @@ -15,32 +15,41 @@ export function SelectTheme() { "flex cursor-pointer items-center gap-2 rounded-lg text-center", open && "flex-1 justify-end", )} + onClick={() => setTheme(theme === "light" ? "dark" : "light")} > -
setTheme(theme === "light" ? "dark" : "light")} - className="hover:bg-accent flex size-7 items-center justify-center rounded-lg" - > - - - - - - - - +
+ +
); } + +export const SunIcon: React.FC> = (props) => ( + + + +); + +export const MoonIcon: React.FC> = (props) => ( + + + +); diff --git a/frontend/components/ui/ui-input.tsx b/frontend/components/ui/ui-input.tsx index 437dc91f..ad000fa0 100644 --- a/frontend/components/ui/ui-input.tsx +++ b/frontend/components/ui/ui-input.tsx @@ -6,8 +6,6 @@ import { Button } from "@/components/ui/button"; import { SpinnerGapIcon, CopyIcon, - ThumbsDownIcon, - ThumbsUpIcon, CheckIcon, CheckCircleIcon, ArrowsLeftRightIcon, @@ -86,7 +84,7 @@ const MessageComponent = memo(({ return isInline ? ( ) : ( -
-
-
{match ? match[1] : "text"}
-
- - -
-
- - {codeContent} - -
+
+
+

+ {match ? match[1] : "text"} +

+
+ + +
+
+ + {codeContent} + +
); }, - strong: (props: any) => ( - {props.children} - ), - a: (props: any) => ( - - {props.children} - - ), - h1: (props: any) => ( -

- {props.children} -

- ), - h2: (props: any) => ( -

- {props.children} -

- ), - h3: (props: any) => ( -

- {props.children} -

- ), + strong: (props: any) => ( + {props.children} + ), + a: (props: any) => ( + + {props.children} + + ), + h1: (props: any) => ( +

+ {props.children} +

+ ), + h2: (props: any) => ( +

+ {props.children} +

+ ), + h3: (props: any) => ( +

+ {props.children} +

+ ), }), [copied, isWrapped, toggleWrap, onCopy, resolvedTheme, geistMono]); return ( @@ -212,14 +214,14 @@ const MessageComponent = memo(({ key={message.id} className={`group mb-8 flex w-full flex-col ${message.role === "assistant" ? "items-start" : "items-end"} gap-2`} > -
+
)} - {message.role === "user" && ( - - )} + {message.role === "user" && ( + + )}

); @@ -312,12 +314,12 @@ const UIInput = ({ const abortControllerRef = useRef(null); const [isWrapped, setIsWrapped] = useState(false); const [conversationId, setConversationId] = useState( - initialConversationId || v4() + initialConversationId || v4(), ); const { resolvedTheme } = useTheme(); const { user, isLoading: isUserLoading } = useUser(); const { conversation, loading: converstionLoading } = useConversationById( - initialConversationId + initialConversationId, ); const { userCredits, @@ -403,8 +405,8 @@ const UIInput = ({ updateTimeout = setTimeout(() => { setMessages((prev) => prev.map((msg) => - msg.id === tempMessageId ? { ...msg, content } : msg - ) + msg.id === tempMessageId ? { ...msg, content } : msg, + ), ); }, 50); }; @@ -417,8 +419,8 @@ const UIInput = ({ prev.map((msg) => msg.id === tempMessageId ? { ...msg, content: accumulatedContent } - : msg - ) + : msg, + ), ); if (updateTimeout) { @@ -473,8 +475,8 @@ const UIInput = ({ prev.map((msg) => msg.id === tempMessageId ? { ...msg, content: "Error: Failed to process response" } - : msg - ) + : msg, + ), ); } finally { setIsLoading(false); @@ -590,19 +592,10 @@ const UIInput = ({ return (
-
- {!query && showWelcome && messages.length === 0 ? ( -
-
- -
-
- ) : ( -
-
+
+ {showWelcome && messages.length === 0 ? null : ( +
+
+ {isLoading && (
@@ -624,24 +618,29 @@ const UIInput = ({
)} + {showWelcome && messages.length === 0 && ( +

+ What's on your mind today? +

+ )} + {/* Show upgrade prompt when user has no credits */} {userCredits && userCredits.credits <= 0 && !userCredits.isPremium && ( -
-
- -
+
+
)} -
-
+
+