Monish Vijay Kumar
Deploy Northwestern CS Kiosk Docker Space with checkpoint and inference runtime
7056be6
Raw
History Blame
17.5 kB
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import './App.css';
import { fetchHistory, fetchProviders, fetchSessions, sendQuery } from './api/client';
const STORAGE_KEYS = {
sessionId: 'backend.sessionId',
sessionTitle: 'backend.sessionTitle',
};
const DEFAULT_SESSION_TITLE = 'Your virtual receptionist for faculty, research, and events.';
const formatTimestamp = (ts) => {
if (!ts) {
return '';
}
const date = new Date(ts * 1000);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
const friendlyTitleFromDate = (date = new Date()) =>
`Chat – ${date.toLocaleString([], {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
})}`;
const toHistoryEntry = (entry, index = 0) => ({
id: `${entry.session_id || 'session'}-${entry.timestamp || index}`,
question: entry.question,
answer: entry.answer,
timestamp: entry.timestamp,
blueprint: entry.blueprint,
facts: entry.facts || [],
notes: entry.notes || [],
usage: entry.usage,
action: entry.action,
pending: Boolean(entry.pending),
});
const buildUsageMeta = (entry, providerCatalog) => {
if (entry.pending) {
return 'Thinking…';
}
const usage = entry.usage || {};
const metaParts = [];
if (usage.provider) {
const providerLabel = providerCatalog[usage.provider]?.name || usage.provider;
metaParts.push(`Provider: ${providerLabel}`);
}
if (usage.model) {
metaParts.push(`Model: ${usage.model}`);
}
if (usage.total_tokens != null) {
metaParts.push(`Tokens: ${usage.total_tokens}`);
} else if (usage.response_tokens != null) {
metaParts.push(`Tokens: ${usage.response_tokens}`);
}
if (usage.fallback) {
metaParts.push('Fallback: rule-based');
}
if (metaParts.length) {
return metaParts.join(' • ');
}
return formatTimestamp(entry.timestamp);
};
const hasProvenance = (entry) =>
Boolean(
entry.blueprint ||
entry.facts?.length ||
entry.notes?.length ||
entry.action ||
(entry.usage && Object.keys(entry.usage).length),
);
const formatContent = (value) => {
if (value == null) {
return '';
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
};
function Message({ role, text, meta, pending }) {
return (
<div className={`message ${role}${pending ? ' pending' : ''}`}>
<div className="content">{text}</div>
{meta ? <div className="meta">{meta}</div> : null}
</div>
);
}
function Provenance({ entry, providerCatalog }) {
if (!hasProvenance(entry)) {
return null;
}
const facts = entry.facts || [];
const notes = entry.notes || [];
const usage = entry.usage || {};
const action = entry.action || {};
const providerLabel = usage.provider ? providerCatalog[usage.provider]?.name || usage.provider : null;
return (
<details className="provenance">
<summary>Show provenance</summary>
<div className="provenance-body">
{entry.blueprint ? (
<p>
<strong>Blueprint:</strong> {entry.blueprint}
</p>
) : null}
{action.type ? (
<p>
<strong>Action:</strong> {action.type}
{action.arguments ? ` — ${JSON.stringify(action.arguments)}` : ''}
</p>
) : null}
{providerLabel || usage.model || usage.total_tokens != null || usage.response_tokens != null || usage.fallback ? (
<p>
<strong>Usage:</strong>{' '}
{[
providerLabel ? `Provider: ${providerLabel}` : null,
usage.model ? `Model: ${usage.model}` : null,
usage.total_tokens != null
? `Tokens: ${usage.total_tokens}`
: usage.response_tokens != null
? `Tokens: ${usage.response_tokens}`
: null,
usage.fallback ? 'Fallback: rule-based' : null,
]
.filter(Boolean)
.join(' • ')}
</p>
) : null}
{facts.length ? (
<>
<h4>Facts</h4>
<ul>
{facts.map((fact, index) => (
<li key={`${fact.subject || fact.predicate || index}-${index}`}>
<strong>{formatContent(fact.subject) || 'Subject'}</strong> —{' '}
{formatContent(fact.predicate) || 'Detail'}: {formatContent(fact.value) || 'N/A'}
{fact.source ? <em> ({formatContent(fact.source)})</em> : null}
</li>
))}
</ul>
</>
) : null}
{notes.length ? (
<>
<h4>Notes</h4>
<ul>
{notes.map((note, index) => (
<li key={`${index}-${formatContent(note)}`}>{formatContent(note) || 'Note'}</li>
))}
</ul>
</>
) : null}
</div>
</details>
);
}
function HistoryEntry({ entry, providerCatalog }) {
return (
<div className="history-entry">
<Message role="user" text={entry.question} meta={formatTimestamp(entry.timestamp)} />
<Message
role="assistant"
text={entry.answer || (entry.pending ? 'Thinking…' : '')}
meta={buildUsageMeta(entry, providerCatalog)}
pending={entry.pending}
/>
<Provenance entry={entry} providerCatalog={providerCatalog} />
</div>
);
}
function App() {
const storedSessionId = localStorage.getItem(STORAGE_KEYS.sessionId);
const storedSessionTitle = localStorage.getItem(STORAGE_KEYS.sessionTitle);
const [sessionId, setSessionId] = useState(storedSessionId || `session-${Date.now()}`);
const [sessionTitle, setSessionTitle] = useState(storedSessionTitle || DEFAULT_SESSION_TITLE);
const [historyEntries, setHistoryEntries] = useState([]);
const [historyLoading, setHistoryLoading] = useState(false);
const [sessions, setSessions] = useState([]);
const [providers, setProviders] = useState([]);
const [selectedProvider, setSelectedProvider] = useState(null);
const [providerError, setProviderError] = useState('');
const [sessionsError, setSessionsError] = useState('');
const [historyError, setHistoryError] = useState('');
const [composerValue, setComposerValue] = useState('');
const [isSending, setIsSending] = useState(false);
const composerRef = useRef(null);
const providerCatalog = useMemo(() => {
const catalog = {};
providers.forEach((provider) => {
catalog[provider.id] = provider;
});
return catalog;
}, [providers]);
const providerNote = useMemo(() => {
if (providerError) {
return providerError;
}
if (!selectedProvider) {
return 'Select a model to route responses.';
}
const provider = providerCatalog[selectedProvider];
if (!provider) {
return 'Select a model to route responses.';
}
if (!provider.configured) {
return provider.note || 'Configure this provider before using it.';
}
return `Default model: ${provider.default_model || 'default'}`;
}, [providerCatalog, providerError, selectedProvider]);
const persistSessionId = useCallback((value) => {
localStorage.setItem(STORAGE_KEYS.sessionId, value);
setSessionId(value);
}, []);
const persistSessionTitle = useCallback((value) => {
localStorage.setItem(STORAGE_KEYS.sessionTitle, value);
setSessionTitle(value);
}, []);
const loadProviders = useCallback(async () => {
try {
setProviderError('');
const payload = await fetchProviders();
const entries = Object.values(payload.providers || {});
entries.sort((a, b) => {
if (a.id === 'vanilla') return -1;
if (b.id === 'vanilla') return 1;
return a.name.localeCompare(b.name);
});
setProviders(entries);
const available = new Set(entries.map((entry) => entry.id));
const fallback =
(payload.default_provider && available.has(payload.default_provider) && payload.default_provider) ||
entries[0]?.id ||
null;
setSelectedProvider((current) => {
if (current && available.has(current)) {
return current;
}
return fallback;
});
} catch (error) {
setProviderError(error.message || 'Unable to load providers.');
}
}, []);
const loadSessions = useCallback(async () => {
try {
setSessionsError('');
const payload = await fetchSessions();
setSessions(payload.sessions || []);
} catch (error) {
setSessionsError(error.message || 'Unable to load sessions.');
}
}, []);
const loadHistoryData = useCallback(
async (targetSessionId) => {
if (!targetSessionId) {
return;
}
try {
setHistoryError('');
setHistoryLoading(true);
const payload = await fetchHistory(targetSessionId);
const entries = (payload.history || []).map((entry, index) =>
toHistoryEntry(
{
...entry,
session_id: targetSessionId,
},
index,
),
);
setHistoryEntries(entries);
if (payload.title) {
persistSessionTitle(payload.title);
}
} catch (error) {
setHistoryError(error.message || 'Unable to load history.');
} finally {
setHistoryLoading(false);
}
},
[persistSessionTitle],
);
useEffect(() => {
const updateShellHeight = () => {
const root = document.getElementById('root');
if (!root) {
return;
}
const topOffset = root.getBoundingClientRect().top;
const available = window.innerHeight - topOffset;
const clamped = Math.max(available, 320);
document.documentElement.style.setProperty('--app-shell-height', `${clamped}px`);
};
updateShellHeight();
window.addEventListener('resize', updateShellHeight);
window.addEventListener('orientationchange', updateShellHeight);
return () => {
window.removeEventListener('resize', updateShellHeight);
window.removeEventListener('orientationchange', updateShellHeight);
};
}, []);
useEffect(() => {
loadProviders();
loadSessions();
}, [loadProviders, loadSessions]);
useEffect(() => {
loadHistoryData(sessionId);
}, [loadHistoryData, sessionId]);
useEffect(() => {
composerRef.current?.focus();
}, []);
const handleSelectSession = (session) => {
if (!session) {
return;
}
persistSessionId(session.session_id);
persistSessionTitle(session.title || friendlyTitleFromDate(new Date(session.created_at * 1000)));
loadHistoryData(session.session_id);
};
const handleNewChat = () => {
const newSessionId = `session-${Date.now()}`;
const title = friendlyTitleFromDate();
persistSessionId(newSessionId);
persistSessionTitle(title);
setHistoryEntries([]);
setComposerValue('');
};
const sendMessage = async () => {
if (isSending) {
return;
}
const question = composerValue.trim();
if (!question) {
return;
}
setComposerValue('');
setIsSending(true);
setHistoryError('');
const tempId = `pending-${Date.now()}`;
const timestamp = Date.now() / 1000;
setHistoryEntries((prev) => [
...prev,
{
id: tempId,
session_id: sessionId,
question,
answer: 'Thinking…',
timestamp,
pending: true,
facts: [],
notes: [],
usage: null,
action: null,
},
]);
try {
const payload = await sendQuery({
sessionId,
question,
provider: selectedProvider || undefined,
});
setHistoryEntries((prev) =>
prev.map((entry) =>
entry.id === tempId
? {
...entry,
id: `${payload.session_id || sessionId}-${timestamp}`,
pending: false,
answer: payload.answer || entry.answer,
blueprint: payload.blueprint,
facts: payload.facts || [],
notes: payload.notes || [],
usage: payload.usage || {},
action: payload.action || {},
}
: entry,
),
);
if (payload.session_id && payload.session_id !== sessionId) {
persistSessionId(payload.session_id);
}
if (payload.session_title) {
persistSessionTitle(payload.session_title);
}
loadSessions();
} catch (error) {
setHistoryEntries((prev) =>
prev.map((entry) =>
entry.id === tempId
? {
...entry,
pending: false,
answer: `Sorry, something went wrong: ${error.message}`,
}
: entry,
),
);
} finally {
setIsSending(false);
composerRef.current?.focus();
}
};
const handleSend = (event) => {
event.preventDefault();
void sendMessage();
};
const handleComposerKeyDown = (event) => {
if ((event.key === 'Enter' || event.key === 'NumpadEnter') && !event.shiftKey) {
if (event.nativeEvent.isComposing) {
return;
}
event.preventDefault();
const form = event.currentTarget.form;
if (form && typeof form.requestSubmit === 'function') {
form.requestSubmit();
} else {
void sendMessage();
}
}
};
const providerOptions = providers.map((provider) => {
const label =
provider.id === 'vanilla'
? provider.name
: provider.default_model
? `${provider.name} (${provider.default_model})`
: provider.name;
return (
<option key={provider.id} value={provider.id}>
{label}
{!provider.configured ? ' – configure to enable' : ''}
</option>
);
});
const hasActiveSession = sessions.some((session) => session.session_id === sessionId);
const placeholderSession = (
<button type="button" className="session-item active" key="draft">
<span className="title">{sessionTitle || friendlyTitleFromDate()}</span>
<span className="timestamp">Draft</span>
</button>
);
return (
<div className="app-shell">
<aside className="sidebar">
<div className="sidebar-header">
<div>
<h2>Conversations</h2>
<p>Jump back into a previous session or start fresh.</p>
</div>
<button type="button" className="new-chat" onClick={handleNewChat}>
New chat
</button>
</div>
<div className="session-list">
{sessionsError ? <p className="error">{sessionsError}</p> : null}
{!sessions.length && placeholderSession}
{sessions.map((session) => (
<button
type="button"
key={session.session_id}
className={`session-item${session.session_id === sessionId ? ' active' : ''}`}
onClick={() => handleSelectSession(session)}
>
<span className="title">{session.title || session.session_id}</span>
<span className="timestamp">
{session.updated_at ? new Date(session.updated_at * 1000).toLocaleString() : ''}
</span>
</button>
))}
{!hasActiveSession && sessions.length ? placeholderSession : null}
</div>
</aside>
<main className="chat-pane">
<header className="chat-header">
<div>
<h1>Northwestern CS Kiosk (Vanilla LM)</h1>
<p>{sessionTitle || DEFAULT_SESSION_TITLE}</p>
</div>
<div className="model-picker">
<label htmlFor="provider-select">Response model</label>
<select
id="provider-select"
value={selectedProvider || ''}
onChange={(event) => setSelectedProvider(event.target.value || null)}
>
{providerOptions}
</select>
<p className="model-note">{providerNote}</p>
</div>
</header>
<section className="chat-log">
{historyLoading ? <p className="status">Loading conversation…</p> : null}
{historyError ? <p className="error">{historyError}</p> : null}
{!historyEntries.length && !historyLoading ? (
<div className="empty-state">
<p>Ask about faculty, research, upcoming events, and more.</p>
</div>
) : null}
{historyEntries.map((entry) => (
<HistoryEntry key={entry.id} entry={entry} providerCatalog={providerCatalog} />
))}
</section>
<form className="composer" onSubmit={handleSend}>
<textarea
ref={composerRef}
placeholder="Ask anything about the CS department…"
value={composerValue}
onChange={(event) => setComposerValue(event.target.value)}
onKeyDown={handleComposerKeyDown}
disabled={isSending}
rows={3}
/>
<button type="submit" disabled={isSending || !composerValue.trim()}>
{isSending ? 'Sending…' : 'Send'}
</button>
</form>
</main>
</div>
);
}
export default App;