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
316 lines
8.7 KiB
TypeScript
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,
|
|
};
|
|
});
|
|
}
|