- Add ChatCodeFilePreview component with hljs syntax highlighting - Fetch file content from contentPath with credentials - Use DOMParser-based safe rendering (no dangerouslySetInnerHTML) - Include copy button, language label, and ChatFileCard download below - Add extToLang extension-to-language mapping - Register 14 common languages with hljs - Add highlight.js as direct dependency in ui/package.json
94 lines
3 KiB
TypeScript
94 lines
3 KiB
TypeScript
import { useState } from "react";
|
|
import { Download, File, FileCode, FileText, FolderUp, ImageIcon } from "lucide-react";
|
|
import { cn } from "../lib/utils";
|
|
import type { ChatFile } from "@paperclipai/shared";
|
|
import { chatApi } from "../api/chat";
|
|
|
|
interface ChatFileCardProps {
|
|
file: ChatFile;
|
|
contentPath: string;
|
|
className?: string;
|
|
projectId?: string | null;
|
|
onPromoted?: (file: ChatFile) => void;
|
|
}
|
|
|
|
export function formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
function FileIcon({ category }: { category: ChatFile["category"] }) {
|
|
const cls = "h-5 w-5 shrink-0 text-muted-foreground";
|
|
switch (category) {
|
|
case "image":
|
|
return <ImageIcon className={cls} />;
|
|
case "code":
|
|
return <FileCode className={cls} />;
|
|
case "document":
|
|
return <FileText className={cls} />;
|
|
default:
|
|
return <File className={cls} />;
|
|
}
|
|
}
|
|
|
|
export function ChatFileCard({ file, contentPath, className, projectId, onPromoted }: ChatFileCardProps) {
|
|
const [promoting, setPromoting] = useState(false);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex items-center gap-3 rounded-lg border border-border bg-muted p-3",
|
|
className,
|
|
)}
|
|
>
|
|
<FileIcon category={file.category} />
|
|
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<span
|
|
className="truncate text-sm font-medium text-foreground"
|
|
title={file.originalFilename}
|
|
>
|
|
{file.originalFilename}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{formatFileSize(file.sizeBytes)}
|
|
</span>
|
|
</div>
|
|
|
|
{file.projectId === null && projectId && onPromoted && (
|
|
<button
|
|
onClick={async (e) => {
|
|
e.stopPropagation();
|
|
setPromoting(true);
|
|
try {
|
|
const updated = await chatApi.promoteFile(file.id, projectId);
|
|
onPromoted(updated);
|
|
} finally {
|
|
setPromoting(false);
|
|
}
|
|
}}
|
|
disabled={promoting}
|
|
className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
|
aria-label="Promote to project"
|
|
title="Promote to project"
|
|
>
|
|
<FolderUp className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
|
|
<a
|
|
href={contentPath}
|
|
download={file.originalFilename}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
aria-label={`Download ${file.originalFilename}`}
|
|
title="Download"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|