feat(workspace): add react prompt status
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user