Files
ds-chat/frontend/src/pages/HelpPage.tsx
T
ksmithandClaude Sonnet 5.5 6975ed4dc0 Add a collapsible markdown formatting toolbar to the composer (#75)
Markdown support was already complete and documented, but nothing in the
composer pointed at it -- unlike @, # and : (which advertise themselves
when typed), formatting syntax was only discoverable via Help, buried in
the account menu.

The composer stays a plain markdown textarea; a new "Aa" button opens a
toolbar that inserts the syntax for you: bold, italic, strikethrough,
inline code, code block, link, quote, bulleted/numbered lists, H1-H3,
sub/superscript. Collapsed by default, remembered across reloads and
room switches. Clicking a button again removes the formatting (native
undo doesn't track programmatic edits), and bold/italic/sub/strike
toggles are run-length aware so "***x***" is handled correctly. The
selection math is pure functions in lib/markdownFormat.ts, unit-tested.

The "?" opens /help#formatting in a new tab so the draft isn't lost;
HelpPage now scrolls to the hash once the guide has loaded, since the
browser's own hash scroll fired before the async-fetched headings
existed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 16:06:56 -06:00

73 lines
2.5 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import Markdown from 'markdown-to-jsx'
import { Link, useLocation } from 'react-router-dom'
import { createMarkdownOptions, preprocessMarkdown } from '../components/MessageContent'
import { TopBar } from '../components/TopBar'
import './HelpPage.css'
// Single source of truth is the repo-root USER_GUIDE.md -- frontend/public/
// symlinks to it, so this fetches the same file a developer sees when
// browsing the repo, rather than duplicating its content into the bundle.
const GUIDE_URL = '/USER_GUIDE.md'
export function HelpPage() {
const [content, setContent] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
// #21: heading-id/sub/superscript support -- the user guide is exactly
// the kind of document that benefits from an explicit anchor override.
const markdownPreview = useMemo(
() => (content !== null ? preprocessMarkdown(content) : null),
[content],
)
// #75: the composer's "?" links straight to /help#formatting, but the guide
// is fetched after the page loads -- the browser's own scroll-to-hash has
// already run (and found nothing) by the time the headings exist.
const { hash } = useLocation()
useEffect(() => {
if (!markdownPreview || !hash) return
document.getElementById(hash.slice(1))?.scrollIntoView()
}, [markdownPreview, hash])
useEffect(() => {
let cancelled = false
fetch(GUIDE_URL)
.then((res) => {
if (!res.ok) throw new Error(`Failed to load guide (${res.status})`)
return res.text()
})
.then((text) => {
if (!cancelled) setContent(text)
})
.catch((err) => {
if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to load guide')
})
return () => {
cancelled = true
}
}, [])
return (
<div className="help-page">
<TopBar />
<div className="help-body">
<div className="help-header">
<h1>Help</h1>
<Link to="/rooms" className="btn-secondary">
Back to chat
</Link>
</div>
{error && <p className="admin-error">{error}</p>}
{!error && content === null && <p className="help-loading">Loading…</p>}
{!error && markdownPreview && (
<div className="message-text help-content">
<Markdown options={createMarkdownOptions(markdownPreview.headingIds)}>
{markdownPreview.text}
</Markdown>
</div>
)}
</div>
</div>
)
}