ChatBot / ui /src /App.tsx
krishnadhulipalla's picture
fixed build errors
96b23d9
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
TrashIcon,
SparklesIcon,
PaperClipIcon,
MicrophoneIcon,
PaperAirplaneIcon,
ArrowPathIcon,
DocumentDuplicateIcon,
ChatBubbleLeftRightIcon,
CommandLineIcon,
} from "@heroicons/react/24/outline";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
// Internal hooks/types
import { useChat } from "./useChat";
import type { ThreadMeta } from "./threads";
// --- Constants & Config ---
const APP_TITLE = "Krishna's Digital Twin";
const BOT_AVATAR =
"https://api.dicebear.com/9.x/bottts-neutral/svg?seed=Krishna1&backgroundColor=6366f1"; // Cleaner 3D-ish Robot
/*
const DID_YOU_KNOW = [
"Krishna achieved a 3.95 GPA during his M.S. at Virginia Tech.",
"Krishna built an agent that automates Android UI tasks with 80%+ accuracy.",
"Krishna optimized genomic ETL pipelines reducing runtime by 70%.",
"Krishna specializes in building autonomous agents and RAG systems.",
];
*/
const SUGGESTIONS = [
{
title: "Summarize Experience",
text: "Give me a 90-second intro to Krishna Vamsi Dhulipalla—recent work, top strengths, and impact.",
icon: "✨",
},
{
title: "Download Resume",
text: "Share Krishna’s latest resume and provide a download link.",
icon: "📄",
},
{
title: "Capabilities",
text: "What tools and actions can you perform for me?",
icon: "🛠️",
},
{
title: "Schedule Meeting",
text: "Schedule a 30-minute meeting with Krishna next week.",
icon: "📅",
},
];
/*
function DidYouKnowRotator() {
const [index, setIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setIndex((prev) => (prev + 1) % DID_YOU_KNOW.length);
}, 5000); // Rotate every 5 seconds
return () => clearInterval(interval);
}, []);
return (
<div className="mt-8 p-4 rounded-xl bg-white/5 border border-white/5 max-w-lg mx-auto text-center animate-in fade-in slide-in-from-bottom-2 duration-700">
<p className="text-[10px] uppercase tracking-widest text-zinc-500 font-semibold mb-2">
Did you know?
</p>
<p
key={index}
className="text-sm text-zinc-300 italic min-h-[20px] transition-all duration-500"
>
"{DID_YOU_KNOW[index]}"
</p>
</div>
);
}
*/
// --- Helper: Date Grouping for Sidebar ---
function groupThreadsByDate(threads: ThreadMeta[]) {
const today = new Date();
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const groups: Record<string, ThreadMeta[]> = {
Today: [],
Yesterday: [],
"Previous 7 Days": [],
Older: [],
};
threads.forEach((t) => {
const d = new Date(t.lastAt);
if (d.toDateString() === today.toDateString()) {
groups["Today"].push(t);
} else if (d.toDateString() === yesterday.toDateString()) {
groups["Yesterday"].push(t);
} else if (d.getTime() > today.getTime() - 7 * 24 * 60 * 60 * 1000) {
groups["Previous 7 Days"].push(t);
} else {
groups["Older"].push(t);
}
});
return groups;
}
// --- Main Component ---
export default function App() {
const {
threads,
active,
messages,
setActiveThread,
newChat,
clearChat,
deleteThread,
send,
isStreaming,
hasFirstToken,
} = useChat();
const [input, setInput] = useState("");
const bottomRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const prevThreadId = useRef<string | null>(null);
// --- Voice Input Logic ---
const [isListening, setIsListening] = useState(false);
const recognitionRef = useRef<any>(null);
useEffect(() => {
const SpeechRecognition =
(window as any).SpeechRecognition ||
(window as any).webkitSpeechRecognition;
if (SpeechRecognition) {
recognitionRef.current = new SpeechRecognition();
recognitionRef.current.continuous = false;
recognitionRef.current.interimResults = false;
recognitionRef.current.lang = "en-US";
recognitionRef.current.onresult = (event: any) => {
const transcript = event.results[0][0].transcript;
setInput((prev) => (prev ? prev + " " + transcript : transcript));
setIsListening(false);
};
recognitionRef.current.onerror = (event: any) => {
console.error("Speech recognition error", event.error);
setIsListening(false);
};
recognitionRef.current.onend = () => {
setIsListening(false);
};
}
}, []);
const toggleListening = useCallback(() => {
if (!recognitionRef.current) {
alert("Browser does not support Speech Recognition");
return;
}
if (isListening) {
recognitionRef.current.stop();
setIsListening(false);
} else {
recognitionRef.current.start();
setIsListening(true);
}
}, [isListening]);
// Auto-scroll logic
useEffect(() => {
const currentThreadId = active?.id ?? null;
if (currentThreadId !== prevThreadId.current) {
prevThreadId.current = currentThreadId;
bottomRef.current?.scrollIntoView({ behavior: "auto" });
} else {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [messages, active?.id]);
const sendMessage = useCallback(() => {
const text = input.trim();
if (!text || isStreaming) return;
send(text);
setInput("");
}, [input, isStreaming, send]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
},
[sendMessage]
);
const groupedThreads = useMemo(() => groupThreadsByDate(threads), [threads]);
return (
<div className="flex h-screen w-screen bg-[#05050A] text-zinc-100 font-sans selection:bg-indigo-500/30">
{/* Background Ambience */}
<div className="fixed inset-0 z-0 pointer-events-none">
<div className="absolute top-[-10%] right-[-5%] w-[500px] h-[500px] bg-indigo-900/20 rounded-full blur-[128px]" />
<div className="absolute bottom-[-10%] left-[-10%] w-[600px] h-[600px] bg-purple-900/10 rounded-full blur-[128px]" />
</div>
{/* --- Sidebar --- */}
<aside className="hidden md:flex flex-col w-[280px] z-10 bg-zinc-950/40 backdrop-blur-xl border-r border-white/5 transition-all">
{/* Header */}
<div className="p-4 border-b border-white/5">
<button
onClick={newChat}
className="group flex items-center gap-3 w-full px-3 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/5 transition-all duration-200"
>
<div className="p-1.5 rounded-lg bg-indigo-500/10 text-indigo-400 group-hover:text-indigo-300">
<SparklesIcon className="w-5 h-5" />
</div>
<span className="text-sm font-medium text-zinc-200">New Chat</span>
</button>
</div>
{/* History List */}
<div className="flex-1 overflow-y-auto p-3 space-y-6 scrollbar-thin scrollbar-thumb-zinc-800">
{Object.entries(groupedThreads).map(([label, group]) => {
if (group.length === 0) return null;
return (
<div key={label}>
<h3 className="px-3 mb-2 text-[11px] font-semibold uppercase tracking-wider text-zinc-500">
{label}
</h3>
<div className="space-y-0.5">
{group.map((t) => (
<div
key={t.id}
onClick={() => setActiveThread(t)}
className={`group relative flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer transition-colors ${
active?.id === t.id
? "bg-white/10 text-zinc-100"
: "text-zinc-400 hover:bg-white/5 hover:text-zinc-200"
}`}
>
<span
className="text-sm flex-1 line-clamp-1 break-all"
title={t.title || "New Conversation"}
>
{t.title || "New Conversation"}
</span>
{/* Delete Action */}
<button
onClick={(e) => {
e.stopPropagation();
if (window.confirm("Delete thread?"))
deleteThread(t.id);
}}
className="opacity-0 group-hover:opacity-100 p-1 rounded hover:bg-red-500/20 text-zinc-500 hover:text-red-400 transition-all"
>
<TrashIcon className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
</div>
);
})}
</div>
{/* Sidebar Footer */}
<div className="p-4 border-t border-white/5">
<a
href="https://github.com/krishna-dhulipalla/LangGraph_ChatBot"
target="_blank"
rel="noreferrer"
className="flex items-center gap-2 text-xs text-zinc-500 hover:text-zinc-300 transition-colors"
>
<CommandLineIcon className="w-4 h-4" />
<span>View Source Code</span>
</a>
</div>
</aside>
{/* --- Main Chat Area --- */}
<main className="relative z-10 flex-1 flex flex-col h-full overflow-hidden">
{/* Top Navigation */}
<header className="h-16 flex items-center justify-between px-6 border-b border-white/5 bg-zinc-950/20 backdrop-blur-sm">
<div className="flex items-center gap-3">
<div className="relative">
<img
src={BOT_AVATAR}
alt="Bot"
className="w-9 h-9 rounded-full ring-2 ring-indigo-500/20 shadow-lg shadow-indigo-500/10"
/>
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-emerald-500 border-2 border-[#05050A] rounded-full"></span>
</div>
<div>
<h1 className="text-sm font-semibold text-zinc-100">
{APP_TITLE}
</h1>
<p className="text-[10px] text-zinc-400 flex items-center gap-1">
<span className="inline-block w-1 h-1 rounded-full bg-indigo-500 animate-pulse" />
Online & Ready
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={clearChat}
className="p-2 text-zinc-400 hover:text-zinc-200 hover:bg-white/5 rounded-full transition-all"
title="Clear Chat"
>
<ArrowPathIcon className="w-5 h-5" />
</button>
</div>
</header>
{/* Chat Stream */}
<div className="flex-1 overflow-y-auto p-4 md:p-8 scroll-smooth">
{messages.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center max-w-2xl mx-auto animate-in fade-in duration-500">
<div className="mb-8 p-4 rounded-full bg-white/5 ring-1 ring-white/10 shadow-2xl shadow-indigo-500/10">
<ChatBubbleLeftRightIcon className="w-10 h-10 text-indigo-400" />
</div>
<h2 className="text-2xl font-semibold text-transparent bg-clip-text bg-gradient-to-br from-zinc-100 to-zinc-500 mb-2">
How can I help you today?
</h2>
<p className="text-zinc-400 text-sm mb-6 max-w-md text-center">
I'm Krishna's digital twin. Ask me about his architecture
skills, recent projects, or schedule a meeting.
</p>
{/* <DidYouKnowRotator /> */}
{/* <div className="h-8"></div> */}
{/* Suggestions Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 w-full">
{SUGGESTIONS.map((s, i) => (
<button
key={i}
onClick={() => {
// Just send immediately without prefilling input state visually
if (!isStreaming) send(s.text);
}}
className="group flex items-start gap-4 p-4 rounded-2xl bg-white/5 hover:bg-white/10 border border-white/5 hover:border-white/10 text-left transition-all hover:-translate-y-0.5 hover:shadow-lg hover:shadow-indigo-500/10"
>
<span className="text-xl grayscale group-hover:grayscale-0 transition-all">
{s.icon}
</span>
<div>
<div className="text-sm font-medium text-zinc-200 group-hover:text-indigo-300 transition-colors">
{s.title}
</div>
<div className="text-xs text-zinc-500 mt-1 line-clamp-1 group-hover:text-zinc-400 transition-colors">
{s.text}
</div>
</div>
</button>
))}
</div>
</div>
) : (
<div className="max-w-3xl mx-auto space-y-6">
{messages.map((m, idx) => (
<Bubble
key={m.id || idx}
message={m}
isStreaming={isStreaming && idx === messages.length - 1}
/>
))}
{/* Streaming Indicator */}
{isStreaming && !hasFirstToken && (
<div className="flex gap-1 ml-14">
<span className="w-2 h-2 rounded-full bg-zinc-600 animate-bounce" />
<span className="w-2 h-2 rounded-full bg-zinc-600 animate-bounce [animation-delay:0.1s]" />
<span className="w-2 h-2 rounded-full bg-zinc-600 animate-bounce [animation-delay:0.2s]" />
</div>
)}
<div ref={bottomRef} className="h-4" />
</div>
)}
</div>
{/* --- Input Area --- */}
<div className="p-4 md:p-6 pb-8">
<div className="max-w-3xl mx-auto relative">
<div className="relative flex flex-col gap-2 p-2 rounded-3xl bg-zinc-900/50 backdrop-blur-xl border border-white/10 shadow-2xl focus-within:ring-1 focus-within:ring-indigo-500/50 transition-all">
<textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type a message..."
className="w-full bg-transparent text-zinc-100 placeholder-zinc-500 px-4 py-3 min-h-[50px] max-h-[200px] resize-none outline-none text-[15px] leading-relaxed scrollbar-hide"
rows={1}
/>
{/* Input Actions Bar */}
<div className="flex items-center justify-between px-2 pb-1">
<div className="flex items-center gap-1">
<button
className="cursor-not-allowed opacity-50 p-2 rounded-xl text-zinc-400 hover:text-zinc-200 hover:bg-white/5 transition-colors"
title="Attach file (coming soon)"
>
<PaperClipIcon className="w-5 h-5" />
</button>
<button
onClick={toggleListening}
className={`p-2 rounded-xl transition-all duration-300 ${
isListening
? "text-red-400 bg-red-500/10 animate-pulse ring-1 ring-red-500/20"
: "text-zinc-400 hover:text-zinc-200 hover:bg-white/5"
}`}
title={isListening ? "Stop Listening" : "Voice Input"}
>
<MicrophoneIcon className="w-5 h-5" />
</button>
</div>
<button
onClick={sendMessage}
disabled={!input.trim() || isStreaming}
className={`p-2 rounded-xl flex items-center gap-2 transition-all ${
input.trim() && !isStreaming
? "bg-indigo-600 text-white shadow-lg shadow-indigo-500/20 hover:bg-indigo-500"
: "bg-zinc-800 text-zinc-500 cursor-not-allowed"
}`}
>
{isStreaming ? (
<span className="w-5 h-5 border-2 border-white/20 border-t-white rounded-full animate-spin" />
) : (
<PaperAirplaneIcon className="w-5 h-5 -ml-0.5 transform -rotate-45 translate-x-0.5" />
)}
</button>
</div>
</div>
<div className="text-center mt-3">
<p className="text-[10px] text-zinc-600">
AI can make mistakes. Please verify important information.
</p>
</div>
</div>
</div>
</main>
</div>
);
}
// --- Message Bubble Component ---
function Bubble({
message,
isStreaming,
}: {
message: any;
isStreaming: boolean;
}) {
const isUser = message.role === "user";
return (
<div
className={`flex gap-4 ${isUser ? "justify-end" : "justify-start group"}`}
>
{!isUser && (
<div className="shrink-0 flex flex-col gap-2">
<img
src={BOT_AVATAR}
alt="AI"
className="w-8 h-8 rounded-full ring-1 ring-white/10"
/>
</div>
)}
<div
className={`relative max-w-[85%] md:max-w-[75%] rounded-2xl px-5 py-3.5 shadow-sm text-[15px] leading-7 ${
isUser
? "bg-gradient-to-br from-indigo-600 to-violet-600 text-white rounded-br-sm shadow-md shadow-indigo-500/10"
: "bg-white/5 border border-white/5 text-zinc-100 rounded-bl-sm backdrop-blur-md"
}`}
>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ ...props }) => (
<a
{...props}
className="text-blue-400 hover:underline"
target="_blank"
/>
),
code: ({ inline, className, children, ...props }: any) => {
if (inline)
return (
<code
className="bg-white/10 px-1 py-0.5 rounded font-mono text-sm"
{...props}
>
{children}
</code>
);
return (
<pre className="bg-zinc-950/50 p-3 rounded-xl border border-white/5 overflow-x-auto my-2 text-sm">
<code className={className} {...props}>
{children}
</code>
</pre>
);
},
ul: (props) => (
<ul
className="list-disc list-inside ml-2 space-y-1 my-2"
{...props}
/>
),
ol: (props) => (
<ol
className="list-decimal list-inside ml-2 space-y-1 my-2"
{...props}
/>
),
p: (props) => <p className="mb-2 last:mb-0" {...props} />,
}}
>
{message.content}
</ReactMarkdown>
{/* Actions for Assistant */}
{!isUser && !isStreaming && (
<div className="absolute -bottom-6 left-0 opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-2">
<ActionButton
icon={<DocumentDuplicateIcon className="w-3.5 h-3.5" />}
label="Copy"
onClick={() => navigator.clipboard.writeText(message.content)}
/>
<ActionButton
icon={<ArrowPathIcon className="w-3.5 h-3.5" />}
label="Regenerate"
onClick={() => {}}
/>
</div>
)}
</div>
</div>
);
}
function ActionButton({
icon,
label,
onClick,
}: {
icon: any;
label: string;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
className="flex items-center gap-1 text-[10px] text-zinc-500 hover:text-zinc-300 bg-white/5 px-2 py-1 rounded-md transition-colors"
>
{icon} {label}
</button>
);
}