Files
Genarrative/apps/ai-game-creator-shell/src/features/project-workspace/DeveloperProjectPanels.tsx
T
AIGameCreator App 759951ab12 继续拆分客户端工作区与 Agent 运行时模块
拆出项目工作区视图、命令策略、摘要命令与开发面板模块。

拆出 Runtime action、driver 与 protocol 的职责模块并保留稳定 facade。

保留测试模块、兼容重导出与原有可见性边界。

补充结构拆分决策、踩坑记录与实施计划验收结果。
2026-07-22 16:03:34 +08:00

420 lines
13 KiB
TypeScript

import type { Dispatch, FormEventHandler, SetStateAction } from 'react';
import type { GameCreationAppLimitedRunCommandDescriptor } from '../../../../../packages/shared/src/contracts/gameCreationApp';
import type {
InitLocalProjectResult,
LocalPreviewResult,
LocalProjectFileEntry,
MemoryScope,
UploadLocalAssetResult,
} from '../../app/types';
type DeveloperProjectPanelsProps = {
assetCanvasProjectId: string;
assetGenerationPrompt: string;
assetKind: string;
assetLocalPath: string;
assetMediaType: string;
assetObjectId: string;
assetResourceId: string;
assetSourceKind: string;
assetStatus: string;
canvasExportPath: string;
commandLog: string[];
editorBaseUrl: string;
fileDraft: string;
filePath: string;
fileStatus: string;
handleAssetRegister: () => void;
handleCanvasAssetGenerate: () => void;
handleCanvasAssetImport: () => void;
handleCanvasExportImport: () => void;
handleCanvasProjectOpen: () => void;
handleCanvasProjectSync: () => void;
handleFileDelete: () => void;
handleFileList: () => void | Promise<void>;
handleFileRead: () => void | Promise<void>;
handleFileWrite: () => void;
handleLimitedCommandRun: (
command: GameCreationAppLimitedRunCommandDescriptor,
) => void;
handleMemoryDelete: () => void;
handleMemoryRead: () => void | Promise<void>;
handleMemoryWrite: () => void;
handlePreviewStart: () => void;
handlePreviewStatus: () => void;
handlePreviewStop: () => void;
handleProjectInit: FormEventHandler<HTMLFormElement>;
handleProjectLogRead: (path: string) => void | Promise<void>;
limitedCommandStatus: string;
limitedLocalCommands: GameCreationAppLimitedRunCommandDescriptor[];
localProject: InitLocalProjectResult | null;
memoryDraft: string;
memoryScope: MemoryScope;
memoryStatus: string;
preview: LocalPreviewResult | null;
previewStatus: string;
projectFiles: LocalProjectFileEntry[];
projectLogContent: string;
projectLogStatus: string;
projectPath: string;
projectStatus: string;
refreshLimitedLocalCommands: () => void | Promise<void>;
setAssetCanvasProjectId: Dispatch<SetStateAction<string>>;
setAssetGenerationPrompt: Dispatch<SetStateAction<string>>;
setAssetKind: Dispatch<SetStateAction<string>>;
setAssetLocalPath: Dispatch<SetStateAction<string>>;
setAssetMediaType: Dispatch<SetStateAction<string>>;
setAssetObjectId: Dispatch<SetStateAction<string>>;
setAssetResourceId: Dispatch<SetStateAction<string>>;
setAssetSourceKind: Dispatch<SetStateAction<string>>;
setCanvasExportPath: Dispatch<SetStateAction<string>>;
setEditorBaseUrl: Dispatch<SetStateAction<string>>;
setFileDraft: Dispatch<SetStateAction<string>>;
setFilePath: Dispatch<SetStateAction<string>>;
setMemoryDraft: Dispatch<SetStateAction<string>>;
setMemoryScope: Dispatch<SetStateAction<MemoryScope>>;
setProjectPath: Dispatch<SetStateAction<string>>;
uploadedAssets: UploadLocalAssetResult[];
};
export function DeveloperProjectPanels({
assetCanvasProjectId,
assetGenerationPrompt,
assetKind,
assetLocalPath,
assetMediaType,
assetObjectId,
assetResourceId,
assetSourceKind,
assetStatus,
canvasExportPath,
commandLog,
editorBaseUrl,
fileDraft,
filePath,
fileStatus,
handleAssetRegister,
handleCanvasAssetGenerate,
handleCanvasAssetImport,
handleCanvasExportImport,
handleCanvasProjectOpen,
handleCanvasProjectSync,
handleFileDelete,
handleFileList,
handleFileRead,
handleFileWrite,
handleLimitedCommandRun,
handleMemoryDelete,
handleMemoryRead,
handleMemoryWrite,
handlePreviewStart,
handlePreviewStatus,
handlePreviewStop,
handleProjectInit,
handleProjectLogRead,
limitedCommandStatus,
limitedLocalCommands,
localProject,
memoryDraft,
memoryScope,
memoryStatus,
preview,
previewStatus,
projectFiles,
projectLogContent,
projectLogStatus,
projectPath,
projectStatus,
refreshLimitedLocalCommands,
setAssetCanvasProjectId,
setAssetGenerationPrompt,
setAssetKind,
setAssetLocalPath,
setAssetMediaType,
setAssetObjectId,
setAssetResourceId,
setAssetSourceKind,
setCanvasExportPath,
setEditorBaseUrl,
setFileDraft,
setFilePath,
setMemoryDraft,
setMemoryScope,
setProjectPath,
uploadedAssets,
}: DeveloperProjectPanelsProps) {
return (
<>
<section className="developer-panel asset-pane" aria-label="文件和资产">
<h2>本地项目</h2>
<form className="local-project-form" onSubmit={handleProjectInit}>
<input
aria-label="本地项目目录"
value={projectPath}
onChange={(event) => setProjectPath(event.currentTarget.value)}
/>
<button type="submit">初始化</button>
</form>
<p className="status-line">{projectStatus}</p>
<p className="status-line">{assetStatus}</p>
<div className="asset-register-form">
<input
aria-label="资产路径"
value={assetLocalPath}
onChange={(event) => setAssetLocalPath(event.currentTarget.value)}
/>
<input
aria-label="资产类型"
value={assetKind}
onChange={(event) => setAssetKind(event.currentTarget.value)}
/>
<input
aria-label="媒体类型"
value={assetMediaType}
onChange={(event) => setAssetMediaType(event.currentTarget.value)}
/>
<select
aria-label="资产来源"
value={assetSourceKind}
onChange={(event) => setAssetSourceKind(event.currentTarget.value)}
>
<option value="generated">generated</option>
<option value="uploaded">uploaded</option>
<option value="canvas">canvas</option>
</select>
<input
aria-label="画板项目"
value={assetCanvasProjectId}
onChange={(event) =>
setAssetCanvasProjectId(event.currentTarget.value)
}
/>
<input
aria-label="资源 ID"
value={assetResourceId}
onChange={(event) => setAssetResourceId(event.currentTarget.value)}
/>
<input
aria-label="资产对象 ID"
value={assetObjectId}
onChange={(event) => setAssetObjectId(event.currentTarget.value)}
/>
<input
aria-label="编辑器地址"
value={editorBaseUrl}
onChange={(event) => setEditorBaseUrl(event.currentTarget.value)}
/>
<input
aria-label="画板导出 ZIP"
value={canvasExportPath}
onChange={(event) => setCanvasExportPath(event.currentTarget.value)}
/>
<input
aria-label="美术生成提示词"
value={assetGenerationPrompt}
onChange={(event) =>
setAssetGenerationPrompt(event.currentTarget.value)
}
/>
<button type="button" onClick={handleCanvasProjectOpen}>
打开画板
</button>
<button type="button" onClick={handleCanvasProjectSync}>
同步画板项目
</button>
<button type="button" onClick={handleAssetRegister}>
登记资产
</button>
<button type="button" onClick={handleCanvasAssetGenerate}>
生成美术资产
</button>
<button type="button" onClick={handleCanvasAssetImport}>
导入画板资产
</button>
<button type="button" onClick={handleCanvasExportImport}>
导入导出包
</button>
</div>
<code>game/</code>
<code>assets/</code>
<code>memory/</code>
<code>.agent/manifest.json</code>
{uploadedAssets.map((asset) => (
<code key={asset.id}>{asset.localPath}</code>
))}
{localProject ? (
<p className="manifest-path">{localProject.manifestPath}</p>
) : null}
</section>
<section
className="developer-panel developer-panel--wide file-pane"
aria-label="项目文件"
>
<header className="panel-header">
<h2>项目文件</h2>
<div className="panel-actions">
<button type="button" onClick={() => void handleFileList()}>
列出
</button>
<button type="button" onClick={() => void handleFileRead()}>
读取
</button>
<button type="button" onClick={handleFileWrite}>
保存
</button>
<button type="button" onClick={handleFileDelete}>
删除
</button>
</div>
</header>
<input
aria-label="项目文件路径"
value={filePath}
onChange={(event) => setFilePath(event.currentTarget.value)}
/>
<textarea
aria-label="项目文件内容"
value={fileDraft}
onChange={(event) => setFileDraft(event.currentTarget.value)}
/>
<p className="status-line">{fileStatus}</p>
<div className="file-list">
{projectFiles.map((file) => (
<button
key={file.path}
type="button"
onClick={() => setFilePath(file.path)}
>
{file.kind === 'directory' ? `${file.path}/` : file.path}
</button>
))}
</div>
</section>
<section
className="developer-panel developer-panel--wide memory-pane"
aria-label="记忆"
>
<header className="panel-header">
<h2>记忆</h2>
<div className="panel-actions">
<select
aria-label="记忆范围"
value={memoryScope}
onChange={(event) =>
setMemoryScope(event.currentTarget.value as MemoryScope)
}
>
<option value="long">长期</option>
<option value="short">短期</option>
<option value="blackboard">黑板</option>
</select>
<button type="button" onClick={() => void handleMemoryRead()}>
读取
</button>
<button type="button" onClick={handleMemoryWrite}>
保存
</button>
<button type="button" onClick={handleMemoryDelete}>
删除
</button>
</div>
</header>
<textarea
aria-label="记忆内容"
value={memoryDraft}
onChange={(event) => setMemoryDraft(event.currentTarget.value)}
/>
<p className="status-line">{memoryStatus}</p>
</section>
<section
className="developer-panel developer-panel--wide preview-pane"
aria-label="预览"
>
<header className="panel-header">
<h2>预览</h2>
<div className="panel-actions">
<button type="button" onClick={handlePreviewStatus}>
状态
</button>
<button type="button" onClick={handlePreviewStart}>
启动
</button>
<button type="button" onClick={handlePreviewStop}>
停止
</button>
</div>
</header>
<p className="status-line">{previewStatus}</p>
{preview ? (
<iframe
className="preview-frame"
src={preview.url}
title="本地游戏预览"
/>
) : (
<div className="preview-frame">127.0.0.1:&lt;port&gt;</div>
)}
</section>
<section
className="developer-panel developer-panel--wide log-pane"
aria-label="日志"
>
<header className="panel-header">
<h2>日志</h2>
<div className="panel-actions">
<button
type="button"
onClick={() =>
void handleProjectLogRead('.agent/logs/command.log')
}
>
命令日志
</button>
<button
type="button"
onClick={() =>
void handleProjectLogRead('.agent/logs/preview.log')
}
>
预览日志
</button>
<button
type="button"
onClick={() => void handleProjectLogRead('.agent/logs/agent.log')}
>
Agent日志
</button>
<button type="button" onClick={refreshLimitedLocalCommands}>
刷新
</button>
</div>
</header>
<div className="limited-command-list">
{limitedLocalCommands.map((command) => (
<button
key={command.id}
type="button"
onClick={() => handleLimitedCommandRun(command)}
>
{command.title}
</button>
))}
</div>
<p className="status-line">{limitedCommandStatus}</p>
<p className="status-line">{projectLogStatus}</p>
{projectLogContent ? (
<pre className="project-log-content">{projectLogContent}</pre>
) : null}
{commandLog.map((entry, index) => (
<p key={`${entry}-${index}`}>{entry}</p>
))}
</section>
</>
);
}