Files
Teamup/client/src/components/MarkdownEditor.tsx
T
soroush.asadi 4758e4b5de Markdown Edit/Preview tabs + read-only .md viewer for skills & profiles
Adds MarkdownEditor (react-markdown + remark-gfm, no raw HTML — authored/retrieved
content is data, not markup) with Edit | Preview tabs, wired into the AGENTS.md and
SKILL.md editors, the agent persona, and the review artifact.

Adds a read-only "View" on every skill and agent-profile card — including builtins,
which previously had no way to be inspected at all — rendering the full SKILL.md /
AGENTS.md (frontmatter + body + actions/golden tests). Collapses a same-version
builtin that an org has forked so its own copy shadows it, keeping the version
picker unambiguous and the item clearly editable/versionable.

Also lands the agent-face wiring on the seat configurator (a live xl preview with a
state cycler) and the review inbox header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:26:14 +03:30

100 lines
3.4 KiB
TypeScript

import { useState } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
import './markdown.css'
interface MarkdownEditorProps {
value: string
onChange?: (value: string) => void
rows?: number
/** Monospace editing font — use for raw .md files (AGENTS.md / SKILL.md). */
mono?: boolean
/** Split a leading YAML frontmatter block and show it above the rendered body in the preview. */
frontmatter?: boolean
placeholder?: string
id?: string
/** Which tab to open on first render. Defaults to Preview when read-only (no onChange), else Edit. */
defaultTab?: Tab
/** Extra classes for the textarea (edit tab). */
className?: string
}
type Tab = 'edit' | 'preview'
/** Strips a leading `---\n…\n---` frontmatter block so the preview can render the body as prose. */
function splitFrontmatter(src: string): { fm: string | null; body: string } {
const match = src.match(/^---\n([\s\S]*?)\n---\n?/)
return match ? { fm: match[1], body: src.slice(match[0].length) } : { fm: null, body: src }
}
/**
* A markdown field with Edit | Preview tabs. Edit is the familiar textarea; Preview renders
* GitHub-flavored markdown (react-markdown + remark-gfm, no raw HTML — retrieved/authored content is
* data, not markup). Used wherever the app authors markdown: AGENTS.md, SKILL.md, agent persona, and
* the review artifact.
*/
export function MarkdownEditor({
value,
onChange,
rows = 8,
mono = false,
frontmatter = false,
placeholder,
id,
defaultTab,
className,
}: MarkdownEditorProps) {
const [tab, setTab] = useState<Tab>(defaultTab ?? (onChange ? 'edit' : 'preview'))
const { fm, body } = frontmatter ? splitFrontmatter(value) : { fm: null, body: value }
const hasContent = (frontmatter ? body : value).trim().length > 0
return (
<div className="flex flex-col">
<div className="flex items-center gap-1 border-b" role="tablist">
{(['edit', 'preview'] as Tab[]).map((t) => (
<button
key={t}
type="button"
role="tab"
aria-selected={tab === t}
onClick={() => setTab(t)}
className={cn(
'-mb-px border-b-2 px-3 py-1.5 text-xs font-medium capitalize transition-colors',
tab === t
? 'border-primary text-foreground'
: 'border-transparent text-muted-foreground hover:text-foreground',
)}
>
{t === 'edit' && !onChange ? 'source' : t}
</button>
))}
</div>
{tab === 'edit' ? (
<Textarea
id={id}
value={value}
onChange={(e) => onChange?.(e.target.value)}
rows={rows}
placeholder={placeholder}
readOnly={!onChange}
className={cn('mt-2 rounded-t-none', mono && 'font-mono text-xs', className)}
/>
) : (
<div className="mt-2 rounded-md border bg-background px-3 py-2" style={{ minHeight: rows * 22 }}>
{hasContent ? (
<div className="md-prose">
{frontmatter && fm && <div className="md-frontmatter">{fm}</div>}
<ReactMarkdown remarkPlugins={[remarkGfm]}>{body}</ReactMarkdown>
</div>
) : (
<p className="text-sm text-muted-foreground/70">Nothing to preview yet.</p>
)}
</div>
)}
</div>
)
}