"use client"; import { type CSSProperties, useState } from "react"; import { CornerDownRight, File, FileText, Loader2, MessageSquare, Search, Table2, Users, } from "lucide-react"; import { PageHeader } from "@/app/components/shared/PageHeader"; import { RenameableTitle } from "@/app/components/shared/RenameableTitle"; import type { Project } from "@/app/components/shared/types"; import type { DocumentVersion } from "@/app/lib/mikeApi"; import { RowActions } from "@/app/components/shared/RowActions"; export type ProjectTab = "documents" | "assistant" | "reviews"; export type ProjectContextMenu = { x: number; y: number; docId?: string | null; folderId: string | null; showFolderActions: boolean; }; export const NAME_COL_W = "w-[332px] shrink-0"; export const DOC_NAME_COL_W = "w-[292px] sm:w-[332px] md:w-[392px] lg:w-[452px] xl:w-[532px] 2xl:w-[592px] shrink-0"; const TREE_CONTROL_WIDTH_PX = 32; const TREE_NAME_PADDING_PX = 16; export function treeNameCellStyle(depth: number): CSSProperties | undefined { if (depth <= 0) return undefined; return { paddingLeft: TREE_NAME_PADDING_PX + depth * TREE_CONTROL_WIDTH_PX, }; } export function formatBytes(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`; } export function formatDate(iso: string) { return new Date(iso).toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric", }); } export function DocIcon({ fileType }: { fileType: string | null }) { if (fileType === "pdf") return ; if (fileType === "docx" || fileType === "doc") return ; return ; } export function DocVersionHistory({ docId, filename, fileType, activeVersionNumber, currentVersionId, loading, versions, depth = 0, onDownloadVersion, onOpenVersion, onRenameVersion, onExtensionChangeBlocked, }: { docId: string; filename: string; fileType: string | null; activeVersionNumber: number | null; currentVersionId: string | null; loading: boolean; versions: DocumentVersion[]; depth?: number; onDownloadVersion: ( docId: string, versionId: string, filename: string, ) => void; onOpenVersion?: (versionId: string, versionLabel: string) => void; onRenameVersion?: ( versionId: string, filename: string | null, ) => Promise | void; onExtensionChangeBlocked?: (filename: string) => void; }) { const [editingVersionId, setEditingVersionId] = useState( null, ); const [editingValue, setEditingValue] = useState(""); const commit = async (versionId: string) => { const trimmed = editingValue.trim(); const previousFilename = versions .find((version) => version.id === versionId) ?.filename?.trim(); if ( previousFilename && (trimmed.length === 0 || hasFilenameExtensionChange(previousFilename, trimmed)) ) { onExtensionChangeBlocked?.(previousFilename); return; } setEditingVersionId(null); const next = trimmed.length > 0 ? trimmed : null; await onRenameVersion?.(versionId, next); }; if (loading && versions.length === 0) { const skeletonCount = Math.max(0, (activeVersionNumber ?? 1) - 1); return ( <> {Array.from({ length: skeletonCount }).map((_, index) => (
))} ); } if (versions.length === 0) { return (
No version history.
); } const olderVersions = versions.filter((v) => v.id !== currentVersionId); if (olderVersions.length === 0) return null; const ordered = [...olderVersions].reverse(); return ( <> {ordered.map((v) => { const numberLabel = typeof v.version_number === "number" && v.version_number >= 1 ? `${v.version_number}` : v.source === "upload" ? "Original" : "—"; const displayLabel = v.filename?.trim() || numberLabel; const dt = new Date(v.created_at); const dateLabel = Number.isNaN(dt.valueOf()) ? "" : dt.toLocaleString(undefined, { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit", }); const isEditing = editingVersionId === v.id; const rowBg = "bg-gray-100"; return (
{ if (isEditing) return; onOpenVersion?.(v.id, displayLabel); }} className={`group flex h-10 cursor-pointer items-center pr-8 text-sm text-gray-500 transition-colors hover:bg-gray-200 ${rowBg}`} >
{isEditing ? ( e.stopPropagation()} onChange={(e) => setEditingValue(e.target.value) } onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); void commit(v.id); } else if (e.key === "Escape") { setEditingVersionId(null); } }} onBlur={() => void commit(v.id)} className="min-w-0 flex-1 border-b border-gray-300 bg-transparent text-sm text-gray-800 outline-none focus:border-gray-500" /> ) : ( {displayLabel} )}
{fileType ?? }
{numberLabel}
{dateLabel ? formatDate(v.created_at) : }
e.stopPropagation()} > { setEditingVersionId(v.id); setEditingValue(v.filename ?? ""); } : undefined } renameLabel="Rename version" onDownload={() => onDownloadVersion(docId, v.id, filename) } />
); })} ); } export function ProjectPageSkeleton() { return (
, }, { disabled: true, iconOnly: true, title: "People with access", icon: , }, ], [ { disabled: true, icon: , label: New Chat, }, { disabled: true, icon: , label: New Review, }, ], ]} />
{[1, 2, 3, 4, 5].map((i) => (
))}
); } export function ProjectPageHeader({ project, tab, search, creatingChat, creatingReview, docsCount, onBackToProjects, onTitleCommit, onSearchChange, onOpenPeople, onNewChat, onNewReview, }: { project: Project; tab: ProjectTab; search: string; creatingChat: boolean; creatingReview: boolean; docsCount: number; onBackToProjects: () => void; onTitleCommit: (newName: string) => void | Promise; onSearchChange: (search: string) => void; onOpenPeople: () => void; onNewChat: () => void; onNewReview: () => void; }) { return ( ), suffix: project.cm_number ? ( (#{project.cm_number}) ) : null, }, ]} align="start" actionGap="lg" actionGroups={[ [ { type: "search", value: search, onChange: onSearchChange, placeholder: "Search…", }, { onClick: onOpenPeople, iconOnly: true, title: "People with access", icon: , }, ], [ { onClick: onNewChat, disabled: creatingChat, icon: creatingChat ? ( ) : ( ), label: New Chat, }, { onClick: onNewReview, disabled: docsCount === 0 || creatingReview, icon: creatingReview ? ( ) : ( ), label: ( New Review ), tooltip: docsCount === 0 ? "Upload a document first" : null, }, ], ]} /> ); } function filenameExtension(filename: string) { const trimmed = filename.trim(); const dotIndex = trimmed.lastIndexOf("."); if (dotIndex <= 0 || dotIndex === trimmed.length - 1) return null; return trimmed.slice(dotIndex); } function hasFilenameExtensionChange(previous: string, next: string) { const previousExtension = filenameExtension(previous); if (previousExtension == null) return false; return ( filenameExtension(next)?.toLowerCase() !== previousExtension.toLowerCase() ); }