2025-09-16 15:08:11 +01:00
|
|
|
/**
|
|
|
|
* Reusable ToolChain component with smart truncation and tooltip expansion
|
|
|
|
* Used across FileListItem, FileDetails, and FileThumbnail for consistent display
|
|
|
|
*/
|
|
|
|
|
|
|
|
import React from 'react';
|
|
|
|
import { Text, Tooltip, Badge, Group } from '@mantine/core';
|
|
|
|
import { ToolOperation } from '../../types/file';
|
2025-09-22 11:46:56 +01:00
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { ToolId } from '../../types/toolId';
|
2025-09-16 15:08:11 +01:00
|
|
|
|
|
|
|
interface ToolChainProps {
|
|
|
|
toolChain: ToolOperation[];
|
|
|
|
maxWidth?: string;
|
|
|
|
displayStyle?: 'text' | 'badges' | 'compact';
|
|
|
|
size?: 'xs' | 'sm' | 'md';
|
|
|
|
color?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
const ToolChain: React.FC<ToolChainProps> = ({
|
|
|
|
toolChain,
|
|
|
|
maxWidth = '100%',
|
|
|
|
displayStyle = 'text',
|
|
|
|
size = 'xs',
|
|
|
|
color = 'var(--mantine-color-blue-7)'
|
|
|
|
}) => {
|
|
|
|
if (!toolChain || toolChain.length === 0) return null;
|
|
|
|
|
2025-09-22 11:46:56 +01:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
|
|
const toolIds = toolChain.map(tool => tool.toolId);
|
|
|
|
|
|
|
|
const getToolName = (toolId: ToolId) => {
|
|
|
|
return t(`home.${toolId}.title`, toolId);
|
|
|
|
}
|
2025-09-16 15:08:11 +01:00
|
|
|
|
|
|
|
// Create full tool chain for tooltip
|
|
|
|
const fullChainDisplay = displayStyle === 'badges' ? (
|
|
|
|
<Group gap="xs" wrap="wrap">
|
|
|
|
{toolChain.map((tool, index) => (
|
2025-09-22 11:46:56 +01:00
|
|
|
<React.Fragment key={`${tool.toolId}-${index}`}>
|
2025-09-16 15:08:11 +01:00
|
|
|
<Badge size="sm" variant="light" color="blue">
|
2025-09-22 11:46:56 +01:00
|
|
|
{getToolName(tool.toolId)}
|
2025-09-16 15:08:11 +01:00
|
|
|
</Badge>
|
|
|
|
{index < toolChain.length - 1 && (
|
|
|
|
<Text size="sm" c="dimmed">→</Text>
|
|
|
|
)}
|
|
|
|
</React.Fragment>
|
|
|
|
))}
|
|
|
|
</Group>
|
|
|
|
) : (
|
2025-09-22 11:46:56 +01:00
|
|
|
<Text size="sm">{toolIds.map(getToolName).join(' → ')}</Text>
|
2025-09-16 15:08:11 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
// Create truncated display based on available space
|
|
|
|
const getTruncatedDisplay = () => {
|
2025-09-22 11:46:56 +01:00
|
|
|
if (toolIds.length <= 2) {
|
2025-09-16 15:08:11 +01:00
|
|
|
// Show all tools if 2 or fewer
|
2025-09-22 11:46:56 +01:00
|
|
|
return { text: toolIds.map(getToolName).join(' → '), isTruncated: false };
|
2025-09-16 15:08:11 +01:00
|
|
|
} else {
|
|
|
|
// Show first tool ... last tool for longer chains
|
|
|
|
return {
|
2025-09-22 11:46:56 +01:00
|
|
|
text: `${getToolName(toolIds[0])} → +${toolIds.length-2} → ${getToolName(toolIds[toolIds.length - 1])}`,
|
|
|
|
isTruncated: true,
|
2025-09-16 15:08:11 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const { text: truncatedText, isTruncated } = getTruncatedDisplay();
|
|
|
|
|
|
|
|
// Compact style for very small spaces
|
|
|
|
if (displayStyle === 'compact') {
|
2025-09-22 11:46:56 +01:00
|
|
|
const compactText = toolIds.length === 1 ? getToolName(toolIds[0]) : `${toolIds.length} tools`;
|
|
|
|
const isCompactTruncated = toolIds.length > 1;
|
2025-09-16 15:08:11 +01:00
|
|
|
|
|
|
|
const compactElement = (
|
|
|
|
<Text
|
|
|
|
size={size}
|
|
|
|
style={{
|
|
|
|
color,
|
|
|
|
fontWeight: 500,
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
overflow: 'hidden',
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
maxWidth: `${maxWidth}`,
|
|
|
|
cursor: isCompactTruncated ? 'help' : 'default'
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{compactText}
|
|
|
|
</Text>
|
|
|
|
);
|
|
|
|
|
|
|
|
return isCompactTruncated ? (
|
|
|
|
<Tooltip label={fullChainDisplay} multiline withinPortal>
|
|
|
|
{compactElement}
|
|
|
|
</Tooltip>
|
|
|
|
) : compactElement;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Badge style for file details
|
|
|
|
if (displayStyle === 'badges') {
|
|
|
|
const isBadgesTruncated = toolChain.length > 3;
|
|
|
|
|
|
|
|
const badgesElement = (
|
|
|
|
<div style={{ maxWidth: `${maxWidth}`, overflow: 'hidden' }}>
|
|
|
|
<Group gap="2px" wrap="nowrap">
|
|
|
|
{toolChain.slice(0, 3).map((tool, index) => (
|
2025-09-22 11:46:56 +01:00
|
|
|
<React.Fragment key={`${tool.toolId}-${index}`}>
|
2025-09-16 15:08:11 +01:00
|
|
|
<Badge size={size} variant="light" color="blue">
|
2025-09-22 11:46:56 +01:00
|
|
|
{getToolName(tool.toolId)}
|
2025-09-16 15:08:11 +01:00
|
|
|
</Badge>
|
|
|
|
{index < Math.min(toolChain.length - 1, 2) && (
|
|
|
|
<Text size="xs" c="dimmed">→</Text>
|
|
|
|
)}
|
|
|
|
</React.Fragment>
|
|
|
|
))}
|
|
|
|
{toolChain.length > 3 && (
|
|
|
|
<>
|
|
|
|
<Text size="xs" c="dimmed">...</Text>
|
|
|
|
<Badge size={size} variant="light" color="blue">
|
2025-09-22 11:46:56 +01:00
|
|
|
{getToolName(toolChain[toolChain.length - 1].toolId)}
|
2025-09-16 15:08:11 +01:00
|
|
|
</Badge>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</Group>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
return isBadgesTruncated ? (
|
2025-09-22 11:46:56 +01:00
|
|
|
<Tooltip label={`${toolIds.map(getToolName).join(' → ')}`} withinPortal>
|
2025-09-16 15:08:11 +01:00
|
|
|
{badgesElement}
|
|
|
|
</Tooltip>
|
|
|
|
) : badgesElement;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Text style (default) for file list items
|
|
|
|
const textElement = (
|
|
|
|
<Text
|
|
|
|
size={size}
|
|
|
|
style={{
|
|
|
|
color,
|
|
|
|
fontWeight: 500,
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
overflow: 'hidden',
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
maxWidth: `${maxWidth}`,
|
|
|
|
cursor: isTruncated ? 'help' : 'default'
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{truncatedText}
|
|
|
|
</Text>
|
|
|
|
);
|
|
|
|
|
|
|
|
return isTruncated ? (
|
|
|
|
<Tooltip label={fullChainDisplay} withinPortal>
|
|
|
|
{textElement}
|
|
|
|
</Tooltip>
|
|
|
|
) : textElement;
|
|
|
|
};
|
|
|
|
|
|
|
|
export default ToolChain;
|