import { useRef } from "react"; import type { FileSummary, BodyElement } from "../../lib/api"; interface FileListProps { files: FileSummary[]; loading: boolean; onSelect: (id: string) => void; onDelete: (id: string) => void; onCreate: () => void; onUploadMarkdown?: (name: string, body: BodyElement[]) => void; } /** * Parse markdown text into BodyElements. * Converts image embeds to links instead of images. */ function parseMarkdown(markdown: string): BodyElement[] { const elements: BodyElement[] = []; const lines = markdown.split('\n'); let currentParagraph: string[] = []; let inCodeBlock = false; let codeBlockLanguage: string | undefined; let codeBlockContent: string[] = []; let currentList: { ordered: boolean; items: string[] } | null = null; const flushParagraph = () => { if (currentParagraph.length > 0) { const text = currentParagraph.join('\n').trim(); if (text) { elements.push({ type: "paragraph", text }); } currentParagraph = []; } }; const flushCodeBlock = () => { if (codeBlockContent.length > 0 || inCodeBlock) { elements.push({ type: "code", language: codeBlockLanguage || undefined, content: codeBlockContent.join('\n'), }); codeBlockContent = []; codeBlockLanguage = undefined; } }; const flushList = () => { if (currentList && currentList.items.length > 0) { elements.push({ type: "list", ordered: currentList.ordered, items: currentList.items, }); currentList = null; } }; // Convert image syntax ![alt](url) to link syntax [alt](url) or [image](url) const convertImagesToLinks = (text: string): string => { return text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, url) => { const linkText = alt || 'image'; return `[${linkText}](${url})`; }); }; for (const rawLine of lines) { // Check for code block fence (``` or ~~~) const codeFenceMatch = rawLine.match(/^(`{3,}|~{3,})(\w*)?$/); if (codeFenceMatch) { if (!inCodeBlock) { // Starting a code block flushParagraph(); flushList(); inCodeBlock = true; codeBlockLanguage = codeFenceMatch[2] || undefined; codeBlockContent = []; } else { // Ending a code block flushCodeBlock(); inCodeBlock = false; } continue; } // If inside a code block, add line as-is if (inCodeBlock) { codeBlockContent.push(rawLine); continue; } // Convert images to links in all lines const line = convertImagesToLinks(rawLine); // Check for headings const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { flushParagraph(); flushList(); const level = headingMatch[1].length; const text = headingMatch[2].trim(); elements.push({ type: "heading", level, text }); continue; } // Check for unordered list items (-, *, +) const unorderedMatch = line.match(/^[\s]*[-*+]\s+(.+)$/); if (unorderedMatch) { flushParagraph(); const itemText = unorderedMatch[1].trim(); if (currentList && currentList.ordered) { // Switch from ordered to unordered flushList(); } if (!currentList) { currentList = { ordered: false, items: [] }; } currentList.items.push(itemText); continue; } // Check for ordered list items (1. 2. etc) const orderedMatch = line.match(/^[\s]*\d+\.\s+(.+)$/); if (orderedMatch) { flushParagraph(); const itemText = orderedMatch[1].trim(); if (currentList && !currentList.ordered) { // Switch from unordered to ordered flushList(); } if (!currentList) { currentList = { ordered: true, items: [] }; } currentList.items.push(itemText); continue; } // Empty line - flush everything if (line.trim() === '') { flushParagraph(); flushList(); continue; } // Regular text - flush list first, then add to paragraph flushList(); currentParagraph.push(line); } // Flush any remaining content if (inCodeBlock) { flushCodeBlock(); } flushParagraph(); flushList(); return elements; } function formatDuration(seconds: number | null): string { if (seconds === null) return "-"; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, "0")}`; } function formatDate(dateStr: string): string { const date = new Date(dateStr); return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit", }); } export function FileList({ files, loading, onSelect, onDelete, onCreate, onUploadMarkdown, }: FileListProps) { const fileInputRef = useRef(null); const handleFileUpload = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file || !onUploadMarkdown) return; const reader = new FileReader(); reader.onload = (e) => { const content = e.target?.result as string; if (content) { const body = parseMarkdown(content); // Use filename without extension as the name const name = file.name.replace(/\.md$/i, '') || 'Imported Document'; onUploadMarkdown(name, body); } }; reader.readAsText(file); // Reset input so the same file can be uploaded again if (fileInputRef.current) { fileInputRef.current.value = ''; } }; if (loading) { return (
Loading files...
); } return (
FILES//
{onUploadMarkdown && ( <> )}
{files.length === 0 ? (
No saved files yet. Start recording to create one.
) : (
{files.map((file) => (
))}
)}
); }