d5e05d79fe
## 优化项目名称显示与命名链路 关联:#245 按 Issue 中的需求文档实现: - 新增项目名称修改入口,保存到本地项目 \.agent/manifest.json\ - 项目页、当前项目上下文、窗口标题在重命名后同步刷新 - 首页自动创建工作区时,根据用户输入用 LLM 提炼项目名 - LLM 命名失败或返回非法内容时,回退现有默认名,不阻断创作流程 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/248 Co-authored-by: suzmii <suzmii@qq.com> Co-committed-by: suzmii <suzmii@qq.com>
433 lines
15 KiB
TypeScript
433 lines
15 KiB
TypeScript
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
import { launcherNotifications } from '../../app/constants';
|
|
import { closeDialogOnEscape } from '../../app/dialogs';
|
|
import { resolveTauriInvoke } from '../../app/tauri';
|
|
import type { LocalGameProjectRevisionStatus } from '../../app/types';
|
|
import {
|
|
useWindowChrome,
|
|
WINDOW_CHROME_DEFAULT_TITLE,
|
|
} from '../../components/windowChromeContext';
|
|
import HomeView from '../../view/home';
|
|
import { type LauncherView, Sidebar } from '../../view/layout';
|
|
import ProjectDevelopmentView from '../../view/project-development';
|
|
import {
|
|
createProjectManifestMergeState,
|
|
mergeProjectManifestSnapshot,
|
|
type ProjectManifestMergeState,
|
|
type ProjectManifestSnapshot,
|
|
type ProjectManifestSnapshotMetadata,
|
|
} from '../../view/project-development/projectResourceLiveUpdateModel';
|
|
import { RuntimeConfigDialog } from '../runtime-config/RuntimeConfigDialog';
|
|
import { AccountWalletBar, AccountWalletDialogs } from './AccountWallet';
|
|
import {
|
|
DeveloperAgentDialogs,
|
|
DeveloperAgentPanel,
|
|
} from './DeveloperAgentPanel';
|
|
import type { WorkspaceLauncherShellProps } from './model';
|
|
import { NonEmptyProjectDialog, ProjectsPage } from './ProjectCreation';
|
|
import { useAccountWallet } from './useAccountWallet';
|
|
import { useDeveloperAgentPanel } from './useDeveloperAgentPanel';
|
|
import { useHomeProjectCreation } from './useHomeProjectCreation';
|
|
import { useRecentProjects } from './useRecentProjects';
|
|
|
|
export function WorkspaceLauncherShell({
|
|
currentUser,
|
|
onLogout,
|
|
initialView = 'home',
|
|
ProjectSupervisor,
|
|
}: WorkspaceLauncherShellProps) {
|
|
const {
|
|
isWindowChrome,
|
|
setTitle: setWindowTitle,
|
|
walletSlot,
|
|
} = useWindowChrome();
|
|
const accountWallet = useAccountWallet(currentUser.id);
|
|
const [status, setStatus] = useState('');
|
|
const recentProjects = useRecentProjects(setStatus);
|
|
const { recentProjectRows, rememberRecentWorkspace } = recentProjects;
|
|
const [launcherView, setLauncherView] = useState<LauncherView>(initialView);
|
|
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
|
|
const [playRequest, setPlayRequest] = useState<{
|
|
projectPath: string;
|
|
requestId: number;
|
|
} | null>(null);
|
|
const playRequestIdRef = useRef(0);
|
|
const [launcherNotice, setLauncherNotice] = useState<{
|
|
title: string;
|
|
message: string;
|
|
} | null>(null);
|
|
const developerAgent = useDeveloperAgentPanel(launcherView);
|
|
const homeProject = useHomeProjectCreation({
|
|
setStatus,
|
|
setLauncherView,
|
|
setAgentChatProjectPath: developerAgent.setAgentChatProjectPath,
|
|
rememberRecentWorkspace,
|
|
});
|
|
const {
|
|
projectPath,
|
|
setProjectPath,
|
|
currentProjectContext,
|
|
setCurrentProjectContext,
|
|
activeProjectPreview,
|
|
setActiveProjectPreview,
|
|
activeProjectAgentRuntimeSummaries,
|
|
setAgentRuntimeSummaries: setActiveProjectAgentRuntimeSummaries,
|
|
activeProjectAgentResults,
|
|
setAgentResults: setActiveProjectAgentResults,
|
|
resetLauncherHomeDraft,
|
|
startGameFromApprovedGdd,
|
|
createHomeDraftAutomatically,
|
|
openProject,
|
|
} = homeProject;
|
|
const activeProjectContextRef = useRef(currentProjectContext);
|
|
const manifestMergeRef = useRef<ProjectManifestMergeState | null>(null);
|
|
activeProjectContextRef.current = currentProjectContext;
|
|
|
|
useEffect(() => {
|
|
const nextTitle =
|
|
launcherView === 'project-development'
|
|
? currentProjectContext?.projectName.trim()
|
|
: '';
|
|
setWindowTitle(nextTitle || WINDOW_CHROME_DEFAULT_TITLE);
|
|
return () => setWindowTitle(WINDOW_CHROME_DEFAULT_TITLE);
|
|
}, [currentProjectContext?.projectName, launcherView, setWindowTitle]);
|
|
|
|
useEffect(() => {
|
|
const current = activeProjectContextRef.current;
|
|
manifestMergeRef.current =
|
|
current?.projectRevision === null || !current
|
|
? null
|
|
: createProjectManifestMergeState({
|
|
projectPath: current.projectPath,
|
|
projectId: current.manifest.projectId,
|
|
revision: current.projectRevision,
|
|
manifest: current.manifest,
|
|
source: 'initial',
|
|
});
|
|
}, [
|
|
currentProjectContext?.createdAt,
|
|
currentProjectContext?.projectName,
|
|
currentProjectContext?.projectPath,
|
|
]);
|
|
|
|
const applyManifestSnapshot = useCallback(
|
|
(snapshot: ProjectManifestSnapshot) => {
|
|
const current = activeProjectContextRef.current;
|
|
if (
|
|
!current ||
|
|
current.projectPath !== snapshot.projectPath ||
|
|
current.manifest.projectId !== snapshot.projectId
|
|
) {
|
|
return;
|
|
}
|
|
let previous = manifestMergeRef.current;
|
|
if (
|
|
!previous ||
|
|
previous.projectPath !== current.projectPath ||
|
|
previous.projectId !== current.manifest.projectId
|
|
) {
|
|
previous =
|
|
current.projectRevision === null
|
|
? null
|
|
: createProjectManifestMergeState({
|
|
projectPath: current.projectPath,
|
|
projectId: current.manifest.projectId,
|
|
revision: current.projectRevision,
|
|
manifest: current.manifest,
|
|
source: 'initial',
|
|
});
|
|
}
|
|
if (!previous) {
|
|
manifestMergeRef.current = createProjectManifestMergeState(snapshot);
|
|
} else {
|
|
const merged = mergeProjectManifestSnapshot(previous, snapshot);
|
|
manifestMergeRef.current = merged.state;
|
|
if (merged.decision !== 'accepted') {
|
|
return;
|
|
}
|
|
}
|
|
setCurrentProjectContext((active) =>
|
|
active &&
|
|
active.projectPath === snapshot.projectPath &&
|
|
active.manifest.projectId === snapshot.projectId
|
|
? {
|
|
...active,
|
|
manifest: snapshot.manifest,
|
|
projectRevision: snapshot.revision,
|
|
}
|
|
: active,
|
|
);
|
|
},
|
|
[setCurrentProjectContext],
|
|
);
|
|
const syncActiveProjectManifest = useCallback(
|
|
(
|
|
sourceProjectPath: string,
|
|
manifest: NonNullable<typeof currentProjectContext>['manifest'],
|
|
metadata?: ProjectManifestSnapshotMetadata,
|
|
) => {
|
|
const current = activeProjectContextRef.current;
|
|
if (!current || current.projectPath !== sourceProjectPath) {
|
|
return;
|
|
}
|
|
if (metadata) {
|
|
applyManifestSnapshot({
|
|
projectPath: sourceProjectPath,
|
|
manifest,
|
|
...metadata,
|
|
});
|
|
return;
|
|
}
|
|
const invoke = resolveTauriInvoke();
|
|
if (!invoke) {
|
|
if (current.projectRevision === null) {
|
|
setCurrentProjectContext((active) =>
|
|
active?.projectPath === sourceProjectPath
|
|
? { ...active, manifest }
|
|
: active,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
void invoke<LocalGameProjectRevisionStatus>(
|
|
'get_local_game_project_revision',
|
|
{ projectPath: sourceProjectPath },
|
|
)
|
|
.then((status) => {
|
|
applyManifestSnapshot({
|
|
projectPath: sourceProjectPath,
|
|
projectId: manifest.projectId,
|
|
revision: status.revision,
|
|
manifest,
|
|
source: 'supervisor',
|
|
});
|
|
})
|
|
.catch(() => {
|
|
if (activeProjectContextRef.current?.projectRevision === null) {
|
|
setCurrentProjectContext((active) =>
|
|
active?.projectPath === sourceProjectPath
|
|
? { ...active, manifest }
|
|
: active,
|
|
);
|
|
}
|
|
});
|
|
},
|
|
[applyManifestSnapshot, setCurrentProjectContext],
|
|
);
|
|
|
|
const requestCurrentProjectPlay = useCallback((nextProjectPath: string) => {
|
|
playRequestIdRef.current += 1;
|
|
setPlayRequest({
|
|
projectPath: nextProjectPath,
|
|
requestId: playRequestIdRef.current,
|
|
});
|
|
}, []);
|
|
|
|
const handlePlayRequestHandled = useCallback((requestId: number) => {
|
|
setPlayRequest((current) =>
|
|
current?.requestId === requestId ? null : current,
|
|
);
|
|
}, []);
|
|
|
|
function showLauncherNotice(title: string) {
|
|
setLauncherNotice({
|
|
title,
|
|
message: `${title}正在接入中,当前版本会先保留入口。`,
|
|
});
|
|
}
|
|
|
|
const currentHelpTitle =
|
|
launcherView === 'guide'
|
|
? '使用指南'
|
|
: launcherView === 'contact'
|
|
? '联系我们'
|
|
: '最新动态';
|
|
const currentHelpItems =
|
|
launcherView === 'guide'
|
|
? ['创建游戏项目', '上传参考素材', '选择审批档位', '生成试玩原型']
|
|
: launcherView === 'contact'
|
|
? ['产品反馈', '商务合作', '账号与充值支持']
|
|
: [
|
|
'GameAgent V1.0 首页改版',
|
|
'项目工作区和 Agent 会话建设中',
|
|
'美术生成将接入平台 API',
|
|
];
|
|
|
|
return (
|
|
<main
|
|
className="launcher-shell platform-theme platform-theme--light"
|
|
aria-label="GameAgent 客户端"
|
|
>
|
|
<Sidebar
|
|
activeView={launcherView}
|
|
currentUser={currentUser}
|
|
onLogout={() => {
|
|
resetLauncherHomeDraft();
|
|
accountWallet.resetWalletBalance();
|
|
onLogout();
|
|
}}
|
|
onNoticeRequest={showLauncherNotice}
|
|
onRechargeRequest={accountWallet.openRecharge}
|
|
onRuntimeConfigOpen={() => setRuntimeConfigOpen(true)}
|
|
onViewChange={setLauncherView}
|
|
/>
|
|
<section
|
|
className={
|
|
launcherNotifications.length > 0
|
|
? 'launcher-main launcher-main-with-promo'
|
|
: 'launcher-main'
|
|
}
|
|
>
|
|
{launcherNotifications.length > 0 ? (
|
|
<header className="launcher-promo" aria-label="通知">
|
|
{launcherNotifications.map((notification) => (
|
|
<Fragment key={`${notification.label}:${notification.detail}`}>
|
|
<strong>{notification.label}</strong>
|
|
<span>{notification.detail}</span>
|
|
<button type="button">{notification.actionLabel}</button>
|
|
</Fragment>
|
|
))}
|
|
</header>
|
|
) : null}
|
|
|
|
{launcherView === 'home' ? (
|
|
<HomeView
|
|
hasPromo={launcherNotifications.length > 0}
|
|
onStatusChange={setStatus}
|
|
recentProjectRows={recentProjectRows}
|
|
onCreateDraftAutomatically={createHomeDraftAutomatically}
|
|
onProjectsOpen={() => setLauncherView('projects')}
|
|
onProjectOpen={(path) => {
|
|
setProjectPath(path);
|
|
void openProject(path, 'open');
|
|
}}
|
|
onProjectPick={() => void homeProject.pickAndOpenProject()}
|
|
/>
|
|
) : launcherView === 'projects' ? (
|
|
<ProjectsPage
|
|
status={status}
|
|
homeProject={homeProject}
|
|
recentProjects={recentProjects}
|
|
/>
|
|
) : launcherView === 'agent-chat' ? (
|
|
<DeveloperAgentPanel
|
|
controller={developerAgent}
|
|
onRuntimeConfigOpen={() => setRuntimeConfigOpen(true)}
|
|
/>
|
|
) : launcherView === 'project-development' && currentProjectContext ? (
|
|
<ProjectDevelopmentView
|
|
orchestrationMode="single-supervisor"
|
|
projectName={currentProjectContext.projectName}
|
|
projectPath={currentProjectContext.projectPath}
|
|
manifest={currentProjectContext.manifest}
|
|
attachments={currentProjectContext.attachments}
|
|
recentRunStatus={currentProjectContext.recentRunStatus}
|
|
recentRunStopReason={currentProjectContext.recentRunStopReason}
|
|
preview={activeProjectPreview}
|
|
agentRuntimeSummaries={activeProjectAgentRuntimeSummaries}
|
|
agentResults={activeProjectAgentResults}
|
|
planningStartMode={currentProjectContext.startMode === 'planning'}
|
|
onPlay={() =>
|
|
requestCurrentProjectPlay(currentProjectContext.projectPath)
|
|
}
|
|
onManifestChange={syncActiveProjectManifest}
|
|
onHomeOpen={() => setLauncherView('home')}
|
|
onProjectsOpen={() => setLauncherView('projects')}
|
|
supervisor={
|
|
<ProjectSupervisor
|
|
key={currentProjectContext.projectPath}
|
|
initialProjectPath={currentProjectContext.projectPath}
|
|
initialProjectManifest={currentProjectContext.manifest}
|
|
initialProjectKind={currentProjectContext.projectKind}
|
|
initialSupervisorMessage={currentProjectContext.initialPrompt}
|
|
initialCreationType={currentProjectContext.creationType}
|
|
initialAttachments={currentProjectContext.attachments}
|
|
orchestrationMode="single-supervisor"
|
|
projectSupervisorOnly
|
|
planningStartMode={
|
|
currentProjectContext.startMode === 'planning'
|
|
}
|
|
playRequest={playRequest}
|
|
onPlayRequestHandled={handlePlayRequestHandled}
|
|
onManifestChange={syncActiveProjectManifest}
|
|
onPreviewChange={setActiveProjectPreview}
|
|
onAgentRuntimeSummariesChange={
|
|
setActiveProjectAgentRuntimeSummaries
|
|
}
|
|
onAgentResultsChange={setActiveProjectAgentResults}
|
|
onMakeGameFromApprovedGdd={startGameFromApprovedGdd}
|
|
/>
|
|
}
|
|
/>
|
|
) : (
|
|
<section className="launcher-page launcher-help-page">
|
|
<header>
|
|
<div>
|
|
<h1>{currentHelpTitle}</h1>
|
|
<p>陶泥儿 GameAgent</p>
|
|
</div>
|
|
</header>
|
|
<div className="launcher-help-cards">
|
|
{currentHelpItems.map((item) => (
|
|
<article key={item}>{item}</article>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</section>
|
|
{runtimeConfigOpen ? (
|
|
<RuntimeConfigDialog
|
|
projectPath={
|
|
currentProjectContext?.projectPath ||
|
|
developerAgent.agentChatProjectPath ||
|
|
projectPath
|
|
}
|
|
onClose={() => setRuntimeConfigOpen(false)}
|
|
/>
|
|
) : null}
|
|
<AccountWalletDialogs controller={accountWallet} />
|
|
{walletSlot ? (
|
|
createPortal(
|
|
<AccountWalletBar controller={accountWallet} />,
|
|
walletSlot,
|
|
)
|
|
) : isWindowChrome ? null : (
|
|
<AccountWalletBar controller={accountWallet} />
|
|
)}
|
|
<DeveloperAgentDialogs controller={developerAgent} />
|
|
{launcherNotice ? (
|
|
<div
|
|
className="launcher-dialog-backdrop"
|
|
role="presentation"
|
|
onMouseDown={(event) => {
|
|
if (event.target === event.currentTarget) {
|
|
setLauncherNotice(null);
|
|
}
|
|
}}
|
|
>
|
|
<section
|
|
aria-labelledby="launcher-notice-title"
|
|
aria-modal="true"
|
|
className="launcher-dialog"
|
|
role="dialog"
|
|
onKeyDown={(event) =>
|
|
closeDialogOnEscape(event, () => setLauncherNotice(null))
|
|
}
|
|
>
|
|
<h2 id="launcher-notice-title">{launcherNotice.title}</h2>
|
|
<p>{launcherNotice.message}</p>
|
|
<div className="launcher-dialog-actions">
|
|
<button type="button" onClick={() => setLauncherNotice(null)}>
|
|
知道了
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
) : null}
|
|
<NonEmptyProjectDialog controller={homeProject} />
|
|
</main>
|
|
);
|
|
}
|