import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { BookmarkIcon } from "@heroicons/react/24/outline"; import { TrashIcon } from "@heroicons/react/24/outline"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { SparklesIcon } from "@heroicons/react/24/outline"; // const API_PATH = "/chat"; // const THREAD_KEY = "lg_thread_id"; import { useChat } from "./useChat"; import type { ThreadMeta } from "./threads"; const SUGGESTIONS = [ { title: "Quick intro to Krishna", text: "Give me a 90-second intro to Krishna Vamsi Dhulipalla—recent work, top strengths, and impact.", }, { title: "Get Krishna’s resume", text: "Share Krishna’s latest resume and provide a download link.", }, { title: "What this agent can do", text: "What tools and actions can you perform for me? Show examples and how to use them.", }, { title: "Schedule/modify a meeting", text: "Schedule a 30-minute meeting with Krishna next week and show how I can reschedule or cancel.", }, ]; // --- Helpers for message actions --- const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text); } catch { console.error("Failed to copy text to clipboard"); } }; const getLastUserMessage = (msgs: { role: string; content: string }[]) => [...msgs].reverse().find((m) => m.role === "user") || null; export default function App() { const { threads, active, messages, setActiveThread, newChat, clearChat, deleteThread, send, isStreaming, hasFirstToken, } = useChat(); const [input, setInput] = useState(""); const bottomRef = useRef(null); const inputRef = useRef(null); const prevThreadId = useRef(null); // Scroll on message changes useEffect(() => { const currentThreadId = active?.id ?? null; // If the thread changed, scroll instantly to bottom if (currentThreadId !== prevThreadId.current) { prevThreadId.current = currentThreadId; bottomRef.current?.scrollIntoView({ behavior: "auto" }); // instant scroll } else { // If same thread but messages changed, smooth scroll bottomRef.current?.scrollIntoView({ behavior: "smooth" }); } }, [messages, active?.id]); const handleShare = async () => { const url = window.location.href; const title = document.title || "My Chat"; try { if (navigator.share) { await navigator.share({ title, url }); } else { await navigator.clipboard.writeText(url); // optionally toast: "Link copied" } } catch { // ignored } }; const handleBookmark = () => { // Browsers don't allow programmatic bookmarks; show the right shortcut. const isMac = navigator.platform.toUpperCase().includes("MAC"); const combo = isMac ? "⌘ + D" : "Ctrl + D"; alert(`Press ${combo} to bookmark this page.`); }; const sendMessage = useCallback(() => { const text = input.trim(); if (!text || isStreaming) return; send(text); setInput(""); }, [input, isStreaming, send]); const selectSuggestion = useCallback((text: string) => { setInput(text); requestAnimationFrame(() => inputRef.current?.focus()); }, []); const sendSuggestion = useCallback( (text: string) => { if (isStreaming) return; setInput(text); setTimeout(() => sendMessage(), 0); }, [isStreaming, sendMessage] ); const onKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }, [sendMessage] ); const REGEN_PREFIX = "Regenerate this response with a different angle:\n\n"; const sendPrefixed = useCallback( (prefix: string, text: string) => { if (!text || isStreaming) return; // your hook’s `send` already appends messages & streams send(`${prefix}${text}`); setInput(""); }, [send, isStreaming] ); // Sidebar const Sidebar = useMemo( () => ( ), [active?.id, clearChat, newChat, setActiveThread, deleteThread, threads] ); return (
{Sidebar} {/* Main column */}
{/* Header minimal */}
Krishna’s Assistant
ID: {active?.id ? active.id.slice(0, 8) : "…"}
{/* LinkedIn */} {/* GitHub */}
{/* Messages */}
{messages.length === 0 ? ( ) : (
{messages.map((m, idx) => { const isAssistant = m.role === "assistant"; const emptyAssistant = isAssistant && (!m.content || m.content.trim() === ""); if (emptyAssistant) return null; // hide blank bubble const key = m.id ?? `m-${idx}`; // NEW stable key return (
{/* bubble row */} {/* actions row – only for assistant & only when not streaming */} {isAssistant && !isStreaming && (
{ // Prefill composer with the *last user* prompt (ChatGPT-style “Edit”) const lastUser = getLastUserMessage(messages); setInput(lastUser ? lastUser.content : m.content); requestAnimationFrame(() => inputRef.current?.focus() ); }} onRegenerate={() => { // Resend last user prompt const lastUser = getLastUserMessage(messages); if (!lastUser) return; sendPrefixed(REGEN_PREFIX, lastUser.content); }} />
)}
); })} {/* Thinking indicator (only BEFORE first token) */} {isStreaming && !hasFirstToken && (
)}
)}
{/* Warm bottom glow (simple bar + blur)
*/} {/* Composer */}