diff options
| author | soryu <soryu@soryu.co> | 2026-01-06 04:08:11 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-11 03:01:13 +0000 |
| commit | 8b17a175c3e7e27b789812eba4e3cd760beadb10 (patch) | |
| tree | 7864dcaa2fa9db47fdfd4e8bfdb0b1dde832aa33 /makima/frontend/src/components/files/FileList.tsx | |
| parent | f79c416c58557d2f946aa5332989afdfa8c021cd (diff) | |
| download | soryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.tar.gz soryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.zip | |
Initial Control system
Diffstat (limited to 'makima/frontend/src/components/files/FileList.tsx')
| -rw-r--r-- | makima/frontend/src/components/files/FileList.tsx | 207 |
1 files changed, 200 insertions, 7 deletions
diff --git a/makima/frontend/src/components/files/FileList.tsx b/makima/frontend/src/components/files/FileList.tsx index a859aa1..c537846 100644 --- a/makima/frontend/src/components/files/FileList.tsx +++ b/makima/frontend/src/components/files/FileList.tsx @@ -1,4 +1,5 @@ -import type { FileSummary } from "../../lib/api"; +import { useRef } from "react"; +import type { FileSummary, BodyElement } from "../../lib/api"; interface FileListProps { files: FileSummary[]; @@ -6,6 +7,154 @@ interface FileListProps { 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 { @@ -32,7 +181,32 @@ export function FileList({ 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"> @@ -47,12 +221,31 @@ export function FileList({ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> FILES// </div> - <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 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"> |
