Files
Genarrative/apps/ai-game-creator-shell/src/features/app-shell/model.ts
T
menghao f389716149 融合最新主线并同步项目导入能力
合并 master 的现有 Godot 项目导入与客户端 HTTP 调整。

保留无限画布和非破坏性资源编辑分支行为。

# Conflicts:
#	apps/ai-game-creator-shell/src-tauri/Cargo.lock
#	docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md
#	docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md
2026-08-11 14:39:47 +08:00

316 lines
8.7 KiB
TypeScript

import { FileText, Gamepad2, Image } from 'lucide-react';
import type { ComponentType } from 'react';
import type { AuthUser } from '../../../../../packages/shared/src/contracts/auth';
import type {
GameCreationAppManifest,
GameCreationAppPreviewState,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import type { ChatMessage, LocalProjectDirectoryStatus } from '../../app/types';
import type {
HomeAgentMode,
HomeAgentModeItem,
HomeAttachmentDraft,
} from '../../view/home';
import type { LauncherView } from '../../view/layout';
import type {
ProjectAgentResultSummary,
ProjectAgentRuntimeSummary,
} from '../../view/project-development';
import type { ProjectManifestSnapshotMetadata } from '../../view/project-development/projectResourceLiveUpdateModel';
import { isAbsoluteProjectPath } from '../project-summary/projectSummary';
const RECENT_WORKSPACES_STORAGE_KEY =
'genarrative-ai-game-creator.recent-workspaces.v1';
const SUPERVISOR_CHAT_DRAFT_STORAGE_PREFIX =
'genarrative.supervisor-chat.draft';
export type WorkspaceLauncherProps = {
currentUser: AuthUser;
onLogout: () => void;
initialView?: LauncherView;
};
export type ProjectSupervisorComponentProps = {
initialProjectPath?: string;
initialProjectManifest?: GameCreationAppManifest;
initialProjectKind?: 'web' | 'godot';
projectSupervisorOnly?: boolean;
onManifestChange?: (
projectPath: string,
manifest: GameCreationAppManifest,
metadata?: ProjectManifestSnapshotMetadata,
) => void;
onPreviewChange?: (preview: GameCreationAppPreviewState | null) => void;
onAgentRuntimeSummariesChange?: (
summaries: ProjectAgentRuntimeSummary[],
) => void;
onAgentResultsChange?: (results: ProjectAgentResultSummary[]) => void;
};
export type WorkspaceLauncherShellProps = WorkspaceLauncherProps & {
ProjectSupervisor: ComponentType<ProjectSupervisorComponentProps>;
};
export type RecentProjectRow = {
path: string;
name: string;
status: string;
canReveal: boolean;
canOpen: boolean;
};
export type LauncherNotice = {
title: string;
message: string;
};
function normalizeRecentWorkspaceList(values: unknown[]) {
const recent: string[] = [];
for (const value of values) {
if (typeof value !== 'string') {
continue;
}
const workspace = value.trim();
if (
!workspace ||
!isAbsoluteProjectPath(workspace) ||
recent.includes(workspace)
) {
continue;
}
recent.push(workspace);
if (recent.length >= 8) {
break;
}
}
return recent;
}
export function readRecentWorkspaces() {
try {
const raw = window.localStorage.getItem(RECENT_WORKSPACES_STORAGE_KEY);
const parsed: unknown = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? normalizeRecentWorkspaceList(parsed) : [];
} catch {
return [];
}
}
export function writeRecentWorkspace(path: string) {
const recent = normalizeRecentWorkspaceList([
path,
...readRecentWorkspaces(),
]);
try {
window.localStorage.setItem(
RECENT_WORKSPACES_STORAGE_KEY,
JSON.stringify(recent),
);
} catch {
// WebView storage can be unavailable in restricted test shells.
}
return recent;
}
export function removeRecentWorkspace(path: string) {
const trimmedPath = path.trim();
const recent = readRecentWorkspaces().filter(
(workspace) => workspace !== trimmedPath,
);
try {
if (recent.length > 0) {
window.localStorage.setItem(
RECENT_WORKSPACES_STORAGE_KEY,
JSON.stringify(recent),
);
} else {
window.localStorage.removeItem(RECENT_WORKSPACES_STORAGE_KEY);
}
} catch {
// WebView storage can be unavailable in restricted test shells.
}
return recent;
}
export function clearRecentWorkspaces() {
try {
window.localStorage.removeItem(RECENT_WORKSPACES_STORAGE_KEY);
} catch {
// WebView storage can be unavailable in restricted test shells.
}
return [];
}
export function isTransientProjectOpenMessage(
message: ChatMessage,
projectPath: string,
) {
return (
message.role === 'assistant' &&
message.text === `已设置本地项目:${projectPath}`
);
}
export function latestVisibleItems<T>(items: T[], visibleCount: number) {
if (items.length <= visibleCount) {
return items;
}
return items.slice(items.length - visibleCount);
}
export function isDeveloperMode() {
if (!import.meta.env.DEV) {
return false;
}
const params = new URLSearchParams(window.location.search);
return params.has('dev') || window.location.hash === '#dev';
}
export function readInitialProjectPath() {
const params = new URLSearchParams(window.location.search);
return params.get('projectPath') ?? '';
}
function supervisorChatDraftStorageKey(projectPath: string) {
return `${SUPERVISOR_CHAT_DRAFT_STORAGE_PREFIX}:${projectPath}`;
}
export function readSupervisorChatDraft(projectPath: string) {
const normalizedProjectPath = projectPath.trim();
if (!normalizedProjectPath) {
return '';
}
try {
return (
window.sessionStorage.getItem(
supervisorChatDraftStorageKey(normalizedProjectPath),
) ?? ''
);
} catch {
return '';
}
}
export function persistSupervisorChatDraft(projectPath: string, draft: string) {
const normalizedProjectPath = projectPath.trim();
if (!normalizedProjectPath) {
return;
}
try {
const storageKey = supervisorChatDraftStorageKey(normalizedProjectPath);
if (draft) {
window.sessionStorage.setItem(storageKey, draft);
} else {
window.sessionStorage.removeItem(storageKey);
}
} catch {
// A disabled session store must not block the development chat surface.
}
}
export const homeAgentModeItems: HomeAgentModeItem[] = [
{
mode: 'game',
label: '做游戏',
placeholder: '今天想把什么灵感做成游戏',
emptyPrompt: '请输入游戏灵感或上传参考素材',
icon: Gamepad2,
},
{
mode: 'art',
label: '做素材',
placeholder: '今天想做什么样的美术素材',
emptyPrompt: '请输入素材需求或上传参考图',
icon: Image,
},
{
mode: 'doc',
label: '做方案',
placeholder: '今天有什么设计需要帮你整理',
emptyPrompt: '请输入方案需求或上传资料',
icon: FileText,
},
];
const homeAgentModeLabels: Record<HomeAgentMode, string> = {
game: '做游戏',
art: '做素材',
doc: '做方案',
};
export function buildHomeConversationContent(
mode: HomeAgentMode,
prompt: string,
attachments: HomeAttachmentDraft[],
) {
const lines = [
`初始意图:${mode} / ${homeAgentModeLabels[mode]}`,
prompt.trim() || '用户上传了参考附件,等待后续补充需求。',
];
if (attachments.length > 0) {
lines.push('');
lines.push(
`附件:${attachments.map((attachment) => attachment.file.name).join('、')}`,
);
}
return lines.join('\n');
}
export function buildRecentProjectRows(
recentWorkspaces: string[],
recentWorkspaceStatuses: Record<
string,
LocalProjectDirectoryStatus | null | undefined
>,
recentWorkspaceRefreshing: boolean,
): RecentProjectRow[] {
return recentWorkspaces.map((workspace) => {
const directoryStatus = recentWorkspaceStatuses[workspace];
const isPendingStatus = directoryStatus === undefined;
const projectName =
directoryStatus?.projectName ||
workspace.split(/[\\/]/).filter(Boolean).pop() ||
workspace;
const status = recentWorkspaceRefreshing
? '检查中'
: isPendingStatus
? '检查中'
: directoryStatus === null
? '检查失败'
: directoryStatus?.exists === false
? '未找到'
: directoryStatus?.isDirectory === false
? '不是文件夹'
: directoryStatus?.manifestError
? '无法读取'
: directoryStatus?.isGameCreatorProject === false
? '未初始化'
: directoryStatus?.recentRunStatus
? `run: ${directoryStatus.recentRunStatus}${
directoryStatus.recentRunStopReason
? ` · ${directoryStatus.recentRunStopReason}`
: ''
}`
: '本地项目';
const canReveal =
!recentWorkspaceRefreshing &&
Boolean(directoryStatus) &&
directoryStatus?.exists !== false &&
directoryStatus?.isDirectory !== false;
return {
path: workspace,
name: projectName,
status,
canReveal,
canOpen:
!recentWorkspaceRefreshing &&
Boolean(directoryStatus) &&
directoryStatus?.exists !== false &&
directoryStatus?.isDirectory !== false &&
!directoryStatus?.manifestError &&
directoryStatus?.isGameCreatorProject !== false,
};
});
}