/** * TaskSelector Component * ADR-023: Agent-as-User & Infinite Chat Architecture * * A panel for selecting and binding tasks to chat conversations */ import { useState, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { apiClient } from '@/shared/utils/apiClient'; import { cn } from '@/shared/utils/cn'; import { Link2, Loader2, Search, CheckCircle2, X, ChevronDown, Table2, Hash } from 'lucide-react'; interface TaskRow { id: number; data: Record; table_id: number; created_at: string; } interface TaskTable { id: number; name: string; slug: string; display_column?: string; } export interface TaskSelectorProps { spaceId?: number; boundTaskId?: number; boundTableId?: number; onBind: (tableId: number, rowId: number) => void; onUnbind: () => void; className?: string; } export function TaskSelector({ spaceId, boundTaskId, boundTableId, onBind, onUnbind, className }: TaskSelectorProps) { const [isExpanded, setIsExpanded] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [selectedTableId, setSelectedTableId] = useState(boundTableId || null); // Fetch tables that can have tasks (configured as task tables) const { data: taskTables = [], isLoading: isLoadingTables } = useQuery({ queryKey: ['task-tables', spaceId], queryFn: async () => { if (!spaceId) return []; const response = await apiClient.get<{ success: boolean; data: TaskTable[]; }>(`/tables?spaceId=${spaceId}`); return response.success ? response.data : []; }, enabled: !!spaceId && isExpanded }); // Fetch rows from selected table const { data: tableRows = [], isLoading: isLoadingRows } = useQuery({ queryKey: ['task-rows', selectedTableId, searchQuery], queryFn: async () => { if (!selectedTableId) return []; const params = new URLSearchParams(); if (searchQuery) params.append('search', searchQuery); params.append('limit', '20'); const response = await apiClient.get<{ success: boolean; data: { rows: TaskRow[] }; }>(`/tables/${selectedTableId}/rows?${params}`); return response.success ? response.data.rows : []; }, enabled: !!selectedTableId && isExpanded }); // Get the display value for a row const getRowDisplayValue = (row: TaskRow, table?: TaskTable) => { const displayColumn = table?.display_column || 'name'; const data = row.data as Record; return String(data[displayColumn] || data['title'] || data['name'] || `#${row.id}`); }; // Find bound task display const boundTaskDisplay = useMemo(() => { if (!boundTaskId || !boundTableId) return null; const table = taskTables.find(t => t.id === boundTableId); const row = tableRows.find(r => r.id === boundTaskId); if (row && table) { return { tableName: table.name, taskName: getRowDisplayValue(row, table) }; } return { tableName: table?.name || 'Unknown Table', taskName: `Task #${boundTaskId}` }; }, [boundTaskId, boundTableId, taskTables, tableRows]); return (
{/* Header - Bound Task or Toggle */}
!boundTaskId && setIsExpanded(!isExpanded)} className={cn( 'w-full flex items-center gap-2 px-3 py-2 text-left transition-colors cursor-pointer', boundTaskId ? 'bg-[var(--color-primary-500)]/10 hover:bg-[var(--color-primary-500)]/15' : 'hover:bg-[var(--bg-tertiary)]' )} >
{ if (boundTaskId) { e.stopPropagation(); setIsExpanded(!isExpanded); } }} > {boundTaskId && boundTaskDisplay ? (
{boundTaskDisplay.tableName}
{boundTaskDisplay.taskName}
) : ( Привязать к задаче... )}
{boundTaskId ? ( ) : ( )}
{/* Expanded Panel */} {isExpanded && (
{/* Table Selector */}
Таблица
{isLoadingTables ? (
) : (
{taskTables.slice(0, 5).map((table) => ( ))}
)}
{/* Search */} {selectedTableId && (
setSearchQuery(e.target.value)} placeholder="Поиск..." className="w-full pl-7 pr-2 py-1.5 text-xs bg-[var(--bg-tertiary)] border border-[var(--border-primary)] rounded-md text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary-500)]" />
)} {/* Rows List */} {selectedTableId && (
{isLoadingRows ? (
) : tableRows.length === 0 ? (
Нет записей
) : (
{tableRows.map((row) => { const isSelected = boundTaskId === row.id && boundTableId === selectedTableId; const table = taskTables.find(t => t.id === selectedTableId); return ( ); })}
)}
)}
)}
); } export default TaskSelector;