Improve export/import UX: rich frontmatter preview, cleaner warnings

- Separate terminated agent messages from warnings into info notes
  (shown with subtle styling instead of amber warning banners)
- Clean up warning banner styles for dark mode compatibility
  (use amber-500/20 borders and amber-500/5 backgrounds)
- Parse YAML frontmatter in markdown files and render as structured
  data cards showing name, title, reportsTo, skills etc.
- Apply same warning style cleanup to import page

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-03-15 14:52:07 -05:00
parent 76d30ff835
commit c6ea491000
2 changed files with 150 additions and 10 deletions

View File

@@ -18,6 +18,7 @@ import {
FileText,
Folder,
FolderOpen,
Info,
Package,
} from "lucide-react";
@@ -307,6 +308,106 @@ function ExportFileTree({
);
}
// ── Frontmatter helpers ───────────────────────────────────────────────
type FrontmatterData = Record<string, string | string[]>;
function parseFrontmatter(content: string): { data: FrontmatterData; body: string } | null {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!match) return null;
const data: FrontmatterData = {};
const rawYaml = match[1];
const body = match[2];
let currentKey: string | null = null;
let currentList: string[] | null = null;
for (const line of rawYaml.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
// List item under current key
if (trimmed.startsWith("- ") && currentKey) {
if (!currentList) currentList = [];
currentList.push(trimmed.slice(2).trim().replace(/^["']|["']$/g, ""));
continue;
}
// Flush previous list
if (currentKey && currentList) {
data[currentKey] = currentList;
currentList = null;
currentKey = null;
}
const kvMatch = trimmed.match(/^([a-zA-Z_][\w-]*)\s*:\s*(.*)$/);
if (kvMatch) {
const key = kvMatch[1];
const val = kvMatch[2].trim().replace(/^["']|["']$/g, "");
if (val) {
data[key] = val;
currentKey = null;
} else {
currentKey = key;
}
}
}
// Flush trailing list
if (currentKey && currentList) {
data[currentKey] = currentList;
}
return Object.keys(data).length > 0 ? { data, body } : null;
}
const FRONTMATTER_FIELD_LABELS: Record<string, string> = {
name: "Name",
title: "Title",
kind: "Kind",
reportsTo: "Reports to",
skills: "Skills",
status: "Status",
description: "Description",
priority: "Priority",
assignee: "Assignee",
project: "Project",
targetDate: "Target date",
};
function FrontmatterCard({ data }: { data: FrontmatterData }) {
return (
<div className="rounded-md border border-border bg-accent/20 px-4 py-3 mb-4">
<dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-sm">
{Object.entries(data).map(([key, value]) => (
<div key={key} className="contents">
<dt className="text-muted-foreground whitespace-nowrap py-0.5">
{FRONTMATTER_FIELD_LABELS[key] ?? key}
</dt>
<dd className="py-0.5">
{Array.isArray(value) ? (
<div className="flex flex-wrap gap-1.5">
{value.map((item) => (
<span
key={item}
className="inline-flex items-center rounded-md border border-border bg-background px-2 py-0.5 text-xs"
>
{item}
</span>
))}
</div>
) : (
<span>{value}</span>
)}
</dd>
</div>
))}
</dl>
</div>
);
}
// ── Preview pane ──────────────────────────────────────────────────────
function ExportPreviewPane({
@@ -323,6 +424,7 @@ function ExportPreviewPane({
}
const isMarkdown = selectedFile.endsWith(".md");
const parsed = isMarkdown ? parseFrontmatter(content) : null;
return (
<div className="min-w-0">
@@ -330,7 +432,12 @@ function ExportPreviewPane({
<div className="truncate font-mono text-sm">{selectedFile}</div>
</div>
<div className="min-h-[560px] px-5 py-5">
{isMarkdown ? (
{parsed ? (
<>
<FrontmatterCard data={parsed.data} />
{parsed.body.trim() && <MarkdownBody>{parsed.body}</MarkdownBody>}
</>
) : isMarkdown ? (
<MarkdownBody>{content}</MarkdownBody>
) : (
<pre className="overflow-x-auto whitespace-pre-wrap break-words border-0 bg-transparent p-0 font-mono text-sm text-foreground">
@@ -408,6 +515,21 @@ export function CompanyExport() {
const totalFiles = useMemo(() => countFiles(tree), [tree]);
const selectedCount = checkedFiles.size;
// Separate info notes (terminated agents) from real warnings
const { notes, warnings } = useMemo(() => {
if (!exportData) return { notes: [] as string[], warnings: [] as string[] };
const notes: string[] = [];
const warnings: string[] = [];
for (const w of exportData.warnings) {
if (/terminated agent/i.test(w)) {
notes.push(w);
} else {
warnings.push(w);
}
}
return { notes, warnings };
}, [exportData]);
function handleToggleDir(path: string) {
setExpandedDirs((prev) => {
const next = new Set(prev);
@@ -489,9 +611,15 @@ export function CompanyExport() {
<span className="text-muted-foreground">
{selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected
</span>
{exportData.warnings.length > 0 && (
<span className="text-amber-600">
{exportData.warnings.length} warning{exportData.warnings.length === 1 ? "" : "s"}
{warnings.length > 0 && (
<span className="text-amber-500">
{warnings.length} warning{warnings.length === 1 ? "" : "s"}
</span>
)}
{notes.length > 0 && (
<span className="text-muted-foreground flex items-center gap-1">
<Info className="h-3 w-3" />
{notes.length} note{notes.length === 1 ? "" : "s"}
</span>
)}
</div>
@@ -506,11 +634,23 @@ export function CompanyExport() {
</div>
</div>
{/* Notes (informational, e.g. terminated agents) */}
{notes.length > 0 && (
<div className="border-b border-border px-5 py-2 flex items-start gap-2">
<Info className="h-3.5 w-3.5 mt-0.5 shrink-0 text-muted-foreground" />
<div>
{notes.map((n) => (
<div key={n} className="text-xs text-muted-foreground">{n}</div>
))}
</div>
</div>
)}
{/* Warnings */}
{exportData.warnings.length > 0 && (
<div className="border-b border-amber-300/60 bg-amber-50/60 px-5 py-2">
{exportData.warnings.map((w) => (
<div key={w} className="text-xs text-amber-700">{w}</div>
{warnings.length > 0 && (
<div className="border-b border-amber-500/20 bg-amber-500/5 px-5 py-2">
{warnings.map((w) => (
<div key={w} className="text-xs text-amber-500">{w}</div>
))}
</div>
)}

View File

@@ -644,9 +644,9 @@ export function CompanyImport() {
{/* Warnings */}
{importPreview.warnings.length > 0 && (
<div className="border-b border-amber-300/60 bg-amber-50/60 px-5 py-2">
<div className="border-b border-amber-500/20 bg-amber-500/5 px-5 py-2">
{importPreview.warnings.map((w) => (
<div key={w} className="text-xs text-amber-700">{w}</div>
<div key={w} className="text-xs text-amber-500">{w}</div>
))}
</div>
)}