mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
* feat(web): markdown Source | Preview toggle in session file pane Add Source | Preview toggle for .md/.mdx files in the session file route, defaulting to preview with localStorage persistence. Reuse chat markdown pipeline via MarkdownRenderer standalone mode (no assistant-ui thread). Includes unit tests, Playwright smoke, and e2e fixture. Closes tiann/hapi#954 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): cast standalone MarkdownRenderer components for react-markdown Soup verify gate: defaultComponents merge type is wider than react-markdown Components; standalone file-pane path needs explicit cast. * fix(web): route file-pane markdown fences through SyntaxHighlighter Standalone file preview now mirrors chat code-block rendering: fenced blocks use SyntaxHighlighter and MARKDOWN_COMPONENTS_BY_LANGUAGE (mermaid included) without requiring ThreadPrimitive context. Addresses HAPI Bot Major on tiann/hapi#957. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): detect fenced vs inline code in standalone markdown preview Move block detection to the pre override (react-markdown v10 does not pass inline to custom code components). Add inline-code regression test. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
70 lines
1.7 KiB
TypeScript
70 lines
1.7 KiB
TypeScript
export type MarkdownPreviewMode = 'source' | 'preview'
|
|
|
|
export const MARKDOWN_PREVIEW_MODE_STORAGE_KEY = 'hapi.filePreview.markdownMode.v1'
|
|
export const DEFAULT_MARKDOWN_PREVIEW_MODE: MarkdownPreviewMode = 'preview'
|
|
|
|
function isBrowser(): boolean {
|
|
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
}
|
|
|
|
function safeGetItem(key: string): string | null {
|
|
if (!isBrowser()) {
|
|
return null
|
|
}
|
|
try {
|
|
return localStorage.getItem(key)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function safeSetItem(key: string, value: string): void {
|
|
if (!isBrowser()) {
|
|
return
|
|
}
|
|
try {
|
|
localStorage.setItem(key, value)
|
|
} catch {
|
|
// Ignore storage errors
|
|
}
|
|
}
|
|
|
|
function safeRemoveItem(key: string): void {
|
|
if (!isBrowser()) {
|
|
return
|
|
}
|
|
try {
|
|
localStorage.removeItem(key)
|
|
} catch {
|
|
// Ignore storage errors
|
|
}
|
|
}
|
|
|
|
export function isMarkdownFile(path: string): boolean {
|
|
const parts = path.split('.')
|
|
if (parts.length <= 1) {
|
|
return false
|
|
}
|
|
const ext = parts[parts.length - 1]?.toLowerCase()
|
|
return ext === 'md' || ext === 'mdx'
|
|
}
|
|
|
|
function parseMarkdownPreviewMode(raw: string | null): MarkdownPreviewMode {
|
|
if (raw === 'source' || raw === 'preview') {
|
|
return raw
|
|
}
|
|
return DEFAULT_MARKDOWN_PREVIEW_MODE
|
|
}
|
|
|
|
export function getInitialMarkdownPreviewMode(): MarkdownPreviewMode {
|
|
return parseMarkdownPreviewMode(safeGetItem(MARKDOWN_PREVIEW_MODE_STORAGE_KEY))
|
|
}
|
|
|
|
export function persistMarkdownPreviewMode(mode: MarkdownPreviewMode): void {
|
|
if (mode === DEFAULT_MARKDOWN_PREVIEW_MODE) {
|
|
safeRemoveItem(MARKDOWN_PREVIEW_MODE_STORAGE_KEY)
|
|
return
|
|
}
|
|
safeSetItem(MARKDOWN_PREVIEW_MODE_STORAGE_KEY, mode)
|
|
}
|