Files
makelore/electron/image-workspace/local-workspace.ts
brother7 03dae62cf3 feat: 支持 AI 设计项目多会话交互
需求:用户可在同一设计项目中新建和切换会话,任务列表保持项目级共享。

实现:
- 增加会话选择、新建入口及本地数据迁移
- Conversation 独立消息、Brief、Quote 与流式状态
- 保留项目任务并修复 Task revision 与 ABA 异步竞态
- 保持 Enter 发送、Shift+Enter 换行及 IME 保护
2026-08-06 18:22:15 +08:00

791 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createHash, randomUUID } from 'node:crypto';
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import type {
DesignCapabilities,
DesignConversation,
DesignConversationSummary,
DesignConfirmGenerationInput,
DesignCreateConversationInput,
DesignCreateWorkspaceInput,
DesignGenerationQuote,
DesignGenerationTask,
DesignMedium,
DesignRenameWorkspaceInput,
DesignSubmitMessageInput,
DesignWorkspace,
DesignWorkspaceBootstrap,
DesignWorkspaceSummary,
} from '../../shared/image-workspace';
import { designAssetContentPath } from '../../shared/image-workspace';
import { DesignWorkspaceModuleError, type DesignWorkspaceModule } from './module';
const LOCAL_WORKSPACE_SCHEMA_VERSION = 3;
const LOCAL_WORKSPACE_DIRECTORY = 'image-workspace-development';
const LOCAL_WORKSPACE_FILE = 'design-workspace-v3.json';
const LEGACY_LOCAL_WORKSPACE_FILE = 'design-workspace-v2.json';
const MAX_PROJECT_NAME_LENGTH = 80;
const MAX_MESSAGE_LENGTH = 4_000;
const LOCAL_CAPABILITIES: DesignCapabilities = {
conversation: true,
generation: true,
image: true,
video: true,
};
type PersistedWorkspace = DesignWorkspaceSummary & {
clientWorkspaceId: string;
};
type PersistedConversation = DesignConversation & {
clientConversationId: string;
requestHashes: Record<string, string>;
};
type PersistedAsset = {
workspaceId: string;
mimeType: string;
contentBase64: string;
};
type PersistedImageWorkspace = {
schemaVersion: typeof LOCAL_WORKSPACE_SCHEMA_VERSION;
workspaces: PersistedWorkspace[];
conversationsByWorkspaceId: Record<string, PersistedConversation[]>;
tasksByWorkspaceId: Record<string, DesignGenerationTask[]>;
assetsById: Record<string, PersistedAsset>;
};
type LegacyPersistedWorkspace = {
workspaceId: string;
clientWorkspaceId: string;
title: string;
turnRevision: number;
viewRevision: number;
phase: DesignConversation['phase'];
brief: DesignConversation['brief'];
messages: DesignConversation['messages'];
requestHashes: Record<string, string>;
updatedAt: string;
};
type LegacyPersistedImageWorkspace = {
schemaVersion: 2;
workspaces: LegacyPersistedWorkspace[];
tasksByWorkspaceId: Record<string, DesignGenerationTask[]>;
assetsById: Record<string, PersistedAsset>;
};
type LocalImageWorkspaceOptions = {
userDataDir: string;
now?: () => Date;
createId?: () => string;
};
export class LocalImageWorkspaceError extends DesignWorkspaceModuleError {
constructor(status: number, code: string, message: string) {
super(status, code, message);
this.name = 'LocalImageWorkspaceError';
}
}
function createEmptyState(): PersistedImageWorkspace {
return {
schemaVersion: LOCAL_WORKSPACE_SCHEMA_VERSION,
workspaces: [],
conversationsByWorkspaceId: {},
tasksByWorkspaceId: {},
assetsById: {},
};
}
function clone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isPersistedWorkspace(value: unknown): value is PersistedImageWorkspace {
return isRecord(value)
&& value.schemaVersion === LOCAL_WORKSPACE_SCHEMA_VERSION
&& Array.isArray(value.workspaces)
&& isRecord(value.conversationsByWorkspaceId)
&& isRecord(value.tasksByWorkspaceId)
&& isRecord(value.assetsById);
}
function isLegacyPersistedWorkspace(value: unknown): value is LegacyPersistedImageWorkspace {
return isRecord(value)
&& value.schemaVersion === 2
&& Array.isArray(value.workspaces)
&& isRecord(value.tasksByWorkspaceId)
&& isRecord(value.assetsById);
}
function escapeXml(value: string): string {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
}
function messageHash(value: unknown): string {
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
}
function detectMedium(message: string): DesignMedium {
return /视频|动画|动效|镜头|转场/.test(message) ? 'video' : 'image';
}
function placeholderSvg(title: string, summary: string): string {
return [
'<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720">',
'<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">',
'<stop stop-color="#eef2ff"/><stop offset="1" stop-color="#ffe4e6"/>',
'</linearGradient></defs>',
'<rect width="1280" height="720" rx="36" fill="url(#g)"/>',
'<circle cx="1080" cy="130" r="170" fill="#ffffff" opacity=".58"/>',
'<circle cx="170" cy="640" r="230" fill="#ffffff" opacity=".42"/>',
`<text x="86" y="285" font-family="Arial,sans-serif" font-size="32" fill="#6b7280">${escapeXml(title)}</text>`,
`<text x="86" y="370" font-family="Arial,sans-serif" font-size="54" font-weight="700" fill="#111827">${escapeXml(summary.slice(0, 28))}</text>`,
'<text x="86" y="450" font-family="Arial,sans-serif" font-size="24" fill="#6b7280">本地开发预览 · AI Design Workspace</text>',
'</svg>',
].join('');
}
function validateTitle(value: string): string {
const title = value.trim();
if (!title || title.length > MAX_PROJECT_NAME_LENGTH) {
throw new LocalImageWorkspaceError(
400,
'IMAGE_WORKSPACE_INVALID_PROJECT_NAME',
'项目名称需为 180 个字符',
);
}
return title;
}
function validateMessage(value: string): string {
const message = value.trim();
if (!message || message.length > MAX_MESSAGE_LENGTH) {
throw new LocalImageWorkspaceError(
400,
'IMAGE_WORKSPACE_INVALID_MESSAGE',
'设计描述需为 14000 个字符',
);
}
return message;
}
export function isLocalImageWorkspaceDevelopmentEnabled(options: {
isPackaged: boolean;
configuredMode?: string | null;
isDevelopmentServer?: boolean;
}): boolean {
if (options.isPackaged) return false;
const configuredMode = options.configuredMode?.trim().toLowerCase();
if (configuredMode === 'cloud') return false;
if (configuredMode === 'local') return true;
return options.isDevelopmentServer === true;
}
export function getLocalImageWorkspaceDirectory(userDataDir: string): string {
return join(userDataDir, LOCAL_WORKSPACE_DIRECTORY);
}
export class LocalImageWorkspace implements DesignWorkspaceModule {
private readonly rootDirectory: string;
private readonly stateFile: string;
private readonly legacyStateFile: string;
private readonly now: () => Date;
private readonly createId: () => string;
private state: PersistedImageWorkspace | null = null;
private operation: Promise<void> = Promise.resolve();
constructor(options: LocalImageWorkspaceOptions) {
const userDataDirectory = resolve(options.userDataDir);
this.rootDirectory = resolve(getLocalImageWorkspaceDirectory(userDataDirectory));
if (dirname(this.rootDirectory) !== userDataDirectory) {
throw new Error('Invalid local AI design workspace directory');
}
this.stateFile = join(this.rootDirectory, LOCAL_WORKSPACE_FILE);
this.legacyStateFile = join(this.rootDirectory, LEGACY_LOCAL_WORKSPACE_FILE);
this.now = options.now ?? (() => new Date());
this.createId = options.createId ?? randomUUID;
}
async bootstrap(): Promise<DesignWorkspaceBootstrap> {
const state = await this.load();
return this.bootstrapView(state);
}
getSnapshot(): Promise<DesignWorkspaceBootstrap> {
return this.bootstrap();
}
async getCapabilities(): Promise<DesignCapabilities> {
return clone(LOCAL_CAPABILITIES);
}
createWorkspace(input: DesignCreateWorkspaceInput): Promise<DesignWorkspace> {
return this.mutateWorkspace((state) => {
const existing = state.workspaces.find(
(workspace) => workspace.clientWorkspaceId === input.clientWorkspaceId,
);
if (existing) return existing;
const timestamp = this.now().toISOString();
const workspaceId = `local-workspace-${this.createId()}`;
const workspace: PersistedWorkspace = {
workspaceId,
clientWorkspaceId: input.clientWorkspaceId,
title: validateTitle(input.title),
viewRevision: 0,
conversationCount: 1,
phase: 'shaping',
updatedAt: timestamp,
};
const conversation = this.createEmptyConversation(
workspaceId,
`local-conversation-${this.createId()}`,
`local-client-conversation-${this.createId()}`,
'新会话',
timestamp,
);
state.workspaces.unshift(workspace);
state.conversationsByWorkspaceId[workspaceId] = [conversation];
state.tasksByWorkspaceId[workspaceId] = [];
return workspace;
});
}
createConversation(input: DesignCreateConversationInput): Promise<DesignConversation> {
return this.mutateConversation((state) => {
const workspace = this.requireWorkspace(state, input.workspaceId);
const conversations = state.conversationsByWorkspaceId[input.workspaceId] ?? [];
const existing = conversations.find(
(conversation) => conversation.clientConversationId === input.clientConversationId,
);
if (existing) return existing;
const timestamp = this.now().toISOString();
const conversation = this.createEmptyConversation(
input.workspaceId,
`local-conversation-${this.createId()}`,
input.clientConversationId,
input.title,
timestamp,
);
conversations.unshift(conversation);
state.conversationsByWorkspaceId[input.workspaceId] = conversations;
workspace.conversationCount = conversations.length;
workspace.viewRevision += 1;
workspace.updatedAt = timestamp;
return conversation;
});
}
async getConversation(
workspaceId: string,
conversationId: string,
): Promise<DesignConversation> {
const state = await this.load();
return clone(this.requireConversation(state, workspaceId, conversationId));
}
renameWorkspace(input: DesignRenameWorkspaceInput): Promise<DesignWorkspace> {
return this.mutateWorkspace((state) => {
const workspace = this.requireWorkspace(state, input.workspaceId);
workspace.title = validateTitle(input.title);
workspace.viewRevision += 1;
workspace.updatedAt = this.now().toISOString();
return workspace;
});
}
async getWorkspace(workspaceId: string): Promise<DesignWorkspace> {
const state = await this.load();
return this.workspaceView(state, this.requireWorkspace(state, workspaceId));
}
submitMessage(input: DesignSubmitMessageInput): Promise<DesignConversation> {
return this.mutateConversation((state) => {
const workspace = this.requireWorkspace(state, input.workspaceId);
const conversation = this.requireConversation(
state,
input.workspaceId,
input.conversationId,
);
const message = validateMessage(input.message);
const hash = messageHash(input);
if (this.isRequestReplay(conversation, input.clientTurnId, hash)) return conversation;
this.assertRevision(conversation, input.expectedTurnRevision);
this.supersedeQuotes(conversation);
const timestamp = this.now().toISOString();
const nextRevision = conversation.turnRevision + 1;
const medium = detectMedium(message);
const quote: DesignGenerationQuote = {
quoteId: `local-quote-${this.createId()}`,
status: 'active',
medium,
briefVersion: nextRevision,
briefSummary: message,
quotedDesignPoints: medium === 'video' ? 2 : 1,
expiresAt: new Date(this.now().getTime() + 15 * 60 * 1000).toISOString(),
};
conversation.messages.push(
{
id: `local-message-${this.createId()}`,
role: 'user',
kind: 'user',
text: message,
quickReplies: [],
generationQuote: null,
turnRevision: nextRevision,
createdAt: timestamp,
},
{
id: `local-message-${this.createId()}`,
role: 'assistant',
kind: 'confirmation',
text: medium === 'video'
? '方向已整理为视频方案。确认后会创建视频生成任务,你也可以继续补充镜头、节奏或动效。'
: '方向已整理为图像方案。确认后会创建生图任务,你也可以继续调整构图、色彩或风格。',
quickReplies: ['确认生成', '继续调整'],
generationQuote: quote,
turnRevision: nextRevision,
createdAt: timestamp,
},
);
conversation.turnRevision = nextRevision;
conversation.phase = 'awaiting_confirmation';
conversation.brief = {
version: nextRevision,
status: 'ready',
medium,
summary: message,
ready: true,
missingDecision: null,
};
conversation.updatedAt = timestamp;
conversation.requestHashes[input.clientTurnId] = hash;
workspace.phase = conversation.phase;
workspace.viewRevision += 1;
workspace.updatedAt = timestamp;
return conversation;
});
}
confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignConversation> {
return this.mutateConversation((state) => {
const workspace = this.requireWorkspace(state, input.workspaceId);
const conversation = this.requireConversation(
state,
input.workspaceId,
input.conversationId,
);
const hash = messageHash(input);
if (this.isRequestReplay(conversation, input.clientTurnId, hash)) return conversation;
this.assertRevision(conversation, input.expectedTurnRevision);
const quote = conversation.messages
.map((message) => message.generationQuote)
.find((candidate) => candidate?.quoteId === input.quoteId);
if (!quote || quote.status !== 'active') {
throw new LocalImageWorkspaceError(409, 'generation_quote_invalid', '当前生成报价已失效');
}
if (new Date(quote.expiresAt).getTime() <= this.now().getTime()) {
quote.status = 'expired';
throw new LocalImageWorkspaceError(409, 'generation_quote_expired', '当前生成报价已过期');
}
quote.status = 'consumed';
const timestamp = this.now().toISOString();
const nextRevision = conversation.turnRevision + 1;
conversation.messages.push(
{
id: `local-message-${this.createId()}`,
role: 'user',
kind: 'user',
text: '确认生成',
quickReplies: [],
generationQuote: null,
turnRevision: nextRevision,
createdAt: timestamp,
},
{
id: `local-message-${this.createId()}`,
role: 'assistant',
kind: 'confirmation',
text: `${quote.medium === 'video' ? '视频' : '图片'}任务已创建,可在右侧任务列表查看进度。`,
quickReplies: [],
generationQuote: null,
turnRevision: nextRevision,
createdAt: timestamp,
},
);
conversation.turnRevision = nextRevision;
conversation.phase = 'shaping';
conversation.brief = { ...conversation.brief, status: 'confirmed' };
conversation.updatedAt = timestamp;
conversation.requestHashes[input.clientTurnId] = hash;
workspace.phase = conversation.phase;
workspace.viewRevision += 1;
workspace.updatedAt = timestamp;
state.tasksByWorkspaceId[workspace.workspaceId] ??= [];
state.tasksByWorkspaceId[workspace.workspaceId].unshift({
taskId: `local-task-${this.createId()}`,
workspaceId: workspace.workspaceId,
conversationId: conversation.conversationId,
medium: quote.medium,
status: 'queued',
briefVersion: quote.briefVersion,
briefSummary: quote.briefSummary,
quoteId: quote.quoteId,
quotedDesignPoints: quote.quotedDesignPoints,
failureCode: null,
resultAssets: [],
createdAt: timestamp,
updatedAt: timestamp,
});
return conversation;
});
}
private createEmptyConversation(
workspaceId: string,
conversationId: string,
clientConversationId: string,
title: string,
timestamp: string,
): PersistedConversation {
return {
conversationId,
clientConversationId,
workspaceId,
title: validateTitle(title),
turnRevision: 0,
phase: 'shaping',
brief: {
version: 0,
status: 'draft',
medium: null,
summary: '正在建立作品的视觉方向',
ready: false,
missingDecision: '作品形式',
},
messages: [],
requestHashes: {},
createdAt: timestamp,
updatedAt: timestamp,
};
}
listTasks(workspaceId: string): Promise<DesignGenerationTask[]> {
return this.enqueue(async () => {
const state = await this.load();
const workspace = this.requireWorkspace(state, workspaceId);
const tasks = state.tasksByWorkspaceId[workspaceId] ?? [];
const visible = clone(tasks);
let changed = false;
for (const task of tasks) {
if (task.status === 'queued') {
task.status = 'running';
task.updatedAt = this.now().toISOString();
workspace.viewRevision += 1;
workspace.updatedAt = task.updatedAt;
changed = true;
continue;
}
if (task.status !== 'running') continue;
const assetId = `local-asset-${this.createId()}`;
const svg = placeholderSvg(workspace.title, task.briefSummary);
const asset = {
assetId,
workspaceId,
mediaType: task.medium,
mimeType: 'image/svg+xml',
width: 1280,
height: 720,
durationMilliseconds: task.medium === 'video' ? 5_000 : null,
createdAt: this.now().toISOString(),
contentPath: designAssetContentPath(workspaceId, assetId),
};
state.assetsById[assetId] = {
workspaceId,
mimeType: asset.mimeType,
contentBase64: Buffer.from(svg).toString('base64'),
};
task.status = 'succeeded';
task.resultAssets = [asset];
task.updatedAt = asset.createdAt;
workspace.viewRevision += 1;
workspace.updatedAt = asset.createdAt;
changed = true;
}
if (changed) await this.persist(state);
return visible;
});
}
async openAssetContent(
workspaceId: string,
assetId: string,
_range?: string,
): Promise<Response> {
const state = await this.load();
this.requireWorkspace(state, workspaceId);
const asset = state.assetsById[assetId];
if (!asset || asset.workspaceId !== workspaceId) {
throw new LocalImageWorkspaceError(404, 'asset_not_found', '设计资产不存在');
}
const body = Buffer.from(asset.contentBase64, 'base64');
return new Response(body, {
status: 200,
headers: {
'Content-Type': asset.mimeType,
'Content-Length': String(body.byteLength),
'Cache-Control': 'private, max-age=60',
},
});
}
reset(): Promise<DesignWorkspaceBootstrap> {
return this.enqueue(async () => {
await rm(this.rootDirectory, { recursive: true, force: true });
this.state = createEmptyState();
return this.bootstrapView(this.state);
});
}
private mutateWorkspace(
operation: (state: PersistedImageWorkspace) => PersistedWorkspace,
): Promise<DesignWorkspace> {
return this.enqueue(async () => {
const state = await this.load();
const workspace = operation(state);
await this.persist(state);
return this.workspaceView(state, workspace);
});
}
private mutateConversation(
operation: (state: PersistedImageWorkspace) => PersistedConversation,
): Promise<DesignConversation> {
return this.enqueue(async () => {
const state = await this.load();
const conversation = operation(state);
await this.persist(state);
return this.conversationView(conversation);
});
}
private enqueue<T>(operation: () => Promise<T>): Promise<T> {
const result = this.operation.then(operation, operation);
this.operation = result.then(() => undefined, () => undefined);
return result;
}
private async load(): Promise<PersistedImageWorkspace> {
if (this.state) return this.state;
try {
const parsed = JSON.parse(await readFile(this.stateFile, 'utf8')) as unknown;
if (!isPersistedWorkspace(parsed)) {
throw new LocalImageWorkspaceError(
500,
'IMAGE_WORKSPACE_LOCAL_DATA_INVALID',
'AI 设计本地数据暂时无法读取',
);
}
this.state = parsed;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
this.state = await this.loadLegacyState();
} else {
throw error;
}
}
return this.state;
}
private async loadLegacyState(): Promise<PersistedImageWorkspace> {
try {
const parsed = JSON.parse(await readFile(this.legacyStateFile, 'utf8')) as unknown;
if (!isLegacyPersistedWorkspace(parsed)) {
throw new LocalImageWorkspaceError(
500,
'IMAGE_WORKSPACE_LOCAL_DATA_INVALID',
'AI 设计本地数据暂时无法读取',
);
}
const migrated = this.migrateLegacyState(parsed);
await this.persist(migrated);
return migrated;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return createEmptyState();
throw error;
}
}
private migrateLegacyState(legacy: LegacyPersistedImageWorkspace): PersistedImageWorkspace {
const migrated = createEmptyState();
migrated.assetsById = clone(legacy.assetsById);
migrated.tasksByWorkspaceId = clone(legacy.tasksByWorkspaceId);
for (const legacyWorkspace of legacy.workspaces) {
const conversationId = `local-conversation-${this.createId()}`;
const createdAt = legacyWorkspace.messages[0]?.createdAt ?? legacyWorkspace.updatedAt;
const conversation: PersistedConversation = {
conversationId,
clientConversationId: `migrated-${legacyWorkspace.workspaceId}`,
workspaceId: legacyWorkspace.workspaceId,
title: '历史会话',
turnRevision: legacyWorkspace.turnRevision,
phase: legacyWorkspace.phase,
brief: clone(legacyWorkspace.brief),
messages: clone(legacyWorkspace.messages),
requestHashes: clone(legacyWorkspace.requestHashes),
createdAt,
updatedAt: legacyWorkspace.updatedAt,
};
migrated.workspaces.push({
workspaceId: legacyWorkspace.workspaceId,
clientWorkspaceId: legacyWorkspace.clientWorkspaceId,
title: legacyWorkspace.title,
viewRevision: legacyWorkspace.viewRevision,
conversationCount: 1,
phase: legacyWorkspace.phase,
updatedAt: legacyWorkspace.updatedAt,
});
migrated.conversationsByWorkspaceId[legacyWorkspace.workspaceId] = [conversation];
migrated.tasksByWorkspaceId[legacyWorkspace.workspaceId] = (
migrated.tasksByWorkspaceId[legacyWorkspace.workspaceId] ?? []
).map((task) => ({ ...task, conversationId }));
}
return migrated;
}
private async persist(state: PersistedImageWorkspace): Promise<void> {
await mkdir(this.rootDirectory, { recursive: true });
const temporaryFile = `${this.stateFile}.${process.pid}.tmp`;
await writeFile(temporaryFile, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
try {
await rename(temporaryFile, this.stateFile);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code !== 'EEXIST' && code !== 'EPERM') throw error;
await rm(this.stateFile, { force: true });
await rename(temporaryFile, this.stateFile);
}
this.state = state;
}
private requireWorkspace(
state: PersistedImageWorkspace,
workspaceId: string,
): PersistedWorkspace {
const workspace = state.workspaces.find((item) => item.workspaceId === workspaceId);
if (!workspace) {
throw new LocalImageWorkspaceError(404, 'workspace_not_found', '设计项目不存在');
}
return workspace;
}
private requireConversation(
state: PersistedImageWorkspace,
workspaceId: string,
conversationId: string,
): PersistedConversation {
this.requireWorkspace(state, workspaceId);
const conversation = (state.conversationsByWorkspaceId[workspaceId] ?? [])
.find((item) => item.conversationId === conversationId);
if (!conversation) {
throw new LocalImageWorkspaceError(404, 'conversation_not_found', '设计会话不存在');
}
return conversation;
}
private assertRevision(
conversation: PersistedConversation,
expectedRevision: number,
): void {
if (conversation.turnRevision !== expectedRevision) {
throw new LocalImageWorkspaceError(
409,
'conversation_revision_conflict',
'设计会话已在其他位置更新,请刷新后重试',
);
}
}
private isRequestReplay(
conversation: PersistedConversation,
clientTurnId: string,
hash: string,
): boolean {
const existing = conversation.requestHashes[clientTurnId];
if (!existing) return false;
if (existing !== hash) {
throw new LocalImageWorkspaceError(
409,
'idempotency_conflict',
'这次操作与已经提交的请求冲突',
);
}
return true;
}
private supersedeQuotes(conversation: PersistedConversation): void {
for (const message of conversation.messages) {
if (message.generationQuote?.status === 'active') {
message.generationQuote.status = 'superseded';
}
}
}
private bootstrapView(state: PersistedImageWorkspace): DesignWorkspaceBootstrap {
return {
capabilities: clone(LOCAL_CAPABILITIES),
workspaces: state.workspaces.map(({ clientWorkspaceId: _clientId, ...summary }) => (
clone(summary)
)),
};
}
private workspaceView(
state: PersistedImageWorkspace,
workspace: PersistedWorkspace,
): DesignWorkspace {
const { clientWorkspaceId: _clientId, ...summary } = workspace;
return clone({
...summary,
conversationCount: state.conversationsByWorkspaceId[workspace.workspaceId]?.length ?? 0,
conversations: (state.conversationsByWorkspaceId[workspace.workspaceId] ?? [])
.map((conversation) => this.conversationSummaryView(conversation)),
});
}
private conversationSummaryView(
conversation: PersistedConversation,
): DesignConversationSummary {
const {
clientConversationId: _clientId,
requestHashes: _requestHashes,
messages: _messages,
...summary
} = conversation;
return clone(summary);
}
private conversationView(conversation: PersistedConversation): DesignConversation {
const {
clientConversationId: _clientId,
requestHashes: _requestHashes,
...view
} = conversation;
return clone(view);
}
}