Unexpected token
TablePageTemplate.tsx:
This commit is contained in:
@@ -28,12 +28,10 @@ import Input from '../components/form/input/InputField';
|
|||||||
import SelectDropdown from '../components/form/SelectDropdown';
|
import SelectDropdown from '../components/form/SelectDropdown';
|
||||||
import { Dropdown } from '../components/ui/dropdown/Dropdown';
|
import { Dropdown } from '../components/ui/dropdown/Dropdown';
|
||||||
import { DropdownItem } from '../components/ui/dropdown/DropdownItem';
|
import { DropdownItem } from '../components/ui/dropdown/DropdownItem';
|
||||||
import Alert from '../components/ui/alert/Alert';
|
|
||||||
import AlertModal from '../components/ui/alert/AlertModal';
|
import AlertModal from '../components/ui/alert/AlertModal';
|
||||||
import { ChevronDownIcon, MoreDotIcon, PlusIcon } from '../icons';
|
import { ChevronDownIcon, MoreDotIcon, PlusIcon } from '../icons';
|
||||||
import { useHeaderMetrics } from '../context/HeaderMetricsContext';
|
import { useHeaderMetrics } from '../context/HeaderMetricsContext';
|
||||||
import { useToast } from '../components/ui/toast/ToastContainer';
|
import { useToast } from '../components/ui/toast/ToastContainer';
|
||||||
import { pageNotifications } from '../config/pages/notifications.config';
|
|
||||||
import { getDeleteModalConfig } from '../config/pages/delete-modal.config';
|
import { getDeleteModalConfig } from '../config/pages/delete-modal.config';
|
||||||
import { getBulkActionModalConfig } from '../config/pages/bulk-action-modal.config';
|
import { getBulkActionModalConfig } from '../config/pages/bulk-action-modal.config';
|
||||||
import { getTableActionsConfig } from '../config/pages/table-actions.config';
|
import { getTableActionsConfig } from '../config/pages/table-actions.config';
|
||||||
@@ -102,7 +100,7 @@ interface TablePageTemplateProps {
|
|||||||
onCreate?: () => void;
|
onCreate?: () => void;
|
||||||
createLabel?: string;
|
createLabel?: string;
|
||||||
onCreateIcon?: ReactNode; // Icon for create button
|
onCreateIcon?: ReactNode; // Icon for create button
|
||||||
onExport?: () => void;
|
onExportCSV?: () => void; // CSV export button handler
|
||||||
onExportIcon?: ReactNode; // Icon for export button
|
onExportIcon?: ReactNode; // Icon for export button
|
||||||
onImport?: () => void;
|
onImport?: () => void;
|
||||||
onImportIcon?: ReactNode; // Icon for import button
|
onImportIcon?: ReactNode; // Icon for import button
|
||||||
@@ -146,7 +144,7 @@ export default function TablePageTemplate({
|
|||||||
subtitle,
|
subtitle,
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
loading = false,
|
loading: _loading = false, // Unused - component uses showContent for loading state
|
||||||
showContent = true,
|
showContent = true,
|
||||||
filters = [],
|
filters = [],
|
||||||
filterValues = {},
|
filterValues = {},
|
||||||
@@ -157,7 +155,7 @@ export default function TablePageTemplate({
|
|||||||
onCreate,
|
onCreate,
|
||||||
createLabel = '+ Add',
|
createLabel = '+ Add',
|
||||||
onCreateIcon,
|
onCreateIcon,
|
||||||
onExport,
|
onExportCSV,
|
||||||
onExportIcon,
|
onExportIcon,
|
||||||
onImport,
|
onImport,
|
||||||
onImportIcon,
|
onImportIcon,
|
||||||
@@ -172,17 +170,17 @@ export default function TablePageTemplate({
|
|||||||
onBulkUpdateStatus,
|
onBulkUpdateStatus,
|
||||||
onBulkAction,
|
onBulkAction,
|
||||||
onRowAction,
|
onRowAction,
|
||||||
|
onExport,
|
||||||
getItemDisplayName = (row: any) => row.name || row.keyword || row.title || String(row.id),
|
getItemDisplayName = (row: any) => row.name || row.keyword || row.title || String(row.id),
|
||||||
className = '',
|
className = '',
|
||||||
}: TablePageTemplateProps) {
|
}: TablePageTemplateProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false);
|
const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false);
|
||||||
const [openRowActions, setOpenRowActions] = useState<Map<string | number, boolean>>(new Map());
|
const [openRowActions, setOpenRowActions] = useState<Map<string | number, boolean>>(new Map());
|
||||||
const rowActionButtonRefs = React.useRef<Map<string | number, React.RefObject<HTMLButtonElement>>>(new Map());
|
const rowActionButtonRefs = React.useRef<Map<string | number, React.RefObject<HTMLButtonElement | null>>>(new Map());
|
||||||
const bulkActionsButtonRef = React.useRef<HTMLButtonElement>(null);
|
const bulkActionsButtonRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
// Get notification config for current page
|
// Get notification config for current page
|
||||||
const notificationConfig = pageNotifications[location.pathname];
|
|
||||||
const deleteModalConfig = getDeleteModalConfig(location.pathname);
|
const deleteModalConfig = getDeleteModalConfig(location.pathname);
|
||||||
const bulkActionModalConfig = getBulkActionModalConfig(location.pathname);
|
const bulkActionModalConfig = getBulkActionModalConfig(location.pathname);
|
||||||
const tableActionsConfig = getTableActionsConfig(location.pathname);
|
const tableActionsConfig = getTableActionsConfig(location.pathname);
|
||||||
@@ -303,7 +301,7 @@ export default function TablePageTemplate({
|
|||||||
} else if (actionKey === 'delete' && onDelete && deleteModalConfig) {
|
} else if (actionKey === 'delete' && onDelete && deleteModalConfig) {
|
||||||
handleDeleteClick(row);
|
handleDeleteClick(row);
|
||||||
} else if (actionKey === 'export' && onExport) {
|
} else if (actionKey === 'export' && onExport) {
|
||||||
onExport(row);
|
await onExport(row);
|
||||||
} else if (onRowAction) {
|
} else if (onRowAction) {
|
||||||
// For custom row actions, use onRowAction with full row object
|
// For custom row actions, use onRowAction with full row object
|
||||||
onRowAction(actionKey, row).catch((error: any) => {
|
onRowAction(actionKey, row).catch((error: any) => {
|
||||||
@@ -649,7 +647,7 @@ export default function TablePageTemplate({
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={isBulkActionsDropdownOpen && selectedIds.length > 0}
|
isOpen={isBulkActionsDropdownOpen && selectedIds.length > 0}
|
||||||
onClose={() => setIsBulkActionsDropdownOpen(false)}
|
onClose={() => setIsBulkActionsDropdownOpen(false)}
|
||||||
anchorRef={bulkActionsButtonRef}
|
anchorRef={bulkActionsButtonRef as React.RefObject<HTMLElement>}
|
||||||
placement="bottom-left"
|
placement="bottom-left"
|
||||||
className="w-48 p-2"
|
className="w-48 p-2"
|
||||||
>
|
>
|
||||||
@@ -683,12 +681,12 @@ export default function TablePageTemplate({
|
|||||||
|
|
||||||
{/* Action Buttons - Right aligned */}
|
{/* Action Buttons - Right aligned */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{onExport && (
|
{onExportCSV && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="md"
|
size="md"
|
||||||
endIcon={onExportIcon}
|
endIcon={onExportIcon}
|
||||||
onClick={onExport}
|
onClick={onExportCSV}
|
||||||
>
|
>
|
||||||
Export CSV
|
Export CSV
|
||||||
</Button>
|
</Button>
|
||||||
@@ -747,16 +745,24 @@ export default function TablePageTemplate({
|
|||||||
key={column.key}
|
key={column.key}
|
||||||
isHeader
|
isHeader
|
||||||
className={`px-5 py-3 font-medium text-gray-500 text-${column.align || 'start'} text-theme-xs dark:text-gray-400 ${column.sortable ? 'cursor-pointer hover:text-gray-700 dark:hover:text-gray-300' : ''} ${isLastColumn && rowActions.length > 0 ? 'pr-16' : ''}`}
|
className={`px-5 py-3 font-medium text-gray-500 text-${column.align || 'start'} text-theme-xs dark:text-gray-400 ${column.sortable ? 'cursor-pointer hover:text-gray-700 dark:hover:text-gray-300' : ''} ${isLastColumn && rowActions.length > 0 ? 'pr-16' : ''}`}
|
||||||
onClick={() => column.sortable && handleSort(column)}
|
|
||||||
>
|
>
|
||||||
{column.label}
|
{column.sortable ? (
|
||||||
{getSortIcon(column)}
|
<div onClick={() => handleSort(column)} className="flex items-center">
|
||||||
|
{column.label}
|
||||||
|
{getSortIcon(column)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{column.label}
|
||||||
|
{getSortIcon(column)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className="divide-y divide-gray-100 dark:divide-white/[0.05] igny8-table-body" style={{ position: 'relative' }}>
|
<TableBody className="divide-y divide-gray-100 dark:divide-white/[0.05] igny8-table-body">
|
||||||
{!showContent ? (
|
{!showContent ? (
|
||||||
// Loading Skeleton Rows - Always show 10 rows to match page size
|
// Loading Skeleton Rows - Always show 10 rows to match page size
|
||||||
Array.from({ length: 10 }).map((_, index) => (
|
Array.from({ length: 10 }).map((_, index) => (
|
||||||
@@ -769,7 +775,6 @@ export default function TablePageTemplate({
|
|||||||
))
|
))
|
||||||
) : data.length === 0 ? null : (
|
) : data.length === 0 ? null : (
|
||||||
data.map((row, index) => {
|
data.map((row, index) => {
|
||||||
const rowId = row.id?.toString() || index.toString();
|
|
||||||
// Use consistent key for expandedRows (number or index)
|
// Use consistent key for expandedRows (number or index)
|
||||||
const rowKey = row.id || index;
|
const rowKey = row.id || index;
|
||||||
const isRowExpanded = expandedRows.has(rowKey);
|
const isRowExpanded = expandedRows.has(rowKey);
|
||||||
@@ -810,8 +815,7 @@ export default function TablePageTemplate({
|
|||||||
return (
|
return (
|
||||||
<React.Fragment key={row.id || index}>
|
<React.Fragment key={row.id || index}>
|
||||||
<TableRow
|
<TableRow
|
||||||
className={`igny8-data-row ${isRowAdded ? 'bg-blue-50 dark:bg-blue-500/10' : ''}`}
|
className={`igny8-data-row ${isRowAdded ? 'bg-blue-50 dark:bg-blue-500/10' : ''}`}
|
||||||
style={{ animationDelay: `${index * 0.03}s` }}
|
|
||||||
>
|
>
|
||||||
{selection && (
|
{selection && (
|
||||||
<TableCell className="px-5 py-4 text-start">
|
<TableCell className="px-5 py-4 text-start">
|
||||||
@@ -830,7 +834,8 @@ export default function TablePageTemplate({
|
|||||||
// Get or create ref for this row's actions button
|
// Get or create ref for this row's actions button
|
||||||
if (isLastColumn && rowActions.length > 0) {
|
if (isLastColumn && rowActions.length > 0) {
|
||||||
if (!rowActionButtonRefs.current.has(rowId)) {
|
if (!rowActionButtonRefs.current.has(rowId)) {
|
||||||
rowActionButtonRefs.current.set(rowId, React.createRef<HTMLButtonElement>());
|
const ref = React.createRef<HTMLButtonElement>();
|
||||||
|
rowActionButtonRefs.current.set(rowId, ref);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,7 +843,6 @@ export default function TablePageTemplate({
|
|||||||
<TableCell
|
<TableCell
|
||||||
key={column.key}
|
key={column.key}
|
||||||
className={`px-5 py-4 text-${column.align || 'start'} text-gray-800 dark:text-white/90 ${isLastColumn && rowActions.length > 0 ? 'relative pr-16' : ''}`}
|
className={`px-5 py-4 text-${column.align || 'start'} text-gray-800 dark:text-white/90 ${isLastColumn && rowActions.length > 0 ? 'relative pr-16' : ''}`}
|
||||||
style={isLastColumn && rowActions.length > 0 ? { position: 'relative', overflow: 'visible' } : undefined}
|
|
||||||
>
|
>
|
||||||
<div className={`flex items-center ${column.toggleable && hasToggleContent ? 'justify-between w-full' : ''} gap-2`}>
|
<div className={`flex items-center ${column.toggleable && hasToggleContent ? 'justify-between w-full' : ''} gap-2`}>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -849,14 +853,15 @@ export default function TablePageTemplate({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{column.toggleable && hasToggleContent && (
|
{column.toggleable && hasToggleContent && (
|
||||||
<ToggleButton
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
isExpanded={isRowExpanded}
|
<ToggleButton
|
||||||
onClick={(e) => {
|
isExpanded={isRowExpanded}
|
||||||
e.stopPropagation();
|
onClick={() => {
|
||||||
handleToggle(!isRowExpanded, rowKey);
|
handleToggle(!isRowExpanded, rowKey);
|
||||||
}}
|
}}
|
||||||
hasContent={hasToggleContent}
|
hasContent={hasToggleContent}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -901,7 +906,8 @@ export default function TablePageTemplate({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Multiple actions - use dropdown
|
// Multiple actions - use dropdown
|
||||||
const buttonRef = rowActionButtonRefs.current.get(rowId)!;
|
const buttonRef = rowActionButtonRefs.current.get(rowId);
|
||||||
|
if (!buttonRef) return null;
|
||||||
const isOpen = openRowActions.get(rowId) || false;
|
const isOpen = openRowActions.get(rowId) || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -936,11 +942,11 @@ export default function TablePageTemplate({
|
|||||||
return newMap;
|
return newMap;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
anchorRef={buttonRef}
|
anchorRef={buttonRef as React.RefObject<HTMLElement>}
|
||||||
placement="right"
|
placement="right"
|
||||||
className="w-48 p-2"
|
className="w-48 p-2"
|
||||||
>
|
>
|
||||||
{rowActions.map((action, actionIndex) => {
|
{rowActions.map((action) => {
|
||||||
const isEdit = action.key === 'edit';
|
const isEdit = action.key === 'edit';
|
||||||
const isDelete = action.key === 'delete';
|
const isDelete = action.key === 'delete';
|
||||||
const isExport = action.key === 'export';
|
const isExport = action.key === 'export';
|
||||||
@@ -949,21 +955,21 @@ export default function TablePageTemplate({
|
|||||||
const getIconWithColor = () => {
|
const getIconWithColor = () => {
|
||||||
if (!action.icon) return null;
|
if (!action.icon) return null;
|
||||||
const iconElement = action.icon as React.ReactElement;
|
const iconElement = action.icon as React.ReactElement;
|
||||||
const existingClassName = iconElement.props?.className || "";
|
const existingClassName = (iconElement.props as any)?.className || "";
|
||||||
const baseSize = existingClassName.includes("w-") ? "" : "w-5 h-5 ";
|
const baseSize = existingClassName.includes("w-") ? "" : "w-5 h-5 ";
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
return React.cloneElement(iconElement, {
|
return React.cloneElement(iconElement, {
|
||||||
className: `${baseSize}text-blue-light-500 ${existingClassName}`.trim()
|
className: `${baseSize}text-blue-light-500 ${existingClassName}`.trim()
|
||||||
});
|
} as any);
|
||||||
} else if (isDelete) {
|
} else if (isDelete) {
|
||||||
return React.cloneElement(iconElement, {
|
return React.cloneElement(iconElement, {
|
||||||
className: `${baseSize}text-error-500 ${existingClassName}`.trim()
|
className: `${baseSize}text-error-500 ${existingClassName}`.trim()
|
||||||
});
|
} as any);
|
||||||
} else if (isExport) {
|
} else if (isExport) {
|
||||||
return React.cloneElement(iconElement, {
|
return React.cloneElement(iconElement, {
|
||||||
className: `${baseSize}text-gray-600 dark:text-gray-400 ${existingClassName}`.trim()
|
className: `${baseSize}text-gray-600 dark:text-gray-400 ${existingClassName}`.trim()
|
||||||
});
|
} as any);
|
||||||
}
|
}
|
||||||
return action.icon;
|
return action.icon;
|
||||||
};
|
};
|
||||||
@@ -1090,276 +1096,6 @@ export default function TablePageTemplate({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* AI Request Logs Section - DEPRECATED: Now using backend console logging */}
|
|
||||||
{/* <AIRequestLogsSection /> */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI Request Logs Component - DEPRECATED: Now using backend console logging
|
|
||||||
// All AI logging is now handled by backend console logging (ConsoleStepTracker)
|
|
||||||
/*
|
|
||||||
function AIRequestLogsSection() {
|
|
||||||
const { logs, clearLogs } = useAIRequestLogsStore();
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
|
||||||
|
|
||||||
if (logs.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTime = (date: Date) => {
|
|
||||||
return new Intl.DateTimeFormat('en-US', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
}).format(date);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'success':
|
|
||||||
return 'text-green-600 dark:text-green-400';
|
|
||||||
case 'error':
|
|
||||||
return 'text-red-600 dark:text-red-400';
|
|
||||||
case 'pending':
|
|
||||||
return 'text-yellow-600 dark:text-yellow-400';
|
|
||||||
default:
|
|
||||||
return 'text-gray-600 dark:text-gray-400';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusBadge = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'success':
|
|
||||||
return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
|
|
||||||
case 'error':
|
|
||||||
return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
|
|
||||||
case 'pending':
|
|
||||||
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400';
|
|
||||||
default:
|
|
||||||
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-6 border-t border-gray-200 dark:border-gray-800 pt-6">
|
|
||||||
{isExpanded && (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="w-1/2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800/50">
|
|
||||||
{/* Header inside panel */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
|
||||||
AI Debug Logs
|
|
||||||
</h3>
|
|
||||||
<span className="text-xs px-2 py-1 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400">
|
|
||||||
{logs.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{isExpanded ? 'Collapse' : 'Expand'}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={clearLogs}
|
|
||||||
className="text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Scrollable content */}
|
|
||||||
<div className="p-4 max-h-96 overflow-y-auto">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{logs.map((log) => (
|
|
||||||
<React.Fragment key={log.id}>
|
|
||||||
<div
|
|
||||||
className="p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-start justify-between mb-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-xs font-semibold text-gray-700 dark:text-gray-300 font-mono-custom">
|
|
||||||
{log.function}
|
|
||||||
</span>
|
|
||||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(log.status)}`}>
|
|
||||||
{log.status.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
{log.duration && (
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{log.duration}ms
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{formatTime(log.timestamp)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-400 mb-3 font-mono-custom">
|
|
||||||
{log.endpoint}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Request/Response Details (Collapsible) */}
|
|
||||||
<details className="mt-3">
|
|
||||||
<summary className="text-xs text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-300">
|
|
||||||
Request/Response Details
|
|
||||||
</summary>
|
|
||||||
<div className="mt-2 space-y-2">
|
|
||||||
<details className="text-xs">
|
|
||||||
<summary className="text-gray-500 dark:text-gray-400 cursor-pointer">Request</summary>
|
|
||||||
<div className="mt-1 p-2 bg-gray-50 dark:bg-gray-800 rounded text-xs overflow-x-auto font-mono-custom">
|
|
||||||
<pre className="whitespace-pre-wrap">
|
|
||||||
{JSON.stringify(log.request.body || log.request.params || {}, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
{log.response && (
|
|
||||||
<details className="text-xs">
|
|
||||||
<summary className="text-gray-500 dark:text-gray-400 cursor-pointer">
|
|
||||||
Response ({log.response.status})
|
|
||||||
{log.response.errorType && (
|
|
||||||
<span className="ml-2 px-1.5 py-0.5 rounded bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 text-[10px] font-semibold">
|
|
||||||
{log.response.errorType}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</summary>
|
|
||||||
<div className="mt-1 p-2 bg-gray-50 dark:bg-gray-800 rounded text-xs overflow-x-auto font-mono-custom">
|
|
||||||
{log.response.error ? (
|
|
||||||
<div className="text-red-600 dark:text-red-400">
|
|
||||||
{log.response.error}
|
|
||||||
</div>
|
|
||||||
) : log.response.data ? (
|
|
||||||
<pre className="whitespace-pre-wrap">
|
|
||||||
{JSON.stringify(log.response.data, null, 2)}
|
|
||||||
</pre>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* AI Debug Steps */}
|
|
||||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-900">
|
|
||||||
<h4 className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
|
||||||
AI Debug Steps ({log.requestSteps.length + log.responseSteps.length} total steps)
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{(() => {
|
|
||||||
// Combine and sort all steps by stepNumber
|
|
||||||
const allSteps = [
|
|
||||||
...log.requestSteps.map((step, idx) => ({ ...step, type: 'request', originalIdx: idx })),
|
|
||||||
...log.responseSteps.map((step, idx) => ({ ...step, type: 'response', originalIdx: idx }))
|
|
||||||
].sort((a, b) => (a.stepNumber || 0) - (b.stepNumber || 0));
|
|
||||||
|
|
||||||
return allSteps.length > 0 ? (
|
|
||||||
allSteps.map((step, idx) => (
|
|
||||||
<div
|
|
||||||
key={`${step.type}-${step.originalIdx}-${idx}`}
|
|
||||||
className="p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<span className="text-xs font-mono text-gray-500 dark:text-gray-400">
|
|
||||||
{step.stepNumber}.
|
|
||||||
</span>
|
|
||||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(step.status)}`}>
|
|
||||||
{step.status.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-400 dark:text-gray-500">
|
|
||||||
{step.type === 'request' ? 'Request' : 'Response'}
|
|
||||||
</span>
|
|
||||||
{step.duration && (
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{step.duration}ms
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
{step.stepName}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-400 font-mono mb-1">
|
|
||||||
{step.functionName}
|
|
||||||
</div>
|
|
||||||
{step.message && (
|
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
||||||
{step.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{step.error && (
|
|
||||||
<div className="text-xs text-red-600 dark:text-red-400 mt-2">
|
|
||||||
Error: {step.error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 italic">
|
|
||||||
No steps logged yet
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isExpanded && logs.length > 0 && (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="w-1/2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800/50">
|
|
||||||
{/* Header when collapsed */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
|
||||||
AI Debug Logs
|
|
||||||
</h3>
|
|
||||||
<span className="text-xs px-2 py-1 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400">
|
|
||||||
{logs.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{isExpanded ? 'Collapse' : 'Expand'}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={clearLogs}
|
|
||||||
className="text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
Click "Expand" to view {logs.length} AI request log{logs.length !== 1 ? 's' : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|||||||
Reference in New Issue
Block a user