chore: fix reload, chat history
Browse files- Frontend/src/api/notesService.ts +12 -0
- Frontend/src/components/auth/SignIn.tsx +11 -11
- Frontend/src/components/context/AuthContext.tsx +20 -11
- Frontend/src/pages/home.tsx +472 -386
- Frontend/src/pages/note.tsx +116 -63
Frontend/src/api/notesService.ts
CHANGED
|
@@ -14,6 +14,13 @@ export interface ChatMessage {
|
|
| 14 |
content: string;
|
| 15 |
}
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
// 1. Fetch the list of PDFs for the Sidebar
|
| 18 |
export const fetchNotes = async (): Promise<Note[]> => {
|
| 19 |
const response: AxiosResponse<Note[]> = await API.get("/notes/");
|
|
@@ -75,6 +82,11 @@ export const fetchChatHistory = async (sessionId: string) => {
|
|
| 75 |
return response.data;
|
| 76 |
};
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
// 7. Stream Chat (Special Handling using fetch API)
|
| 79 |
export const streamChatRequest = async (
|
| 80 |
sessionId: string,
|
|
|
|
| 14 |
content: string;
|
| 15 |
}
|
| 16 |
|
| 17 |
+
export interface Session {
|
| 18 |
+
id: string;
|
| 19 |
+
name: string;
|
| 20 |
+
created_at: string;
|
| 21 |
+
pdf_id: number;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
// 1. Fetch the list of PDFs for the Sidebar
|
| 25 |
export const fetchNotes = async (): Promise<Note[]> => {
|
| 26 |
const response: AxiosResponse<Note[]> = await API.get("/notes/");
|
|
|
|
| 82 |
return response.data;
|
| 83 |
};
|
| 84 |
|
| 85 |
+
export const fetchSessions = async (pdfId: number): Promise<Session[]> => {
|
| 86 |
+
const response = await API.get(`/notes/sessions/${pdfId}`);
|
| 87 |
+
return response.data;
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
// 7. Stream Chat (Special Handling using fetch API)
|
| 91 |
export const streamChatRequest = async (
|
| 92 |
sessionId: string,
|
Frontend/src/components/auth/SignIn.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import API from "../../api/api";
|
|
| 4 |
import { useAuth } from "../context/AuthContext";
|
| 5 |
|
| 6 |
interface SignInProps {
|
| 7 |
-
onSwitchToSignUp: () => void;
|
| 8 |
}
|
| 9 |
|
| 10 |
const SignIn: React.FC<SignInProps> = ({ onSwitchToSignUp }) => {
|
|
@@ -20,19 +20,17 @@ const SignIn: React.FC<SignInProps> = ({ onSwitchToSignUp }) => {
|
|
| 20 |
try {
|
| 21 |
const res = await API.post("/auth/login", { email, password });
|
| 22 |
|
| 23 |
-
// ✅
|
| 24 |
-
login(
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
} catch (err: any) {
|
| 29 |
console.error("Login error:", err);
|
| 30 |
|
| 31 |
let errorMessage = "Login failed.";
|
| 32 |
-
|
| 33 |
if (err.response?.data?.detail) {
|
| 34 |
const detail = err.response.data.detail;
|
| 35 |
-
|
| 36 |
if (typeof detail === "string") errorMessage = detail;
|
| 37 |
else if (Array.isArray(detail)) {
|
| 38 |
const first = detail[0];
|
|
@@ -46,7 +44,9 @@ const SignIn: React.FC<SignInProps> = ({ onSwitchToSignUp }) => {
|
|
| 46 |
|
| 47 |
return (
|
| 48 |
<>
|
| 49 |
-
<h2 className="text-3xl font-bold mb-8 text-center text-white">
|
|
|
|
|
|
|
| 50 |
|
| 51 |
{error && <p className="text-red-400 text-center mb-3">{error}</p>}
|
| 52 |
|
|
@@ -83,7 +83,7 @@ const SignIn: React.FC<SignInProps> = ({ onSwitchToSignUp }) => {
|
|
| 83 |
|
| 84 |
<button
|
| 85 |
type="submit"
|
| 86 |
-
className="w-full px-5 py-3 rounded-lg bg-
|
| 87 |
>
|
| 88 |
<LogIn className="w-5 h-5" />
|
| 89 |
Sign In
|
|
@@ -100,4 +100,4 @@ const SignIn: React.FC<SignInProps> = ({ onSwitchToSignUp }) => {
|
|
| 100 |
);
|
| 101 |
};
|
| 102 |
|
| 103 |
-
export default SignIn;
|
|
|
|
| 4 |
import { useAuth } from "../context/AuthContext";
|
| 5 |
|
| 6 |
interface SignInProps {
|
| 7 |
+
onSwitchToSignUp: () => void;
|
| 8 |
}
|
| 9 |
|
| 10 |
const SignIn: React.FC<SignInProps> = ({ onSwitchToSignUp }) => {
|
|
|
|
| 20 |
try {
|
| 21 |
const res = await API.post("/auth/login", { email, password });
|
| 22 |
|
| 23 |
+
// ✅ UPDATED: Pass token to login() to persist session
|
| 24 |
+
login(
|
| 25 |
+
res.data.username || res.data.user?.username || "User",
|
| 26 |
+
res.data.access_token
|
| 27 |
+
);
|
| 28 |
} catch (err: any) {
|
| 29 |
console.error("Login error:", err);
|
| 30 |
|
| 31 |
let errorMessage = "Login failed.";
|
|
|
|
| 32 |
if (err.response?.data?.detail) {
|
| 33 |
const detail = err.response.data.detail;
|
|
|
|
| 34 |
if (typeof detail === "string") errorMessage = detail;
|
| 35 |
else if (Array.isArray(detail)) {
|
| 36 |
const first = detail[0];
|
|
|
|
| 44 |
|
| 45 |
return (
|
| 46 |
<>
|
| 47 |
+
<h2 className="text-3xl font-bold mb-8 text-center text-white">
|
| 48 |
+
Welcome Back
|
| 49 |
+
</h2>
|
| 50 |
|
| 51 |
{error && <p className="text-red-400 text-center mb-3">{error}</p>}
|
| 52 |
|
|
|
|
| 83 |
|
| 84 |
<button
|
| 85 |
type="submit"
|
| 86 |
+
className="w-full px-5 py-3 rounded-lg bg-gradient-to-r from-blue-500 to-gray-500 text-white flex items-center justify-center gap-2"
|
| 87 |
>
|
| 88 |
<LogIn className="w-5 h-5" />
|
| 89 |
Sign In
|
|
|
|
| 100 |
);
|
| 101 |
};
|
| 102 |
|
| 103 |
+
export default SignIn;
|
Frontend/src/components/context/AuthContext.tsx
CHANGED
|
@@ -1,11 +1,9 @@
|
|
| 1 |
-
import React, { createContext, useContext, useState } from "react";
|
| 2 |
|
| 3 |
interface AuthContextType {
|
| 4 |
isAuthenticated: boolean;
|
| 5 |
-
// 1. Added username to the context type
|
| 6 |
username: string;
|
| 7 |
-
|
| 8 |
-
login: (name: string) => void;
|
| 9 |
logout: () => void;
|
| 10 |
}
|
| 11 |
|
|
@@ -13,19 +11,30 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
| 13 |
|
| 14 |
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
| 15 |
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
| 16 |
-
|
| 17 |
-
const [username, setUsername] = useState('Guest');
|
| 18 |
|
| 19 |
-
//
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
setUsername(name);
|
| 22 |
setIsAuthenticated(true);
|
| 23 |
};
|
| 24 |
|
| 25 |
const logout = () => {
|
| 26 |
-
|
|
|
|
| 27 |
setIsAuthenticated(false);
|
| 28 |
-
setUsername(
|
| 29 |
};
|
| 30 |
|
| 31 |
return (
|
|
@@ -39,4 +48,4 @@ export const useAuth = () => {
|
|
| 39 |
const ctx = useContext(AuthContext);
|
| 40 |
if (!ctx) throw new Error("useAuth must be inside AuthProvider");
|
| 41 |
return ctx;
|
| 42 |
-
};
|
|
|
|
| 1 |
+
import React, { createContext, useContext, useState, useEffect } from "react";
|
| 2 |
|
| 3 |
interface AuthContextType {
|
| 4 |
isAuthenticated: boolean;
|
|
|
|
| 5 |
username: string;
|
| 6 |
+
login: (name: string, token: string) => void;
|
|
|
|
| 7 |
logout: () => void;
|
| 8 |
}
|
| 9 |
|
|
|
|
| 11 |
|
| 12 |
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
| 13 |
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
| 14 |
+
const [username, setUsername] = useState("Guest");
|
|
|
|
| 15 |
|
| 16 |
+
// ✅ FIX: Check localStorage on mount to persist session
|
| 17 |
+
useEffect(() => {
|
| 18 |
+
const token = localStorage.getItem("token");
|
| 19 |
+
const storedUser = localStorage.getItem("username");
|
| 20 |
+
if (token) {
|
| 21 |
+
setIsAuthenticated(true);
|
| 22 |
+
if (storedUser) setUsername(storedUser);
|
| 23 |
+
}
|
| 24 |
+
}, []);
|
| 25 |
+
|
| 26 |
+
const login = (name: string, token: string) => {
|
| 27 |
+
localStorage.setItem("token", token);
|
| 28 |
+
localStorage.setItem("username", name);
|
| 29 |
setUsername(name);
|
| 30 |
setIsAuthenticated(true);
|
| 31 |
};
|
| 32 |
|
| 33 |
const logout = () => {
|
| 34 |
+
localStorage.removeItem("token");
|
| 35 |
+
localStorage.removeItem("username");
|
| 36 |
setIsAuthenticated(false);
|
| 37 |
+
setUsername("Guest");
|
| 38 |
};
|
| 39 |
|
| 40 |
return (
|
|
|
|
| 48 |
const ctx = useContext(AuthContext);
|
| 49 |
if (!ctx) throw new Error("useAuth must be inside AuthProvider");
|
| 50 |
return ctx;
|
| 51 |
+
};
|
Frontend/src/pages/home.tsx
CHANGED
|
@@ -1,409 +1,495 @@
|
|
| 1 |
-
import React, { useState, useEffect, useRef } from
|
| 2 |
-
import {
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
interface Feature {
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
}
|
| 14 |
|
| 15 |
interface Step {
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
}
|
| 20 |
|
| 21 |
-
//
|
| 22 |
interface AIInterviewPlatformProps {
|
| 23 |
-
|
| 24 |
}
|
| 25 |
|
| 26 |
-
const AIInterviewPlatform: React.FC<AIInterviewPlatformProps> = ({
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
</div>
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
<a
|
| 147 |
-
href="#home"
|
| 148 |
-
className="block py-3 px-4 rounded-lg text-gray-200 hover:bg-blue-500/10 hover:text-blue-400 transition-all font-medium"
|
| 149 |
-
onClick={() => setIsMenuOpen(false)}
|
| 150 |
-
>
|
| 151 |
-
Home
|
| 152 |
-
</a>
|
| 153 |
-
<a
|
| 154 |
-
href="#about"
|
| 155 |
-
className="block py-3 px-4 rounded-lg text-gray-200 hover:bg-blue-500/10 hover:text-blue-400 transition-all font-medium"
|
| 156 |
-
onClick={() => setIsMenuOpen(false)}
|
| 157 |
-
>
|
| 158 |
-
About
|
| 159 |
-
</a>
|
| 160 |
-
<a
|
| 161 |
-
href="#features"
|
| 162 |
-
className="block py-3 px-4 rounded-lg text-gray-200 hover:bg-blue-500/10 hover:text-blue-400 transition-all font-medium"
|
| 163 |
-
onClick={() => setIsMenuOpen(false)}
|
| 164 |
-
>
|
| 165 |
-
Features
|
| 166 |
-
</a>
|
| 167 |
-
<a
|
| 168 |
-
href="#how-it-works"
|
| 169 |
-
className="block py-3 px-4 rounded-lg text-gray-200 hover:bg-blue-500/10 hover:text-blue-400 transition-all font-medium"
|
| 170 |
-
onClick={() => setIsMenuOpen(false)}
|
| 171 |
-
>
|
| 172 |
-
How It Works
|
| 173 |
-
</a>
|
| 174 |
-
<button onClick={openSignInModal} className="w-full py-3 rounded-lg border-2 border-blue-400 text-blue-400 hover:bg-blue-400/10 transition-all font-medium">
|
| 175 |
-
Sign In
|
| 176 |
-
</button>
|
| 177 |
-
<button onClick={openSignUpModal} className="w-full py-3 rounded-lg bg-linear-to-br from-blue-500 to-blue-500 hover:from-blue-600 hover:to-blue-900 transition-all font-medium">
|
| 178 |
-
Sign Up
|
| 179 |
-
</button>
|
| 180 |
-
</div>
|
| 181 |
-
</div>
|
| 182 |
-
)}
|
| 183 |
-
</nav>
|
| 184 |
-
|
| 185 |
-
{/* Auth Modals */}
|
| 186 |
-
<AuthModal isOpen={modalType === 'signIn'} onClose={closeModal}>
|
| 187 |
-
<SignIn
|
| 188 |
-
onClose={closeModal}
|
| 189 |
-
onSwitchToSignUp={openSignUpModal}
|
| 190 |
-
onAuthSuccess={handleAuthSuccess}
|
| 191 |
-
/>
|
| 192 |
-
</AuthModal>
|
| 193 |
-
|
| 194 |
-
<AuthModal isOpen={modalType === 'signUp'} onClose={closeModal}>
|
| 195 |
-
<SignUp
|
| 196 |
-
onClose={closeModal}
|
| 197 |
-
onSwitchToSignIn={openSignInModal}
|
| 198 |
-
onAuthSuccess={handleAuthSuccess}
|
| 199 |
-
/>
|
| 200 |
-
</AuthModal>
|
| 201 |
-
|
| 202 |
-
{/* Hero Section */}
|
| 203 |
-
<section ref={heroRef} id="home" className="min-h-[calc(100vh-68px)] flex items-center pt-20 pb-20 px-4 sm:px-6 lg:px-8 relative overflow-hidden bg-black">
|
| 204 |
-
{/* Spotlight applied directly to the container as per the demo structure */}
|
| 205 |
-
<Spotlight parentRef={heroRef} color="#4b9fff" className="mix-blend-screen" />
|
| 206 |
-
<div className="max-w-7xl mx-auto w-full relative z-10">
|
| 207 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
| 208 |
-
{/* Left side - Text content */}
|
| 209 |
-
<div>
|
| 210 |
-
<h1 className="text-5xl sm:text-6xl lg:text-7xl font-bold mb-6 leading-tight">
|
| 211 |
-
Master Your Next
|
| 212 |
-
<span className="block bg-linear-to-br from-blue-500 via-gray-400 to-blue-500 bg-clip-text text-transparent">
|
| 213 |
-
Interview with AI
|
| 214 |
-
</span>
|
| 215 |
-
</h1>
|
| 216 |
-
<p className="text-xl text-gray-300 mb-10 max-w-lg">
|
| 217 |
-
Practice with our intelligent AI interviewer, get instant feedback, and land your dream job with confidence.
|
| 218 |
-
</p>
|
| 219 |
-
<button
|
| 220 |
-
onClick={openSignUpModal}
|
| 221 |
-
className="px-8 py-4 rounded-xl bg-linear-to-br from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 transition duration-300 shadow-xl shadow-blue-600/30 flex items-center gap-2 text-lg font-semibold transform hover:scale-[1.03]"
|
| 222 |
-
>
|
| 223 |
-
Start Free Trial
|
| 224 |
-
<ChevronRight className="w-5 h-5" />
|
| 225 |
-
</button>
|
| 226 |
-
</div>
|
| 227 |
-
|
| 228 |
-
{/* Right side - 3D Scene */}
|
| 229 |
-
<div className="h-[500px] lg:h-[600px] relative">
|
| 230 |
-
<SplineScene
|
| 231 |
-
scene="https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode"
|
| 232 |
-
className="w-full h-full"
|
| 233 |
-
/>
|
| 234 |
-
</div>
|
| 235 |
-
</div>
|
| 236 |
</div>
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
| 243 |
-
<div>
|
| 244 |
-
<h2 className="text-4xl sm:text-5xl font-bold mb-6">
|
| 245 |
-
About InterviewAI
|
| 246 |
-
</h2>
|
| 247 |
-
<p className="text-lg text-gray-300 mb-6">
|
| 248 |
-
We're revolutionizing interview preparation with cutting-edge AI technology. Our platform helps candidates practice, learn, and succeed in their job interviews.
|
| 249 |
-
</p>
|
| 250 |
-
<p className="text-lg text-gray-300 mb-6">
|
| 251 |
-
Founded by industry experts and powered by advanced machine learning, InterviewAI provides personalized interview experiences that adapt to your unique needs and goals.
|
| 252 |
-
</p>
|
| 253 |
-
<div className="space-y-4">
|
| 254 |
-
<div className="flex items-center gap-3">
|
| 255 |
-
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
|
| 256 |
-
<span className="text-gray-300">Advanced AI interview simulations</span>
|
| 257 |
-
</div>
|
| 258 |
-
<div className="flex items-center gap-3">
|
| 259 |
-
<div className="w-2 h-2 bg-blue-600 rounded-full"></div>
|
| 260 |
-
<span className="text-gray-300">Personalized feedback and coaching</span>
|
| 261 |
-
</div>
|
| 262 |
-
<div className="flex items-center gap-3">
|
| 263 |
-
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
|
| 264 |
-
<span className="text-gray-300">Industry-leading success rates</span>
|
| 265 |
-
</div>
|
| 266 |
-
</div>
|
| 267 |
-
</div>
|
| 268 |
-
<div className="bg-linear-to-br from-blue-900/50 to-gray-900/50 rounded-2xl p-8 border border-gray-200/30">
|
| 269 |
-
<div className="space-y-6">
|
| 270 |
-
<div className="bg-white/5 backdrop-blur-sm rounded-lg p-6">
|
| 271 |
-
<div className="text-3xl font-bold text-blue-400 mb-2">2020</div>
|
| 272 |
-
<div className="text-gray-400">Founded</div>
|
| 273 |
-
</div>
|
| 274 |
-
<div className="bg-white/5 backdrop-blur-sm rounded-lg p-6">
|
| 275 |
-
<div className="text-3xl font-bold text-blue-500 mb-2">50K+</div>
|
| 276 |
-
<div className="text-gray-400">Happy Users</div>
|
| 277 |
-
</div>
|
| 278 |
-
<div className="bg-white/5 backdrop-blur-sm rounded-lg p-6">
|
| 279 |
-
<div className="text-3xl font-bold text-blue-400 mb-2">500+</div>
|
| 280 |
-
<div className="text-gray-400">Companies Trust Us</div>
|
| 281 |
-
</div>
|
| 282 |
-
</div>
|
| 283 |
-
</div>
|
| 284 |
-
</div>
|
| 285 |
</div>
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
<div className="
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
<p className="text-xl text-gray-400">
|
| 296 |
-
Everything you need to ace your next interview
|
| 297 |
-
</p>
|
| 298 |
-
</div>
|
| 299 |
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
| 300 |
-
{features.map((feature: Feature, index: number) => (
|
| 301 |
-
<div
|
| 302 |
-
key={index}
|
| 303 |
-
className="bg-linear-to-br from-blue-900/50 to-gray-900/50 backdrop-blur-sm rounded-xl p-8 border border-blue-500/20 hover:border-gray-500/50 transition-all duration-300 hover:transform hover:scale-105"
|
| 304 |
-
>
|
| 305 |
-
<div className="bg-linear-to-br from-blue-500 to-gray-400 w-16 h-16 rounded-lg flex items-center justify-center mb-4 shadow-lg shadow-blue-500/50">
|
| 306 |
-
{feature.icon}
|
| 307 |
-
</div>
|
| 308 |
-
<h3 className="text-xl font-bold mb-3">{feature.title}</h3>
|
| 309 |
-
<p className="text-gray-400">{feature.description}</p>
|
| 310 |
-
</div>
|
| 311 |
-
))}
|
| 312 |
-
</div>
|
| 313 |
</div>
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
<div className="text-center mb-16">
|
| 320 |
-
<h2 className="text-4xl sm:text-5xl font-bold mb-4">
|
| 321 |
-
How It Works
|
| 322 |
-
</h2>
|
| 323 |
-
<p className="text-xl text-gray-400">
|
| 324 |
-
Get started in three simple steps
|
| 325 |
-
</p>
|
| 326 |
-
</div>
|
| 327 |
-
|
| 328 |
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
| 329 |
-
{steps.map((item: Step, index: number) => (
|
| 330 |
-
<div key={index} className="relative text-center">
|
| 331 |
-
<div className="text-7xl font-bold text-white mb-4">
|
| 332 |
-
{item.step}
|
| 333 |
-
</div>
|
| 334 |
-
<h3 className="text-2xl font-bold mb-3">{item.title}</h3>
|
| 335 |
-
<p className="text-gray-400">{item.desc}</p>
|
| 336 |
-
|
| 337 |
-
{index < 2 && (
|
| 338 |
-
<ChevronRight className="hidden md:block absolute top-12 -right-12 w-8 h-8 text-white" />
|
| 339 |
-
)}
|
| 340 |
-
</div>
|
| 341 |
-
))}
|
| 342 |
-
</div>
|
| 343 |
</div>
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
<div className="max-w-4xl mx-auto text-center">
|
| 350 |
-
<h2 className="text-4xl sm:text-5xl font-bold mb-6">
|
| 351 |
-
Ready to Ace Your Interview?
|
| 352 |
-
</h2>
|
| 353 |
-
<p className="text-xl text-gray-300 mb-10">
|
| 354 |
-
Join thousands of successful candidates who prepared with InterviewAI
|
| 355 |
-
</p>
|
| 356 |
-
<button
|
| 357 |
-
onClick={openSignUpModal}
|
| 358 |
-
className="px-10 py-5 rounded-lg bg-linear-to-r from-blue-400 to-blue-700 hover:from-blue-600 hover:to-blue-900 transition shadow-lg shadow-blue-500/50 text-lg font-semibold"
|
| 359 |
-
>
|
| 360 |
-
Start Your Free Trial Today
|
| 361 |
-
</button>
|
| 362 |
</div>
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
</div>
|
| 378 |
-
|
| 379 |
-
{
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
| 403 |
</div>
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
</div>
|
| 406 |
-
|
|
|
|
|
|
|
| 407 |
};
|
| 408 |
|
| 409 |
-
export default AIInterviewPlatform;
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from "react";
|
| 2 |
+
import {
|
| 3 |
+
Sparkles,
|
| 4 |
+
Zap,
|
| 5 |
+
Target,
|
| 6 |
+
Clock,
|
| 7 |
+
Award,
|
| 8 |
+
ChevronRight,
|
| 9 |
+
Menu,
|
| 10 |
+
X,
|
| 11 |
+
} from "lucide-react";
|
| 12 |
+
import AuthModal from "../components/auth/AuthModal";
|
| 13 |
+
import SignIn from "../components/auth/SignIn";
|
| 14 |
+
import SignUp from "../components/auth/SignUp";
|
| 15 |
+
import { SplineScene } from "../components/ui/splite";
|
| 16 |
+
import { Spotlight } from "../components/ui/spotlight";
|
| 17 |
|
| 18 |
interface Feature {
|
| 19 |
+
icon: React.ReactNode;
|
| 20 |
+
title: string;
|
| 21 |
+
description: string;
|
| 22 |
}
|
| 23 |
|
| 24 |
interface Step {
|
| 25 |
+
step: string;
|
| 26 |
+
title: string;
|
| 27 |
+
desc: string;
|
| 28 |
}
|
| 29 |
|
| 30 |
+
// ✅ UPDATED: Interface now accepts token
|
| 31 |
interface AIInterviewPlatformProps {
|
| 32 |
+
onLogin: (username: string, token: string) => void;
|
| 33 |
}
|
| 34 |
|
| 35 |
+
const AIInterviewPlatform: React.FC<AIInterviewPlatformProps> = ({
|
| 36 |
+
onLogin,
|
| 37 |
+
}) => {
|
| 38 |
+
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
|
| 39 |
+
const [scrolled, setScrolled] = useState<boolean>(false);
|
| 40 |
+
const [modalType, setModalType] = useState<"none" | "signIn" | "signUp">(
|
| 41 |
+
"none"
|
| 42 |
+
);
|
| 43 |
+
|
| 44 |
+
// Handlers for opening/closing modals
|
| 45 |
+
const openSignInModal = () => {
|
| 46 |
+
setModalType("signIn");
|
| 47 |
+
setIsMenuOpen(false);
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
const openSignUpModal = () => {
|
| 51 |
+
setModalType("signUp");
|
| 52 |
+
setIsMenuOpen(false);
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
const closeModal = () => setModalType("none");
|
| 56 |
+
|
| 57 |
+
// ✅ UPDATED: Handler now accepts and passes token
|
| 58 |
+
const handleAuthSuccess = (username: string, token: string) => {
|
| 59 |
+
closeModal();
|
| 60 |
+
onLogin(username, token);
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
useEffect(() => {
|
| 64 |
+
const handleScroll = (): void => {
|
| 65 |
+
setScrolled(window.scrollY > 50);
|
| 66 |
};
|
| 67 |
+
window.addEventListener("scroll", handleScroll);
|
| 68 |
+
return () => window.removeEventListener("scroll", handleScroll);
|
| 69 |
+
}, []);
|
| 70 |
+
|
| 71 |
+
const features: Feature[] = [
|
| 72 |
+
{
|
| 73 |
+
icon: <Sparkles className="w-8 h-8" />,
|
| 74 |
+
title: "AI-Powered Questions",
|
| 75 |
+
description: "Dynamic questions adapted to your skill level and role",
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
icon: <Zap className="w-8 h-8" />,
|
| 79 |
+
title: "Instant Feedback",
|
| 80 |
+
description: "Get real-time analysis and improvement suggestions",
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
icon: <Target className="w-8 h-8" />,
|
| 84 |
+
title: "Role-Specific Prep",
|
| 85 |
+
description: "Tailored scenarios for your target position",
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
icon: <Clock className="w-8 h-8" />,
|
| 89 |
+
title: "Practice Anytime",
|
| 90 |
+
description: "24/7 access to unlimited mock interviews",
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
icon: <Award className="w-8 h-8" />,
|
| 94 |
+
title: "Performance Analytics",
|
| 95 |
+
description: "Track your progress with detailed insights",
|
| 96 |
+
},
|
| 97 |
+
];
|
| 98 |
+
|
| 99 |
+
const steps: Step[] = [
|
| 100 |
+
{
|
| 101 |
+
step: "01",
|
| 102 |
+
title: "Choose Your Role",
|
| 103 |
+
desc: "Select the job position you're preparing for",
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
step: "02",
|
| 107 |
+
title: "Practice Interview",
|
| 108 |
+
desc: "Answer AI-generated questions in real-time",
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
step: "03",
|
| 112 |
+
title: "Get Feedback",
|
| 113 |
+
desc: "Receive detailed analysis and improvement tips",
|
| 114 |
+
},
|
| 115 |
+
];
|
| 116 |
+
|
| 117 |
+
const heroRef = useRef<HTMLDivElement>(null);
|
| 118 |
+
|
| 119 |
+
return (
|
| 120 |
+
<div className="w-full min-h-screen bg-gradient-to-br from-blue-900 to-gray-400 text-white overflow-x-hidden font-opensans">
|
| 121 |
+
{/* Navigation Bar */}
|
| 122 |
+
<nav
|
| 123 |
+
className={`fixed top-0 left-0 right-0 w-full z-50 transition-all duration-300 ${
|
| 124 |
+
scrolled
|
| 125 |
+
? "bg-slate-900/95 backdrop-blur-md shadow-xl"
|
| 126 |
+
: "bg-slate-900/80 backdrop-blur-sm"
|
| 127 |
+
}`}
|
| 128 |
+
>
|
| 129 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 130 |
+
<div className="flex justify-between items-center h-20">
|
| 131 |
+
{/* Logo */}
|
| 132 |
+
<div className="flex items-center space-x-3">
|
| 133 |
+
<div className="bg-gradient-to-br from-blue-500 to-gray-400 p-2 rounded-lg">
|
| 134 |
+
<Sparkles className="w-6 h-6 text-white" />
|
| 135 |
+
</div>
|
| 136 |
+
<span className="text-2xl font-bold bg-gradient-to-br from-blue-500 to-gray-400 bg-clip-text text-transparent">
|
| 137 |
+
InterviewAI
|
| 138 |
+
</span>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
{/* Desktop Navigation */}
|
| 142 |
+
<div className="hidden lg:flex items-center space-x-8">
|
| 143 |
+
<a
|
| 144 |
+
href="#home"
|
| 145 |
+
className="text-gray-200 hover:text-blue-400 transition-colors font-medium"
|
| 146 |
+
>
|
| 147 |
+
Home
|
| 148 |
+
</a>
|
| 149 |
+
<a
|
| 150 |
+
href="#about"
|
| 151 |
+
className="text-gray-200 hover:text-blue-400 transition-colors font-medium"
|
| 152 |
+
>
|
| 153 |
+
About
|
| 154 |
+
</a>
|
| 155 |
+
<a
|
| 156 |
+
href="#features"
|
| 157 |
+
className="text-gray-200 hover:text-blue-400 transition-colors font-medium"
|
| 158 |
+
>
|
| 159 |
+
Features
|
| 160 |
+
</a>
|
| 161 |
+
<a
|
| 162 |
+
href="#how-it-works"
|
| 163 |
+
className="text-gray-200 hover:text-blue-400 transition-colors font-medium"
|
| 164 |
+
>
|
| 165 |
+
How It Works
|
| 166 |
+
</a>
|
| 167 |
+
<button
|
| 168 |
+
onClick={openSignInModal}
|
| 169 |
+
className="px-6 py-2.5 rounded-lg border-2 border-blue-400 text-blue-400 hover:bg-blue-400/10 transition-all font-medium"
|
| 170 |
+
>
|
| 171 |
+
Sign In
|
| 172 |
+
</button>
|
| 173 |
+
<button
|
| 174 |
+
onClick={openSignUpModal}
|
| 175 |
+
className="px-6 py-2.5 rounded-lg bg-gradient-to-br from-blue-500 to-blue-500 hover:from-blue-600 hover:to-blue-900 transition-all shadow-lg shadow-blue-500/30 font-medium"
|
| 176 |
+
>
|
| 177 |
+
Sign Up
|
| 178 |
+
</button>
|
| 179 |
+
</div>
|
| 180 |
+
|
| 181 |
+
{/* Mobile Menu Button */}
|
| 182 |
+
<button
|
| 183 |
+
className="lg:hidden p-2 rounded-lg hover:bg-white/10 transition-colors"
|
| 184 |
+
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
| 185 |
+
>
|
| 186 |
+
{isMenuOpen ? (
|
| 187 |
+
<X className="w-6 h-6" />
|
| 188 |
+
) : (
|
| 189 |
+
<Menu className="w-6 h-6" />
|
| 190 |
+
)}
|
| 191 |
+
</button>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
|
| 195 |
+
{/* Mobile Menu */}
|
| 196 |
+
{isMenuOpen && (
|
| 197 |
+
<div className="lg:hidden border-t border-white/10 bg-slate-900/98 backdrop-blur-md">
|
| 198 |
+
<div className="px-4 py-4 space-y-3 max-w-7xl mx-auto">
|
| 199 |
+
<a
|
| 200 |
+
href="#home"
|
| 201 |
+
className="block py-3 px-4 rounded-lg text-gray-200 hover:bg-blue-500/10 hover:text-blue-400 transition-all font-medium"
|
| 202 |
+
onClick={() => setIsMenuOpen(false)}
|
| 203 |
+
>
|
| 204 |
+
Home
|
| 205 |
+
</a>
|
| 206 |
+
<a
|
| 207 |
+
href="#about"
|
| 208 |
+
className="block py-3 px-4 rounded-lg text-gray-200 hover:bg-blue-500/10 hover:text-blue-400 transition-all font-medium"
|
| 209 |
+
onClick={() => setIsMenuOpen(false)}
|
| 210 |
+
>
|
| 211 |
+
About
|
| 212 |
+
</a>
|
| 213 |
+
<a
|
| 214 |
+
href="#features"
|
| 215 |
+
className="block py-3 px-4 rounded-lg text-gray-200 hover:bg-blue-500/10 hover:text-blue-400 transition-all font-medium"
|
| 216 |
+
onClick={() => setIsMenuOpen(false)}
|
| 217 |
+
>
|
| 218 |
+
Features
|
| 219 |
+
</a>
|
| 220 |
+
<a
|
| 221 |
+
href="#how-it-works"
|
| 222 |
+
className="block py-3 px-4 rounded-lg text-gray-200 hover:bg-blue-500/10 hover:text-blue-400 transition-all font-medium"
|
| 223 |
+
onClick={() => setIsMenuOpen(false)}
|
| 224 |
+
>
|
| 225 |
+
How It Works
|
| 226 |
+
</a>
|
| 227 |
+
<button
|
| 228 |
+
onClick={openSignInModal}
|
| 229 |
+
className="w-full py-3 rounded-lg border-2 border-blue-400 text-blue-400 hover:bg-blue-400/10 transition-all font-medium"
|
| 230 |
+
>
|
| 231 |
+
Sign In
|
| 232 |
+
</button>
|
| 233 |
+
<button
|
| 234 |
+
onClick={openSignUpModal}
|
| 235 |
+
className="w-full py-3 rounded-lg bg-gradient-to-br from-blue-500 to-blue-500 hover:from-blue-600 hover:to-blue-900 transition-all font-medium"
|
| 236 |
+
>
|
| 237 |
+
Sign Up
|
| 238 |
+
</button>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
)}
|
| 242 |
+
</nav>
|
| 243 |
+
|
| 244 |
+
{/* Auth Modals */}
|
| 245 |
+
<AuthModal isOpen={modalType === "signIn"} onClose={closeModal}>
|
| 246 |
+
<SignIn
|
| 247 |
+
onSwitchToSignUp={openSignUpModal}
|
| 248 |
+
// We don't need onAuthSuccess for SignIn because it uses useAuth() hook directly,
|
| 249 |
+
// but if you unified them, you could pass it.
|
| 250 |
+
// Currently SignIn handles logic internally.
|
| 251 |
+
/>
|
| 252 |
+
</AuthModal>
|
| 253 |
+
|
| 254 |
+
<AuthModal isOpen={modalType === "signUp"} onClose={closeModal}>
|
| 255 |
+
<SignUp
|
| 256 |
+
onClose={closeModal}
|
| 257 |
+
onSwitchToSignIn={openSignInModal}
|
| 258 |
+
onAuthSuccess={handleAuthSuccess}
|
| 259 |
+
/>
|
| 260 |
+
</AuthModal>
|
| 261 |
+
|
| 262 |
+
{/* Hero Section */}
|
| 263 |
+
<section
|
| 264 |
+
ref={heroRef}
|
| 265 |
+
id="home"
|
| 266 |
+
className="min-h-[calc(100vh-68px)] flex items-center pt-20 pb-20 px-4 sm:px-6 lg:px-8 relative overflow-hidden bg-black"
|
| 267 |
+
>
|
| 268 |
+
<Spotlight parentRef={heroRef} className="mix-blend-screen" />
|
| 269 |
+
<div className="max-w-7xl mx-auto w-full relative z-10">
|
| 270 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
| 271 |
+
{/* Left side */}
|
| 272 |
+
<div>
|
| 273 |
+
<h1 className="text-5xl sm:text-6xl lg:text-7xl font-bold mb-6 leading-tight">
|
| 274 |
+
Master Your Next{" "}
|
| 275 |
+
<span className="block bg-gradient-to-br from-blue-500 via-gray-400 to-blue-500 bg-clip-text text-transparent">
|
| 276 |
+
Interview with AI
|
| 277 |
+
</span>
|
| 278 |
+
</h1>
|
| 279 |
+
<p className="text-xl text-gray-300 mb-10 max-w-lg">
|
| 280 |
+
Practice with our intelligent AI interviewer, get instant
|
| 281 |
+
feedback, and land your dream job with confidence.
|
| 282 |
+
</p>
|
| 283 |
+
<button
|
| 284 |
+
onClick={openSignUpModal}
|
| 285 |
+
className="px-8 py-4 rounded-xl bg-gradient-to-br from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 transition duration-300 shadow-xl shadow-blue-600/30 flex items-center gap-2 text-lg font-semibold transform hover:scale-[1.03]"
|
| 286 |
+
>
|
| 287 |
+
Start Free Trial <ChevronRight className="w-5 h-5" />
|
| 288 |
+
</button>
|
| 289 |
+
</div>
|
| 290 |
+
|
| 291 |
+
{/* Right side - 3D Scene */}
|
| 292 |
+
<div className="h-[500px] lg:h-[600px] relative">
|
| 293 |
+
<SplineScene
|
| 294 |
+
scene="https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode"
|
| 295 |
+
className="w-full h-full"
|
| 296 |
+
/>
|
| 297 |
+
</div>
|
| 298 |
+
</div>
|
| 299 |
+
</div>
|
| 300 |
+
</section>
|
| 301 |
+
|
| 302 |
+
{/* About Section */}
|
| 303 |
+
<section id="about" className="py-20 px-4 sm:px-6 lg:px-8 bg-black/40">
|
| 304 |
+
<div className="max-w-6xl mx-auto">
|
| 305 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
| 306 |
+
<div>
|
| 307 |
+
<h2 className="text-4xl sm:text-5xl font-bold mb-6">
|
| 308 |
+
About InterviewAI
|
| 309 |
+
</h2>
|
| 310 |
+
<p className="text-lg text-gray-300 mb-6">
|
| 311 |
+
We're revolutionizing interview preparation with cutting-edge AI
|
| 312 |
+
technology. Our platform helps candidates practice, learn, and
|
| 313 |
+
succeed in their job interviews.
|
| 314 |
+
</p>
|
| 315 |
+
<p className="text-lg text-gray-300 mb-6">
|
| 316 |
+
Founded by industry experts and powered by advanced machine
|
| 317 |
+
learning, InterviewAI provides personalized interview
|
| 318 |
+
experiences that adapt to your unique needs and goals.
|
| 319 |
+
</p>
|
| 320 |
+
<div className="space-y-4">
|
| 321 |
+
<div className="flex items-center gap-3">
|
| 322 |
+
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
|
| 323 |
+
<span className="text-gray-300">
|
| 324 |
+
Advanced AI interview simulations
|
| 325 |
+
</span>
|
| 326 |
</div>
|
| 327 |
+
<div className="flex items-center gap-3">
|
| 328 |
+
<div className="w-2 h-2 bg-blue-600 rounded-full"></div>
|
| 329 |
+
<span className="text-gray-300">
|
| 330 |
+
Personalized feedback and coaching
|
| 331 |
+
</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
</div>
|
| 333 |
+
<div className="flex items-center gap-3">
|
| 334 |
+
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
|
| 335 |
+
<span className="text-gray-300">
|
| 336 |
+
Industry-leading success rates
|
| 337 |
+
</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
</div>
|
| 339 |
+
</div>
|
| 340 |
+
</div>
|
| 341 |
+
<div className="bg-gradient-to-br from-blue-900/50 to-gray-900/50 rounded-2xl p-8 border border-gray-200/30">
|
| 342 |
+
<div className="space-y-6">
|
| 343 |
+
<div className="bg-white/5 backdrop-blur-sm rounded-lg p-6">
|
| 344 |
+
<div className="text-3xl font-bold text-blue-400 mb-2">
|
| 345 |
+
2020
|
| 346 |
+
</div>
|
| 347 |
+
<div className="text-gray-400">Founded</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
</div>
|
| 349 |
+
<div className="bg-white/5 backdrop-blur-sm rounded-lg p-6">
|
| 350 |
+
<div className="text-3xl font-bold text-blue-500 mb-2">
|
| 351 |
+
50K+
|
| 352 |
+
</div>
|
| 353 |
+
<div className="text-gray-400">Happy Users</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
</div>
|
| 355 |
+
<div className="bg-white/5 backdrop-blur-sm rounded-lg p-6">
|
| 356 |
+
<div className="text-3xl font-bold text-blue-400 mb-2">
|
| 357 |
+
500+
|
| 358 |
+
</div>
|
| 359 |
+
<div className="text-gray-400">Companies Trust Us</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
</div>
|
| 361 |
+
</div>
|
| 362 |
+
</div>
|
| 363 |
+
</div>
|
| 364 |
+
</div>
|
| 365 |
+
</section>
|
| 366 |
+
|
| 367 |
+
{/* Features Section */}
|
| 368 |
+
<section id="features" className="py-20 px-4 sm:px-6 lg:px-8 bg-black">
|
| 369 |
+
<div className="max-w-7xl mx-auto">
|
| 370 |
+
<div className="text-center mb-16">
|
| 371 |
+
<h2 className="text-4xl sm:text-5xl font-bold mb-4">
|
| 372 |
+
Why Choose InterviewAI?
|
| 373 |
+
</h2>
|
| 374 |
+
<p className="text-xl text-gray-400">
|
| 375 |
+
Everything you need to ace your next interview
|
| 376 |
+
</p>
|
| 377 |
+
</div>
|
| 378 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
| 379 |
+
{features.map((feature: Feature, index: number) => (
|
| 380 |
+
<div
|
| 381 |
+
key={index}
|
| 382 |
+
className="bg-gradient-to-br from-blue-900/50 to-gray-900/50 backdrop-blur-sm rounded-xl p-8 border border-blue-500/20 hover:border-gray-500/50 transition-all duration-300 hover:transform hover:scale-105"
|
| 383 |
+
>
|
| 384 |
+
<div className="bg-gradient-to-br from-blue-500 to-gray-400 w-16 h-16 rounded-lg flex items-center justify-center mb-4 shadow-lg shadow-blue-500/50">
|
| 385 |
+
{feature.icon}
|
| 386 |
</div>
|
| 387 |
+
<h3 className="text-xl font-bold mb-3">{feature.title}</h3>
|
| 388 |
+
<p className="text-gray-400">{feature.description}</p>
|
| 389 |
+
</div>
|
| 390 |
+
))}
|
| 391 |
+
</div>
|
| 392 |
+
</div>
|
| 393 |
+
</section>
|
| 394 |
+
|
| 395 |
+
{/* How It Works */}
|
| 396 |
+
<section
|
| 397 |
+
id="how-it-works"
|
| 398 |
+
className="py-20 px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-blue-900/50 to-gray-900/50 rounded-2xl border border-gray-200/30"
|
| 399 |
+
>
|
| 400 |
+
<div className="max-w-7xl mx-auto">
|
| 401 |
+
<div className="text-center mb-16">
|
| 402 |
+
<h2 className="text-4xl sm:text-5xl font-bold mb-4">
|
| 403 |
+
How It Works
|
| 404 |
+
</h2>
|
| 405 |
+
<p className="text-xl text-gray-400">
|
| 406 |
+
Get started in three simple steps
|
| 407 |
+
</p>
|
| 408 |
+
</div>
|
| 409 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
| 410 |
+
{steps.map((item: Step, index: number) => (
|
| 411 |
+
<div key={index} className="relative text-center">
|
| 412 |
+
<div className="text-7xl font-bold text-white mb-4">
|
| 413 |
+
{item.step}
|
| 414 |
</div>
|
| 415 |
+
<h3 className="text-2xl font-bold mb-3">{item.title}</h3>
|
| 416 |
+
<p className="text-gray-400">{item.desc}</p>
|
| 417 |
+
{index < 2 && (
|
| 418 |
+
<ChevronRight className="hidden md:block absolute top-12 -right-12 w-8 h-8 text-white" />
|
| 419 |
+
)}
|
| 420 |
+
</div>
|
| 421 |
+
))}
|
| 422 |
+
</div>
|
| 423 |
+
</div>
|
| 424 |
+
</section>
|
| 425 |
+
|
| 426 |
+
{/* CTA Section */}
|
| 427 |
+
<section className="py-20 px-4 sm:px-6 lg:px-8 bg-black">
|
| 428 |
+
<div className="max-w-4xl mx-auto text-center">
|
| 429 |
+
<h2 className="text-4xl sm:text-5xl font-bold mb-6">
|
| 430 |
+
Ready to Ace Your Interview?
|
| 431 |
+
</h2>
|
| 432 |
+
<p className="text-xl text-gray-300 mb-10">
|
| 433 |
+
Join thousands of successful candidates who prepared with
|
| 434 |
+
InterviewAI
|
| 435 |
+
</p>
|
| 436 |
+
<button
|
| 437 |
+
onClick={openSignUpModal}
|
| 438 |
+
className="px-10 py-5 rounded-lg bg-gradient-to-r from-blue-400 to-blue-700 hover:from-blue-600 hover:to-blue-900 transition shadow-lg shadow-blue-500/50 text-lg font-semibold"
|
| 439 |
+
>
|
| 440 |
+
Start Your Free Trial Today
|
| 441 |
+
</button>
|
| 442 |
+
</div>
|
| 443 |
+
</section>
|
| 444 |
+
|
| 445 |
+
{/* Footer */}
|
| 446 |
+
<footer className="w-full py-6 px-6 bg-slate-900/95 backdrop-blur-md text-gray-200 flex flex-col sm:flex-row items-center justify-between shadow-inner">
|
| 447 |
+
<div className="flex items-center space-x-3">
|
| 448 |
+
<div className="bg-gradient-to-br from-blue-500 to-gray-400 p-2 rounded-lg">
|
| 449 |
+
<Sparkles className="w-6 h-6 text-white" />
|
| 450 |
+
</div>
|
| 451 |
+
<p className="text-sm">
|
| 452 |
+
Copyright © {new Date().getFullYear()} — All rights reserved
|
| 453 |
+
</p>
|
| 454 |
+
</div>
|
| 455 |
+
<div className="flex items-center gap-5 mt-4 sm:mt-0">
|
| 456 |
+
<a href="#" className="hover:text-blue-400 transition">
|
| 457 |
+
<svg
|
| 458 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 459 |
+
width="24"
|
| 460 |
+
height="24"
|
| 461 |
+
viewBox="0 0 24 24"
|
| 462 |
+
className="fill-current"
|
| 463 |
+
>
|
| 464 |
+
<path d="M24 4.557a9.93 9.93 0 01-2.828.775A4.93 4.93 0 0023.337 3a9.864 9.864 0 01-3.127 1.195A4.92 4.92 0 0016.616 3c-2.72 0-4.924 2.21-4.924 4.932 0 .39.042.765.124 1.126C7.728 8.89 4.1 6.91 1.67 3.917a4.936 4.936 0 00-.665 2.48c0 1.71.86 3.213 2.17 4.096A4.9 4.9 0 01.96 9.96v.06c0 2.387 1.68 4.374 3.91 4.828a4.93 4.93 0 01-2.224.086c.626 1.956 2.444 3.384 4.6 3.425A9.874 9.874 0 010 21.54 13.945 13.945 0 007.548 24c9.056 0 14.01-7.512 14.01-14.015 0-.213-.005-.426-.015-.637A9.94 9.94 0 0024 4.557z" />
|
| 465 |
+
</svg>
|
| 466 |
+
</a>
|
| 467 |
+
<a href="#" className="hover:text-red-500 transition">
|
| 468 |
+
<svg
|
| 469 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 470 |
+
width="24"
|
| 471 |
+
height="24"
|
| 472 |
+
viewBox="0 0 24 24"
|
| 473 |
+
className="fill-current"
|
| 474 |
+
>
|
| 475 |
+
<path d="M19.615 3.184C21.403 3.67 22 5.84 22 12s-.597 8.33-2.385 8.816C17.42 21.27 12 21.27 12 21.27s-5.42 0-7.615-.454C2.597 20.33 2 18.16 2 12s.597-8.33 2.385-8.816C6.58 2.73 12 2.73 12 2.73s5.42 0 7.615.454zM10 8.5l6 3.5-6 3.5v-7z" />
|
| 476 |
+
</svg>
|
| 477 |
+
</a>
|
| 478 |
+
<a href="#" className="hover:text-blue-500 transition">
|
| 479 |
+
<svg
|
| 480 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 481 |
+
width="24"
|
| 482 |
+
height="24"
|
| 483 |
+
viewBox="0 0 24 24"
|
| 484 |
+
className="fill-current"
|
| 485 |
+
>
|
| 486 |
+
<path d="M9 8H6v4h3v12h5V12h3.642L18 8h-4V6.333C14 5.378 14.2 5 15.112 5H18V0h-3.667C10.55 0 9 1.517 9 4.308V8z" />
|
| 487 |
+
</svg>
|
| 488 |
+
</a>
|
| 489 |
</div>
|
| 490 |
+
</footer>
|
| 491 |
+
</div>
|
| 492 |
+
);
|
| 493 |
};
|
| 494 |
|
| 495 |
+
export default AIInterviewPlatform;
|
Frontend/src/pages/note.tsx
CHANGED
|
@@ -1,17 +1,14 @@
|
|
| 1 |
import ReactMarkdown from "react-markdown";
|
| 2 |
import remarkGfm from "remark-gfm";
|
| 3 |
-
import React, { useEffect, useState, useRef } from "react";
|
| 4 |
import {
|
| 5 |
-
History,
|
| 6 |
-
RefreshCw,
|
| 7 |
-
Plus,
|
| 8 |
Upload,
|
| 9 |
Menu,
|
| 10 |
X,
|
| 11 |
Send,
|
| 12 |
-
MessageSquare,
|
| 13 |
Loader2,
|
| 14 |
FileText,
|
|
|
|
| 15 |
} from "lucide-react";
|
| 16 |
import {
|
| 17 |
fetchNotes,
|
|
@@ -20,6 +17,7 @@ import {
|
|
| 20 |
createChatSession,
|
| 21 |
streamChatRequest,
|
| 22 |
fetchChatHistory,
|
|
|
|
| 23 |
type Note,
|
| 24 |
type ChatMessage,
|
| 25 |
} from "../api/notesService";
|
|
@@ -31,6 +29,10 @@ const Notes: React.FC = () => {
|
|
| 31 |
const [isUploading, setIsUploading] = useState(false);
|
| 32 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
// --- Data State ---
|
| 35 |
const [notes, setNotes] = useState<Note[]>([]);
|
| 36 |
const [currentNote, setCurrentNote] = useState<Note | null>(null);
|
|
@@ -47,6 +49,32 @@ const Notes: React.FC = () => {
|
|
| 47 |
loadNotes();
|
| 48 |
}, []);
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
const loadNotes = async () => {
|
| 51 |
try {
|
| 52 |
const data = await fetchNotes();
|
|
@@ -56,7 +84,6 @@ const Notes: React.FC = () => {
|
|
| 56 |
}
|
| 57 |
};
|
| 58 |
|
| 59 |
-
// 2. Handle File Upload
|
| 60 |
const handleUploadClick = () => fileInputRef.current?.click();
|
| 61 |
|
| 62 |
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -66,8 +93,8 @@ const Notes: React.FC = () => {
|
|
| 66 |
setIsUploading(true);
|
| 67 |
try {
|
| 68 |
const newNote = await uploadNote(file);
|
| 69 |
-
setNotes([newNote, ...notes]);
|
| 70 |
-
handleNoteSelect(newNote);
|
| 71 |
} catch (error) {
|
| 72 |
console.error("Upload failed", error);
|
| 73 |
alert("Failed to upload PDF");
|
|
@@ -76,14 +103,15 @@ const Notes: React.FC = () => {
|
|
| 76 |
}
|
| 77 |
};
|
| 78 |
|
| 79 |
-
//
|
| 80 |
const handleNoteSelect = async (note: Note) => {
|
| 81 |
setCurrentNote(note);
|
| 82 |
-
setPdfUrl(null);
|
| 83 |
-
setMessages([]);
|
| 84 |
setSessionId(null);
|
|
|
|
| 85 |
|
| 86 |
-
// A. Fetch PDF Blob
|
| 87 |
try {
|
| 88 |
const blob = await fetchNoteBlob(note.id);
|
| 89 |
const url = URL.createObjectURL(blob);
|
|
@@ -92,59 +120,81 @@ const Notes: React.FC = () => {
|
|
| 92 |
console.error("Failed to load PDF content", error);
|
| 93 |
}
|
| 94 |
|
| 95 |
-
// B.
|
| 96 |
try {
|
| 97 |
-
|
| 98 |
-
// (In a real app, you might check for existing sessions first)
|
| 99 |
-
const session = await createChatSession(
|
| 100 |
-
note.id,
|
| 101 |
-
`Chat - ${note.filename}`
|
| 102 |
-
);
|
| 103 |
-
setSessionId(session.id);
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
} catch (error) {
|
| 110 |
console.error("Failed to init chat session", error);
|
| 111 |
}
|
| 112 |
};
|
| 113 |
|
| 114 |
-
//
|
| 115 |
const handleSendMessage = async () => {
|
| 116 |
if (!inputMessage.trim() || !sessionId) return;
|
| 117 |
|
| 118 |
const userMsg = inputMessage;
|
| 119 |
-
setInputMessage("");
|
| 120 |
|
| 121 |
-
// Add User Message Optimistically
|
| 122 |
setMessages((prev) => [...prev, { role: "user", content: userMsg }]);
|
| 123 |
-
setIsChatLoading(true);
|
| 124 |
|
| 125 |
-
// Placeholder
|
| 126 |
setMessages((prev) => [...prev, { role: "assistant", content: "" }]);
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
};
|
| 149 |
|
| 150 |
return (
|
|
@@ -160,8 +210,6 @@ const Notes: React.FC = () => {
|
|
| 160 |
<h3 className="text-xl font-bold text-white mb-2 border-b border-gray-700 pb-2">
|
| 161 |
My Notes
|
| 162 |
</h3>
|
| 163 |
-
|
| 164 |
-
{/* Hidden Input for Upload */}
|
| 165 |
<input
|
| 166 |
type="file"
|
| 167 |
ref={fileInputRef}
|
|
@@ -169,7 +217,6 @@ const Notes: React.FC = () => {
|
|
| 169 |
accept="application/pdf"
|
| 170 |
onChange={handleFileChange}
|
| 171 |
/>
|
| 172 |
-
|
| 173 |
<button
|
| 174 |
onClick={handleUploadClick}
|
| 175 |
disabled={isUploading}
|
|
@@ -182,7 +229,6 @@ const Notes: React.FC = () => {
|
|
| 182 |
)}
|
| 183 |
{isUploading ? "Uploading..." : "Upload New PDF"}
|
| 184 |
</button>
|
| 185 |
-
|
| 186 |
<div className="mt-4 pt-4 border-t border-gray-700 space-y-2 overflow-y-auto">
|
| 187 |
<p className="text-sm text-gray-400 uppercase tracking-wider">
|
| 188 |
History
|
|
@@ -229,7 +275,6 @@ const Notes: React.FC = () => {
|
|
| 229 |
)}
|
| 230 |
</header>
|
| 231 |
|
| 232 |
-
{/* PDF Frame */}
|
| 233 |
<div className="flex-1 w-full h-full pt-16">
|
| 234 |
{pdfUrl ? (
|
| 235 |
<iframe
|
|
@@ -246,16 +291,27 @@ const Notes: React.FC = () => {
|
|
| 246 |
</div>
|
| 247 |
</div>
|
| 248 |
|
| 249 |
-
{/* ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
<div
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
}`}
|
| 254 |
>
|
| 255 |
{isChatOpen && (
|
| 256 |
<>
|
| 257 |
<header className="flex justify-between items-center p-4 border-b border-gray-700">
|
| 258 |
<h3 className="text-lg font-bold text-white">AI Chat</h3>
|
|
|
|
| 259 |
<button
|
| 260 |
onClick={() => setIsChatOpen(false)}
|
| 261 |
className="text-gray-400 hover:text-white"
|
|
@@ -264,7 +320,6 @@ const Notes: React.FC = () => {
|
|
| 264 |
</button>
|
| 265 |
</header>
|
| 266 |
|
| 267 |
-
{/* Messages */}
|
| 268 |
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
| 269 |
{messages.length === 0 && (
|
| 270 |
<p className="text-gray-500 text-center text-sm mt-10">
|
|
@@ -285,7 +340,6 @@ const Notes: React.FC = () => {
|
|
| 285 |
: "bg-gray-700 text-gray-200 prose prose-invert max-w-none"
|
| 286 |
}`}
|
| 287 |
>
|
| 288 |
-
{/* --- MARKDOWN RENDERING CHANGE IS HERE --- */}
|
| 289 |
{msg.role === "assistant" ? (
|
| 290 |
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
| 291 |
{msg.content}
|
|
@@ -305,7 +359,6 @@ const Notes: React.FC = () => {
|
|
| 305 |
)}
|
| 306 |
</div>
|
| 307 |
|
| 308 |
-
{/* Input */}
|
| 309 |
<div className="p-4 border-t border-gray-700">
|
| 310 |
<div className="flex gap-2">
|
| 311 |
<input
|
|
@@ -315,12 +368,12 @@ const Notes: React.FC = () => {
|
|
| 315 |
onKeyDown={(e) => e.key === "Enter" && handleSendMessage()}
|
| 316 |
placeholder="Type your question..."
|
| 317 |
className="flex-1 bg-gray-800 text-white rounded-lg px-4 py-2 border border-gray-700 focus:outline-none focus:border-blue-500"
|
| 318 |
-
disabled={!sessionId}
|
| 319 |
/>
|
| 320 |
<button
|
| 321 |
onClick={handleSendMessage}
|
| 322 |
disabled={!sessionId || isChatLoading}
|
| 323 |
-
className="bg-blue-600 p-2 rounded-lg text-white hover:bg-blue-700 disabled:opacity-50"
|
| 324 |
>
|
| 325 |
<Send size={20} />
|
| 326 |
</button>
|
|
|
|
| 1 |
import ReactMarkdown from "react-markdown";
|
| 2 |
import remarkGfm from "remark-gfm";
|
| 3 |
+
import React, { useEffect, useState, useRef, useCallback } from "react";
|
| 4 |
import {
|
|
|
|
|
|
|
|
|
|
| 5 |
Upload,
|
| 6 |
Menu,
|
| 7 |
X,
|
| 8 |
Send,
|
|
|
|
| 9 |
Loader2,
|
| 10 |
FileText,
|
| 11 |
+
MessageSquare,
|
| 12 |
} from "lucide-react";
|
| 13 |
import {
|
| 14 |
fetchNotes,
|
|
|
|
| 17 |
createChatSession,
|
| 18 |
streamChatRequest,
|
| 19 |
fetchChatHistory,
|
| 20 |
+
fetchSessions, // ✅ Import this
|
| 21 |
type Note,
|
| 22 |
type ChatMessage,
|
| 23 |
} from "../api/notesService";
|
|
|
|
| 29 |
const [isUploading, setIsUploading] = useState(false);
|
| 30 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 31 |
|
| 32 |
+
// ✅ Resizable Chat State
|
| 33 |
+
const [chatWidth, setChatWidth] = useState(450); // Default width
|
| 34 |
+
const [isResizing, setIsResizing] = useState(false);
|
| 35 |
+
|
| 36 |
// --- Data State ---
|
| 37 |
const [notes, setNotes] = useState<Note[]>([]);
|
| 38 |
const [currentNote, setCurrentNote] = useState<Note | null>(null);
|
|
|
|
| 49 |
loadNotes();
|
| 50 |
}, []);
|
| 51 |
|
| 52 |
+
// ✅ Handle Resizing Logic
|
| 53 |
+
const startResizing = useCallback(() => setIsResizing(true), []);
|
| 54 |
+
const stopResizing = useCallback(() => setIsResizing(false), []);
|
| 55 |
+
|
| 56 |
+
const resize = useCallback(
|
| 57 |
+
(mouseMoveEvent: MouseEvent) => {
|
| 58 |
+
if (isResizing) {
|
| 59 |
+
// Calculate new width based on mouse position from the right edge
|
| 60 |
+
const newWidth = document.body.clientWidth - mouseMoveEvent.clientX;
|
| 61 |
+
if (newWidth > 300 && newWidth < 800) {
|
| 62 |
+
setChatWidth(newWidth);
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
[isResizing]
|
| 67 |
+
);
|
| 68 |
+
|
| 69 |
+
useEffect(() => {
|
| 70 |
+
window.addEventListener("mousemove", resize);
|
| 71 |
+
window.addEventListener("mouseup", stopResizing);
|
| 72 |
+
return () => {
|
| 73 |
+
window.removeEventListener("mousemove", resize);
|
| 74 |
+
window.removeEventListener("mouseup", stopResizing);
|
| 75 |
+
};
|
| 76 |
+
}, [resize, stopResizing]);
|
| 77 |
+
|
| 78 |
const loadNotes = async () => {
|
| 79 |
try {
|
| 80 |
const data = await fetchNotes();
|
|
|
|
| 84 |
}
|
| 85 |
};
|
| 86 |
|
|
|
|
| 87 |
const handleUploadClick = () => fileInputRef.current?.click();
|
| 88 |
|
| 89 |
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
| 93 |
setIsUploading(true);
|
| 94 |
try {
|
| 95 |
const newNote = await uploadNote(file);
|
| 96 |
+
setNotes([newNote, ...notes]);
|
| 97 |
+
handleNoteSelect(newNote);
|
| 98 |
} catch (error) {
|
| 99 |
console.error("Upload failed", error);
|
| 100 |
alert("Failed to upload PDF");
|
|
|
|
| 103 |
}
|
| 104 |
};
|
| 105 |
|
| 106 |
+
// ✅ FIX: Load History Logic (Issue 1)
|
| 107 |
const handleNoteSelect = async (note: Note) => {
|
| 108 |
setCurrentNote(note);
|
| 109 |
+
setPdfUrl(null);
|
| 110 |
+
setMessages([]);
|
| 111 |
setSessionId(null);
|
| 112 |
+
setIsChatOpen(true); // Auto open chat on select
|
| 113 |
|
| 114 |
+
// A. Fetch PDF Blob
|
| 115 |
try {
|
| 116 |
const blob = await fetchNoteBlob(note.id);
|
| 117 |
const url = URL.createObjectURL(blob);
|
|
|
|
| 120 |
console.error("Failed to load PDF content", error);
|
| 121 |
}
|
| 122 |
|
| 123 |
+
// B. Check for existing sessions -> Get History OR Create New
|
| 124 |
try {
|
| 125 |
+
const existingSessions = await fetchSessions(note.id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
+
if (existingSessions.length > 0) {
|
| 128 |
+
// Load the most recent session
|
| 129 |
+
const lastSession = existingSessions[0];
|
| 130 |
+
setSessionId(lastSession.id);
|
| 131 |
+
|
| 132 |
+
// Fetch actual messages
|
| 133 |
+
const history = await fetchChatHistory(lastSession.id);
|
| 134 |
+
// Map backend history to frontend format
|
| 135 |
+
const formattedHistory: ChatMessage[] = history.map((msg: any) => ({
|
| 136 |
+
role: msg.role,
|
| 137 |
+
content: msg.content,
|
| 138 |
+
}));
|
| 139 |
+
setMessages(formattedHistory);
|
| 140 |
+
} else {
|
| 141 |
+
// No session exists, create one
|
| 142 |
+
const session = await createChatSession(
|
| 143 |
+
note.id,
|
| 144 |
+
`Chat - ${note.filename}`
|
| 145 |
+
);
|
| 146 |
+
setSessionId(session.id);
|
| 147 |
+
setMessages([
|
| 148 |
+
{
|
| 149 |
+
role: "assistant",
|
| 150 |
+
content: `Ready to chat about ${note.filename}!`,
|
| 151 |
+
},
|
| 152 |
+
]);
|
| 153 |
+
}
|
| 154 |
} catch (error) {
|
| 155 |
console.error("Failed to init chat session", error);
|
| 156 |
}
|
| 157 |
};
|
| 158 |
|
| 159 |
+
// ✅ FIX: Loading State Bug (Issue 4)
|
| 160 |
const handleSendMessage = async () => {
|
| 161 |
if (!inputMessage.trim() || !sessionId) return;
|
| 162 |
|
| 163 |
const userMsg = inputMessage;
|
| 164 |
+
setInputMessage("");
|
| 165 |
|
|
|
|
| 166 |
setMessages((prev) => [...prev, { role: "user", content: userMsg }]);
|
| 167 |
+
setIsChatLoading(true); // Start loading
|
| 168 |
|
| 169 |
+
// Placeholder
|
| 170 |
setMessages((prev) => [...prev, { role: "assistant", content: "" }]);
|
| 171 |
|
| 172 |
+
try {
|
| 173 |
+
await streamChatRequest(
|
| 174 |
+
sessionId,
|
| 175 |
+
userMsg,
|
| 176 |
+
(chunk) => {
|
| 177 |
+
setMessages((prev) => {
|
| 178 |
+
const newArr = [...prev];
|
| 179 |
+
const lastIndex = newArr.length - 1;
|
| 180 |
+
newArr[lastIndex] = {
|
| 181 |
+
...newArr[lastIndex],
|
| 182 |
+
content: newArr[lastIndex].content + chunk,
|
| 183 |
+
};
|
| 184 |
+
return newArr;
|
| 185 |
+
});
|
| 186 |
+
},
|
| 187 |
+
(err) => {
|
| 188 |
+
console.error("Stream error", err);
|
| 189 |
+
// Don't set loading false here, let finally handle it
|
| 190 |
+
}
|
| 191 |
+
);
|
| 192 |
+
} catch (e) {
|
| 193 |
+
console.error("Chat Request Error", e);
|
| 194 |
+
} finally {
|
| 195 |
+
// ✅ Ensure loading stops regardless of success/fail so button enables
|
| 196 |
+
setIsChatLoading(false);
|
| 197 |
+
}
|
| 198 |
};
|
| 199 |
|
| 200 |
return (
|
|
|
|
| 210 |
<h3 className="text-xl font-bold text-white mb-2 border-b border-gray-700 pb-2">
|
| 211 |
My Notes
|
| 212 |
</h3>
|
|
|
|
|
|
|
| 213 |
<input
|
| 214 |
type="file"
|
| 215 |
ref={fileInputRef}
|
|
|
|
| 217 |
accept="application/pdf"
|
| 218 |
onChange={handleFileChange}
|
| 219 |
/>
|
|
|
|
| 220 |
<button
|
| 221 |
onClick={handleUploadClick}
|
| 222 |
disabled={isUploading}
|
|
|
|
| 229 |
)}
|
| 230 |
{isUploading ? "Uploading..." : "Upload New PDF"}
|
| 231 |
</button>
|
|
|
|
| 232 |
<div className="mt-4 pt-4 border-t border-gray-700 space-y-2 overflow-y-auto">
|
| 233 |
<p className="text-sm text-gray-400 uppercase tracking-wider">
|
| 234 |
History
|
|
|
|
| 275 |
)}
|
| 276 |
</header>
|
| 277 |
|
|
|
|
| 278 |
<div className="flex-1 w-full h-full pt-16">
|
| 279 |
{pdfUrl ? (
|
| 280 |
<iframe
|
|
|
|
| 291 |
</div>
|
| 292 |
</div>
|
| 293 |
|
| 294 |
+
{/* --- ✅ Resizable Chat Panel (Issue 3) --- */}
|
| 295 |
+
{isChatOpen && (
|
| 296 |
+
// Drag Handle
|
| 297 |
+
<div
|
| 298 |
+
className="w-1.5 cursor-col-resize bg-gray-800 hover:bg-blue-500 transition-colors z-20 flex items-center justify-center"
|
| 299 |
+
onMouseDown={startResizing}
|
| 300 |
+
>
|
| 301 |
+
{/* Tiny indicator for grip */}
|
| 302 |
+
<div className="h-8 w-0.5 bg-gray-600 rounded"></div>
|
| 303 |
+
</div>
|
| 304 |
+
)}
|
| 305 |
+
|
| 306 |
<div
|
| 307 |
+
style={{ width: isChatOpen ? chatWidth : 0 }}
|
| 308 |
+
className={`flex flex-col bg-gray-900 border-l border-gray-700 flex-shrink-0 transition-all duration-75 ease-out`}
|
|
|
|
| 309 |
>
|
| 310 |
{isChatOpen && (
|
| 311 |
<>
|
| 312 |
<header className="flex justify-between items-center p-4 border-b border-gray-700">
|
| 313 |
<h3 className="text-lg font-bold text-white">AI Chat</h3>
|
| 314 |
+
{/* Close Button is here */}
|
| 315 |
<button
|
| 316 |
onClick={() => setIsChatOpen(false)}
|
| 317 |
className="text-gray-400 hover:text-white"
|
|
|
|
| 320 |
</button>
|
| 321 |
</header>
|
| 322 |
|
|
|
|
| 323 |
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
| 324 |
{messages.length === 0 && (
|
| 325 |
<p className="text-gray-500 text-center text-sm mt-10">
|
|
|
|
| 340 |
: "bg-gray-700 text-gray-200 prose prose-invert max-w-none"
|
| 341 |
}`}
|
| 342 |
>
|
|
|
|
| 343 |
{msg.role === "assistant" ? (
|
| 344 |
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
| 345 |
{msg.content}
|
|
|
|
| 359 |
)}
|
| 360 |
</div>
|
| 361 |
|
|
|
|
| 362 |
<div className="p-4 border-t border-gray-700">
|
| 363 |
<div className="flex gap-2">
|
| 364 |
<input
|
|
|
|
| 368 |
onKeyDown={(e) => e.key === "Enter" && handleSendMessage()}
|
| 369 |
placeholder="Type your question..."
|
| 370 |
className="flex-1 bg-gray-800 text-white rounded-lg px-4 py-2 border border-gray-700 focus:outline-none focus:border-blue-500"
|
| 371 |
+
disabled={!sessionId || isChatLoading}
|
| 372 |
/>
|
| 373 |
<button
|
| 374 |
onClick={handleSendMessage}
|
| 375 |
disabled={!sessionId || isChatLoading}
|
| 376 |
+
className="bg-blue-600 p-2 rounded-lg text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
| 377 |
>
|
| 378 |
<Send size={20} />
|
| 379 |
</button>
|