merge: codex/mindspace-ui into main

This commit is contained in:
john
2026-06-29 07:15:47 +08:00
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>
+288 -45
View File
@@ -6389,6 +6389,16 @@ body,
.mindspace-recent-work-grid.is-all-pages {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.mindspace-item-grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
}
.mindspace-image-grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
}
}
@media (min-width: 761px) {
@@ -6488,22 +6498,22 @@ body,
.mindspace-image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
gap: 14px;
margin-bottom: 24px;
grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
gap: 10px;
margin-bottom: 16px;
}
.mindspace-image-grid.is-compact {
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
gap: 10px;
margin-bottom: 12px;
grid-template-columns: repeat(auto-fill, minmax(76px, 1fr));
gap: 8px;
margin-bottom: 10px;
}
.mindspace-image-card {
position: relative;
display: flex;
flex-direction: column;
gap: 8px;
gap: 6px;
}
.mindspace-image-card.is-compact {
@@ -6516,7 +6526,7 @@ body,
aspect-ratio: 1;
padding: 0;
border: 1px solid rgba(24, 33, 29, 0.1);
border-radius: 14px;
border-radius: 12px;
overflow: hidden;
background: rgba(47, 111, 87, 0.08);
cursor: pointer;
@@ -6526,7 +6536,7 @@ body,
}
.mindspace-image-grid.is-compact .mindspace-image-thumb {
border-radius: 10px;
border-radius: 8px;
}
.mindspace-image-grid.is-compact .mindspace-image-thumb:hover {
@@ -6562,45 +6572,49 @@ body,
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
font-size: 11px;
font-weight: 600;
}
.mindspace-image-size {
color: #68716c;
font-size: 11px;
font-size: 10px;
}
.mindspace-image-card .mindspace-item-actions {
gap: 6px;
gap: 4px;
}
.mindspace-image-card .mindspace-item-actions button,
.mindspace-image-card .mindspace-item-actions .mindspace-asset-share {
padding: 5px 9px;
font-size: 12px;
min-height: 24px;
padding: 0 8px;
font-size: 10px;
}
.mindspace-asset-select {
position: absolute;
z-index: 2;
top: 8px;
left: 8px;
top: 12px;
left: 12px;
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 8px;
gap: 6px;
padding: 7px 10px;
border-radius: 999px;
color: #18211d;
background: rgba(255, 252, 244, 0.92);
box-shadow: 0 4px 12px rgba(24, 33, 29, 0.1);
background: rgba(255, 250, 240, 0.94);
border: 1px solid rgba(24, 33, 29, 0.08);
box-shadow: 0 10px 24px rgba(24, 33, 29, 0.1);
font-size: 12px;
font-weight: 700;
backdrop-filter: blur(10px);
}
.mindspace-asset-select.is-card {
position: static;
justify-self: start;
top: 18px;
left: auto;
right: 18px;
}
.mindspace-asset-select input {
@@ -6612,21 +6626,51 @@ body,
.mindspace-item-card {
position: relative;
display: grid;
gap: 12px;
padding: 16px;
border: 1px solid rgba(24, 33, 29, 0.12);
border-radius: 22px;
background: rgba(255, 252, 244, 0.9);
gap: 0;
padding: 10px;
border: 1px solid rgba(24, 33, 29, 0.08);
border-radius: 18px;
background: linear-gradient(180deg, rgba(255, 252, 244, 0.98), rgba(245, 248, 241, 0.94));
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.85) inset,
0 18px 40px rgba(24, 33, 29, 0.08);
overflow: hidden;
transition:
transform 0.18s ease,
box-shadow 0.18s ease,
border-color 0.18s ease;
}
.mindspace-item-card.is-work {
background:
linear-gradient(145deg, rgba(255, 252, 244, 0.96), rgba(224, 235, 223, 0.88));
radial-gradient(circle at top right, rgba(220, 232, 225, 0.9), transparent 34%),
linear-gradient(180deg, rgba(255, 252, 244, 0.98), rgba(232, 240, 232, 0.96));
}
.mindspace-item-card:hover {
transform: translateY(-3px);
border-color: rgba(47, 111, 87, 0.18);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.9) inset,
0 24px 52px rgba(24, 33, 29, 0.12);
}
.mindspace-item-card-frame {
display: grid;
gap: 9px;
}
.mindspace-item-card-head {
display: flex;
align-items: center;
justify-content: flex-start;
min-height: 18px;
padding-right: 72px;
}
.mindspace-item-card-main {
display: grid;
gap: 14px;
gap: 9px;
padding: 0;
border: 0;
color: inherit;
@@ -6640,14 +6684,19 @@ body,
.mindspace-item-cover {
display: grid;
place-items: center;
min-height: 88px;
border-radius: 16px;
min-height: 112px;
border-radius: 14px;
overflow: hidden;
background: rgba(47, 111, 87, 0.1);
background:
linear-gradient(180deg, rgba(243, 247, 241, 0.96), rgba(224, 234, 224, 0.92));
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.7) inset,
0 18px 32px rgba(24, 33, 29, 0.08);
}
.mindspace-item-icon {
font-size: 28px;
font-size: 22px;
color: #244536;
}
.mindspace-item-cover iframe {
@@ -6659,23 +6708,49 @@ body,
transform-origin: top left;
}
.mindspace-item-cover-img,
.mindspace-item-cover .mindspace-preview-embed,
.mindspace-item-cover .mindspace-preview-embed iframe {
width: 100%;
height: 100%;
}
.mindspace-item-cover-img {
display: block;
object-fit: cover;
}
.mindspace-item-copy h3 {
margin: 8px 0 6px;
margin: 0 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 18px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
white-space: normal;
font-size: clamp(14px, 1.15vw, 17px);
line-height: 1.18;
letter-spacing: -0.04em;
}
.mindspace-item-copy p {
margin: 0;
color: #68716c;
font-size: 12px;
color: #5e6862;
font-size: 10px;
line-height: 1.35;
}
.mindspace-item-badge {
color: #9b6518;
font-size: 11px;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 7px;
border-radius: 999px;
color: #8a5a18;
background: rgba(255, 246, 225, 0.96);
border: 1px solid rgba(155, 101, 24, 0.14);
box-shadow: 0 6px 16px rgba(155, 101, 24, 0.08);
font-size: 9px;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
@@ -6684,24 +6759,135 @@ body,
.mindspace-item-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 6px;
align-items: center;
padding-top: 2px;
border-top: 1px solid rgba(24, 33, 29, 0.08);
}
.mindspace-item-actions button,
.mindspace-item-actions .mindspace-asset-share {
padding: 7px 11px;
border: 0;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 28px;
padding: 0 10px;
border: 1px solid rgba(24, 33, 29, 0.1);
border-radius: 999px;
color: #18211d;
background: rgba(24, 33, 29, 0.08);
background: rgba(255, 250, 240, 0.92);
cursor: pointer;
font: inherit;
font-size: 11px;
font-weight: 700;
line-height: 1;
text-decoration: none;
white-space: nowrap;
transition:
transform 0.16s ease,
border-color 0.16s ease,
box-shadow 0.16s ease,
background 0.16s ease,
color 0.16s ease;
}
.mindspace-item-actions button:hover,
.mindspace-item-actions .mindspace-asset-share:hover {
transform: translateY(-1px);
border-color: rgba(47, 111, 87, 0.2);
box-shadow: 0 10px 20px rgba(24, 33, 29, 0.08);
}
.mindspace-item-action.is-primary {
color: #fffaf0;
background: linear-gradient(135deg, #204b3a, #2f6f57);
border-color: rgba(32, 75, 58, 0.9);
}
.mindspace-item-action.is-danger {
color: #8b2d20;
background: rgba(139, 45, 32, 0.08);
border-color: rgba(139, 45, 32, 0.12);
}
.mindspace-item-actions .mindspace-delete-confirm {
color: #fffaf0;
background: #8b2d20;
background: linear-gradient(135deg, #8b2d20, #ad3e2c);
border-color: rgba(139, 45, 32, 0.9);
}
.mindspace-item-actions button:disabled,
.mindspace-item-actions .mindspace-asset-share:disabled {
opacity: 0.6;
cursor: wait;
transform: none;
box-shadow: none;
}
.mindspace-item-more {
position: relative;
margin-left: auto;
}
.mindspace-item-more-trigger {
min-width: 28px;
padding: 0 8px 2px;
letter-spacing: 0.08em;
}
.mindspace-item-more-menu {
position: absolute;
right: 0;
top: calc(100% + 8px);
z-index: 12;
display: grid;
gap: 4px;
min-width: 132px;
padding: 6px;
border: 1px solid rgba(24, 33, 29, 0.1);
border-radius: 14px;
background: rgba(255, 250, 240, 0.98);
box-shadow: 0 18px 36px rgba(24, 33, 29, 0.14);
backdrop-filter: blur(14px);
}
.mindspace-item-more-entry {
display: flex;
align-items: center;
min-height: 32px;
padding: 0 9px;
border: 0;
border-radius: 10px;
color: #18211d;
background: transparent;
font: inherit;
font-size: 11px;
font-weight: 700;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
}
.mindspace-item-more-entry:hover {
background: rgba(24, 33, 29, 0.06);
}
.mindspace-item-more-entry.is-danger {
color: #8b2d20;
}
.mindspace-asset-blocked {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
color: #6b726d;
background: rgba(24, 33, 29, 0.06);
border: 1px dashed rgba(24, 33, 29, 0.12);
font-size: 10px;
font-weight: 700;
white-space: nowrap;
}
.share-sheet-backdrop {
@@ -7914,6 +8100,51 @@ body,
grid-template-columns: 1fr;
}
.mindspace-item-card {
padding: 10px;
border-radius: 16px;
}
.mindspace-asset-select.is-card {
top: 10px;
right: 10px;
}
.mindspace-item-card-head {
padding-right: 70px;
}
.mindspace-item-icon,
.mindspace-item-cover {
min-height: 106px;
border-radius: 14px;
}
.mindspace-item-copy h3 {
font-size: 14px;
}
.mindspace-item-copy p {
font-size: 10px;
}
.mindspace-item-actions {
gap: 6px;
}
.mindspace-item-actions button,
.mindspace-item-actions .mindspace-asset-share,
.mindspace-asset-blocked {
min-height: 28px;
padding-inline: 10px;
font-size: 10px;
}
.mindspace-item-more-menu {
right: 0;
min-width: 124px;
}
.page-save-layout {
grid-template-columns: 1fr;
}
@@ -7983,6 +8214,18 @@ body,
}
}
@media (min-width: 1380px) {
.mindspace-item-grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
}
.mindspace-image-grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
}
}
.logout-btn {
color: var(--color-text-error);
}