feat: 完善图像工作区与创作工具体验
This commit is contained in:
@@ -57,7 +57,10 @@ export const aiModules: readonly AiModuleDefinition[] = [
|
||||
];
|
||||
|
||||
export function getAiModuleForPath(pathname: string): AiModuleId {
|
||||
if (pathname === '/image-canvas' || pathname.startsWith('/image-canvas/')) {
|
||||
if (pathname === '/image-canvas'
|
||||
|| pathname.startsWith('/image-canvas/')
|
||||
|| pathname === '/image-prompts'
|
||||
|| pathname.startsWith('/image-prompts/')) {
|
||||
return 'painting';
|
||||
}
|
||||
if (pathname === '/ai-hardware' || pathname.startsWith('/ai-hardware/')) {
|
||||
|
||||
94
src/lib/image-prompt-museum.ts
Normal file
94
src/lib/image-prompt-museum.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { AppError } from '@/lib/error-model';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import {
|
||||
IMAGE_PROMPT_MUSEUM_API_PATH,
|
||||
type PromptMuseumEntry,
|
||||
type PromptMuseumListQuery,
|
||||
type PromptMuseumPage,
|
||||
} from '../../shared/image-prompt-museum';
|
||||
|
||||
type PromptMuseumEnvelope<T> = {
|
||||
success?: boolean;
|
||||
status?: number;
|
||||
code?: string;
|
||||
error?: string;
|
||||
data?: T;
|
||||
};
|
||||
|
||||
export class PromptMuseumApiError extends Error {
|
||||
readonly status: number;
|
||||
readonly code: string;
|
||||
|
||||
constructor(status: number, code: string, message: string) {
|
||||
super(message);
|
||||
this.name = 'PromptMuseumApiError';
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function errorStatus(error: unknown): number {
|
||||
if (error instanceof AppError && typeof error.details?.status === 'number') {
|
||||
return error.details.status;
|
||||
}
|
||||
return 502;
|
||||
}
|
||||
|
||||
function errorCode(error: unknown, status: number): string {
|
||||
if (error instanceof AppError && typeof error.details?.backendCode === 'string') {
|
||||
return error.details.backendCode;
|
||||
}
|
||||
if (status === 401) return 'PROMPT_MUSEUM_AUTH_REQUIRED';
|
||||
if (status === 404) return 'PROMPT_MUSEUM_NOT_FOUND';
|
||||
return 'PROMPT_MUSEUM_REQUEST_FAILED';
|
||||
}
|
||||
|
||||
function createQuery(query: PromptMuseumListQuery): string {
|
||||
const params = new URLSearchParams();
|
||||
const values: Array<[string, string | undefined]> = [
|
||||
['q', query.q],
|
||||
['use_case', query.useCase],
|
||||
['style', query.style],
|
||||
['subject', query.subject],
|
||||
['language', query.language],
|
||||
['model', query.model],
|
||||
['cursor', query.cursor],
|
||||
['limit', query.limit === undefined ? undefined : String(query.limit)],
|
||||
];
|
||||
for (const [key, value] of values) {
|
||||
if (value?.trim()) params.set(key, value.trim());
|
||||
}
|
||||
const encoded = params.toString();
|
||||
return encoded ? `?${encoded}` : '';
|
||||
}
|
||||
|
||||
async function requestData<T>(path: string): Promise<T> {
|
||||
let response: PromptMuseumEnvelope<T>;
|
||||
try {
|
||||
response = await hostApiFetch<PromptMuseumEnvelope<T>>(path);
|
||||
} catch (error) {
|
||||
const status = errorStatus(error);
|
||||
throw new PromptMuseumApiError(
|
||||
status,
|
||||
errorCode(error, status),
|
||||
error instanceof Error ? error.message : '提示词博物馆请求失败',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.success || response.data === undefined) {
|
||||
throw new PromptMuseumApiError(
|
||||
response.status ?? 502,
|
||||
response.code ?? 'PROMPT_MUSEUM_REQUEST_FAILED',
|
||||
response.error ?? '提示词博物馆请求失败',
|
||||
);
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function fetchPromptMuseumPage(query: PromptMuseumListQuery = {}): Promise<PromptMuseumPage> {
|
||||
return await requestData(`${IMAGE_PROMPT_MUSEUM_API_PATH}${createQuery(query)}`);
|
||||
}
|
||||
|
||||
export async function fetchPromptMuseumEntry(entryId: string): Promise<PromptMuseumEntry> {
|
||||
return await requestData(`${IMAGE_PROMPT_MUSEUM_API_PATH}/${encodeURIComponent(entryId)}`);
|
||||
}
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
type DesignAsset,
|
||||
type DesignAssetSaveResult,
|
||||
type DesignConversation,
|
||||
type DesignDeleteWorkspaceResult,
|
||||
type DesignGenerationParameters,
|
||||
type DesignGenerationQuote,
|
||||
type DesignGenerationTask,
|
||||
type DesignWorkspace,
|
||||
type DesignWorkspaceBootstrap,
|
||||
@@ -122,6 +125,15 @@ export function renameImageWorkspaceProject(
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteImageWorkspaceProject(
|
||||
workspaceId: string,
|
||||
): Promise<DesignDeleteWorkspaceResult> {
|
||||
return requestData(
|
||||
`${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}`,
|
||||
{ method: 'DELETE' },
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchImageWorkspaceProject(workspaceId: string): Promise<DesignWorkspace> {
|
||||
return requestData(
|
||||
`${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}`,
|
||||
@@ -182,6 +194,8 @@ export function confirmImageWorkspaceGeneration(
|
||||
expectedTurnRevision: number,
|
||||
quoteId: string,
|
||||
clientTurnId = createImageWorkspaceTurnId(),
|
||||
finalPrompt?: string,
|
||||
generationParameters?: DesignGenerationParameters,
|
||||
): Promise<DesignConversation> {
|
||||
return requestData(
|
||||
`${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/conversations/${encodeURIComponent(conversationId)}/quotes/${encodeURIComponent(quoteId)}/confirm`,
|
||||
@@ -190,6 +204,29 @@ export function confirmImageWorkspaceGeneration(
|
||||
body: JSON.stringify({
|
||||
clientTurnId,
|
||||
expectedTurnRevision,
|
||||
...(finalPrompt === undefined ? {} : { finalPrompt }),
|
||||
...(generationParameters === undefined ? {} : { generationParameters }),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function updateImageWorkspaceGenerationQuote(
|
||||
workspaceId: string,
|
||||
quoteId: string,
|
||||
finalPrompt: string,
|
||||
generationParameters: DesignGenerationParameters,
|
||||
): Promise<DesignGenerationQuote> {
|
||||
return requestData(
|
||||
`${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/generation-quotes/${encodeURIComponent(quoteId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
finalPrompt,
|
||||
model: generationParameters.model,
|
||||
resolution: generationParameters.resolution,
|
||||
aspectRatio: generationParameters.aspectRatio,
|
||||
durationSeconds: generationParameters.durationSeconds,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -219,12 +256,15 @@ export async function resolveImageWorkspaceAssetUrl(contentPath: string): Promis
|
||||
return `${getHostApiBase()}${contentPath}${separator}token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
const IMAGE_FILE_EXTENSIONS: Record<string, string> = {
|
||||
const ASSET_FILE_EXTENSIONS: Record<string, string> = {
|
||||
'image/gif': 'gif',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/svg+xml': 'svg',
|
||||
'image/webp': 'webp',
|
||||
'video/mp4': 'mp4',
|
||||
'video/quicktime': 'mov',
|
||||
'video/webm': 'webm',
|
||||
};
|
||||
|
||||
function assetDownloadFileName(asset: DesignAsset): string {
|
||||
@@ -232,7 +272,8 @@ function assetDownloadFileName(asset: DesignAsset): string {
|
||||
.replace(/[^a-zA-Z0-9_-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 32) || 'image';
|
||||
const extension = IMAGE_FILE_EXTENSIONS[asset.mimeType.toLowerCase()] ?? 'png';
|
||||
const extension = ASSET_FILE_EXTENSIONS[asset.mimeType.toLowerCase()]
|
||||
?? (asset.mediaType === 'video' ? 'mp4' : 'png');
|
||||
return `Makelore-AI-Design-${safeAssetId}.${extension}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ const SKILL_DISPLAY_BY_ID: Record<string, SkillDisplayInfo> = {
|
||||
name: '项目演示',
|
||||
description: '把项目内容整理成可播放的 16:9 HTML 幻灯片,不生成 PPTX 或云端发布。',
|
||||
},
|
||||
'game-engine': {
|
||||
name: '游戏引擎',
|
||||
description: '构建基于 HTML5、Canvas、WebGL 和 JavaScript 的 2D/3D 游戏,处理循环、物理、碰撞、输入、音频与本地预览。',
|
||||
},
|
||||
grilling: {
|
||||
name: '方案质询',
|
||||
description: '在重要工作开始前逐项澄清目标、范围和关键取舍,确认后再执行。',
|
||||
|
||||
Reference in New Issue
Block a user