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 (
{text}
{meta ?
{meta}
: null}
);
}
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 (
Show provenance
{entry.blueprint ? (
Blueprint: {entry.blueprint}
) : null}
{action.type ? (
Action: {action.type}
{action.arguments ? ` — ${JSON.stringify(action.arguments)}` : ''}
) : null}
{providerLabel || usage.model || usage.total_tokens != null || usage.response_tokens != null || usage.fallback ? (
Usage:{' '}
{[
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(' • ')}
) : null}
{facts.length ? (
<>
Facts
{facts.map((fact, index) => (
-
{formatContent(fact.subject) || 'Subject'} —{' '}
{formatContent(fact.predicate) || 'Detail'}: {formatContent(fact.value) || 'N/A'}
{fact.source ? ({formatContent(fact.source)}) : null}
))}
>
) : null}
{notes.length ? (
<>
Notes
{notes.map((note, index) => (
- {formatContent(note) || 'Note'}
))}
>
) : null}
);
}
function HistoryEntry({ entry, providerCatalog }) {
return (
);
}
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 (
);
});
const hasActiveSession = sessions.some((session) => session.session_id === sessionId);
const placeholderSession = (
);
return (
{historyLoading ? Loading conversation…
: null}
{historyError ? {historyError}
: null}
{!historyEntries.length && !historyLoading ? (
Ask about faculty, research, upcoming events, and more.
) : null}
{historyEntries.map((entry) => (
))}
);
}
export default App;