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  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<HTMLInputElement>(null);
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
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 (
<div className="panel h-full flex items-center justify-center">
<div className="font-mono text-[#9bc3ff] text-sm">Loading files...</div>
</div>
);
}
return (
<div className="panel h-full flex flex-col">
<div className="flex items-center justify-between p-4 pb-2 border-b border-dashed border-[rgba(117,170,252,0.35)]">
<div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
FILES//
</div>
<div className="flex items-center gap-2">
{onUploadMarkdown && (
<>
<input
ref={fileInputRef}
type="file"
accept=".md,.markdown,text/markdown"
onChange={handleFileUpload}
className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors uppercase"
>
Upload .md
</button>
</>
)}
<button
onClick={onCreate}
className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors uppercase"
>
+ New
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto">
{files.length === 0 ? (
<div className="text-center text-[#9bc3ff] text-sm font-mono opacity-60 py-8">
No saved files yet. Start recording to create one.
</div>
) : (
<div className="divide-y divide-[rgba(117,170,252,0.15)]">
{files.map((file) => (
<div
key={file.id}
className="p-4 hover:bg-[rgba(117,170,252,0.05)] transition-colors"
>
<div className="flex items-start justify-between gap-4">
<button
onClick={() => onSelect(file.id)}
className="flex-1 text-left"
>
<h3 className="font-mono text-sm text-[#dbe7ff] mb-1">
{file.name}
</h3>
{file.description && (
<p className="font-mono text-xs text-[#9bc3ff] mb-2 line-clamp-2">
{file.description}
</p>
)}
<div className="flex gap-4 font-mono text-[10px] text-[#75aafc]">
<span>{file.transcriptCount} segments</span>
<span>{formatDuration(file.duration)}</span>
<span>{formatDate(file.createdAt)}</span>
</div>
</button>
<button
onClick={(e) => {
e.stopPropagation();
onDelete(file.id);
}}
className="px-2 py-1 font-mono text-[10px] text-red-400 hover:bg-red-400/10 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
>
Delete
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}