Files
omniai-web/src/features/workbench/workbenchStorage.ts
T
stringadmin 59efc78c0e refactor(workbench): extract types, constants, utils, sub-components from WorkbenchPage
WorkbenchPage.tsx: 4146 → 3047 lines (-27%)

Extracted to 6 sibling modules:
- workbenchConstants.ts (403L): types, MODE_META, option arrays, shared helpers
- workbenchStorage.ts (172L): localStorage read/write/persist functions
- workbenchReferenceUtils.ts (210L): image compression, fingerprint, file helpers
- workbenchMentionUtils.tsx (79L): prompt mention parsing and token rendering
- WorkbenchPromptPreview.tsx (87L): ReferencePreview, PromptPreviewLayer components
- WorkbenchSelectChips.tsx (263L): SelectChip, CompoundSelectChip, InlineOptionChip

All extracted code is imported back via ES module imports — no logic changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 14:18:26 +08:00

173 lines
6.4 KiB
TypeScript

import {
userKey,
MESSAGE_STORAGE_KEY,
ACTIVE_CONVERSATION_STORAGE_KEY,
PROMPT_HISTORY_STORAGE_KEY,
TASK_KEEPALIVE_STORAGE_KEY,
WORKBENCH_TASK_STALE_MS,
type ChatMessage,
type WorkbenchKeepaliveTask,
} from "./workbenchConstants";
import { parseWorkbenchTimestampValue } from "./workbenchConstants";
export function readStoredMessages(): ChatMessage[] {
if (typeof window === "undefined") return [];
try {
const raw = window.localStorage.getItem(userKey(MESSAGE_STORAGE_KEY));
if (!raw) return [];
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) return [];
return parsed.filter((item): item is ChatMessage => {
if (!item || typeof item !== "object") return false;
const candidate = item as Partial<ChatMessage>;
return (
typeof candidate.id === "string" &&
(candidate.role === "user" || candidate.role === "assistant") &&
typeof candidate.body === "string"
);
});
} catch {
return [];
}
}
export function readStoredPromptHistory(): string[] {
if (typeof window === "undefined") return [];
try {
const raw = window.localStorage.getItem(userKey(PROMPT_HISTORY_STORAGE_KEY));
if (!raw) return [];
const parsed = JSON.parse(raw) as unknown;
return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === "string") : [];
} catch {
return [];
}
}
export function readStoredActiveConversationId(messages: ChatMessage[] = []): number | null {
if (typeof window === "undefined") return null;
try {
const raw = window.localStorage.getItem(userKey(ACTIVE_CONVERSATION_STORAGE_KEY));
const value = raw ? Number(raw) : NaN;
if (Number.isFinite(value) && value > 0) return value;
} catch {
// Active conversation recovery is optional.
}
for (let index = messages.length - 1; index >= 0; index -= 1) {
const candidate = messages[index]?.conversationId;
if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) {
return candidate;
}
}
return null;
}
export function persistActiveConversationId(conversationId: number | null) {
if (typeof window === "undefined") return;
try {
if (conversationId && Number.isFinite(conversationId)) {
window.localStorage.setItem(userKey(ACTIVE_CONVERSATION_STORAGE_KEY), String(conversationId));
} else {
window.localStorage.removeItem(userKey(ACTIVE_CONVERSATION_STORAGE_KEY));
}
} catch {
// Local history is a convenience; generation still works without it.
}
}
export function persistMessages(messages: ChatMessage[]) {
try {
window.localStorage.setItem(userKey(MESSAGE_STORAGE_KEY), JSON.stringify(messages.slice(-60)));
} catch {
// Local history is a convenience; generation still works without it.
}
}
export function clearWorkbenchLocalState() {
if (typeof window === "undefined") return;
try {
window.localStorage.removeItem(userKey(MESSAGE_STORAGE_KEY));
window.localStorage.removeItem(userKey(ACTIVE_CONVERSATION_STORAGE_KEY));
window.localStorage.removeItem(userKey(TASK_KEEPALIVE_STORAGE_KEY));
} catch {
// Logout cleanup should never block the UI.
}
}
export function persistPromptHistory(history: string[]) {
try {
window.localStorage.setItem(userKey(PROMPT_HISTORY_STORAGE_KEY), JSON.stringify(history.slice(0, 20)));
} catch {
// Local history is optional.
}
}
// ─── Keepalive task persistence ──────────────────────────────────────
export function buildRecoverableTaskFromMessage(conversationId: number, message: ChatMessage): WorkbenchKeepaliveTask | null {
if (message.role !== "assistant") return null;
if (!(message.status === "thinking" && message.taskId && message.mode !== "chat")) return null;
if (message.mode !== "image" && message.mode !== "video") return null;
if (Date.now() - parseWorkbenchTimestampValue(message.createdAt) > WORKBENCH_TASK_STALE_MS) return null;
const specs = message.result?.specs || [];
return {
taskId: message.taskId,
conversationId,
assistantMessageId: message.id,
operation: message.taskStatusLabel?.includes("超分") ? "video-super-resolution" : "generation",
mode: message.mode,
modelLabel: specs[0] || message.author || message.mode,
specs,
referenceCount: message.attachments?.length || 0,
progress: Math.max(10, Math.min(99, Number(message.taskProgress || 30))),
statusLabel: message.taskStatusLabel || "任务恢复中...",
startedAt: parseWorkbenchTimestampValue(message.createdAt) || Date.now(),
};
}
export function readStoredKeepaliveTasks(): Record<string, WorkbenchKeepaliveTask> {
if (typeof window === "undefined") return {};
try {
const raw = window.localStorage.getItem(userKey(TASK_KEEPALIVE_STORAGE_KEY));
if (!raw) return {};
const parsed = JSON.parse(raw) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
const tasks: Record<string, WorkbenchKeepaliveTask> = {};
Object.values(parsed as Record<string, Partial<WorkbenchKeepaliveTask>>).forEach((task) => {
if (
task &&
typeof task.taskId === "string" &&
typeof task.conversationId === "number" &&
typeof task.assistantMessageId === "string" &&
(task.mode === "image" || task.mode === "video")
) {
tasks[task.taskId] = {
taskId: task.taskId,
conversationId: task.conversationId,
assistantMessageId: task.assistantMessageId,
concurrencySlotId: typeof task.concurrencySlotId === "string" ? task.concurrencySlotId : undefined,
operation: task.operation === "video-super-resolution" ? "video-super-resolution" : "generation",
mode: task.mode,
modelLabel: task.modelLabel || task.mode,
specs: Array.isArray(task.specs) ? task.specs.filter((item): item is string => typeof item === "string") : [],
referenceCount: Number(task.referenceCount || 0),
progress: Number(task.progress || 0),
statusLabel: task.statusLabel || "Generating...",
startedAt: Number(task.startedAt || Date.now()),
};
}
});
return tasks;
} catch {
return {};
}
}
export function persistKeepaliveTasks(tasks: Record<string, WorkbenchKeepaliveTask>) {
try {
window.localStorage.setItem(userKey(TASK_KEEPALIVE_STORAGE_KEY), JSON.stringify(tasks));
} catch {
// Task restore is best-effort.
}
}