FileLinkMenu.tsx3.2 KBView on GitHub 'use client';
import { forwardRef, useImperativeHandle, useMemo, useState } from 'react';
import { FileText, Table2 } from 'lucide-react';
import { formatTableSummary } from '@/modules/documents/table/table-metadata';
import { cn } from '@/lib/utils';
import type { FileLinkSuggestionItem } from './FileLinkSuggestion';
interface FileLinkMenuProps {
items: FileLinkSuggestionItem[];
command: (item: FileLinkSuggestionItem) => void;
}
export interface FileLinkMenuHandle {
onKeyDown: (data: { event: KeyboardEvent }) => boolean;
}
export const FileLinkMenu = forwardRef<FileLinkMenuHandle, FileLinkMenuProps>(
function FileLinkMenu({ items, command }, ref) {
const [selectedIndex, setSelectedIndex] = useState(0);
const safeSelectedIndex = useMemo(() => {
if (items.length === 0) return 0;
return Math.min(selectedIndex, items.length - 1);
}, [items.length, selectedIndex]);
useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (event.key === 'ArrowDown') {
setSelectedIndex((index) => (items.length ? (index + 1) % items.length : 0));
return true;
}
if (event.key === 'ArrowUp') {
setSelectedIndex((index) => (items.length ? (index + items.length - 1) % items.length : 0));
return true;
}
if (event.key === 'Enter') {
const item = items[safeSelectedIndex];
if (item) command(item);
return true;
}
return false;
},
}));
return (
<div className="max-h-72 w-[min(32rem,calc(100vw-2rem))] overflow-x-hidden overflow-y-auto rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg">
{items.length === 0 ? (
<div className="px-3 py-2 text-xs text-muted-foreground">No matching files</div>
) : (
items.map((item, index) => (
<button
key=[redacted]
type="button"
className={cn(
'flex w-full items-start gap-2 rounded-md px-2 py-2 text-left text-sm',
index === safeSelectedIndex ? 'bg-muted' : 'hover:bg-muted/70',
)}
onMouseEnter={() => setSelectedIndex(index)}
onMouseDown={(event) => {
event.preventDefault();
command(item);
}}
>
{item.documentType === 'table' ? (
<Table2 className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
) : (
<FileText className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
)}
<span className="min-w-0 flex-1">
<span className="block truncate font-medium">{item.label}</span>
{/* Prefer the conversation name for conversation-scoped files — the raw
path is `conversation/<uuid>/...`, which is unreadable. A table adds its
shape ("38 rows × 6 columns") when the search surfaced its metadata. */}
<span className="block truncate text-xs text-muted-foreground">
{formatTableSummary(item.metadata) ?? item.conversationName ?? item.path}
</span>
</span>
</button>
))
)}
</div>
);
},
);