diff --git a/index.html b/index.html index a47b0d8..a25b9ff 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,7 @@ @media (prefers-reduced-motion: reduce) { .app-loading-gem { animation: none; transform: rotate(45deg); } } - + diff --git a/package.json b/package.json index 7ccad16..818d8a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amerc-site", - "version": "2.4.0-quartermaster", + "version": "2.5.0-chatmd", "private": true, "type": "module", "scripts": { diff --git a/src/Chat.jsx b/src/Chat.jsx index 32be991..ca0d00f 100644 --- a/src/Chat.jsx +++ b/src/Chat.jsx @@ -1,9 +1,31 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { marked } from 'marked'; import { api } from './api.js'; // Native amerc chat — speaks plain HTTP to amerc-api (long-poll + streamed // file relay). No iframe, no external relay: works whenever the API is up. +// Render chat text as markdown. The agent (or user) owns its links — we only +// make them safe and open them in a new tab; the agent decides whether a link +// points at the netdisk, a showcase, or anywhere else. +marked.setOptions({ breaks: true, gfm: true }); +function mdHtml(text) { + let raw; + try { raw = marked.parse(String(text || '')); } catch { return escapeText(text); } + if (typeof window === 'undefined' || !window.DOMParser) return escapeText(text); + const doc = new DOMParser().parseFromString(raw, 'text/html'); + doc.querySelectorAll('script,style,iframe,object,embed,link,meta,form,input,button').forEach((n) => n.remove()); + doc.querySelectorAll('*').forEach((el) => { + [...el.attributes].forEach((a) => { + if (/^on/i.test(a.name) || a.name === 'style') el.removeAttribute(a.name); + if ((a.name === 'href' || a.name === 'src') && /^\s*(javascript|data):/i.test(a.value)) el.removeAttribute(a.name); + }); + if (el.tagName === 'A') { el.setAttribute('target', '_blank'); el.setAttribute('rel', 'noopener noreferrer'); } + }); + return doc.body.innerHTML; +} +function escapeText(s) { return String(s || '').replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])); } + const FILE_TTL_MS = 5 * 60 * 1000; const fmtBytes = (n) => (n >= 1048576 ? `${(n / 1048576).toFixed(1)} MB` : n >= 1024 ? `${(n / 1024).toFixed(0)} KB` : `${n} B`); const fmtTime = (t) => new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); @@ -136,7 +158,7 @@ export default function ChatPanel({ instance, accesskey, sessionId: resumeId, on const mine = m.role === 'user'; return (
- {m.kind === 'file' ? :

{m.content}

} + {m.kind === 'file' ? :
} {fmtTime(m.created_at)}
); diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx index 41ab2d5..9c4fb27 100644 --- a/src/Scene2D.jsx +++ b/src/Scene2D.jsx @@ -12,7 +12,7 @@ import { api } from './api.js'; const Menu = ({ size = 20 }) => (); const X = ({ size = 20 }) => (); -const RELEASE = '2.4.0-quartermaster'; +const RELEASE = '2.5.0-chatmd'; const NAV = [ { id: 'home', label: 'Tavern' }, @@ -477,6 +477,7 @@ function StatusPage() { } const CHANGELOG = [ + { v: '2.5', date: 'Jun 2026', title: 'Chat speaks markdown', notes: ['Chat messages now render markdown — code blocks, lists, tables and links — so an agent can answer with formatted, clickable replies (it picks its own links, to the netdisk or anywhere)'] }, { v: '2.4', date: 'Jun 2026', title: 'The Quartermaster command post', notes: ['The admin Quartermaster is now amerc’s internal management station — members, agents, companies & products, whiteboards and netdisk all in one panel', 'A command-post dashboard with live counts and quick links to git, docs, the PM workspace and the webagent relay'] }, { v: '2.3', date: 'Jun 2026', title: 'My Mansion gets its villa', notes: ['My Mansion is now a moonlit estate — your villa with glowing windows, and the services (Showcase, File-mapper, Net-disk) are chambers within it'] }, { v: '2.2', date: 'Jun 2026', title: 'My Legion becomes a war camp', notes: ['Your classes are banners and each instance is a soldier — alive and bobbing when online, standing by when pending, fallen when down', 'Tap a soldier to command it: chat with your own running agent, watch its live terminal, or stand it down', 'Statuses refresh on their own so the camp stays current'] }, diff --git a/src/styles.css b/src/styles.css index affee03..ef3ee16 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1856,3 +1856,22 @@ button { .qm-link-go { color: #46c8e0; font-size: 15px; } .qm-body .px-admin { position: static; transform: none; width: 100%; margin: 0; } .qm-body .pm-disk { padding: 0; } + +/* markdown inside chat bubbles (2.5) */ +.ch-md { color: #e8eef8; font-size: 13px; line-height: 1.5; word-break: break-word; } +.ch-md > :first-child { margin-top: 0; } +.ch-md > :last-child { margin-bottom: 0; } +.ch-md p { margin: 0 0 7px; } +.ch-md a { color: #7fd9ea; text-decoration: underline; } +.ch-md a:hover { color: #cfeff7; } +.ch-md code { background: rgba(0,0,0,0.35); border: 1px solid #2a3550; border-radius: 4px; padding: 1px 5px; font-family: ui-monospace, Menlo, monospace; font-size: 11.5px; color: #cfe2f2; } +.ch-md pre { background: #05070c; border: 1px solid #1c2438; border-radius: 8px; padding: 10px; overflow-x: auto; margin: 7px 0; } +.ch-md pre code { background: none; border: none; padding: 0; color: #9fe6c0; } +.ch-md ul, .ch-md ol { margin: 6px 0; padding-left: 20px; } +.ch-md li { margin: 2px 0; } +.ch-md h1, .ch-md h2, .ch-md h3, .ch-md h4 { margin: 8px 0 4px; font-size: 14px; color: #f3e9d2; font-family: Georgia, serif; } +.ch-md blockquote { margin: 6px 0; padding-left: 10px; border-left: 3px solid #443a7e; color: #b6a7d4; } +.ch-md table { border-collapse: collapse; margin: 7px 0; font-size: 12px; } +.ch-md th, .ch-md td { border: 1px solid #2a3550; padding: 4px 8px; } +.ch-md img { max-width: 100%; border-radius: 6px; } +.ch-msg.mine .ch-md code { background: rgba(0,0,0,0.3); }