From d251525f7762c0d1e411eb0b57c8af5cd6fef9fb Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Wed, 8 Jul 2026 17:35:45 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=81=9AGameAgent=E5=8D=95=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E9=A6=96=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 登录后直接进入单窗口客户端首页并保留兼容命令 首页支持做游戏做素材做方案切换、附件队列和目录创建流程 项目组和项目开发占位改为同窗口切换并保存首条需求 顶部泥点充值合并展示,账户帮助配置回到底部侧栏 补充账户泥点精选素材读取与首页项目视觉测试 同步AI游戏创作实施文档、决策记录和上下文术语 --- CONTEXT.md | 4 + .../scripts/check-config.mjs | 29 +- .../src-tauri/tauri.conf.json | 4 +- apps/ai-game-creator-shell/src/App.tsx | 1104 ++++++++++++++--- apps/ai-game-creator-shell/src/main.tsx | 19 +- apps/ai-game-creator-shell/src/styles.css | 395 +++++- .../tests/appSurface.test.ts | 332 +++-- .../shared-memory/decision-log.md | 25 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 26 +- 9 files changed, 1605 insertions(+), 333 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index e5467abc7..1351b1033 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -16,6 +16,10 @@ _Avoid_: 在玩法页面内手写上传、参考图、重绘、预览、删除 独立 `/editor` 中可保存、恢复和继续编辑的图片画布工作状态,包含画布视图、图层布局和资源引用;用于多图对比、生成结果衍生和画布级编辑,不替代玩法页面内的单图资产编辑。 _Avoid_: 玩法结果页单图槽位、发布态作品、只存在前端内存里的临时画布 +**项目开发画布**: +GameAgent 独立客户端中某个本地游戏项目的开发工作区概念,用于承载项目名、路径、首条需求、附件导入结果、最近 run 状态,以及后续真正的项目开发画布与 Agent 协作界面;当前首页改造阶段先落占位页。它属于 AI 游戏创作本地项目域,不等同于 `/editor` 的图片画布工程。 +_Avoid_: `/editor` 图片画布工程、画布资源 / 图层布局、启动器 / 主窗口切换概念、只用于首页输入的临时草稿 + **画布Agent对话**: 图片画布工程右侧的对话式编辑器工具,用户通过自然语言调度画布已有的图片类生成与编辑能力(生成图片、生成角色形象、生成图标素材、生成 UI 设计图、基于附件的图片修改),并可附加画布素材或素材库图片作为参考;对话归属单个图片画布工程,可保存历史、新开会话和软删会话。属于画布域工具,不承接玩法创作、不产出玩法作品或模板,与「表单/图片输入创作工作台」的 Avoid 边界不冲突。 _Avoid_: 对话式玩法创作工作台、绕过模型定价收口的生成入口、把对话消息当作画布布局真相、复用拼图专用 creative-agent 内存会话 diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index 2b723681a..1e0e475fc 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -48,7 +48,10 @@ const rustSharedContractSource = fs.readFileSync( ), 'utf8', ); -const allowedUncalledTauriCommands = []; +const allowedUncalledTauriCommands = [ + 'open_game_creator_launcher_window', + 'open_game_creator_workspace_window', +]; const sourceExtensions = new Set([ '.json', '.md', @@ -380,11 +383,11 @@ if (tauriConfig.app?.withGlobalTauri !== true) { if ( !Array.isArray(tauriConfig.app?.windows) || tauriConfig.app.windows.length !== 1 || - tauriConfig.app.windows[0]?.label !== 'launcher' || - tauriConfig.app.windows[0]?.url !== 'index.html?launcher' + tauriConfig.app.windows[0]?.label !== 'client' || + tauriConfig.app.windows[0]?.url !== 'index.html' ) { throw new Error( - 'AI game creator shell must start with only the launcher window', + 'AI game creator shell must start with only the client window', ); } @@ -409,22 +412,22 @@ if ( const windows = tauriConfig.app?.windows ?? []; if ( windows.length !== 1 || - windows[0]?.label !== 'launcher' || - windows[0]?.url !== 'index.html?launcher' + windows[0]?.label !== 'client' || + windows[0]?.url !== 'index.html' ) { throw new Error( - 'AI game creator shell release config must expose only the launcher window', + 'AI game creator shell release config must expose only the client window', ); } -const launcherWindow = windows[0]; +const clientWindow = windows[0]; if ( - launcherWindow.width !== 820 || - launcherWindow.height !== 640 || - launcherWindow.minWidth !== 720 || - launcherWindow.minHeight !== 520 + clientWindow.width !== 820 || + clientWindow.height !== 640 || + clientWindow.minWidth !== 720 || + clientWindow.minHeight !== 520 ) { - throw new Error('AI game creator shell launcher window must stay compact'); + throw new Error('AI game creator shell client window must stay compact'); } if (tauriConfig.build?.devUrl !== 'http://127.0.0.1:3080/') { diff --git a/apps/ai-game-creator-shell/src-tauri/tauri.conf.json b/apps/ai-game-creator-shell/src-tauri/tauri.conf.json index 5ae277d35..c96c74da5 100644 --- a/apps/ai-game-creator-shell/src-tauri/tauri.conf.json +++ b/apps/ai-game-creator-shell/src-tauri/tauri.conf.json @@ -13,9 +13,9 @@ "withGlobalTauri": true, "windows": [ { - "label": "launcher", + "label": "client", "title": "AI 游戏创作", - "url": "index.html?launcher", + "url": "index.html", "width": 820, "height": 640, "minWidth": 720, diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 817e58742..aa986dc5e 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -16,11 +16,21 @@ import { BookOpen, CircleHelp, Contact, + CreditCard, + FileText, FolderKanban, + Gamepad2, Home, + Image, + LogOut, + MessageSquare, Plus, Settings, + Sparkles, Upload, + User, + Wallet, + X, } from 'lucide-react'; import type { @@ -37,6 +47,11 @@ import { API_RESPONSE_ENVELOPE_VERSION, unwrapApiResponse, } from '../../../packages/shared/src/http'; +import type { + ProfileDashboardSummary, + ProfileWalletLedgerEntry, + ProfileWalletLedgerResponse, +} from '../../../packages/shared/src/contracts/runtime'; import { createGameCreationAppManifest, createGameCreationAppSeedTasks, @@ -83,7 +98,71 @@ const launcherNotifications: Array<{ detail: string; actionLabel: string; }> = []; -type LauncherView = 'home' | 'projects' | 'guide' | 'contact' | 'news'; +type LauncherView = + | 'home' + | 'projects' + | 'guide' + | 'contact' + | 'news' + | 'project-development'; +type HomeAgentMode = 'game' | 'art' | 'doc'; + +type HomeAttachmentDraft = { + id: string; + file: File; +}; + +type LauncherImportedAttachment = { + fileName: string; + mediaType: string; + localPath?: string; + status: 'imported' | 'failed'; + error?: string; +}; + +type LauncherProjectContext = { + projectPath: string; + projectName: string; + mode: HomeAgentMode | null; + initialPrompt: string; + attachments: LauncherImportedAttachment[]; + recentRunStatus: string | null; + recentRunStopReason: string | null; + createdAt: number; +}; + +type PendingNonEmptyProject = + | { + kind: 'home-create'; + projectPath: string; + mode: HomeAgentMode; + prompt: string; + attachments: HomeAttachmentDraft[]; + } + | { + kind: 'project-create'; + projectPath: string; + }; + +type EditorShowcaseResource = { + resourceId: string; + showcaseId?: string | null; + label?: string | null; + imageSrc: string; + width?: number; + height?: number; + prompt?: string | null; + actualPrompt?: string | null; + assetKind?: string | null; + authorDisplayName?: string | null; + authorPublicUserCode?: string | null; + likeCount?: number | null; +}; + +type EditorShowcaseResourceListResponse = { + resources: EditorShowcaseResource[]; + nextCursor?: string | null; +}; interface InitLocalProjectResult { projectPath: string; @@ -1132,9 +1211,135 @@ async function logoutClientAuthSession() { } } -function getAuthUserInitials(user: AuthUser) { - const displayName = user.displayName?.trim() || user.phoneNumberMasked || 'tn'; - return displayName.slice(0, 2); +function getClientProfileDashboard() { + return requestAuthJson( + '/api/profile/dashboard', + { method: 'GET' }, + '读取泥点余额失败', + ); +} + +function getClientProfileWalletLedger() { + return requestAuthJson( + '/api/profile/wallet-ledger', + { method: 'GET' }, + '读取泥点明细失败', + ); +} + +function listClientShowcaseResources() { + return requestAuthJson( + '/api/editor/showcase/resources', + { method: 'GET' }, + '读取灵感推荐失败', + { skipAuth: true }, + ); +} + +const homeAgentModeItems: Array<{ + mode: HomeAgentMode; + label: string; + placeholder: string; + emptyPrompt: string; + icon: typeof Gamepad2; +}> = [ + { + mode: 'game', + label: '做游戏', + placeholder: '今天想把什么灵感做成游戏', + emptyPrompt: '请输入游戏灵感或上传参考素材', + icon: Gamepad2, + }, + { + mode: 'art', + label: '做素材', + placeholder: '今天想做什么样的美术素材', + emptyPrompt: '请输入素材需求或上传参考图', + icon: Image, + }, + { + mode: 'doc', + label: '做方案', + placeholder: '今天有什么设计需要帮你整理', + emptyPrompt: '请输入方案需求或上传资料', + icon: FileText, + }, +]; + +const homeAgentModeLabels: Record = { + game: '做游戏', + art: '做素材', + doc: '做方案', +}; + +function formatMudPoints(value: number | null | undefined) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return '--'; + } + return Math.max(0, Math.floor(value)).toLocaleString('zh-CN'); +} + +function formatLedgerAmount(entry: ProfileWalletLedgerEntry) { + const amount = Math.trunc(entry.amountDelta); + return `${amount >= 0 ? '+' : ''}${amount}`; +} + +function formatLocalDateTime(value: string | number | null | undefined) { + if (value === null || value === undefined || value === '') { + return '未知时间'; + } + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return '未知时间'; + } + return date.toLocaleString('zh-CN', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); +} + +function createHomeAttachmentId(file: File, index: number) { + return `${file.name}:${file.size}:${file.lastModified}:${index}:${Date.now()}`; +} + +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'); +} + +function buildHomeConversationReceipt( + importedAttachments: LauncherImportedAttachment[], +) { + const lines = ['已创建项目草稿,本轮仅进入项目开发占位,不启动生成。']; + if (importedAttachments.length > 0) { + lines.push(''); + lines.push('附件导入结果:'); + for (const attachment of importedAttachments) { + lines.push( + `- ${attachment.fileName}: ${ + attachment.status === 'imported' + ? attachment.localPath || '已导入' + : attachment.error || '导入失败' + }`, + ); + } + } + return lines.join('\n'); } export function AuthenticatedClient({ @@ -1926,6 +2131,14 @@ export function WorkspaceLauncher({ const [projectPath, setProjectPath] = useState(defaultProjectPath); const [status, setStatus] = useState('请选择项目'); const [launcherView, setLauncherView] = useState('home'); + const [homeAgentMode, setHomeAgentMode] = useState('game'); + const [homePrompt, setHomePrompt] = useState(''); + const [homeAttachments, setHomeAttachments] = useState( + [], + ); + const [homeCreationBusy, setHomeCreationBusy] = useState(false); + const [currentProjectContext, setCurrentProjectContext] = + useState(null); const [recentWorkspaces, setRecentWorkspaces] = useState(readRecentWorkspaces); const [recentWorkspaceStatuses, setRecentWorkspaceStatuses] = useState< @@ -1935,9 +2148,23 @@ export function WorkspaceLauncher({ const [recentWorkspaceRefreshing, setRecentWorkspaceRefreshing] = useState(false); const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false); - const [pendingNonEmptyProjectPath, setPendingNonEmptyProjectPath] = useState< - string | null - >(null); + const [pendingNonEmptyProject, setPendingNonEmptyProject] = + useState(null); + const [profileDashboard, setProfileDashboard] = + useState(null); + const [profileStatus, setProfileStatus] = useState('正在读取账户'); + const [walletPanelOpen, setWalletPanelOpen] = useState(false); + const [walletLedger, setWalletLedger] = useState( + [], + ); + const [walletLedgerStatus, setWalletLedgerStatus] = + useState('尚未读取明细'); + const [accountMenuOpen, setAccountMenuOpen] = useState(false); + const [showcaseResources, setShowcaseResources] = useState< + EditorShowcaseResource[] + >([]); + const [showcaseStatus, setShowcaseStatus] = useState('正在读取灵感'); + const homeAttachmentInputRef = useRef(null); useEffect(() => { const invoke = resolveTauriInvoke(); @@ -1972,37 +2199,290 @@ export function WorkspaceLauncher({ }; }, [recentWorkspaces, recentWorkspaceRefreshKey]); - async function openWorkspaceWindow( - trimmedProjectPath: string, - invoke: TauriInvoke, - ) { - await invoke('open_game_creator_workspace_window', { - projectPath: trimmedProjectPath, - }); - setRecentWorkspaces(writeRecentWorkspace(trimmedProjectPath)); - setRecentWorkspaceRefreshKey((current) => current + 1); - } + useEffect(() => { + let disposed = false; + void getClientProfileDashboard() + .then((dashboard) => { + if (disposed) { + return; + } + setProfileDashboard(dashboard); + setProfileStatus('已读取账户'); + }) + .catch((error) => { + if (disposed) { + return; + } + setProfileDashboard(null); + setProfileStatus(error instanceof Error ? error.message : '泥点读取失败'); + }); + void listClientShowcaseResources() + .then((response) => { + if (disposed) { + return; + } + setShowcaseResources(response.resources.slice(0, 6)); + setShowcaseStatus(response.resources.length > 0 ? '已读取灵感' : '暂无灵感'); + }) + .catch((error) => { + if (disposed) { + return; + } + setShowcaseResources([]); + setShowcaseStatus( + error instanceof Error ? error.message : '灵感读取失败', + ); + }); + return () => { + disposed = true; + }; + }, []); - async function createWorkspaceAndOpen( - trimmedProjectPath: string, - invoke: TauriInvoke, - ) { - await invoke('init_local_game_project', { - projectPath: trimmedProjectPath, - projectId: 'local-project-draft', - name: projectNameFromPath(trimmedProjectPath), - }); - await openWorkspaceWindow(trimmedProjectPath, invoke); - } + useEffect(() => { + if (!walletPanelOpen || walletLedgerStatus !== '尚未读取明细') { + return; + } + void loadWalletLedger(); + }, [walletPanelOpen, walletLedgerStatus]); - async function openProject(nextProjectPath: string, mode: 'open' | 'create') { + function validateProjectPath(nextProjectPath: string) { const trimmedProjectPath = nextProjectPath.trim(); if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) { setStatus('请提供项目绝对路径'); - return; + return null; } if (projectPathHasControlCharacter(trimmedProjectPath)) { setStatus('项目目录不能包含控制字符'); + return null; + } + return trimmedProjectPath; + } + + function enterProjectDevelopment(context: LauncherProjectContext) { + setCurrentProjectContext(context); + setProjectPath(context.projectPath); + setLauncherView('project-development'); + setRecentWorkspaces(writeRecentWorkspace(context.projectPath)); + setRecentWorkspaceRefreshKey((current) => current + 1); + } + + async function importHomeAttachments( + invoke: TauriInvoke, + nextProjectPath: string, + attachments: HomeAttachmentDraft[], + ) { + const imported: LauncherImportedAttachment[] = []; + for (const attachment of attachments) { + const mediaType = attachment.file.type || 'application/octet-stream'; + try { + const bytes = Array.from( + new Uint8Array(await attachment.file.arrayBuffer()), + ); + const result = await invoke( + 'upload_local_asset', + { + projectPath: nextProjectPath, + fileName: attachment.file.name, + mediaType, + bytes, + }, + ); + imported.push({ + fileName: attachment.file.name, + mediaType, + localPath: result.localPath, + status: 'imported', + }); + } catch (error) { + imported.push({ + fileName: attachment.file.name, + mediaType, + status: 'failed', + error: error instanceof Error ? error.message : String(error), + }); + } + } + return imported; + } + + async function appendHomeConversation( + invoke: TauriInvoke, + nextProjectPath: string, + mode: HomeAgentMode, + prompt: string, + attachments: HomeAttachmentDraft[], + importedAttachments: LauncherImportedAttachment[], +) { + const userContent = buildHomeConversationContent(mode, prompt, attachments); + await invoke('append_local_conversation_message', { + projectPath: nextProjectPath, + agentId: null, + message: { + role: 'user', + content: userContent, + agentId: null, + }, + }); + await invoke('append_local_conversation_message', { + projectPath: nextProjectPath, + agentId: null, + message: { + role: 'assistant', + content: buildHomeConversationReceipt(importedAttachments), + agentId: null, + }, + }); + } + + async function createHomeProjectFromDirectory( + nextProjectPath: string, + mode: HomeAgentMode, + prompt: string, + attachments: HomeAttachmentDraft[], + skipNonEmptyCheck = false, + ) { + const trimmedProjectPath = validateProjectPath(nextProjectPath); + if (!trimmedProjectPath) { + return; + } + const invoke = resolveTauriInvoke(); + if (!invoke) { + setStatus('需要在 Tauri App 内运行'); + return; + } + setHomeCreationBusy(true); + setStatus('正在创建项目'); + try { + if (!skipNonEmptyCheck) { + const nonEmpty = await invoke( + 'is_local_project_directory_non_empty', + { projectPath: trimmedProjectPath }, + ); + if (nonEmpty) { + setPendingNonEmptyProject({ + kind: 'home-create', + projectPath: trimmedProjectPath, + mode, + prompt, + attachments, + }); + setStatus('目标文件夹不是空的'); + return; + } + } + const result = await invoke( + 'init_local_game_project', + { + projectPath: trimmedProjectPath, + projectId: seedManifest.projectId, + name: projectNameFromPath(trimmedProjectPath), + }, + ); + const importedAttachments = await importHomeAttachments( + invoke, + result.projectPath, + attachments, + ); + try { + await appendHomeConversation( + invoke, + result.projectPath, + mode, + prompt, + attachments, + importedAttachments, + ); + setStatus('已创建项目并记录首条需求'); + } catch (error) { + setStatus( + `已创建项目;首条需求保存失败:${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + enterProjectDevelopment({ + projectPath: result.projectPath, + projectName: + result.manifest.name || projectNameFromPath(result.projectPath), + mode, + initialPrompt: + prompt.trim() || + (attachments.length > 0 ? '用户上传了参考附件,等待后续补充需求。' : ''), + attachments: importedAttachments, + recentRunStatus: null, + recentRunStopReason: null, + createdAt: Date.now(), + }); + setHomePrompt(''); + setHomeAttachments([]); + } catch (error) { + setStatus(error instanceof Error ? error.message : String(error)); + } finally { + setHomeCreationBusy(false); + } + } + + async function createProjectFromProjectPage( + nextProjectPath: string, + skipNonEmptyCheck = false, + ) { + const trimmedProjectPath = validateProjectPath(nextProjectPath); + if (!trimmedProjectPath) { + return; + } + const invoke = resolveTauriInvoke(); + if (!invoke) { + setStatus('需要在 Tauri App 内运行'); + return; + } + setStatus('正在创建项目'); + try { + if (!skipNonEmptyCheck) { + const nonEmpty = await invoke( + 'is_local_project_directory_non_empty', + { projectPath: trimmedProjectPath }, + ); + if (nonEmpty) { + setPendingNonEmptyProject({ + kind: 'project-create', + projectPath: trimmedProjectPath, + }); + setStatus('目标文件夹不是空的'); + return; + } + } + const result = await invoke( + 'init_local_game_project', + { + projectPath: trimmedProjectPath, + projectId: seedManifest.projectId, + name: projectNameFromPath(trimmedProjectPath), + }, + ); + setStatus('已创建项目'); + enterProjectDevelopment({ + projectPath: result.projectPath, + projectName: + result.manifest.name || projectNameFromPath(result.projectPath), + mode: null, + initialPrompt: '', + attachments: [], + recentRunStatus: null, + recentRunStopReason: null, + createdAt: Date.now(), + }); + } catch (error) { + setStatus(error instanceof Error ? error.message : String(error)); + } + } + + async function openProject(nextProjectPath: string, mode: 'open' | 'create') { + if (mode === 'create') { + await createProjectFromProjectPage(nextProjectPath); + return; + } + const trimmedProjectPath = validateProjectPath(nextProjectPath); + if (!trimmedProjectPath) { return; } const invoke = resolveTauriInvoke(); @@ -2012,69 +2492,66 @@ export function WorkspaceLauncher({ } setStatus('正在打开'); try { - if (mode === 'open') { - const directoryStatus = await invoke( - 'inspect_local_project_directory', - { projectPath: trimmedProjectPath }, - ); - if (!directoryStatus.exists) { - setStatus('项目目录不存在'); - return; - } - if (!directoryStatus.isDirectory) { - setStatus('项目路径不是文件夹'); - return; - } - if (!directoryStatus.isGameCreatorProject) { - setStatus('这不是已初始化的 AI 游戏项目,请使用新建项目。'); - return; - } - } else { - const nonEmpty = await invoke( - 'is_local_project_directory_non_empty', - { projectPath: trimmedProjectPath }, - ); - if (nonEmpty) { - setPendingNonEmptyProjectPath(trimmedProjectPath); - setStatus('目标文件夹不是空的'); - return; - } - await createWorkspaceAndOpen(trimmedProjectPath, invoke); + const directoryStatus = await invoke( + 'inspect_local_project_directory', + { projectPath: trimmedProjectPath }, + ); + if (!directoryStatus.exists) { + setStatus('项目目录不存在'); return; } - await openWorkspaceWindow(trimmedProjectPath, invoke); + if (!directoryStatus.isDirectory) { + setStatus('项目路径不是文件夹'); + return; + } + if (!directoryStatus.isGameCreatorProject) { + setStatus('这不是已初始化的 AI 游戏项目,请使用新建项目。'); + return; + } + setStatus('已打开项目'); + enterProjectDevelopment({ + projectPath: trimmedProjectPath, + projectName: + directoryStatus.projectName || projectNameFromPath(trimmedProjectPath), + mode: null, + initialPrompt: '', + attachments: [], + recentRunStatus: directoryStatus.recentRunStatus, + recentRunStopReason: directoryStatus.recentRunStopReason, + createdAt: Date.now(), + }); } catch (error) { setStatus(error instanceof Error ? error.message : String(error)); } } async function confirmCreateInNonEmptyFolder() { - if (!pendingNonEmptyProjectPath) { + if (!pendingNonEmptyProject) { return; } - const targetProjectPath = pendingNonEmptyProjectPath; - setPendingNonEmptyProjectPath(null); - const invoke = resolveTauriInvoke(); - if (!invoke) { - setStatus('需要在 Tauri App 内运行'); + const pendingProject = pendingNonEmptyProject; + setPendingNonEmptyProject(null); + if (pendingProject.kind === 'home-create') { + await createHomeProjectFromDirectory( + pendingProject.projectPath, + pendingProject.mode, + pendingProject.prompt, + pendingProject.attachments, + true, + ); return; } - setStatus('正在打开'); - try { - await createWorkspaceAndOpen(targetProjectPath, invoke); - } catch (error) { - setStatus(error instanceof Error ? error.message : String(error)); - } + await createProjectFromProjectPage(pendingProject.projectPath, true); } function cancelCreateInNonEmptyFolder() { - setPendingNonEmptyProjectPath(null); + setPendingNonEmptyProject(null); setStatus('已取消'); } useEscapeToClose( cancelCreateInNonEmptyFolder, - pendingNonEmptyProjectPath !== null, + pendingNonEmptyProject !== null, ); function handleSubmit(event: FormEvent) { @@ -2082,6 +2559,43 @@ export function WorkspaceLauncher({ void openProject(projectPath, 'open'); } + async function handleHomeSubmit(event: FormEvent) { + event.preventDefault(); + const activeMode = homeAgentModeItems.find( + (item) => item.mode === homeAgentMode, + ); + if (!homePrompt.trim() && homeAttachments.length === 0) { + setStatus(activeMode?.emptyPrompt ?? '请输入需求或上传附件'); + return; + } + const invoke = resolveTauriInvoke(); + if (!invoke) { + setStatus('需要在 Tauri App 内运行'); + return; + } + setHomeCreationBusy(true); + setStatus('请选择项目目录'); + try { + const selectedPath = await invoke( + 'pick_local_project_directory', + ); + if (!selectedPath) { + setStatus('已取消'); + return; + } + await createHomeProjectFromDirectory( + selectedPath, + homeAgentMode, + homePrompt, + homeAttachments, + ); + } catch (error) { + setStatus(error instanceof Error ? error.message : String(error)); + } finally { + setHomeCreationBusy(false); + } + } + async function handlePickProjectDirectory() { const invoke = resolveTauriInvoke(); if (!invoke) { @@ -2129,6 +2643,43 @@ export function WorkspaceLauncher({ } } + function handleHomeAttachmentSelection(event: ChangeEvent) { + const files = Array.from(event.currentTarget.files ?? []); + event.currentTarget.value = ''; + if (files.length === 0) { + return; + } + setHomeAttachments((current) => [ + ...current, + ...files.map((file, index) => ({ + id: createHomeAttachmentId(file, index), + file, + })), + ]); + } + + function removeHomeAttachment(attachmentId: string) { + setHomeAttachments((current) => + current.filter((attachment) => attachment.id !== attachmentId), + ); + } + + async function loadWalletLedger() { + setWalletLedgerStatus('正在读取明细'); + try { + const response = await getClientProfileWalletLedger(); + setWalletLedger(response.entries.slice(0, 5)); + setWalletLedgerStatus( + response.entries.length > 0 ? '已读取明细' : '暂无泥点明细', + ); + } catch (error) { + setWalletLedger([]); + setWalletLedgerStatus( + error instanceof Error ? error.message : '明细读取失败', + ); + } + } + function handleRecentWorkspaceRefresh() { if (recentWorkspaces.length === 0 || recentWorkspaceRefreshing) { return; @@ -2197,6 +2748,13 @@ export function WorkspaceLauncher({ directoryStatus?.isGameCreatorProject !== false, }; }); + const recentProjectRows = projectRows + .filter((project) => project.canOpen) + .slice(0, 3); + const activeHomeMode = + homeAgentModeItems.find((item) => item.mode === homeAgentMode) ?? + homeAgentModeItems[0]!; + const ActiveHomeModeIcon = activeHomeMode.icon; const currentHelpTitle = launcherView === 'guide' ? '使用指南' @@ -2211,12 +2769,12 @@ export function WorkspaceLauncher({ : ['GameAgent V1.0 首页改版', '项目工作区和 Agent 会话建设中', '美术生成将接入平台 API']; return ( -
+
- ☁ 同时出图放大 +91% - - - +
+
+ + {walletPanelOpen ? ( +
+ 泥点 + {profileStatus} +

{formatMudPoints(profileDashboard?.walletBalance)} 泥点

+
+ {walletLedger.length > 0 ? ( + walletLedger.map((entry) => ( +
+ {formatLedgerAmount(entry)} + {formatLocalDateTime(entry.createdAt)} +
+ )) + ) : ( + {walletLedgerStatus} + )} +
+ +
+ ) : null} +
+ +
{launcherView === 'home' ? ( <>
- tn + +

陶泥儿 GameAgent

-

描述你的想法,让 Agent 生成可试玩游戏原型

+

{activeHomeMode.placeholder}

-
+
+ {homeAgentModeItems.map((item) => { + const ModeIcon = item.icon; + return ( + + ); + })} +
+