feat: refine mindspace asset card actions

This commit is contained in:
john
2026-06-29 07:00:29 +08:00
parent ea19ffb5fa
commit 9465803d95
3 changed files with 481 additions and 147 deletions
+12 -10
View File
@@ -39,14 +39,6 @@ function CheckIcon() {
);
}
function ToolSpinnerIcon() {
return (
<span className="tool-badge-spinner" aria-hidden="true">
<span />
</span>
);
}
function ToolDoneIcon() {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
@@ -61,6 +53,16 @@ function ToolDoneIcon() {
);
}
function resolveToolCallName(
tool: Extract<Message['content'][number], { type: 'toolRequest' }>,
): string {
const toolCall = tool.toolCall as {
functionName?: string;
value?: { name?: string };
};
return String(toolCall.functionName ?? toolCall.value?.name ?? '').trim();
}
function ToolBadge({ message }: { message: Message }) {
const tools = message.content.filter((c) => c.type === 'toolRequest' || c.type === 'toolResponse');
if (tools.length === 0) return null;
@@ -72,7 +74,7 @@ function ToolBadge({ message }: { message: Message }) {
<div className="tool-badges">
{tools.map((tool, i) => {
const isRequest = tool.type === 'toolRequest';
const toolName = isRequest ? String(tool.toolCall.functionName ?? '').trim() : '';
const toolName = isRequest ? resolveToolCallName(tool) : '';
let label: string;
if (isRequest && !toolName) {
@@ -86,7 +88,7 @@ function ToolBadge({ message }: { message: Message }) {
key={`${message.id}-${i}`}
className={`tool-badge${isRequest ? ' is-pending' : ' is-complete'}`}
>
{isRequest ? <ToolSpinnerIcon /> : <ToolDoneIcon />}
{!isRequest ? <ToolDoneIcon /> : null}
<span>{label}</span>
</span>
);
+181 -92
View File
@@ -486,6 +486,7 @@ export function MindSpaceView({
const [assetFilter, setAssetFilter] = useState<AssetFilter>('all');
const [imagePage, setImagePage] = useState(0);
const [previewAssetId, setPreviewAssetId] = useState<string | null>(null);
const [openAssetMenuId, setOpenAssetMenuId] = useState<string | null>(null);
const [editingAssetId, setEditingAssetId] = useState<string | null>(null);
const [cleanupOpen, setCleanupOpen] = useState(false);
const [cleanupItems, setCleanupItems] = useState<MindSpaceCleanupItem[]>([]);
@@ -561,6 +562,18 @@ export function MindSpaceView({
void bindMindSpacePageLiveEdit(selectedPageId, session.id).catch(() => {});
}, [selectedPageId, session?.id, previewMode, pageFullscreenPreviewOpen]);
useEffect(() => {
if (!openAssetMenuId) return;
const handlePointerDown = (event: PointerEvent) => {
const target = event.target;
if (!(target instanceof Element)) return;
if (target.closest('.mindspace-item-actions')) return;
setOpenAssetMenuId(null);
};
document.addEventListener('pointerdown', handlePointerDown);
return () => document.removeEventListener('pointerdown', handlePointerDown);
}, [openAssetMenuId]);
const mindspaceChatContext = useMemo(() => {
if (!space) return null;
return buildMindSpaceChatContext({
@@ -1392,68 +1405,140 @@ export function MindSpaceView({
);
const previewAssetFrame = previewAsset ? buildAssetPreviewFrameConfig(previewAsset) : null;
const renderAssetActions = (asset: MindSpaceAsset) => (
<div className="mindspace-item-actions">
{isPreviewableAsset(asset) && (
<button type="button" onClick={() => setPreviewAssetId(asset.id)}>
</button>
)}
{isWorkAsset(asset) && (
<button
type="button"
onClick={() => void openAssetEditor(asset)}
disabled={editingAssetId === asset.id}
>
{editingAssetId === asset.id ? '打开中…' : '编辑'}
</button>
)}
{asset.sourcePageId && (
<button type="button" onClick={() => showPage(asset.sourcePageId!, true)}>
广
</button>
)}
{asset.publicUrl && (
<a
className="mindspace-asset-share"
href={asset.publicUrl}
target="_blank"
rel="noreferrer"
>
</a>
)}
{asset.status === 'ready' && asset.scanStatus !== 'blocked' ? (
<button
type="button"
className="mindspace-asset-share"
onClick={() => setSharePayload(buildAssetSharePayload(asset, selectedCategory?.code))}
>
</button>
) : (
<span className="mindspace-asset-blocked"></span>
)}
{pendingDeleteId === asset.id ? (
<>
const renderAssetActions = (asset: MindSpaceAsset) => {
const shareReady = asset.status === 'ready' && asset.scanStatus !== 'blocked';
const menuOpen = openAssetMenuId === asset.id;
return (
<div className="mindspace-item-actions">
{isWorkAsset(asset) ? (
<button
type="button"
className="mindspace-delete-confirm"
onClick={() => void removeAsset(asset.id)}
className="mindspace-item-action is-primary"
onClick={() => void openAssetEditor(asset)}
disabled={editingAssetId === asset.id}
>
{editingAssetId === asset.id ? '打开中…' : '编辑'}
</button>
<button type="button" onClick={() => setPendingDeleteId(null)}>
) : isPreviewableAsset(asset) ? (
<button
type="button"
className="mindspace-item-action is-primary"
onClick={() => setPreviewAssetId(asset.id)}
>
</button>
</>
) : (
<button type="button" onClick={() => setPendingDeleteId(asset.id)}>
</button>
)}
</div>
);
) : (
<span className="mindspace-asset-blocked"></span>
)}
{shareReady ? (
<button
type="button"
className="mindspace-asset-share mindspace-item-action"
onClick={() => setSharePayload(buildAssetSharePayload(asset, selectedCategory?.code))}
>
</button>
) : (
<span className="mindspace-asset-blocked"></span>
)}
<div className={`mindspace-item-more${menuOpen ? ' is-open' : ''}`}>
<button
type="button"
className="mindspace-item-more-trigger"
aria-haspopup="menu"
aria-expanded={menuOpen}
aria-label={`${asset.displayName} 更多操作`}
onClick={() => setOpenAssetMenuId((current) => (current === asset.id ? null : asset.id))}
>
···
</button>
{menuOpen && (
<div className="mindspace-item-more-menu" role="menu" aria-label="更多操作">
{isPreviewableAsset(asset) && !isWorkAsset(asset) && (
<button
type="button"
className="mindspace-item-more-entry"
role="menuitem"
onClick={() => {
setPreviewAssetId(asset.id);
setOpenAssetMenuId(null);
}}
>
</button>
)}
{asset.sourcePageId && (
<button
type="button"
className="mindspace-item-more-entry"
role="menuitem"
onClick={() => {
showPage(asset.sourcePageId!, true);
setOpenAssetMenuId(null);
}}
>
广
</button>
)}
{asset.publicUrl && (
<a
className="mindspace-item-more-entry"
role="menuitem"
href={asset.publicUrl}
target="_blank"
rel="noreferrer"
onClick={() => setOpenAssetMenuId(null)}
>
</a>
)}
{pendingDeleteId === asset.id ? (
<>
<button
type="button"
className="mindspace-item-more-entry is-danger"
role="menuitem"
onClick={() => {
void removeAsset(asset.id);
setOpenAssetMenuId(null);
}}
>
</button>
<button
type="button"
className="mindspace-item-more-entry"
role="menuitem"
onClick={() => {
setPendingDeleteId(null);
setOpenAssetMenuId(null);
}}
>
</button>
</>
) : (
<button
type="button"
className="mindspace-item-more-entry is-danger"
role="menuitem"
onClick={() => {
setPendingDeleteId(asset.id);
setOpenAssetMenuId(null);
}}
>
</button>
)}
</div>
)}
</div>
</div>
);
};
const createPage = async () => {
if (!newPageTitle.trim() || !newPageContent.trim()) return;
@@ -2280,45 +2365,49 @@ export function MindSpaceView({
/>
<span></span>
</label>
<button
type="button"
className="mindspace-item-card-main"
onClick={() => {
if (isWorkAsset(asset)) {
void openAssetEditor(asset);
return;
}
if (isPreviewableAsset(asset)) setPreviewAssetId(asset.id);
}}
>
{isWorkAsset(asset) ? (
<div className="mindspace-item-cover">
{isPreviewableAsset(asset) ? (
<WorkCover
thumbnailUrl={`/api/mindspace/v1/assets/${asset.id}/thumbnail?v=${asset.updatedAt}`}
previewAsset={asset}
fallbackLabel={assetIcon(asset)}
title={asset.displayName}
/>
) : (
<span>{assetIcon(asset)}</span>
)}
</div>
) : (
<div className="mindspace-item-icon">{assetIcon(asset)}</div>
)}
<div className="mindspace-item-copy">
<div className="mindspace-item-card-frame">
<div className="mindspace-item-card-head">
<span className="mindspace-item-badge">
{assetSourceLabel(asset)}
</span>
<h3>{asset.displayName}</h3>
<p>
{formatBytes(asset.sizeBytes)} ·{' '}
{new Date(asset.updatedAt).toLocaleDateString()}
</p>
</div>
</button>
{renderAssetActions(asset)}
<button
type="button"
className="mindspace-item-card-main"
onClick={() => {
if (isWorkAsset(asset)) {
void openAssetEditor(asset);
return;
}
if (isPreviewableAsset(asset)) setPreviewAssetId(asset.id);
}}
>
{isWorkAsset(asset) ? (
<div className="mindspace-item-cover">
{isPreviewableAsset(asset) ? (
<WorkCover
thumbnailUrl={`/api/mindspace/v1/assets/${asset.id}/thumbnail?v=${asset.updatedAt}`}
previewAsset={asset}
fallbackLabel={assetIcon(asset)}
title={asset.displayName}
/>
) : (
<span>{assetIcon(asset)}</span>
)}
</div>
) : (
<div className="mindspace-item-icon">{assetIcon(asset)}</div>
)}
<div className="mindspace-item-copy">
<h3>{asset.displayName}</h3>
<p>
{formatBytes(asset.sizeBytes)} ·{' '}
{new Date(asset.updatedAt).toLocaleDateString()}
</p>
</div>
</button>
{renderAssetActions(asset)}
</div>
</article>
))}
</div>