const {
createElement: h,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} = React;
const APP_VERSION = (() => {
const raw = document.querySelector('meta[name="formal-ai-version"]')?.content;
if (!raw || raw.startsWith("__") || raw.endsWith("__")) {
return "dev";
}
return raw;
})();
const ASSET_VERSION =
typeof window !== "undefined" ? window.FORMAL_AI_ASSET_VERSION || "" : "";
const ISSUE_REPOSITORY = "link-assistant/formal-ai";
const ISSUE_LABELS = "bug";
const UNKNOWN_ANSWER =
"I do not have a learned symbolic rule for that prompt yet. Add a Links Notation fact or rule, then run the request again.";
const IDENTITY_ANSWER =
"I am formal-ai, a deterministic symbolic AI proof of concept that answers from local Links Notation rules and OpenAI-compatible API shapes. I do not perform neural inference in this demo.";
const EXAMPLE_PROMPTS = [
{ label: "Greeting (en)", text: "Hi" },
{ label: "Greeting (ru)", text: "Привет" },
{ label: "Greeting (hi)", text: "नमस्ते" },
{ label: "Greeting (zh)", text: "你好" },
{ label: "Identity (en)", text: "Who are you?" },
{ label: "Identity (zh)", text: "你是谁?" },
{ label: "Hello world (Rust)", text: "Write me hello world program in Rust" },
{ label: "Hello world (Python)", text: "Create a hello world example in Python" },
{ label: "Hello world (JavaScript)", text: "Write hello world in JavaScript" },
{ label: "Hello world (TypeScript)", text: "Write hello world in TypeScript" },
{ label: "Hello world (Go)", text: "Show hello world in Go" },
{ label: "Hello world (C)", text: "Show hello world in C" },
{ label: "Concept (en)", text: "What is Rust?" },
{ label: "Concept (en/Wikipedia)", text: "Who is Donald Trump?" },
{ label: "Concept (ru/Wikipedia)", text: "Кто такой Илон Маск?" },
{ label: "Concept (ru)", text: "Что такое Википедия?" },
{ label: "Concept (zh)", text: "维基百科是什么?" },
{ label: "Concept in context", text: "What is IIR in machine learning?" },
{ label: "Recall (en)", text: "When did I ask about Rust?" },
{ label: "Recall (cross-conv)", text: "Find Wikipedia in another conversation" },
{ label: "Export memory", text: "Export memory" },
{ label: "Import memory", text: "Import memory" },
];
const DEMO_GREETING_LABELS = new Set([
"Greeting (en)",
"Greeting (ru)",
"Greeting (hi)",
"Greeting (zh)",
]);
const DEMO_EXCLUDED_LABELS = new Set(["Export memory", "Import memory"]);
function demoGreetings() {
return EXAMPLE_PROMPTS.filter((entry) => DEMO_GREETING_LABELS.has(entry.label));
}
function demoFeaturePrompts() {
return EXAMPLE_PROMPTS.filter(
(entry) =>
!DEMO_GREETING_LABELS.has(entry.label) &&
!DEMO_EXCLUDED_LABELS.has(entry.label),
);
}
let demoGreetingCursor = 0;
let demoFeatureCursor = 0;
const MEMORY_ACTION_PHRASES = {
export: [
"export memory",
"export your memory",
"export the memory",
"export full memory",
"экспорт памяти",
"экспортировать память",
"экспортируй память",
"экспортируй свою память",
"स्मृति निर्यात करें",
"अपनी स्मृति निर्यात करें",
"导出记忆",
"导出你的记忆",
"导出全部记忆",
],
import: [
"import memory",
"import new memory",
"import your new memory",
"import your memory",
"импорт памяти",
"импортировать память",
"импортируй память",
"импортируй новую память",
"स्मृति आयात करें",
"नई स्मृति आयात करें",
"अपनी नई स्मृति आयात करें",
"导入记忆",
"导入新记忆",
"导入你的新记忆",
],
};
function normalizeMemoryPrompt(text) {
return String(text || "")
.toLowerCase()
.replace(/[\s -]+/g, " ")
.replace(/[!?.,;:。!?,;:、]+$/g, "")
.trim();
}
function recognizeMemoryAction(text) {
const normalized = normalizeMemoryPrompt(text);
if (!normalized) return null;
if (MEMORY_ACTION_PHRASES.export.some((phrase) => normalized === phrase)) {
return "export";
}
if (MEMORY_ACTION_PHRASES.import.some((phrase) => normalized === phrase)) {
return "import";
}
return null;
}
const RECALL_QUERY_PATTERNS = [
{ prefix: "when did i ask about ", scope: "all" },
{ prefix: "when did i ask ", scope: "all" },
{ prefix: "when did i mention ", scope: "all" },
{ prefix: "when did i talk about ", scope: "all" },
{ prefix: "have i asked about ", scope: "all" },
{ prefix: "have i mentioned ", scope: "all" },
{ prefix: "did i ask about ", scope: "all" },
{ prefix: "did i mention ", scope: "all" },
{ prefix: "search my conversations for ", scope: "all" },
{ prefix: "search conversations for ", scope: "all" },
{ prefix: "search my chats for ", scope: "all" },
{ prefix: "recall ", scope: "all" },
{ prefix: "когда я спрашивал про ", scope: "all" },
{ prefix: "когда я спрашивал о ", scope: "all" },
{ prefix: "когда я спрашивал ", scope: "all" },
{ prefix: "когда я упоминал ", scope: "all" },
{ prefix: "поиск по беседам ", scope: "all" },
{ prefix: "поиск в беседах ", scope: "all" },
{ prefix: "найди в беседах ", scope: "all" },
{ prefix: "我什么时候问过 ", scope: "all" },
{ prefix: "我什么时候问过", scope: "all" },
{ prefix: "我什么时候提到 ", scope: "all" },
{ prefix: "我什么时候提到", scope: "all" },
{ prefix: "搜索我的对话 ", scope: "all" },
{ prefix: "搜索我的对话", scope: "all" },
{ prefix: "在对话中搜索 ", scope: "all" },
{ prefix: "在对话中搜索", scope: "all" },
];
const RECALL_OTHER_SUFFIXES = [
" in another conversation",
" in other conversations",
" in my other conversations",
" in my conversations",
" in another chat",
" in other chats",
" в другой беседе",
" в других беседах",
" в других чатах",
"在其他对话中",
"在另一个对话中",
];
const RECALL_OTHER_PREFIXES = [
"find ",
"search for ",
"look for ",
"найди ",
"поищи ",
"查找 ",
"查找",
"搜索 ",
"搜索",
];
function stripRecallTerm(term) {
return String(term || "")
.replace(/^["'«»『「]+/, "")
.replace(/["'«»』」]+$/, "")
.replace(/[!?.,;:。!?,;:、]+$/g, "")
.trim();
}
function recoverOriginalRange(original, normalized, start, end) {
let nIdx = 0;
let captured = "";
let i = 0;
let prevWasSpace = false;
while (i < original.length && nIdx < end) {
const ch = original[i];
const lower = ch.toLowerCase();
if (/[\s\u00A0\u2000-\u200B]/.test(ch)) {
if (!prevWasSpace) {
if (nIdx >= start) captured += " ";
nIdx++;
prevWasSpace = true;
}
i++;
continue;
}
prevWasSpace = false;
if (nIdx >= start) captured += ch;
nIdx += lower.length;
i++;
}
return captured.trim();
}
function recognizeRecallQuery(text) {
const original = String(text || "").trim();
if (!original) return null;
const normalized = normalizeMemoryPrompt(text);
if (!normalized) return null;
for (const suffix of RECALL_OTHER_SUFFIXES) {
const suffixIdx = normalized.lastIndexOf(suffix);
if (suffixIdx < 0) continue;
const beforeSuffix = normalized.slice(0, suffixIdx);
for (const prefix of RECALL_OTHER_PREFIXES) {
if (beforeSuffix.startsWith(prefix)) {
const normalizedTerm = stripRecallTerm(beforeSuffix.slice(prefix.length));
if (!normalizedTerm) continue;
const originalTerm = stripRecallTerm(
recoverOriginalRange(original, normalized, prefix.length, suffixIdx),
);
return { term: originalTerm || normalizedTerm, scope: "other" };
}
}
}
for (const { prefix, scope } of RECALL_QUERY_PATTERNS) {
if (normalized.startsWith(prefix)) {
const normalizedTerm = stripRecallTerm(normalized.slice(prefix.length));
if (!normalizedTerm) continue;
const originalTerm = stripRecallTerm(
recoverOriginalRange(original, normalized, prefix.length, normalized.length),
);
return { term: originalTerm || normalizedTerm, scope };
}
}
return null;
}
function buildRecallReport({ events, term, scope, currentConversationId, triggerText }) {
const safeEvents = Array.isArray(events) ? events : [];
const needle = String(term || "").toLowerCase();
if (!needle) {
return {
content: "No search term recognised in the recall request.",
matches: [],
};
}
const triggerNormalized = String(triggerText || "").trim().toLowerCase();
const groups = new Map();
for (const event of safeEvents) {
if (!event || (event.kind && event.kind !== "message")) continue;
const content = String(event.content || "");
if (!content.toLowerCase().includes(needle)) continue;
if (triggerNormalized && content.trim().toLowerCase() === triggerNormalized) {
continue;
}
const id = event.conversationId || "legacy";
if (scope === "other" && id === (currentConversationId || "")) continue;
let entry = groups.get(id);
if (!entry) {
entry = { id, title: "", events: [] };
groups.set(id, entry);
}
if (!entry.title && event.role === "user" && event.conversationTitle) {
entry.title = event.conversationTitle;
}
entry.events.push(event);
}
const groupList = Array.from(groups.values());
for (const group of groupList) {
if (!group.title) {
const firstUser = group.events.find((e) => e.role === "user");
if (firstUser && firstUser.content) {
group.title = deriveConversationTitle(firstUser.content);
} else if (group.id === "legacy") {
group.title = "Earlier conversation";
} else {
group.title = "Untitled conversation";
}
}
group.events.sort((a, b) => String(a.sentAt || "").localeCompare(String(b.sentAt || "")));
}
groupList.sort((left, right) => {
const lLast = left.events[left.events.length - 1]?.sentAt || "";
const rLast = right.events[right.events.length - 1]?.sentAt || "";
return String(rLast).localeCompare(String(lLast));
});
const totalMatches = groupList.reduce((sum, g) => sum + g.events.length, 0);
if (totalMatches === 0) {
const scopeNote = scope === "other" ? " in any other conversation" : "";
return {
content: `No mentions of "${term}" found${scopeNote}.`,
matches: [],
};
}
const lines = [];
const conversationCount = groupList.length;
lines.push(
`Found **${totalMatches}** mention${totalMatches === 1 ? "" : "s"} of "${term}" across **${conversationCount}** conversation${conversationCount === 1 ? "" : "s"}.`,
);
for (const group of groupList) {
lines.push("");
lines.push(`### ${group.title}`);
for (const event of group.events) {
const stamp = event.sentAt ? event.sentAt : "(no timestamp)";
const role = event.role === "user" ? "user" : "assistant";
const snippet = String(event.content || "").replace(/\s+/g, " ").trim();
const trimmed = snippet.length > 160 ? `${snippet.slice(0, 157)}…` : snippet;
lines.push(`- ${stamp} — ${role}: ${trimmed}`);
}
}
return { content: lines.join("\n"), matches: groupList };
}
const PREFERENCE_DEFAULTS = {
demoMode: true,
diagnosticsMode: false,
sidebarPromptsCollapsed: false,
sidebarToolsCollapsed: false,
sidebarTraceCollapsed: false,
sidebarConversationsCollapsed: false,
greetingVariations: true,
currentConversationId: "",
agentMode: false,
uiLanguage: "auto",
};
const MEMORY_EXPORT_FILENAME = "formal-ai-memory.lino";
function withAssetVersion(path) {
if (!ASSET_VERSION) {
return path;
}
const separator = path.includes("?") ? "&" : "?";
return `${path}${separator}v=${encodeURIComponent(ASSET_VERSION)}`;
}
function recordMemoryEvent(payload) {
if (typeof window === "undefined" || !window.FormalAiMemory) {
return Promise.resolve(null);
}
try {
return window.FormalAiMemory.appendEvent(payload).catch(() => null);
} catch (_error) {
return Promise.resolve(null);
}
}
function downloadTextFile(filename, text) {
if (typeof window === "undefined" || typeof document === "undefined") {
return;
}
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
function loadPreferences() {
if (typeof window === "undefined" || !window.FormalAiPreferences) {
return { ...PREFERENCE_DEFAULTS };
}
try {
return window.FormalAiPreferences.load(PREFERENCE_DEFAULTS);
} catch (_error) {
return { ...PREFERENCE_DEFAULTS };
}
}
function persistPreferences(values) {
if (typeof window === "undefined" || !window.FormalAiPreferences) {
return;
}
try {
window.FormalAiPreferences.save(values);
} catch (_error) {
}
}
function i18nApi() {
return typeof window !== "undefined" && window.FormalAiI18n
? window.FormalAiI18n
: null;
}
function normalizeUiLanguagePreference(value) {
if (!value || value === "auto") return "auto";
const api = i18nApi();
const normalized = api && api.normalizeLanguageTag
? api.normalizeLanguageTag(value)
: String(value).toLowerCase().split(/[-_]/)[0];
return normalized || "auto";
}
function detectUiLanguage(preference) {
const api = i18nApi();
if (api && api.detectLanguage) {
return api.detectLanguage(preference === "auto" ? "" : preference);
}
return "en";
}
function translateUi(key, language, params) {
const api = i18nApi();
if (api && api.t) {
return api.t(key, language, params);
}
return key;
}
function browserLanguagesList() {
if (typeof navigator === "undefined") return [];
if (Array.isArray(navigator.languages) && navigator.languages.length > 0) {
return Array.from(navigator.languages);
}
return navigator.language ? [navigator.language] : [];
}
function currentColorScheme() {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
return "unknown";
}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function resolvedLocale() {
try {
return Intl.DateTimeFormat().resolvedOptions().locale || "";
} catch (_error) {
return "";
}
}
function resolvedTimeZone() {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone || "";
} catch (_error) {
return "";
}
}
function collectUserContext({ uiLanguage, uiLanguagePreference }) {
const browserLanguages = browserLanguagesList();
const nav = typeof navigator !== "undefined" ? navigator : {};
const screenInfo =
typeof screen !== "undefined"
? `${screen.width}x${screen.height} @${window.devicePixelRatio || 1}x`
: "";
const viewportInfo =
typeof window !== "undefined" ? `${window.innerWidth}x${window.innerHeight}` : "";
return {
uiLanguage,
uiLanguagePreference,
browserLanguage: nav.language || "",
browserLanguages: browserLanguages.join(", "),
locale: resolvedLocale(),
timeZone: resolvedTimeZone(),
colorScheme: currentColorScheme(),
viewport: viewportInfo,
screen: screenInfo,
platform:
(nav.userAgentData && nav.userAgentData.platform) ||
nav.platform ||
"",
online: typeof nav.onLine === "boolean" ? (nav.onLine ? "yes" : "no") : "",
locationInference:
"time zone / locale only; exact geolocation was not requested",
};
}
function appendUserContextBlock(lines, context) {
const safe = context && typeof context === "object" ? context : {};
lines.push("## User Context");
lines.push("");
lines.push(`- **UI Language**: ${safe.uiLanguage || "unknown"}`);
lines.push(
`- **UI Language Preference**: ${safe.uiLanguagePreference || "auto"}`,
);
lines.push(`- **Browser Language**: ${safe.browserLanguage || "unknown"}`);
lines.push(`- **Browser Languages**: ${safe.browserLanguages || "unknown"}`);
lines.push(`- **Locale**: ${safe.locale || "unknown"}`);
lines.push(`- **Time Zone**: ${safe.timeZone || "unknown"}`);
lines.push(`- **Color Scheme**: ${safe.colorScheme || "unknown"}`);
lines.push(`- **Viewport**: ${safe.viewport || "unknown"}`);
lines.push(`- **Screen**: ${safe.screen || "unknown"}`);
lines.push(`- **Platform**: ${safe.platform || "unknown"}`);
lines.push(`- **Online**: ${safe.online || "unknown"}`);
lines.push(
`- **Location Inference**: ${safe.locationInference || "unknown"}`,
);
lines.push("");
}
function randomItem(items) {
return items[Math.floor(Math.random() * items.length)];
}
function generateConversationId() {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return `conv-${crypto.randomUUID()}`;
}
const random = Math.random().toString(16).slice(2, 10);
return `conv-${Date.now().toString(16)}-${random}`;
}
function deriveConversationTitle(text) {
const trimmed = String(text || "").trim().replace(/\s+/g, " ");
if (!trimmed) {
return "New conversation";
}
if (trimmed.length <= 60) {
return trimmed;
}
return `${trimmed.slice(0, 57)}…`;
}
function groupConversations(events) {
const safe = Array.isArray(events) ? events : [];
const map = new Map();
for (let index = 0; index < safe.length; index += 1) {
const event = safe[index];
if (!event || event.kind && event.kind !== "message") {
continue;
}
const id = event.conversationId || "legacy";
let entry = map.get(id);
if (!entry) {
entry = {
id,
title: id === "legacy" ? "Earlier conversation" : "",
firstAt: event.sentAt || "",
lastAt: event.sentAt || "",
messageCount: 0,
};
map.set(id, entry);
}
if (event.role === "user" && !entry.title && event.conversationTitle) {
entry.title = event.conversationTitle;
} else if (event.role === "user" && !entry.title) {
entry.title = deriveConversationTitle(event.content);
}
if (event.sentAt && (!entry.firstAt || event.sentAt < entry.firstAt)) {
entry.firstAt = event.sentAt;
}
if (event.sentAt && (!entry.lastAt || event.sentAt > entry.lastAt)) {
entry.lastAt = event.sentAt;
}
entry.messageCount += 1;
}
const list = Array.from(map.values());
list.sort((left, right) => {
if (left.lastAt && right.lastAt) {
return right.lastAt.localeCompare(left.lastAt);
}
return 0;
});
return list;
}
const AGENT_STEP_SEPARATORS = [
/\s*;\s+/,
/\s+then(?:\s*,)?\s+/i,
/\s*,\s+(?:and\s+then|then|next)\s+/i,
/\s*,\s+after\s+that\s+/i,
/\s+потом\s+/i,
/\s+затем\s+/i,
/\s+после\s+этого\s+/i,
/\s+然后\s*/,
/\s+接着\s*/,
];
const AGENT_LEADING_CONJUNCTIONS =
/^(?:and\s+then|then|next|after\s+that|потом|затем|после\s+этого|然后|接着)[\s,:]+/i;
function decomposeAgentTask(text) {
const trimmed = String(text || "").trim();
if (!trimmed) return [];
let segments = [trimmed];
for (const sep of AGENT_STEP_SEPARATORS) {
const next = [];
for (const segment of segments) {
const parts = segment.split(sep);
for (const part of parts) {
const cleaned = part.trim();
if (cleaned) next.push(cleaned);
}
}
segments = next;
}
return segments.map((segment) =>
segment.replace(AGENT_LEADING_CONJUNCTIONS, "").trim(),
).filter((segment) => segment.length > 0);
}
function messagesForConversation(events, conversationId) {
if (!conversationId) {
return [];
}
const safe = Array.isArray(events) ? events : [];
const out = [];
for (let index = 0; index < safe.length; index += 1) {
const event = safe[index];
if (!event || event.kind && event.kind !== "message") continue;
if ((event.conversationId || "legacy") !== conversationId) continue;
const evidence = Array.isArray(event.evidence) ? event.evidence : [];
out.push(
createMessage(event.role || "assistant", String(event.content || ""), {
intent: event.intent,
evidence,
}),
);
}
return out;
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function timeLabel() {
return new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
}
function createMessage(role, content, extra = {}) {
return {
id: `${role}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
role,
author: role === "user" ? "You" : "formal-ai",
content,
sentAt: timeLabel(),
...extra,
};
}
function escapeHtml(value) {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function markdownHtml(value) {
const text = String(value ?? "");
if (window.marked && window.DOMPurify) {
const html = window.marked.parse(text, {
breaks: true,
gfm: true,
});
return { __html: window.DOMPurify.sanitize(html) };
}
return { __html: escapeHtml(text).replaceAll("\n", "<br>") };
}
function normalizePrompt(prompt) {
return prompt.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
}
function isIdentityPrompt(normalized) {
const tokens = normalized ? normalized.split(/\s+/) : [];
const has = (token) => tokens.includes(token);
return (
[
"who are you",
"what are you",
"who is formal ai",
"what is formal ai",
"who is formalai",
"what is formalai",
"tell me about yourself",
"introduce yourself",
].includes(normalized) ||
(has("who") && has("you")) ||
(has("what") && has("you")) ||
((has("who") || has("what")) && has("formal") && has("ai")) ||
(has("tell") && has("yourself")) ||
(has("introduce") && has("yourself"))
);
}
function localFallbackAnswer(prompt) {
const normalized = normalizePrompt(prompt);
if (["hi", "hello", "hey"].includes(normalized)) {
return {
intent: "greeting",
content: "Hi, how may I help you?",
};
}
if (isIdentityPrompt(normalized)) {
return {
intent: "identity",
content: IDENTITY_ANSWER,
};
}
return {
intent: "unknown",
content: UNKNOWN_ANSWER,
};
}
function createDemoTurns() {
const greetings = demoGreetings();
const features = demoFeaturePrompts();
const turns = [];
if (greetings.length > 0) {
const greeting = greetings[demoGreetingCursor % greetings.length];
demoGreetingCursor = (demoGreetingCursor + 1) % greetings.length;
turns.push({ text: greeting.text, label: greeting.label });
}
if (features.length > 0) {
const feature = features[demoFeatureCursor % features.length];
demoFeatureCursor = (demoFeatureCursor + 1) % features.length;
turns.push({ text: feature.text, label: feature.label });
}
return turns;
}
function appendCodeBlock(lines, value) {
const text = String(value ?? "");
const fence = text.includes("```") ? "````" : "```";
lines.push(fence);
lines.push(text);
lines.push(fence);
}
function pickDialogFence(messages) {
let fence = "```";
while (messages.some((message) => String(message.content ?? "").includes(fence))) {
fence += "`";
}
return fence;
}
function appendDialogBlock(lines, messages, effectiveFocus) {
if (messages.length === 0) {
lines.push("No messages have been sent yet.");
return;
}
lines.push("Legend: `U` = user, `A` = agent.");
lines.push("");
const fence = pickDialogFence(messages);
lines.push(fence);
messages.forEach((message) => {
const prefix = message.role === "user" ? "U" : "A";
const annotations = [];
if (message.intent === "unknown") {
annotations.push(`intent: ${message.intent}`);
}
if (effectiveFocus && effectiveFocus.id === message.id) {
if (message.intent && message.intent !== "unknown") {
annotations.push(`intent: ${message.intent}`);
}
annotations.push("reported");
}
const head = annotations.length > 0 ? `${prefix} (${annotations.join(", ")})` : prefix;
const content = String(message.content ?? "");
const [first, ...rest] = content.split("\n");
lines.push(`${head}: ${first}`);
rest.forEach((row) => lines.push(` ${row}`));
});
lines.push(fence);
}
function shortText(value, limit = 70) {
const normalized = String(value ?? "").replace(/\s+/g, " ").trim();
if (normalized.length <= limit) {
return normalized;
}
return `${normalized.slice(0, limit - 3)}...`;
}
function promptBeforeMessage(messages, focusMessage) {
let prompt = "";
for (const message of messages) {
if (message.role === "user") {
prompt = message.content;
}
if (focusMessage && message.id === focusMessage.id) {
break;
}
}
return prompt;
}
function lastUnknownAssistantMessage(messages) {
for (let i = messages.length - 1; i >= 0; i -= 1) {
if (messages[i].role === "assistant" && messages[i].intent === "unknown") {
return messages[i];
}
}
return null;
}
function createIssueTitle(messages, focusMessage) {
const effectiveFocus = focusMessage ?? lastUnknownAssistantMessage(messages);
const prompt = promptBeforeMessage(messages, effectiveFocus);
if (effectiveFocus?.intent === "unknown" && prompt) {
return `Unknown prompt: ${shortText(prompt, 80)}`;
}
if (prompt) {
return `Issue with dialog: ${shortText(prompt, 80)}`;
}
return "formal-ai demo issue report";
}
function createIssueReportBody({
messages,
focusMessage,
workerState,
demoMode,
demoStatus,
diagnosticsMode,
userContext,
}) {
const effectiveFocus = focusMessage ?? lastUnknownAssistantMessage(messages);
const lines = [
"## Environment",
"",
`- **Version**: ${APP_VERSION}`,
`- **URL**: ${window.location.href}`,
`- **User Agent**: ${navigator.userAgent}`,
`- **Worker**: ${workerState}`,
`- **Mode**: ${demoMode ? "demo" : "manual"}`,
`- **Status**: ${demoStatus}`,
`- **Diagnostics**: ${diagnosticsMode ? "on" : "off"}`,
`- **Timestamp**: ${new Date().toISOString()}`,
"",
];
appendUserContextBlock(lines, userContext);
lines.push("## Dialog");
lines.push("");
appendDialogBlock(lines, messages, effectiveFocus);
const prompt = promptBeforeMessage(messages, effectiveFocus);
lines.push("");
lines.push("## Reproduction Steps");
lines.push("");
lines.push(`1. Open ${window.location.href}`);
if (prompt) {
lines.push(`2. Send the prompt "${shortText(prompt, 120)}"`);
lines.push("3. Click the report link on the dialog message");
} else {
lines.push("2. Use the demo until the issue occurs");
lines.push("3. Click Report issue");
}
lines.push("");
lines.push("## Description");
lines.push("");
lines.push("<!-- Please describe what looked wrong or incomplete. -->");
lines.push("");
lines.push("## Attach full memory (optional)");
lines.push("");
lines.push(
"Click **Export memory** in the topbar to save `formal-ai-memory.lino`, then attach it as a [GitHub Gist](https://gist.github.com) or wrap it in a `.zip` first. Redact sensitive content before uploading. See the [upload-memory guide](https://github.com/link-assistant/formal-ai/blob/main/docs/upload-memory.md) for the full walkthrough.",
);
lines.push("");
return lines.join("\n");
}
function createIssueUrl(context) {
const params = new URLSearchParams({
title: createIssueTitle(context.messages, context.focusMessage),
body: createIssueReportBody(context),
labels: ISSUE_LABELS,
});
return `https://github.com/${ISSUE_REPOSITORY}/issues/new?${params.toString()}`;
}
function Message({ message, diagnosticsMode, reportIssueUrl, t }) {
const evidence = diagnosticsMode ? (message.evidence ?? []) : [];
const thinkingSteps = diagnosticsMode ? (message.thinkingSteps ?? []) : [];
const reportLabel =
message.intent === "unknown"
? t("buttons.reportMissingRule")
: t("buttons.reportIssue");
const [iframeExpanded, setIframeExpanded] = useState(true);
return h(
"article",
{
className: `message ${message.role}`,
"data-testid": "chat-message",
"data-demo-label": message.demoLabel || null,
},
h("div", { className: "avatar", "aria-hidden": "true" }, message.role === "user" ? "Y" : "FA"),
h(
"div",
{ className: "message-body" },
h(
"div",
{ className: "message-meta" },
h(
"strong",
null,
message.role === "user" ? t("message.author.user") : message.author,
),
h("time", null, message.sentAt),
diagnosticsMode && message.intent
? h("span", { className: "intent" }, `intent:${message.intent}`)
: null,
),
h("div", {
className: "markdown-body",
dangerouslySetInnerHTML: markdownHtml(message.content),
}),
message.iframeUrl
? h(
"div",
{ className: "fetch-iframe-container", "data-testid": "fetch-iframe-container" },
h(
"div",
{ className: "fetch-iframe-header" },
h(
"button",
{
type: "button",
className: "fetch-iframe-toggle",
onClick: () => setIframeExpanded((prev) => !prev),
"aria-expanded": iframeExpanded ? "true" : "false",
},
iframeExpanded
? `▼ ${t("fetch.collapse")}`
: `▶ ${t("fetch.expand")}`,
),
h("span", { className: "fetch-iframe-url" }, message.iframeUrl),
h(
"a",
{
href: message.iframeUrl,
target: "_blank",
rel: "noopener noreferrer",
className: "fetch-iframe-open",
},
t("fetch.openInNewTab"),
),
),
iframeExpanded
? h("iframe", {
className: "fetch-iframe",
src: message.iframeUrl,
title: t("fetch.frameTitle", { url: message.iframeUrl }),
sandbox: "allow-scripts allow-same-origin allow-forms allow-popups",
loading: "lazy",
"data-testid": "fetch-iframe",
})
: null,
)
: null,
evidence.length
? h(
"div",
{ className: "evidence-list" },
evidence.map((item) => h("span", { key: item }, item)),
)
: null,
thinkingSteps.length
? h(
"div",
{ className: "thinking-steps" },
h("strong", null, t("message.thinking")),
h(
"ol",
null,
thinkingSteps.map((item) => h("li", { key: item }, item)),
),
)
: null,
reportIssueUrl
? h(
"div",
{ className: "message-actions" },
h(
"a",
{
href: reportIssueUrl,
target: "_blank",
rel: "noopener noreferrer",
},
reportLabel,
),
)
: null,
),
);
}
function CollapsibleSection({
title,
collapsed,
onToggle,
testId,
children,
}) {
return h(
"section",
{
className: `sidebar-section ${collapsed ? "is-collapsed" : "is-expanded"}`,
"data-testid": testId,
"data-collapsed": collapsed ? "true" : "false",
},
h(
"button",
{
type: "button",
className: "sidebar-section-header",
"aria-expanded": collapsed ? "false" : "true",
onClick: onToggle,
},
h("span", { className: "sidebar-section-caret", "aria-hidden": "true" }, collapsed ? "▶" : "▼"),
h("h2", null, title),
),
collapsed
? null
: h("div", { className: "sidebar-section-body" }, children),
);
}
function App() {
const workerRef = useRef(null);
const pendingResponses = useRef(new Map());
const transcriptEndRef = useRef(null);
const importInputRef = useRef(null);
const [messages, setMessages] = useState([]);
const [prompt, setPrompt] = useState("");
const [pending, setPending] = useState(false);
const [workerState, setWorkerState] = useState("wasm worker");
const [memoryStatus, setMemoryStatus] = useState("");
const [seed, setSeed] = useState({
raw: {},
tools: [],
concepts: [],
responses: {},
});
const initialPreferences = useRef(loadPreferences());
const [uiLanguagePreference] = useState(
normalizeUiLanguagePreference(initialPreferences.current.uiLanguage),
);
const [i18nRuntimeTick, setI18nRuntimeTick] = useState(0);
const uiLanguage = detectUiLanguage(uiLanguagePreference);
const t = useCallback(
(key, params) => translateUi(key, uiLanguage, params),
[uiLanguage, i18nRuntimeTick],
);
const [demoMode, setDemoMode] = useState(initialPreferences.current.demoMode);
const [demoPhase, setDemoPhase] = useState("manual");
const [demoCountdown, setDemoCountdown] = useState(null);
const [diagnosticsMode, setDiagnosticsMode] = useState(
initialPreferences.current.diagnosticsMode,
);
const [sidebarPromptsCollapsed, setSidebarPromptsCollapsed] = useState(
initialPreferences.current.sidebarPromptsCollapsed,
);
const [sidebarToolsCollapsed, setSidebarToolsCollapsed] = useState(
initialPreferences.current.sidebarToolsCollapsed,
);
const [sidebarTraceCollapsed, setSidebarTraceCollapsed] = useState(
initialPreferences.current.sidebarTraceCollapsed,
);
const [sidebarConversationsCollapsed, setSidebarConversationsCollapsed] = useState(
initialPreferences.current.sidebarConversationsCollapsed,
);
const [greetingVariations, setGreetingVariations] = useState(
initialPreferences.current.greetingVariations,
);
const [agentMode, setAgentMode] = useState(
initialPreferences.current.agentMode,
);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [colorSchemeTick, setColorSchemeTick] = useState(0);
const [currentConversationId, setCurrentConversationId] = useState(
initialPreferences.current.currentConversationId || "",
);
const [conversations, setConversations] = useState([]);
const currentConversationRef = useRef(currentConversationId);
const conversationTitlesRef = useRef(new Map());
useEffect(() => {
if (typeof document === "undefined") return;
document.documentElement.lang = uiLanguage;
document.documentElement.dir = "ltr";
}, [uiLanguage]);
useEffect(() => {
if (typeof window === "undefined") return undefined;
let cancelled = false;
const update = () => {
if (!cancelled) {
setI18nRuntimeTick((value) => value + 1);
}
};
window.addEventListener("formal-ai:i18n-ready", update);
const api = i18nApi();
if (api && api.ready && typeof api.ready.then === "function") {
api.ready.then(update).catch(() => null);
}
return () => {
cancelled = true;
window.removeEventListener("formal-ai:i18n-ready", update);
};
}, []);
useEffect(() => {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
return undefined;
}
const media = window.matchMedia("(prefers-color-scheme: dark)");
const update = () => setColorSchemeTick((value) => value + 1);
if (typeof media.addEventListener === "function") {
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}
if (typeof media.addListener === "function") {
media.addListener(update);
return () => media.removeListener(update);
}
return undefined;
}, []);
useEffect(() => {
currentConversationRef.current = currentConversationId;
}, [currentConversationId]);
const userContext = useMemo(
() =>
collectUserContext({
uiLanguage,
uiLanguagePreference,
}),
[uiLanguage, uiLanguagePreference, colorSchemeTick],
);
const userContextRef = useRef(userContext);
useEffect(() => {
userContextRef.current = userContext;
}, [userContext]);
useEffect(() => {
if (typeof window === "undefined" || !window.FormalAiSeed) return;
let cancelled = false;
window.FormalAiSeed.loadAll().then((loaded) => {
if (cancelled) return;
setSeed(loaded);
});
return () => {
cancelled = true;
};
}, []);
const refreshConversations = useCallback(async () => {
if (typeof window === "undefined" || !window.FormalAiMemory) {
return [];
}
try {
const events = await window.FormalAiMemory.listEvents();
const list = groupConversations(events);
list.forEach((entry) => {
if (entry.title) {
conversationTitlesRef.current.set(entry.id, entry.title);
}
});
setConversations(list);
return events;
} catch (_error) {
return [];
}
}, []);
useEffect(() => {
let cancelled = false;
refreshConversations().then((events) => {
if (cancelled || !Array.isArray(events) || events.length === 0) return;
const initialId = initialPreferences.current.currentConversationId;
if (!initialId) return;
const restored = messagesForConversation(events, initialId);
if (restored.length > 0) {
setMessages(restored);
setDemoMode(false);
}
});
return () => {
cancelled = true;
};
}, [refreshConversations]);
const handleExportMemory = useCallback(async () => {
if (typeof window === "undefined" || !window.FormalAiMemory) {
setMemoryStatus(t("status.memoryUnavailable"));
return;
}
try {
const events = await window.FormalAiMemory.listEvents();
const preferences = loadPreferences();
const text = window.FormalAiMemory.exportFullMemory({
seed,
events,
preferences,
info: {
version: APP_VERSION,
url: window.location.href,
userAgent: navigator.userAgent,
workerState,
mode: demoMode ? "demo" : "manual",
...userContext,
},
});
downloadTextFile(MEMORY_EXPORT_FILENAME, text);
const seedFileCount = seed && seed.raw ? Object.keys(seed.raw).length : 0;
setMemoryStatus(
t("status.memoryExported", {
events: events.length,
seedFiles: seedFileCount,
}),
);
} catch (_error) {
setMemoryStatus(t("status.exportFailed"));
}
}, [seed, workerState, demoMode, userContext, t]);
const handleImportMemory = useCallback(async (event) => {
const file = event.target.files && event.target.files[0];
event.target.value = "";
if (!file || typeof window === "undefined" || !window.FormalAiMemory) {
return;
}
try {
const text = await file.text();
const imported = window.FormalAiMemory.importFullMemory(text);
const inserted = await window.FormalAiMemory.importEvents(imported.events);
const current = {
agentInfo: seed && seed.agentInfo ? seed.agentInfo : {},
info: { version: APP_VERSION },
};
const suggestions = window.FormalAiMemory.suggestMigrations({
imported,
current,
});
const headline =
imported.kind === "bundle"
? t("status.memoryImportedBundle", { inserted })
: t("status.memoryImportedEvents", { inserted });
if (suggestions.length > 0) {
setMemoryStatus(
t("status.migration", {
headline,
suggestions: suggestions.join(" / "),
}),
);
} else {
setMemoryStatus(headline);
}
} catch (_error) {
setMemoryStatus(t("status.importFailed"));
}
}, [seed, t]);
const triggerImportMemory = useCallback(() => {
if (importInputRef.current) {
importInputRef.current.click();
}
}, []);
useEffect(() => {
persistPreferences({
demoMode,
diagnosticsMode,
sidebarPromptsCollapsed,
sidebarToolsCollapsed,
sidebarTraceCollapsed,
sidebarConversationsCollapsed,
greetingVariations,
currentConversationId,
agentMode,
uiLanguage: uiLanguagePreference,
});
}, [
demoMode,
diagnosticsMode,
sidebarPromptsCollapsed,
sidebarToolsCollapsed,
sidebarTraceCollapsed,
sidebarConversationsCollapsed,
greetingVariations,
currentConversationId,
agentMode,
uiLanguagePreference,
]);
useEffect(() => {
const worker = new Worker(withAssetVersion("formal_ai_worker.js"));
workerRef.current = worker;
worker.onmessage = (event) => {
if (event.data.kind === "ready") {
setWorkerState(event.data.mode);
return;
}
const requestId = event.data.requestId;
const resolver = pendingResponses.current.get(requestId);
if (resolver) {
pendingResponses.current.delete(requestId);
resolver(event.data);
}
};
return () => worker.terminate();
}, []);
useEffect(() => {
transcriptEndRef.current?.scrollIntoView({ block: "end" });
}, [messages]);
const greetingVariationsRef = useRef(greetingVariations);
useEffect(() => {
greetingVariationsRef.current = greetingVariations;
}, [greetingVariations]);
const agentModeRef = useRef(agentMode);
useEffect(() => {
agentModeRef.current = agentMode;
}, [agentMode]);
const requestAnswer = useCallback((text, history = []) => {
const worker = workerRef.current;
if (!worker) {
return Promise.resolve(localFallbackAnswer(text));
}
return new Promise((resolve) => {
const requestId = `request-${Date.now()}-${Math.random().toString(16).slice(2)}`;
pendingResponses.current.set(requestId, resolve);
worker.postMessage({
prompt: text,
requestId,
history,
prefs: { greetingVariations: greetingVariationsRef.current },
userContext: userContextRef.current,
});
});
}, []);
const ensureConversation = useCallback((seedText) => {
let id = currentConversationRef.current;
let isNew = false;
if (!id) {
id = generateConversationId();
isNew = true;
currentConversationRef.current = id;
setCurrentConversationId(id);
}
let title = conversationTitlesRef.current.get(id);
if (!title && seedText) {
title = deriveConversationTitle(seedText);
conversationTitlesRef.current.set(id, title);
}
return { conversationId: id, conversationTitle: title || "", isNew };
}, []);
const appendUserMessage = useCallback((text, extra = {}) => {
const { conversationId, conversationTitle } = ensureConversation(text);
const message = createMessage("user", text, extra);
setMessages((current) => [...current, message]);
recordMemoryEvent({
kind: "message",
role: "user",
content: text,
sentAt: new Date().toISOString(),
demoLabel: extra.demoLabel,
conversationId,
conversationTitle,
});
}, [ensureConversation]);
const appendAssistantMessage = useCallback((answer) => {
const source = workerRef.current ? "worker" : "fallback";
const solverEvidence = Array.isArray(answer.evidence) ? answer.evidence : [];
const evidence = answer.intent
? [`intent:${answer.intent}`, `source:${source}`, ...solverEvidence]
: solverEvidence;
const thinkingSteps = Array.isArray(answer.steps) && answer.steps.length > 0
? answer.steps.map((entry) => `${entry.step}: ${entry.detail}`)
: [
"Normalize prompt text",
`Select symbolic intent ${answer.intent || "unknown"}`,
`Render deterministic answer from ${source}`,
];
const message = createMessage("assistant", answer.content, {
intent: answer.intent,
evidence,
thinkingSteps,
iframeUrl: answer.iframeUrl || null,
});
setMessages((current) => [...current, message]);
const sentAt = new Date().toISOString();
const { conversationId, conversationTitle } = ensureConversation("");
if (Array.isArray(answer.steps)) {
answer.steps.forEach((entry) => {
recordMemoryEvent({
kind: "reasoning",
role: "assistant",
content: `${entry.step}: ${entry.detail}`,
intent: answer.intent,
sentAt,
conversationId,
conversationTitle,
});
});
}
if (Array.isArray(answer.toolCalls)) {
answer.toolCalls.forEach((call) => {
recordMemoryEvent({
kind: "tool_call",
role: "assistant",
tool: call.tool,
inputs: call.inputs,
outputs: call.outputs,
content: `tool:${call.tool}`,
sentAt,
conversationId,
conversationTitle,
});
});
}
recordMemoryEvent({
kind: "message",
role: "assistant",
content: answer.content,
intent: answer.intent,
evidence,
sentAt,
conversationId,
conversationTitle,
}).then(() => {
refreshConversations();
});
}, [ensureConversation, refreshConversations]);
const conversationHistory = useCallback(
() =>
messages.map((message) => ({
role: message.role,
content: message.content,
intent: message.intent,
evidence: message.evidence,
})),
[messages],
);
const runAgentPlan = useCallback(
async (steps, history) => {
const lines = [];
lines.push(`## Agent plan (${steps.length} steps)`);
steps.forEach((step, index) => {
lines.push(`${index + 1}. ${step}`);
});
lines.push("");
const aggregatedSteps = [];
const aggregatedToolCalls = [];
const aggregatedEvidence = [];
const workingHistory = Array.isArray(history) ? history.slice() : [];
for (let index = 0; index < steps.length; index += 1) {
const step = steps[index];
aggregatedSteps.push({
step: "agent_plan",
detail: `${index + 1}/${steps.length} ${step}`,
});
const answer = await requestAnswer(step, workingHistory);
lines.push(`### Step ${index + 1}: ${step}`);
lines.push(answer.content || "(no output)");
lines.push("");
if (Array.isArray(answer.steps)) {
answer.steps.forEach((entry) => {
aggregatedSteps.push({
step: `agent_${index + 1}_${entry.step}`,
detail: entry.detail,
});
});
}
if (Array.isArray(answer.toolCalls)) {
aggregatedToolCalls.push(...answer.toolCalls);
}
if (Array.isArray(answer.evidence)) {
aggregatedEvidence.push(
...answer.evidence.map((item) => `step_${index + 1}:${item}`),
);
}
workingHistory.push({ role: "user", content: step });
workingHistory.push({ role: "assistant", content: answer.content || "" });
}
appendAssistantMessage({
intent: "agent_plan",
content: lines.join("\n").trim(),
confidence: 0.85,
evidence: ["rule:agent_mode", `steps:${steps.length}`, ...aggregatedEvidence],
steps: aggregatedSteps,
toolCalls: aggregatedToolCalls,
});
},
[requestAnswer, appendAssistantMessage],
);
async function sendText(text, extra = {}) {
const trimmed = text.trim();
if (!trimmed || pending) {
return;
}
setPending(true);
const history = conversationHistory();
appendUserMessage(trimmed, extra);
const memoryAction = recognizeMemoryAction(trimmed);
if (memoryAction === "export") {
await handleExportMemory();
appendAssistantMessage({
intent: "memory_export",
content: t("memory.exportTriggered"),
confidence: 1.0,
evidence: ["rule:memory_export"],
steps: [{ step: "trigger_button", detail: "memory-export" }],
toolCalls: [
{
tool: "export_memory",
inputs: { prompt: trimmed },
outputs: { intent: "memory_export" },
},
],
});
setPending(false);
return;
}
if (memoryAction === "import") {
triggerImportMemory();
appendAssistantMessage({
intent: "memory_import",
content: t("memory.importTriggered"),
confidence: 1.0,
evidence: ["rule:memory_import"],
steps: [{ step: "trigger_button", detail: "memory-import" }],
toolCalls: [
{
tool: "import_memory",
inputs: { prompt: trimmed },
outputs: { intent: "memory_import" },
},
],
});
setPending(false);
return;
}
const recallQuery = recognizeRecallQuery(trimmed);
if (recallQuery && typeof window !== "undefined" && window.FormalAiMemory) {
let events = [];
try {
events = await window.FormalAiMemory.listEvents();
} catch (_error) {
events = [];
}
const report = buildRecallReport({
events,
term: recallQuery.term,
scope: recallQuery.scope,
currentConversationId: currentConversationRef.current,
triggerText: trimmed,
});
appendAssistantMessage({
intent: "conversation_recall",
content: report.content,
confidence: 1.0,
evidence: [
"rule:conversation_recall",
`scope:${recallQuery.scope}`,
`matches:${report.matches.reduce((sum, g) => sum + g.events.length, 0)}`,
],
steps: [
{ step: "extract_term", detail: recallQuery.term },
{ step: "scan_memory", detail: `${events.length} event(s)` },
{ step: "group_by_conversation", detail: `${report.matches.length} group(s)` },
],
toolCalls: [
{
tool: "conversation_recall",
inputs: { term: recallQuery.term, scope: recallQuery.scope },
outputs: {
conversations: report.matches.length,
matches: report.matches.reduce((sum, g) => sum + g.events.length, 0),
},
},
],
});
setPending(false);
return;
}
if (agentModeRef.current) {
const steps = decomposeAgentTask(trimmed);
if (steps.length > 1) {
await runAgentPlan(steps, history);
setPending(false);
return;
}
}
const answer = await requestAnswer(trimmed, history);
appendAssistantMessage(answer);
setPending(false);
}
async function send() {
const text = prompt.trim();
if (!text) {
return;
}
setPrompt("");
await sendText(text);
}
function handleKeyDown(event) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
send();
}
}
useEffect(() => {
if (!demoMode) {
setDemoPhase("manual");
setDemoCountdown(null);
return undefined;
}
let cancelled = false;
let countdownTimer = 0;
async function runCycle() {
const turns = createDemoTurns();
setMessages([]);
setPending(true);
setDemoPhase("playing");
setDemoCountdown(null);
for (const turn of turns) {
if (cancelled) {
return;
}
appendUserMessage(turn.text, { demoLabel: turn.label });
await wait(randomInt(700, 1300));
const answer = await requestAnswer(turn.text);
if (cancelled) {
return;
}
appendAssistantMessage(answer);
await wait(randomInt(900, 1500));
}
setPending(false);
const waitSeconds = randomInt(10, 20);
let remainingSeconds = waitSeconds;
setDemoPhase("waiting");
setDemoCountdown(remainingSeconds);
countdownTimer = window.setInterval(() => {
remainingSeconds -= 1;
if (remainingSeconds <= 0) {
window.clearInterval(countdownTimer);
if (!cancelled) {
runCycle();
}
return;
}
setDemoCountdown(remainingSeconds);
}, 1000);
}
runCycle();
return () => {
cancelled = true;
window.clearInterval(countdownTimer);
setPending(false);
};
}, [appendAssistantMessage, appendUserMessage, demoMode, requestAnswer]);
const lastAssistant = useMemo(
() => [...messages].reverse().find((message) => message.role === "assistant"),
[messages],
);
const demoStatus = demoMode
? demoPhase === "waiting" && demoCountdown !== null
? t("status.nextDialogIn", { seconds: demoCountdown })
: t("status.demoPlaying")
: t("status.manual");
const reportContext = {
messages,
workerState,
demoMode,
demoStatus,
diagnosticsMode,
userContext,
};
const currentReportUrl = createIssueUrl(reportContext);
return h(
"main",
{ className: "app" },
h(
"header",
{ className: "topbar" },
h("div", { className: "brand" }, h("span", { className: "mark" }, "FA"), h("strong", null, "formal-ai")),
h(
"div",
{ className: "topbar-actions" },
h("span", { className: "demo-status", "data-testid": "demo-status", role: "status" }, demoStatus),
diagnosticsMode ? h("span", { className: "status" }, workerState) : null,
h(
"a",
{
className: "report-button",
"data-testid": "report-issue",
href: currentReportUrl,
target: "_blank",
rel: "noopener noreferrer",
title: t("titles.reportIssue"),
"aria-label": t("buttons.reportIssue"),
},
h("span", { className: "btn-icon", "aria-hidden": "true" }, "🐛"),
h("span", { className: "btn-label" }, t("buttons.reportIssue")),
),
h(
"button",
{
type: "button",
className: "memory-button",
"data-testid": "memory-export",
onClick: handleExportMemory,
title: t("titles.exportMemory"),
"aria-label": t("buttons.exportMemory"),
},
h("span", { className: "btn-icon", "aria-hidden": "true" }, "📤"),
h("span", { className: "btn-label" }, t("buttons.exportMemory")),
),
h(
"button",
{
type: "button",
className: "memory-button",
"data-testid": "memory-import",
onClick: triggerImportMemory,
title: t("titles.importMemory"),
"aria-label": t("buttons.importMemory"),
},
h("span", { className: "btn-icon", "aria-hidden": "true" }, "📥"),
h("span", { className: "btn-label" }, t("buttons.importMemory")),
),
h("input", {
ref: importInputRef,
type: "file",
accept: ".lino,text/plain",
style: { display: "none" },
"data-testid": "memory-import-input",
onChange: handleImportMemory,
}),
memoryStatus
? h(
"span",
{
className: "memory-status",
role: "status",
"data-testid": "memory-status",
},
memoryStatus,
)
: null,
h(
"button",
{
type: "button",
className: "diagnostics-toggle",
"aria-pressed": diagnosticsMode,
onClick: () => setDiagnosticsMode((value) => !value),
title: diagnosticsMode
? t("titles.diagnosticsHide")
: t("titles.diagnosticsShow"),
"aria-label": diagnosticsMode
? t("buttons.diagnosticsOn")
: t("buttons.diagnostics"),
},
h("span", { className: "btn-icon", "aria-hidden": "true" }, "🔍"),
h(
"span",
{ className: "btn-label" },
diagnosticsMode ? t("buttons.diagnosticsOn") : t("buttons.diagnostics"),
),
),
h(
"button",
{
type: "button",
className: "agent-toggle",
"data-testid": "agent-toggle",
"aria-pressed": agentMode,
title: agentMode
? t("titles.agentOn")
: t("titles.agentOff"),
"aria-label": agentMode ? t("buttons.agent") : t("buttons.chat"),
onClick: () => setAgentMode((value) => !value),
},
h(
"span",
{ className: "btn-icon", "aria-hidden": "true" },
agentMode ? "🤖" : "💬",
),
h(
"span",
{ className: "btn-label" },
agentMode ? t("buttons.agent") : t("buttons.chat"),
),
),
h(
"button",
{
type: "button",
className: "mode-toggle",
"aria-pressed": demoMode,
onClick: () => setDemoMode((value) => !value),
title: demoMode
? t("titles.demoOn")
: t("titles.demoOff"),
"aria-label": demoMode ? t("buttons.demoOn") : t("buttons.demo"),
},
h("span", { className: "btn-icon", "aria-hidden": "true" }, "🎬"),
h(
"span",
{ className: "btn-label" },
demoMode ? t("buttons.demoOn") : t("buttons.demo"),
),
),
h(
"button",
{
type: "button",
className: "mobile-menu-toggle",
"data-testid": "mobile-menu-toggle",
"aria-pressed": mobileMenuOpen,
"aria-label": mobileMenuOpen
? t("buttons.closeMenu")
: t("buttons.openMenu"),
title: mobileMenuOpen
? t("titles.menuClose")
: t("titles.menuOpen"),
onClick: () => setMobileMenuOpen((value) => !value),
},
h(
"span",
{ className: "btn-icon", "aria-hidden": "true" },
mobileMenuOpen ? "✕" : "☰",
),
),
),
),
mobileMenuOpen
? h("div", {
className: "mobile-menu-backdrop",
"data-testid": "mobile-menu-backdrop",
onClick: () => setMobileMenuOpen(false),
})
: null,
h(
"section",
{ className: "workspace" },
h(
"aside",
{
className: `context-panel${mobileMenuOpen ? " is-mobile-open" : ""}`,
"data-testid": "context-panel",
},
h(CollapsibleSection, {
title: t("sidebar.conversations"),
testId: "sidebar-conversations",
collapsed: sidebarConversationsCollapsed,
onToggle: () => setSidebarConversationsCollapsed((value) => !value),
children: h(
"div",
{ className: "conversation-list", "data-testid": "conversation-list" },
h(
"button",
{
type: "button",
className: "conversation-new",
"data-testid": "conversation-new",
onClick: () => {
currentConversationRef.current = "";
setCurrentConversationId("");
setMessages([]);
setDemoMode(false);
setPrompt("");
},
},
t("conversation.new"),
),
conversations.length === 0
? h(
"p",
{ className: "conversation-empty" },
t("conversation.empty"),
)
: h(
"ul",
{
className: "conversation-entries",
"data-testid": "conversation-entries",
},
conversations.map((entry) =>
h(
"li",
{
key: entry.id,
className:
entry.id === currentConversationId
? "conversation-entry is-active"
: "conversation-entry",
},
h(
"button",
{
type: "button",
className: "conversation-entry-button",
"data-conversation-id": entry.id,
"aria-pressed": entry.id === currentConversationId,
onClick: async () => {
if (entry.id === currentConversationRef.current) {
return;
}
currentConversationRef.current = entry.id;
setCurrentConversationId(entry.id);
setDemoMode(false);
try {
const events =
await window.FormalAiMemory.listEvents();
setMessages(
messagesForConversation(events, entry.id),
);
} catch (_error) {
setMessages([]);
}
},
},
h(
"span",
{ className: "conversation-entry-title" },
entry.title || t("conversation.emptyTitle"),
),
h(
"span",
{ className: "conversation-entry-meta" },
t("conversation.messageCount", {
count: entry.messageCount,
}),
),
),
),
),
),
),
}),
h(CollapsibleSection, {
title: t("sidebar.examplePrompts"),
testId: "sidebar-prompts",
collapsed: sidebarPromptsCollapsed,
onToggle: () => setSidebarPromptsCollapsed((value) => !value),
children: h(
"div",
{ className: "prompt-list", "data-testid": "example-prompts" },
EXAMPLE_PROMPTS.map((entry) =>
h(
"button",
{
key: entry.text,
type: "button",
"data-prompt-label": entry.label,
"data-prompt-text": entry.text,
onClick: () => {
setDemoMode(false);
setPrompt(entry.text);
},
title: entry.label,
},
entry.text,
),
),
),
}),
seed.tools && seed.tools.length > 0
? h(CollapsibleSection, {
title: t("sidebar.tools"),
testId: "sidebar-tools",
collapsed: sidebarToolsCollapsed,
onToggle: () => setSidebarToolsCollapsed((value) => !value),
children: h(
"div",
{ className: "tool-registry", "data-testid": "tool-registry" },
h(
"ul",
{ className: "tool-list" },
seed.tools.map((tool) =>
h(
"li",
{
key: tool.id,
className: `tool tool-mode-${tool.mode || "thinking"}`,
"data-testid": "tool-entry",
"data-tool-id": tool.id,
"data-tool-mode": tool.mode || "thinking",
},
h(
"div",
{ className: "tool-head" },
h("strong", null, tool.name || tool.id),
h(
"span",
{ className: "tool-mode" },
tool.mode === "agent"
? t("toolMode.agent")
: t("toolMode.thinking"),
),
),
tool.description
? h("p", { className: "tool-desc" }, tool.description)
: null,
),
),
),
),
})
: null,
diagnosticsMode
? h(CollapsibleSection, {
title: t("sidebar.trace"),
testId: "sidebar-trace",
collapsed: sidebarTraceCollapsed,
onToggle: () => setSidebarTraceCollapsed((value) => !value),
children: h(
"dl",
{ className: "trace-list" },
h("div", null, h("dt", null, t("trace.model")), h("dd", null, "formal-symbolic-poc")),
h("div", null, h("dt", null, t("trace.mode")), h("dd", null, demoStatus)),
h("div", null, h("dt", null, t("trace.intent")), h("dd", null, lastAssistant?.intent ?? "none")),
h("div", null, h("dt", null, t("trace.data")), h("dd", null, "data/source-index.lino")),
h(
"div",
null,
h("dt", null, t("trace.seedFiles")),
h(
"dd",
null,
Object.keys(seed.raw || {}).join(", ") || "(loading)",
),
),
h(
"div",
null,
h("dt", null, t("trace.toolsLoaded")),
h("dd", null, String((seed.tools || []).length)),
),
h(
"div",
null,
h("dt", null, t("trace.conceptsLoaded")),
h("dd", null, String((seed.concepts || []).length)),
),
),
})
: null,
),
h(
"section",
{ className: "chat-panel" },
h(
"section",
{ className: "messages", "aria-live": "polite", "data-testid": "message-list" },
messages.map((message) =>
h(Message, {
key: message.id,
message,
diagnosticsMode,
t,
reportIssueUrl:
message.role === "assistant"
? createIssueUrl({ ...reportContext, focusMessage: message })
: null,
}),
),
pending
? h(
"article",
{ className: "message assistant pending" },
h("div", { className: "avatar", "aria-hidden": "true" }, "FA"),
h("div", { className: "message-body" }, h("div", { className: "typing" }, t("status.working"))),
)
: null,
h("div", { ref: transcriptEndRef }),
),
h(
"form",
{
className: "composer",
onSubmit: (event) => {
event.preventDefault();
send();
},
},
demoMode
? h(
"p",
{ className: "composer-demo-hint", "data-testid": "composer-demo-hint" },
t("composer.demoHint.before"),
h("span", { className: "composer-demo-hint-icon", "aria-hidden": "true" }, "🎬"),
t("composer.demoHint.after"),
)
: null,
h(
"div",
{ className: "composer-grid" },
h("textarea", {
value: prompt,
rows: 3,
placeholder: agentMode
? t("composer.placeholder.agent")
: t("composer.placeholder.chat"),
onChange: (event) => setPrompt(event.target.value),
onKeyDown: handleKeyDown,
disabled: demoMode,
"data-testid": "chat-composer-input",
}),
h(
"button",
{
className: "send-button",
type: "submit",
disabled: pending || demoMode || !prompt.trim(),
"data-testid": "chat-composer-submit",
},
pending ? "..." : t("composer.send"),
),
),
),
),
),
);
}
function wait(milliseconds) {
return new Promise((resolve) => {
window.setTimeout(resolve, milliseconds);
});
}
ReactDOM.createRoot(document.getElementById("root")).render(h(App));