Files
fusion/packages/dashboard/app/components/FileMentionPopup.tsx
Fusion 9974cc7019 feat(FN-1947): add file mention search and chat autocomplete
- Add file search API wiring in dashboard client/server with route coverage for /api/files/search
- Introduce useFileMention hook and FileMentionPopup component, integrated into ChatView and QuickChatFAB
- Resolve @file references in chat messages so referenced file content is included in AI context
- Add styling, mobile responsiveness, and focused tests for popup UI, hook behavior, chat resolution, and file services
2026-04-18 07:36:57 -07:00

87 lines
2.6 KiB
TypeScript

import { File } from "lucide-react";
import type { FileSearchItem } from "../hooks/useFileMention";
import type { ReactNode } from "react";
export interface FileMentionPopupProps {
visible: boolean;
position: { top: number; left: number };
files: FileSearchItem[];
selectedIndex: number;
onSelect: (file: FileSearchItem) => void;
loading: boolean;
}
/**
* File mention popup component.
* Shows a searchable list of files matching the current # mention query.
*/
export function FileMentionPopup({
visible,
position,
files,
selectedIndex,
onSelect,
loading,
}: FileMentionPopupProps): ReactNode | null {
if (!visible) {
return null;
}
return (
<div
className="file-mention-popup"
style={{ top: position.top, left: position.left }}
data-testid="file-mention-popup"
onMouseDown={(e) => {
// Prevent losing focus from textarea when clicking popup
e.preventDefault();
}}
>
{loading && (
<div className="file-mention-popup-loading" data-testid="file-mention-loading">
<span className="spinner" />
</div>
)}
{!loading && files.length === 0 && (
<div className="file-mention-popup-empty" data-testid="file-mention-empty">
No files found
</div>
)}
{!loading && files.length > 0 && (
<ul className="file-mention-popup-list" role="listbox">
{files.map((file, index) => {
const dirPath = file.path.includes("/") ? file.path.slice(0, file.path.lastIndexOf("/") + 1) : "";
const highlightName = file.name;
return (
<li
key={file.path}
className={`file-mention-popup-item${index === selectedIndex ? " file-mention-popup-item--selected" : ""}`}
onClick={() => onSelect(file)}
onMouseEnter={() => {
// This will be handled by parent through selectedIndex prop
}}
role="option"
aria-selected={index === selectedIndex}
data-testid={`file-mention-item-${index}`}
>
<span className="file-mention-popup-icon">
<File size={14} />
</span>
<div className="file-mention-popup-info">
<span className="file-mention-popup-item-name">{highlightName}</span>
{dirPath && (
<span className="file-mention-popup-item-path">{dirPath}</span>
)}
</div>
</li>
);
})}
</ul>
)}
</div>
);
}