NEON / frontend /src /App.tsx
picklefried706's picture
Upload folder using huggingface_hub
40a9423 verified
import { useEffect, useMemo, useRef, useState } from "react";
type Role = "user" | "assistant" | "system";
type ChatMessage = {
id: string;
role: Role;
content: string;
createdAt: number;
};
type StatusEvent = {
message: string;
};
const SHORTCUTS = [
{ combo: "Ctrl+Enter", action: "Send" },
{ combo: "Ctrl+K", action: "Focus input" },
{ combo: "Esc", action: "Clear" }
];
function useSessionId() {
const [sessionId] = useState(() => {
const cached = localStorage.getItem("novachat_session");
if (cached) return cached;
const created = crypto.randomUUID();
localStorage.setItem("novachat_session", created);
return created;
});
return sessionId;
}
export default function App() {
const sessionId = useSessionId();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState("");
const [status, setStatus] = useState("Idle");
const [isStreaming, setIsStreaming] = useState(false);
const [theme, setTheme] = useState(() => localStorage.getItem("novachat_theme") ?? "dark");
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const bottomRef = useRef<HTMLDivElement | null>(null);
const streamRef = useRef<EventSource | null>(null);
useEffect(() => {
document.documentElement.dataset.theme = theme;
localStorage.setItem("novachat_theme", theme);
}, [theme]);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, status]);
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key.toLowerCase() === "k") {
event.preventDefault();
inputRef.current?.focus();
}
if (event.ctrlKey && event.key === "Enter") {
event.preventDefault();
handleSend();
}
if (event.key === "Escape") {
setInput("");
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
});
const groupedMessages = useMemo(() => {
const groups: ChatMessage[][] = [];
messages.forEach((msg) => {
const lastGroup = groups[groups.length - 1];
if (lastGroup && lastGroup[lastGroup.length - 1].role === msg.role) {
lastGroup.push(msg);
} else {
groups.push([msg]);
}
});
return groups;
}, [messages]);
const handleSend = () => {
const trimmed = input.trim();
if (!trimmed || isStreaming) return;
const userMessage: ChatMessage = {
id: `user_${Date.now()}`,
role: "user",
content: trimmed,
createdAt: Date.now()
};
setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsStreaming(true);
setStatus("Connecting...");
streamRef.current?.close();
const url = new URL("/api/chat/stream", window.location.origin);
url.searchParams.set("sessionId", sessionId);
url.searchParams.set("message", trimmed);
const stream = new EventSource(url);
streamRef.current = stream;
let assistantId = `assistant_${Date.now()}`;
const appendAssistant = (token: string) => {
setMessages((prev) => {
const next = [...prev];
const existing = next.find((msg) => msg.id === assistantId);
if (existing) {
existing.content += token;
return [...next];
}
return [...next, { id: assistantId, role: "assistant", content: token, createdAt: Date.now() }];
});
};
stream.addEventListener("status", (event) => {
const data = JSON.parse((event as MessageEvent).data) as StatusEvent;
setStatus(data.message);
});
stream.addEventListener("delta", (event) => {
const data = JSON.parse((event as MessageEvent).data) as { token: string };
appendAssistant(data.token);
setStatus("Responding...");
});
stream.addEventListener("done", () => {
setIsStreaming(false);
setStatus("Idle");
stream.close();
});
stream.addEventListener("error", (event) => {
setIsStreaming(false);
setStatus("Connection error");
stream.close();
});
};
return (
<div className="app">
<header className="topbar">
<div className="brand">
<div className="brand-mark" />
<div>
<div className="brand-title">NovaChat</div>
<div className="brand-subtitle">Real-time intelligence engine</div>
</div>
</div>
<div className="topbar-actions">
<button className="ghost" onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
{theme === "dark" ? "Light" : "Dark"} mode
</button>
<button className="ghost" onClick={() => setMessages([])}>
New session
</button>
</div>
</header>
<main className="layout">
<section className="chat">
<div className="chat-header">
<div>
<div className="chat-title">Control Room</div>
<div className="chat-subtitle">Ultra-low latency responses with live web search.</div>
</div>
<div className="status">
<span className={isStreaming ? "pulse" : "dot"} />
{status}
</div>
</div>
<div className="chat-body">
{groupedMessages.length === 0 ? (
<div className="empty">
<h2>Ask anything. Get fast, grounded answers.</h2>
<p>Type a request and NovaChat will decide when to pull live web sources.</p>
<div className="shortcut-grid">
{SHORTCUTS.map((shortcut) => (
<div key={shortcut.combo} className="shortcut-card">
<div className="shortcut-combo">{shortcut.combo}</div>
<div className="shortcut-action">{shortcut.action}</div>
</div>
))}
</div>
</div>
) : (
groupedMessages.map((group) => (
<div key={group[0].id} className={`message-group ${group[0].role}`}>
<div className="message-meta">{group[0].role === "user" ? "You" : "Nova"}</div>
<div className="message-stack">
{group.map((msg) => (
<div key={msg.id} className="message-bubble">
{msg.content}
</div>
))}
</div>
</div>
))
)}
{isStreaming && (
<div className="typing">
<span />
<span />
<span />
</div>
)}
<div ref={bottomRef} />
</div>
<div className="chat-input">
<textarea
ref={inputRef}
placeholder="Ask NovaChat to research, summarize, or build something..."
value={input}
onChange={(event) => setInput(event.target.value)}
rows={2}
/>
<div className="input-actions">
<div className="input-hint">Session: {sessionId.slice(0, 8)}</div>
<button className="primary" onClick={handleSend} disabled={isStreaming || !input.trim()}>
{isStreaming ? "Streaming..." : "Send"}
</button>
</div>
</div>
</section>
<aside className="sidebar">
<div className="card">
<h3>Live Ops</h3>
<p>Search cache and streaming are running in parallel for zero-lag delivery.</p>
<div className="metric">
<span>Latency target</span>
<strong>&lt; 800ms</strong>
</div>
<div className="metric">
<span>Streaming</span>
<strong>Token-by-token</strong>
</div>
</div>
<div className="card">
<h3>Search Stack</h3>
<p>Multi-source fetch, extraction, ranking, and summarization pipeline.</p>
<div className="badge-row">
<span>DuckDuckGo</span>
<span>Readability</span>
<span>LRU Cache</span>
</div>
</div>
<div className="card">
<h3>Controls</h3>
<p>NovaChat automatically decides when to search the web.</p>
<button className="ghost full" onClick={() => setStatus("Manual search mode coming soon")}>Enable manual search</button>
</div>
</aside>
</main>
</div>
);
}