Mercurial
view hg-web/src/components/directory-browser.tsx @ 279:b3b547563ec7
Add Google connector service and agent wiki
Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:22:36 -0700 |
| parents | 70de0c80d093 |
| children |
line wrap: on
line source
import React, { useState, useEffect, useRef, useCallback } from 'react'; import createMarkdownModule from 'markdown_converter/markdown_to_html_wasm/markdown_to_html_bin.js'; import hljs from 'third_party/highlight/highlight.min.js'; // --- ICONS (served as static files) --- const ICONS = { folder: "/icons/folder.png", file: "/icons/file.svg", close: "/icons/close.png" }; const API_BASE = '/api/repo'; // File extensions that should be displayed as code const CODE_EXTENSIONS = new Set([ 'js', 'jsx', 'ts', 'tsx', 'py', 'rb', 'java', 'c', 'cpp', 'h', 'hpp', 'cs', 'go', 'rs', 'swift', 'kt', 'scala', 'php', 'pl', 'sh', 'bash', 'zsh', 'fish', 'ps1', 'bat', 'cmd', 'html', 'htm', 'css', 'scss', 'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf', 'md', 'markdown', 'txt', 'log', 'sql', 'graphql', 'vue', 'svelte', 'astro', 'prisma', 'dockerfile', 'makefile', 'cmake', 'gradle', 'pom', 'lock', 'gitignore', 'env', 'example', 'sample', 'bzl', 'bazel' ]); const BAZEL_FILENAMES = new Set([ 'build', 'build.bazel', 'module.bazel', 'workspace', 'workspace.bazel' ]); type StaticPreviewKind = 'image' | 'video' | 'audio' | 'pdf'; const IMAGE_EXTENSIONS = new Set([ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'bmp', 'ico', 'svg' ]); const VIDEO_EXTENSIONS = new Set(['mp4', 'm4v', 'webm', 'mov', 'ogv']); const AUDIO_EXTENSIONS = new Set(['mp3', 'wav', 'ogg', 'oga', 'flac', 'm4a', 'aac']); // Prefetch cache const prefetchCache = new Map<string, Promise<any>>(); function isCodeFile(filename: string): boolean { const ext = filename.split('.').pop()?.toLowerCase() || ''; const basename = filename.toLowerCase(); return CODE_EXTENSIONS.has(ext) || CODE_EXTENSIONS.has(basename) || BAZEL_FILENAMES.has(basename) || basename === 'dockerfile' || basename === 'makefile' || basename.startsWith('.'); } function isMarkdownFile(filename: string): boolean { const ext = filename.split('.').pop()?.toLowerCase() || ''; return ext === 'md' || ext === 'markdown'; } function getStaticPreviewKind(filename: string): StaticPreviewKind | null { const ext = filename.split('.').pop()?.toLowerCase() || ''; if (IMAGE_EXTENSIONS.has(ext)) return 'image'; if (VIDEO_EXTENSIONS.has(ext)) return 'video'; if (AUDIO_EXTENSIONS.has(ext)) return 'audio'; if (ext === 'pdf') return 'pdf'; return null; } function prefetchDirectory(path: string): void { const cacheKey = `dir:${path}`; if (prefetchCache.has(cacheKey)) return; const url = path ? `${API_BASE}/list?path=${encodeURIComponent(path)}` : `${API_BASE}/list`; prefetchCache.set(cacheKey, fetch(url).then(r => r.json()).catch(() => null)); } function prefetchFile(path: string): void { const cacheKey = `file:${path}`; if (prefetchCache.has(cacheKey)) return; prefetchCache.set(cacheKey, fetch(`${API_BASE}/file?path=${encodeURIComponent(path)}`) .then(r => r.ok ? r.text() : null) .catch(() => null) ); } async function getCachedFile(path: string): Promise<string | null> { const cacheKey = `file:${path}`; if (prefetchCache.has(cacheKey)) { return prefetchCache.get(cacheKey); } prefetchFile(path); return prefetchCache.get(cacheKey)!; } /** * Component: Breadcrumb */ function Breadcrumb({ currentPath, onNavigate }: { currentPath: string; onNavigate: (path: string) => void }) { if (!currentPath) { return ( <nav className="breadcrumb"> <span className="nav-item active">root</span> </nav> ); } const parts = currentPath.split('/').filter(p => p); const crumbs = parts.map((part, index) => ({ name: part, fullPath: parts.slice(0, index + 1).join('/') })); return ( <nav className="breadcrumb"> <a href="#" onClick={(e) => { e.preventDefault(); onNavigate(''); }} title="Go to Root" > root </a> {crumbs.map((crumb, index) => { const isLast = index === crumbs.length - 1; return ( <React.Fragment key={crumb.fullPath}> <span className="separator">/</span> {isLast ? ( <span className="nav-item active">{crumb.name}</span> ) : ( <a href="#" onClick={(e) => { e.preventDefault(); onNavigate(crumb.fullPath); }} > {crumb.name} </a> )} </React.Fragment> ); })} </nav> ); } /** * Component: FileViewer * Shows file content inline with syntax highlighting */ function FileViewer({ filePath, onClose }: { filePath: string; onClose: () => void }) { const [content, setContent] = useState<string | null>(null); const [loading, setLoading] = useState(true); const codeRef = useRef<HTMLElement>(null); const filename = filePath.split('/').pop() || filePath; useEffect(() => { setLoading(true); getCachedFile(filePath).then((text) => { setContent(text); setLoading(false); }); }, [filePath]); useEffect(() => { if (content && codeRef.current) { hljs.highlightElement(codeRef.current); } }, [content]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); const getLanguage = () => { const ext = filename.split('.').pop()?.toLowerCase() || ''; const basename = filename.toLowerCase(); if (BAZEL_FILENAMES.has(basename) || ext === 'bzl' || ext === 'bazel') { return 'python'; } const langMap: Record<string, string> = { js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript', py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java', c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp', sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'bash', json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml', html: 'html', htm: 'html', css: 'css', scss: 'scss', sass: 'scss', sql: 'sql', md: 'markdown', markdown: 'markdown', xml: 'xml', dockerfile: 'dockerfile', makefile: 'makefile' }; return langMap[ext] || 'plaintext'; }; const addLineNumbers = (text: string) => { const lines = text.split('\n'); return lines.map((_, i) => i + 1).join('\n'); }; return ( <div className="file-viewer-overlay" onClick={onClose}> <div className="file-viewer" onClick={(e) => e.stopPropagation()}> <div className="file-viewer-header"> <span className="file-viewer-title"> <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} /> {filename} </span> <button className="file-viewer-close" onClick={onClose} title="Close (Esc)"> <img className="icon-invert" src={ICONS.close} alt="Close" /> </button> </div> <div className="file-viewer-content"> {loading ? ( <div className="file-viewer-loading">Loading...</div> ) : content ? ( <pre style={{ display: 'flex' }}> <span className="file-viewer-line-numbers">{addLineNumbers(content)}</span> <code ref={codeRef} className={`language-${getLanguage()}`}>{content}</code> </pre> ) : ( <div className="file-viewer-loading">Unable to load file</div> )} </div> </div> </div> ); } /** * Component: MarkdownViewerModal */ function MarkdownViewerModal({ filePath, onClose }: { filePath: string; onClose: () => void }) { const [content, setContent] = useState<string | null>(null); const [loading, setLoading] = useState(true); const contentRef = useRef<HTMLDivElement>(null); const moduleRef = useRef<any>(null); const [wasmReady, setWasmReady] = useState(false); const filename = filePath.split('/').pop() || filePath; useEffect(() => { createMarkdownModule().then((Module: any) => { moduleRef.current = Module; setWasmReady(true); }); }, []); useEffect(() => { setLoading(true); getCachedFile(filePath).then((text) => { setContent(text); setLoading(false); }); }, [filePath]); useEffect(() => { if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return; const Module = moduleRef.current; const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']); const markdownFree = Module.cwrap('markdown_free', null, ['number']); const ptr = markdownToHtmlPtr(content); const html = Module.UTF8ToString(ptr); markdownFree(ptr); contentRef.current.innerHTML = html; }, [content, wasmReady]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); return ( <div className="file-viewer-overlay" onClick={onClose}> <div className="file-viewer" onClick={(e) => e.stopPropagation()}> <div className="file-viewer-header"> <span className="file-viewer-title"> <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} /> {filename} </span> <button className="file-viewer-close" onClick={onClose} title="Close (Esc)"> <img className="icon-invert" src={ICONS.close} alt="Close" /> </button> </div> <div className="file-viewer-content"> {loading || !wasmReady ? ( <div className="file-viewer-loading">Loading...</div> ) : content ? ( <div className="readme-content" ref={contentRef} /> ) : ( <div className="file-viewer-loading">Unable to load file</div> )} </div> </div> </div> ); } function StaticFileViewer({ filePath, onClose }: { filePath: string; onClose: () => void }) { const filename = filePath.split('/').pop() || filePath; const previewKind = getStaticPreviewKind(filename); const fileUrl = `${API_BASE}/file?path=${encodeURIComponent(filePath)}`; const [failed, setFailed] = useState(false); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); return ( <div className="file-viewer-overlay" onClick={onClose}> <div className="file-viewer static-file-viewer" role="dialog" aria-modal="true" aria-label={`Preview ${filename}`} onClick={(event) => event.stopPropagation()} > <div className="file-viewer-header"> <span className="file-viewer-title"> <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} /> {filename} </span> <span className="file-viewer-actions"> <a href={fileUrl} download={filename}>Download</a> <button className="file-viewer-close" onClick={onClose} title="Close (Esc)"> <img className="icon-invert" src={ICONS.close} alt="Close" /> </button> </span> </div> <div className="file-viewer-content static-file-preview"> {failed && <div className="error-message">Unable to preview this file.</div>} {!failed && previewKind === 'image' && ( <img className="static-file-image" src={fileUrl} alt={filename} onError={() => setFailed(true)} /> )} {!failed && previewKind === 'video' && ( <video className="static-file-video" src={fileUrl} controls onError={() => setFailed(true)} /> )} {!failed && previewKind === 'audio' && ( <audio className="static-file-audio" src={fileUrl} controls onError={() => setFailed(true)} /> )} {!failed && previewKind === 'pdf' && ( <iframe className="static-file-pdf" src={fileUrl} title={filename} onError={() => setFailed(true)} /> )} </div> </div> </div> ); } /** * Component: FileList */ function FileList({ directories, files, onNavigate, onOpenFile }: { directories: any[]; files: any[]; onNavigate: (path: string) => void; onOpenFile: (path: string) => void; }) { const isEmpty = directories.length === 0 && files.length === 0; if (isEmpty) { return ( <div className="file-list-container"> <div className="empty-state">This directory is empty.</div> </div> ); } return ( <div className="file-list-container"> <div className="file-header">Files</div> <div id="fileListBody"> {directories.map((dir) => ( <FileRow key={dir.abspath} item={dir} iconUrl={ICONS.folder} isDir={true} onNavigate={onNavigate} onOpenFile={onOpenFile} /> ))} {files.map((file) => ( <FileRow key={file.abspath} item={file} iconUrl={ICONS.file} isDir={false} onNavigate={onNavigate} onOpenFile={onOpenFile} /> ))} </div> </div> ); } /** * Component: FileRow */ function FileRow({ item, iconUrl, isDir, onNavigate, onOpenFile }: { item: { abspath: string; basename: string }; iconUrl: string; isDir: boolean; onNavigate: (path: string) => void; onOpenFile: (path: string) => void; }) { const handleClick = (e: React.MouseEvent) => { e.preventDefault(); if (isDir) { onNavigate(item.abspath); } else if (isCodeFile(item.basename) || getStaticPreviewKind(item.basename)) { onOpenFile(item.abspath); } else { window.open(`/api/repo/file?path=${encodeURIComponent(item.abspath)}`, '_blank'); } }; const handleMouseEnter = () => { if (isDir) { prefetchDirectory(item.abspath); } else if (isCodeFile(item.basename)) { prefetchFile(item.abspath); } }; const href = isDir ? `#` : `/api/repo/file?path=${encodeURIComponent(item.abspath)}`; return ( <div className="file-row" onMouseEnter={handleMouseEnter}> <span className="icon"> <img className="icon-invert" src={iconUrl} alt={isDir ? "Directory" : "File"} /> </span> <span className="name"> <a href={href} onClick={handleClick}> {item.basename} </a> </span> </div> ); } /** * Component: ReadmeViewer */ function ReadmeViewer({ content }: { content: string | null }) { const contentRef = useRef<HTMLDivElement>(null); const moduleRef = useRef<any>(null); const [wasmReady, setWasmReady] = useState(false); useEffect(() => { createMarkdownModule().then((Module: any) => { moduleRef.current = Module; setWasmReady(true); }); }, []); useEffect(() => { if (!content || !wasmReady || !contentRef.current || !moduleRef.current) return; const Module = moduleRef.current; const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']); const markdownFree = Module.cwrap('markdown_free', null, ['number']); const ptr = markdownToHtmlPtr(content); const html = Module.UTF8ToString(ptr); markdownFree(ptr); contentRef.current.innerHTML = html; }, [content, wasmReady]); if (!content) return null; return ( <div className="readme-section"> <div className="readme-header"> <img className="icon-invert" src={ICONS.file} width="16" alt="" style={{ opacity: 0.5 }} /> README.md </div> <div className="readme-content" ref={contentRef}> {!wasmReady && 'Loading...'} </div> </div> ); } /** * Directory Browser Component (no header/footer - for embedding in app) */ interface DirectoryBrowserProps { initialPath?: string; onPathChange?: (path: string) => void; } function DirectoryBrowser({ initialPath = '', onPathChange }: DirectoryBrowserProps) { const [currentPath, setCurrentPath] = useState(initialPath); const [content, setContent] = useState<{ files: any[]; directories: any[] }>({ files: [], directories: [] }); const [readme, setReadme] = useState<string | null>(null); const [error, setError] = useState<string | null>(null); const [loading, setLoading] = useState(false); const [viewingFile, setViewingFile] = useState<string | null>(null); const requestGeneration = useRef(0); // Sync with initialPath prop useEffect(() => { setCurrentPath(initialPath); }, [initialPath]); useEffect(() => { const generation = ++requestGeneration.current; fetchDirectory(currentPath, generation); fetchReadme(currentPath, generation); }, [currentPath]); const navigate = useCallback((path: string) => { setCurrentPath(path); onPathChange?.(path); }, [onPathChange]); const fetchDirectory = async (path: string, generation: number) => { setLoading(true); setError(null); try { const cacheKey = `dir:${path}`; let data; if (prefetchCache.has(cacheKey)) { data = await prefetchCache.get(cacheKey); prefetchCache.delete(cacheKey); } else { const url = path ? `${API_BASE}/list?path=${encodeURIComponent(path)}` : `${API_BASE}/list`; const response = await fetch(url); if (response.ok) { data = await response.json(); } } if (data?.error) { throw new Error(data.error); } if (generation === requestGeneration.current) { setContent({ files: data?.files || [], directories: data?.directories || [] }); } } catch (err: any) { console.error('Error loading directory:', err); if (generation === requestGeneration.current) setError(err.message); } finally { if (generation === requestGeneration.current) setLoading(false); } }; const fetchReadme = async (path: string, generation: number) => { if (generation === requestGeneration.current) setReadme(null); try { const url = path ? `${API_BASE}/readme?path=${encodeURIComponent(path)}` : `${API_BASE}/readme`; const response = await fetch(url); if (response.ok) { const text = await response.text(); if (generation === requestGeneration.current) setReadme(text || null); } } catch (err) { // Readme is optional } }; const handleOpenFile = useCallback((path: string) => { setViewingFile(path); }, []); const handleCloseFile = useCallback(() => { setViewingFile(null); }, []); return ( <> <Breadcrumb currentPath={currentPath} onNavigate={navigate} /> {error && <div className="error-message">Error: {error}</div>} {loading ? ( <div className="file-list-container"> <div className="loading-state">Loading files...</div> </div> ) : ( <> <FileList directories={content.directories} files={content.files} onNavigate={navigate} onOpenFile={handleOpenFile} /> <ReadmeViewer content={readme} /> </> )} {/* File Viewer Modal */} {viewingFile && ( isMarkdownFile(viewingFile) ? ( <MarkdownViewerModal filePath={viewingFile} onClose={handleCloseFile} /> ) : getStaticPreviewKind(viewingFile) ? ( <StaticFileViewer filePath={viewingFile} onClose={handleCloseFile} /> ) : ( <FileViewer filePath={viewingFile} onClose={handleCloseFile} /> ) )} </> ); } export { DirectoryBrowser };