feat(workspace): add react prompt status

This commit is contained in:
2026-09-16 02:48:56 +08:00
parent 68bdeceb68
commit 8c99ac37f9
5 changed files with 106 additions and 1 deletions
@@ -0,0 +1,50 @@
import { Badge } from "../../components/badge";
import { Button } from "../../components/button";
export interface WorkspacePromptSummary {
id: string;
title: string;
updatedAt: string;
contentLength: number;
}
function formatPromptDate(value: string) {
const date = new Date(value);
return Number.isNaN(date.getTime())
? "时间未知"
: date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
}
export function PromptStatus({
prompts,
onOpen
}: {
prompts: readonly WorkspacePromptSummary[];
onOpen: () => void;
}) {
const visiblePrompts = prompts.slice(0, 3);
const remainingCount = Math.max(0, prompts.length - visiblePrompts.length);
return (
<section className="workspace-prompt-status" aria-label="最近 Prompt">
<div className="workspace-prompt-status-heading">
<div className="workspace-prompt-status-title">
<strong>Recent Prompts</strong>
<Badge>{prompts.length}</Badge>
</div>
<Button size="sm" variant="outline" onClick={onOpen}> Prompts</Button>
</div>
<div className="workspace-prompt-preview">
{!visiblePrompts.length && <div className="workspace-prompt-empty"> Prompt</div>}
{visiblePrompts.map((prompt) => (
<article className="workspace-prompt-card" key={prompt.id}>
<strong title={prompt.title}>{prompt.title}</strong>
<span>{formatPromptDate(prompt.updatedAt)}</span>
<small>{prompt.contentLength} </small>
</article>
))}
{remainingCount > 0 && <span className="workspace-prompt-more"> {remainingCount} Prompts </span>}
</div>
</section>
);
}
@@ -5,6 +5,7 @@ import { ProviderStrip, type ProviderReadiness } from "./features/provider-strip
import { ProviderReadinessPanel, type ProviderPanelAction } from "./features/provider-readiness-panel";
import { CompareStatus, type CompareSummary } from "./features/compare-status";
import { LibraryStatus, type WorkspaceLibrarySummary } from "./features/library-status";
import { PromptStatus, type WorkspacePromptSummary } from "./features/prompt-status";
import { SessionStatus, type WorkspaceSessionSummary } from "./features/session-status";
import { WorkspaceActions, type WorkspaceAction } from "./features/workspace-actions";
@@ -95,6 +96,24 @@ function readSessionSummaries(value: unknown): WorkspaceSessionSummary[] {
}).slice(0, 20);
}
function readPromptSummaries(value: unknown): WorkspacePromptSummary[] {
if (!Array.isArray(value)) return [];
return value.flatMap((entry) => {
if (!entry || typeof entry !== "object") return [];
const record = entry as Record<string, unknown>;
if (typeof record.id !== "string" || !record.id.trim()) return [];
const contentLength = typeof record.contentLength === "number" && Number.isFinite(record.contentLength)
? Math.max(0, Math.floor(record.contentLength))
: 0;
return [{
id: record.id,
title: typeof record.title === "string" && record.title.trim() ? record.title : "Untitled Prompt",
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : "",
contentLength
}];
}).slice(0, 50);
}
function emitSelection(providerIds: readonly ProviderId[]) {
window.dispatchEvent(new CustomEvent("ai-parallel:workspace-set-selection", {
detail: { providerIds: [...providerIds] }
@@ -119,6 +138,7 @@ export function WorkspaceShell() {
const [compare, setCompare] = useState<CompareSummary>(emptyCompareSummary);
const [libraries, setLibraries] = useState<WorkspaceLibrarySummary>(emptyLibrarySummary);
const [sessions, setSessions] = useState<WorkspaceSessionSummary[]>([]);
const [prompts, setPrompts] = useState<WorkspacePromptSummary[]>([]);
useEffect(() => {
let active = true;
@@ -136,6 +156,7 @@ export function WorkspaceShell() {
compare?: unknown;
libraries?: unknown;
sessions?: unknown;
prompts?: unknown;
}>).detail;
if (detail?.selectedProviders) setSelected(readProviderSelection(detail.selectedProviders, providers));
if (layouts.includes(detail?.workspaceLayout as WorkspaceLayout)) setLayout(detail.workspaceLayout as WorkspaceLayout);
@@ -143,6 +164,7 @@ export function WorkspaceShell() {
if (detail?.compare) setCompare(readCompareSummary(detail.compare));
if (detail?.libraries) setLibraries(readLibrarySummary(detail.libraries));
if (detail?.sessions) setSessions(readSessionSummaries(detail.sessions));
if (detail?.prompts) setPrompts(readPromptSummaries(detail.prompts));
};
window.addEventListener("ai-parallel:workspace-state", handleWorkspaceState);
return () => {
@@ -188,6 +210,7 @@ export function WorkspaceShell() {
/>
<CompareStatus selectedCount={selected.length} summary={compare} onOpen={() => triggerLegacyAction("compare")} />
<SessionStatus sessions={sessions} onOpen={() => triggerLegacyAction("session")} />
<PromptStatus prompts={prompts} onOpen={() => triggerLegacyAction("prompt")} />
<LibraryStatus summary={libraries} onOpen={triggerLegacyAction} />
</div>
);
@@ -36,6 +36,16 @@ body[data-react-workspace="true"] #workspaceReactRoot { display: block; grid-col
.workspace-session-card span, .workspace-session-card small { color: var(--ui-muted); font-size: 10px; }
.workspace-session-card small { color: var(--ui-muted); opacity: .78; }
.workspace-session-empty, .workspace-session-more { color: var(--ui-muted); font-size: 10px; }
.workspace-prompt-status { display: grid; gap: 6px; grid-column: 1 / -1; padding: 0 14px 10px; }
.workspace-prompt-status-heading { align-items: center; display: flex; justify-content: space-between; gap: 8px; }
.workspace-prompt-status-title { align-items: center; display: flex; gap: 7px; }
.workspace-prompt-status-title strong { font-size: 11px; }
.workspace-prompt-preview { display: grid; gap: 6px; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); min-width: 0; }
.workspace-prompt-card { background: rgba(17, 18, 23, .58); border: 1px solid var(--ui-border); border-radius: 9px; display: grid; gap: 2px; min-width: 0; padding: 7px 9px; }
.workspace-prompt-card strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; }
.workspace-prompt-card span, .workspace-prompt-card small { color: var(--ui-muted); font-size: 10px; }
.workspace-prompt-card small { opacity: .78; }
.workspace-prompt-empty, .workspace-prompt-more { color: var(--ui-muted); font-size: 10px; }
.workspace-library-status { display: grid; gap: 6px; grid-column: 1 / -1; grid-template-columns: repeat(3, minmax(0, 1fr)); padding: 0 14px 10px; }
.workspace-library-card { align-items: center; background: rgba(17, 18, 23, .58); border: 1px solid var(--ui-border); border-radius: 9px; display: grid; gap: 3px 7px; grid-template-columns: minmax(0, 1fr) auto; padding: 7px 9px; }
.workspace-library-card-heading { align-items: center; display: flex; gap: 7px; min-width: 0; }
@@ -51,5 +61,6 @@ body[data-react-workspace="true"] #workspaceReactRoot { display: block; grid-col
}
@media (max-width: 760px) {
.workspace-session-preview { grid-template-columns: 1fr; }
.workspace-prompt-preview { grid-template-columns: 1fr; }
.workspace-library-status { grid-template-columns: 1fr; }
}
+13 -1
View File
@@ -155,6 +155,17 @@ function workspaceSessionState() {
}));
}
function workspacePromptState() {
return promptLibraryEntries
.filter((entry) => entry && typeof entry.id === "string" && entry.id)
.map((entry) => ({
id: entry.id,
title: typeof entry.title === "string" ? entry.title : "Untitled Prompt",
updatedAt: typeof entry.updatedAt === "string" ? entry.updatedAt : (typeof entry.createdAt === "string" ? entry.createdAt : ""),
contentLength: typeof entry.content === "string" ? entry.content.length : 0
}));
}
function notifyWorkspaceShell() {
window.dispatchEvent(new CustomEvent("ai-parallel:workspace-state", {
detail: {
@@ -163,7 +174,8 @@ function notifyWorkspaceShell() {
providerStates: workspaceProviderStates(),
compare: workspaceCompareState(),
libraries: workspaceLibraryState(),
sessions: workspaceSessionState()
sessions: workspaceSessionState(),
prompts: workspacePromptState()
}
}));
}
+9
View File
@@ -55,6 +55,7 @@ test("Workspace React shell owns the generated toolbar and delegates legacy runt
const compareStatus = read("ui", "workspace", "features", "compare-status.tsx");
const libraryStatus = read("ui", "workspace", "features", "library-status.tsx");
const sessionStatus = read("ui", "workspace", "features", "session-status.tsx");
const promptStatus = read("ui", "workspace", "features", "prompt-status.tsx");
const entrypoint = read("entrypoints", "workspace-shell.tsx");
const workspaceHtml = read("workspace", "index.html");
const legacyWorkspace = read("workspace", "workspace.js");
@@ -90,6 +91,11 @@ test("Workspace React shell owns the generated toolbar and delegates legacy runt
assert.match(sessionStatus, /WorkspaceSessionSummary/);
assert.match(sessionStatus, /promptLength/);
assert.doesNotMatch(sessionStatus, /entry\.prompt|responseBundles|iframe/);
assert.match(shell, /PromptStatus/);
assert.match(shell, /prompts/);
assert.match(promptStatus, /WorkspacePromptSummary/);
assert.match(promptStatus, /contentLength/);
assert.doesNotMatch(promptStatus, /entry\.content|responseBundles|iframe/);
assert.match(shell, /sessionBtn/);
assert.match(shell, /promptLibraryBtn/);
assert.match(shell, /templateLibraryBtn/);
@@ -108,5 +114,8 @@ test("Workspace React shell owns the generated toolbar and delegates legacy runt
assert.match(legacyWorkspace, /function workspaceSessionState/);
assert.match(legacyWorkspace, /promptLength: typeof entry\.prompt === "string" \? entry\.prompt\.length : 0/);
assert.match(legacyWorkspace, /sessions: workspaceSessionState\(\)/);
assert.match(legacyWorkspace, /function workspacePromptState/);
assert.match(legacyWorkspace, /contentLength: typeof entry\.content === "string" \? entry\.content\.length : 0/);
assert.match(legacyWorkspace, /prompts: workspacePromptState\(\)/);
assert.match(legacyWorkspace, /ai-parallel:workspace-set-selection/);
});