Merge branch 'master' into feat/five_min_design
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@genarrative/ai-game-creator-shell",
|
||||
"private": true,
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/start-tauri-dev.mjs",
|
||||
|
||||
@@ -1734,12 +1734,12 @@ if (
|
||||
}
|
||||
|
||||
if (
|
||||
tauriConfig.version !== '0.1.2' ||
|
||||
packageConfig.version !== '0.1.2' ||
|
||||
cargoPackageVersion !== '0.1.2'
|
||||
tauriConfig.version !== '0.1.4' ||
|
||||
packageConfig.version !== '0.1.4' ||
|
||||
cargoPackageVersion !== '0.1.4'
|
||||
) {
|
||||
throw new Error(
|
||||
'AI game creator standard release must remain version 0.1.2 while game-chat uses its dedicated version',
|
||||
'AI game creator standard release must remain version 0.1.4 while game-chat uses its dedicated version',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1695,7 +1695,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"agent-runtime-core",
|
||||
"axum",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Genarrative AI Game Creator",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"identifier": "world.genarrative.ai-game-creator",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
|
||||
|
||||
@@ -1243,6 +1243,9 @@ export function App({
|
||||
const initialProjectOpenedRef = useRef(false);
|
||||
const pendingUiConfirmationActionRef = useRef<(() => void) | null>(null);
|
||||
const queueRunLocalShortcutRef = useRef<() => void>(() => undefined);
|
||||
const executeRunLocalRef = useRef<(announceToChat: boolean) => void>(
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
function requestRuntimeConfigOpen() {
|
||||
if (projectSupervisorOnly && !supervisorChatOnly && !gameChatOnly) {
|
||||
@@ -3283,6 +3286,7 @@ export function App({
|
||||
]);
|
||||
}
|
||||
queueRunLocalShortcutRef.current = queueRunLocalShortcut;
|
||||
executeRunLocalRef.current = executeRunLocal;
|
||||
|
||||
useEffect(() => {
|
||||
const nextProjectPath = localProject?.projectPath;
|
||||
@@ -3300,7 +3304,7 @@ export function App({
|
||||
}
|
||||
handledPlayRequestRef.current = requestKey;
|
||||
onPlayRequestHandled?.(playRequest.requestId);
|
||||
queueRunLocalShortcutRef.current();
|
||||
void executeRunLocalRef.current(true);
|
||||
}, [
|
||||
localProject?.projectPath,
|
||||
onPlayRequestHandled,
|
||||
|
||||
@@ -34,7 +34,7 @@ export function WorkspaceLauncherShell({
|
||||
ProjectSupervisor,
|
||||
}: WorkspaceLauncherShellProps) {
|
||||
const accountWallet = useAccountWallet(currentUser.id);
|
||||
const [status, setStatus] = useState('请选择项目');
|
||||
const [status, setStatus] = useState('');
|
||||
const recentProjects = useRecentProjects(setStatus);
|
||||
const { recentProjectRows, rememberRecentWorkspace } = recentProjects;
|
||||
const [launcherView, setLauncherView] = useState<LauncherView>(initialView);
|
||||
@@ -273,7 +273,6 @@ export function WorkspaceLauncherShell({
|
||||
{launcherView === 'home' ? (
|
||||
<HomeView
|
||||
hasPromo={launcherNotifications.length > 0}
|
||||
status={status}
|
||||
onStatusChange={setStatus}
|
||||
recentProjectRows={recentProjectRows}
|
||||
onCreateDraftAutomatically={createHomeDraftAutomatically}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Send } from 'lucide-react';
|
||||
import { ArrowUp } from 'lucide-react';
|
||||
import type {
|
||||
ComponentProps,
|
||||
Dispatch,
|
||||
@@ -251,14 +251,7 @@ export function ProjectSupervisorView({
|
||||
type="submit"
|
||||
disabled={runtimePanelProps.controlBusy || needsUserInput}
|
||||
>
|
||||
<Send size={16} aria-hidden="true" />
|
||||
<span>
|
||||
{needsUserInput
|
||||
? '等待回答'
|
||||
: runtimePanelProps.controlBusy
|
||||
? '思考中'
|
||||
: '发送'}
|
||||
</span>
|
||||
<ArrowUp size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</form>
|
||||
<small className="project-supervisor-workspace-status">
|
||||
|
||||
@@ -5739,6 +5739,24 @@ iframe.preview-frame {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.game-resource-outline button .game-resource-outline-label {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.game-resource-outline-unread {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -6px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border: 1px solid var(--platform-nav-fill);
|
||||
border-radius: 50%;
|
||||
background: #e5484d;
|
||||
box-shadow: 0 0 0 1px rgb(229 72 77 / 18%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.game-resource-page {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
@@ -6879,6 +6897,7 @@ iframe.preview-frame {
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 156px;
|
||||
padding: 12px;
|
||||
border: 1px solid #eadbd4;
|
||||
border-radius: 12px;
|
||||
@@ -7022,12 +7041,260 @@ iframe.preview-frame {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.game-workbench-chat-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
.game-workbench-chat .supervisor-chat-only-runtime-controls {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.game-workbench-chat
|
||||
.supervisor-chat-only-runtime-controls
|
||||
.pending-command {
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 10px;
|
||||
background: var(--platform-warm-bg);
|
||||
}
|
||||
|
||||
.game-workbench-chat
|
||||
.supervisor-chat-only-runtime-controls
|
||||
.pending-command
|
||||
> span {
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.game-workbench-chat
|
||||
.supervisor-chat-only-runtime-controls
|
||||
.pending-command
|
||||
> span
|
||||
small {
|
||||
color: var(--platform-text-soft);
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.game-workbench-chat .pending-command-actions button {
|
||||
min-width: 52px;
|
||||
height: 34px;
|
||||
flex: 0 0 auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 5px 9px rgb(182 98 63 / 16%));
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
background: var(--platform-button-secondary-fill);
|
||||
color: var(--platform-button-secondary-text);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.game-workbench-chat .pending-command-actions button:last-child {
|
||||
border-color: var(--platform-button-primary-border);
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
}
|
||||
|
||||
.agent-runtime-status .project-runtime-pending-command > span {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.agent-runtime-status .project-runtime-pending-command > span small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--platform-text-soft);
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.agent-runtime-status .project-runtime-pending-command .pending-command-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.agent-runtime-status
|
||||
.project-runtime-pending-command
|
||||
.pending-command-actions
|
||||
button:last-child {
|
||||
border-color: var(--platform-button-primary-border);
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
}
|
||||
|
||||
.game-workbench-chat
|
||||
.agent-runtime-status
|
||||
.project-runtime-pending-command {
|
||||
grid-template-columns: minmax(0, 1fr) 136px;
|
||||
}
|
||||
|
||||
.game-workbench-chat
|
||||
.agent-runtime-status
|
||||
.project-runtime-pending-command
|
||||
.pending-command-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 64px);
|
||||
gap: 8px;
|
||||
width: 136px;
|
||||
flex: 0 0 136px;
|
||||
}
|
||||
|
||||
.game-workbench-chat
|
||||
.agent-runtime-status
|
||||
.project-runtime-pending-command
|
||||
.pending-command-actions
|
||||
button {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
max-width: 64px;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Runtime 确认卡在部分项目状态下位于状态包装层外,工作台样式直接绑定卡片自身,避免回落旧的蓝色按钮样式。 */
|
||||
.game-workbench-chat .project-runtime-pending-command {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 136px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 10px;
|
||||
background: var(--platform-warm-bg);
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-surface > .project-runtime-pending-command,
|
||||
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 136px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 10px;
|
||||
background: var(--platform-warm-bg);
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command > span {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command > span small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 64px);
|
||||
gap: 8px;
|
||||
width: 136px;
|
||||
flex: 0 0 136px;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions button {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
max-width: 64px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 8px;
|
||||
background: var(--platform-button-secondary-fill);
|
||||
color: var(--platform-button-secondary-text);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions button:last-child {
|
||||
border-color: var(--platform-button-primary-border);
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
}
|
||||
|
||||
/* 该卡片的两个操作按钮禁止被全局 button 规则压缩,确保“拒绝 / 确认”完整可见。 */
|
||||
.project-runtime-pending-command .pending-command-actions button {
|
||||
display: inline-flex !important;
|
||||
width: 64px !important;
|
||||
min-width: 64px !important;
|
||||
max-width: 64px !important;
|
||||
flex: 0 0 64px !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 !important;
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-runtime-pending-command > span {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-runtime-pending-command > span small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-runtime-pending-command .pending-command-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 64px);
|
||||
gap: 8px;
|
||||
width: 136px;
|
||||
flex: 0 0 136px;
|
||||
}
|
||||
|
||||
.game-workbench-chat
|
||||
.project-runtime-pending-command
|
||||
.pending-command-actions
|
||||
button {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
max-width: 64px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 8px;
|
||||
background: var(--platform-button-secondary-fill);
|
||||
color: var(--platform-button-secondary-text);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.game-workbench-chat
|
||||
.project-runtime-pending-command
|
||||
.pending-command-actions
|
||||
button:last-child {
|
||||
border-color: var(--platform-button-primary-border);
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-surface {
|
||||
@@ -7281,6 +7548,100 @@ iframe.preview-frame {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 右侧对话采用简洁的 Codex 式消息流与编辑框,减少卡片嵌套和视觉噪音。 */
|
||||
.game-workbench-chat .project-supervisor-message-list {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 104px;
|
||||
left: 0;
|
||||
gap: 34px;
|
||||
height: auto;
|
||||
padding: 18px 20px 24px;
|
||||
scroll-padding-bottom: 24px;
|
||||
border: 1px solid var(--platform-subpanel-border);
|
||||
border-radius: 12px;
|
||||
background: var(--platform-input-fill);
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-message-list .message {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 4px 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--platform-text-base);
|
||||
font-size: 14px;
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-message-list .message + .message {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-message-list .message--user {
|
||||
width: fit-content;
|
||||
max-width: min(82%, 640px);
|
||||
margin-left: auto;
|
||||
padding: 11px 16px;
|
||||
border: 1px solid var(--platform-button-primary-border);
|
||||
border-radius: 14px 14px 4px;
|
||||
background: var(--platform-warm-bg);
|
||||
color: var(--platform-text-strong);
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-composer {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
padding: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-composer textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 88px;
|
||||
max-height: 180px;
|
||||
padding: 14px 48px 38px 15px;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 14px;
|
||||
background: var(--platform-input-fill);
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
resize: vertical;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-composer button {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
display: grid;
|
||||
width: 30px;
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
place-items: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.game-workbench-chat .project-supervisor-composer button svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.game-agent-dock {
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
@@ -7691,6 +8052,7 @@ iframe.preview-frame {
|
||||
.game-workbench-view-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
@@ -7721,6 +8083,11 @@ iframe.preview-frame {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.game-workbench-view-actions .game-workbench-play-button {
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.game-workbench-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -93,7 +93,6 @@ function projectTypeLabel(projectKind: HomeProjectRow['projectKind']) {
|
||||
|
||||
type HomeViewProps = {
|
||||
hasPromo: boolean;
|
||||
status: string;
|
||||
onStatusChange: (status: string) => void;
|
||||
recentProjectRows: readonly HomeProjectRow[];
|
||||
onCreateDraftAutomatically: (
|
||||
@@ -107,7 +106,6 @@ type HomeViewProps = {
|
||||
|
||||
export default function HomeView({
|
||||
hasPromo,
|
||||
status,
|
||||
onStatusChange,
|
||||
recentProjectRows,
|
||||
onCreateDraftAutomatically,
|
||||
@@ -250,11 +248,6 @@ export default function HomeView({
|
||||
</div>
|
||||
</RichInputArea>
|
||||
</form>
|
||||
{status ? (
|
||||
<span className="w-[min(814px,calc(100vw-122px))] text-left text-[12px] text-(--platform-text-soft) max-[760px]:w-[min(100%,calc(100vw-76px))]">
|
||||
{status}
|
||||
</span>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section
|
||||
@@ -266,9 +259,6 @@ export default function HomeView({
|
||||
<h2 className="m-0 text-[15px] text-(--platform-text-strong)">
|
||||
最近项目
|
||||
</h2>
|
||||
<p className="mb-0 mt-1 text-[12px] text-(--platform-text-soft)">
|
||||
选择一个项目继续创作
|
||||
</p>
|
||||
</div>
|
||||
<div className="launcher-project-list-actions">
|
||||
<button
|
||||
|
||||
@@ -45,7 +45,6 @@ import type {
|
||||
GameIterationVersion,
|
||||
ProjectResourceCanvasLayoutMode,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import PRODUCT_LOGO from '../../../../../packages/shared/src/icons/taonier-product-ip.png';
|
||||
import { canonicalAssetBaseName } from '../../features/asset-canvas/assetCanvasNaming';
|
||||
import {
|
||||
type AssetCanvasCommitNotification,
|
||||
@@ -841,6 +840,15 @@ export default function ProjectDevelopmentView({
|
||||
useState<ResourceCanvasViewportBySortMode>(defaultResourceCanvasViewports);
|
||||
const [activeResourceCategory, setActiveResourceCategory] =
|
||||
useState<ResourceCategory | null>(null);
|
||||
const [unreadResourceCategoryState, setUnreadResourceCategoryState] =
|
||||
useState<{
|
||||
scopeKey: string;
|
||||
categories: Set<ResourceCategory>;
|
||||
}>(() => ({ scopeKey: '', categories: new Set() }));
|
||||
const resourceCategorySnapshotRef = useRef<{
|
||||
scopeKey: string;
|
||||
resourceIdsByCategory: Map<ResourceCategory, Set<string>>;
|
||||
} | null>(null);
|
||||
const [selectedResourceId, setSelectedResourceId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
@@ -880,8 +888,10 @@ export default function ProjectDevelopmentView({
|
||||
const [hiddenCommittedResourceId, setHiddenCommittedResourceId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [resourcePreviewVersionByResourceId, setResourcePreviewVersionByResourceId] =
|
||||
useState<Map<string, string>>(() => new Map());
|
||||
const [
|
||||
resourcePreviewVersionByResourceId,
|
||||
setResourcePreviewVersionByResourceId,
|
||||
] = useState<Map<string, string>>(() => new Map());
|
||||
const [approvalMode, setApprovalMode] = useState<ApprovalMode>('strict');
|
||||
const [approvalDialogOpen, setApprovalDialogOpen] = useState(false);
|
||||
const [approvalNotice, setApprovalNotice] = useState('');
|
||||
@@ -1388,6 +1398,24 @@ export default function ProjectDevelopmentView({
|
||||
),
|
||||
[resources],
|
||||
);
|
||||
const resourceIdsByCategory = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
categoryOrder.map((category) => [
|
||||
category,
|
||||
new Set(
|
||||
(projectResourcesByCategory.get(category) ?? []).map(
|
||||
(resource) => resource.id,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
[projectResourcesByCategory],
|
||||
);
|
||||
const resourceCategoryScopeKey = useMemo(
|
||||
() => JSON.stringify([projectPath, manifest.projectId]),
|
||||
[manifest.projectId, projectPath],
|
||||
);
|
||||
const resourcePageCategories = useMemo(
|
||||
() => (resources.length > 0 ? categoryOrder : []),
|
||||
[resources.length],
|
||||
@@ -1401,6 +1429,63 @@ export default function ProjectDevelopmentView({
|
||||
) ??
|
||||
resourcePageCategories[0] ??
|
||||
null;
|
||||
const viewedResourceCategory =
|
||||
mode === 'resources' ? activePageCategory : null;
|
||||
useEffect(() => {
|
||||
const previousSnapshot = resourceCategorySnapshotRef.current;
|
||||
if (
|
||||
!previousSnapshot ||
|
||||
previousSnapshot.scopeKey !== resourceCategoryScopeKey
|
||||
) {
|
||||
resourceCategorySnapshotRef.current = {
|
||||
scopeKey: resourceCategoryScopeKey,
|
||||
resourceIdsByCategory,
|
||||
};
|
||||
setUnreadResourceCategoryState({
|
||||
scopeKey: resourceCategoryScopeKey,
|
||||
categories: new Set(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const categoriesWithNewResources = categoryOrder.filter((category) => {
|
||||
if (category === viewedResourceCategory) {
|
||||
return false;
|
||||
}
|
||||
const previousIds =
|
||||
previousSnapshot.resourceIdsByCategory.get(category) ?? new Set();
|
||||
return Array.from(resourceIdsByCategory.get(category) ?? []).some(
|
||||
(resourceId) => !previousIds.has(resourceId),
|
||||
);
|
||||
});
|
||||
resourceCategorySnapshotRef.current = {
|
||||
scopeKey: resourceCategoryScopeKey,
|
||||
resourceIdsByCategory,
|
||||
};
|
||||
setUnreadResourceCategoryState((current) => {
|
||||
const categories =
|
||||
current.scopeKey === resourceCategoryScopeKey
|
||||
? new Set(current.categories)
|
||||
: new Set<ResourceCategory>();
|
||||
let changed = current.scopeKey !== resourceCategoryScopeKey;
|
||||
if (viewedResourceCategory && categories.delete(viewedResourceCategory)) {
|
||||
changed = true;
|
||||
}
|
||||
categoriesWithNewResources.forEach((category) => {
|
||||
if (!categories.has(category)) {
|
||||
categories.add(category);
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
return changed
|
||||
? { scopeKey: resourceCategoryScopeKey, categories }
|
||||
: current;
|
||||
});
|
||||
}, [resourceCategoryScopeKey, resourceIdsByCategory, viewedResourceCategory]);
|
||||
const unreadResourceCategories =
|
||||
unreadResourceCategoryState.scopeKey === resourceCategoryScopeKey
|
||||
? unreadResourceCategoryState.categories
|
||||
: new Set<ResourceCategory>();
|
||||
resourceCanvasPageWheelRef.current.activeCategory = activePageCategory;
|
||||
const nextPageCategory =
|
||||
activePageCategory && resourcePageCategories.length > 1
|
||||
@@ -1515,8 +1600,7 @@ export default function ProjectDevelopmentView({
|
||||
: { x: 48, y: 48, scale: 1 };
|
||||
const setResourceCanvasViewport = useCallback(
|
||||
(candidate: CanvasViewport) => {
|
||||
const activeCategory =
|
||||
resourceCanvasPageWheelRef.current.activeCategory;
|
||||
const activeCategory = resourceCanvasPageWheelRef.current.activeCategory;
|
||||
if (!activeCategory) {
|
||||
return;
|
||||
}
|
||||
@@ -1800,8 +1884,7 @@ export default function ProjectDevelopmentView({
|
||||
|
||||
const advanceResourceCanvasPage = useCallback(
|
||||
(stepOffset: number) => {
|
||||
const currentCategory =
|
||||
resourceCanvasPageWheelRef.current.activeCategory;
|
||||
const currentCategory = resourceCanvasPageWheelRef.current.activeCategory;
|
||||
const normalizedStepOffset = Math.trunc(stepOffset);
|
||||
if (
|
||||
!currentCategory ||
|
||||
@@ -1849,8 +1932,7 @@ export default function ProjectDevelopmentView({
|
||||
return;
|
||||
}
|
||||
const now = performance.now();
|
||||
const waitMs =
|
||||
pageWheelSwitchIntervalMs - (now - state.lastSwitchAt);
|
||||
const waitMs = pageWheelSwitchIntervalMs - (now - state.lastSwitchAt);
|
||||
if (waitMs > 0) {
|
||||
if (state.pendingTimerId === null) {
|
||||
state.pendingTimerId = window.setTimeout(() => {
|
||||
@@ -1925,8 +2007,7 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
pageWheelState.accumulatedDeltaY += normalizedDeltaY;
|
||||
if (
|
||||
Math.abs(pageWheelState.accumulatedDeltaY) >=
|
||||
pageWheelDeltaThreshold
|
||||
Math.abs(pageWheelState.accumulatedDeltaY) >= pageWheelDeltaThreshold
|
||||
) {
|
||||
const stepCount = Math.max(
|
||||
1,
|
||||
@@ -2063,11 +2144,7 @@ export default function ProjectDevelopmentView({
|
||||
]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (
|
||||
mode !== 'resources' ||
|
||||
!activePageCategory ||
|
||||
!activePageLayoutReady
|
||||
) {
|
||||
if (mode !== 'resources' || !activePageCategory || !activePageLayoutReady) {
|
||||
return undefined;
|
||||
}
|
||||
const canvas = resourceCanvasRef.current;
|
||||
@@ -2752,9 +2829,7 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
drag.changed = true;
|
||||
const coordinate = (start: number, delta: number) =>
|
||||
normalizeResourceCanvasDragCoordinate(
|
||||
start + delta / drag.startScale,
|
||||
);
|
||||
normalizeResourceCanvasDragCoordinate(start + delta / drag.startScale);
|
||||
const preview = {
|
||||
resourceId: drag.resourceId,
|
||||
x: coordinate(drag.startX, deltaX),
|
||||
@@ -3944,27 +4019,37 @@ export default function ProjectDevelopmentView({
|
||||
data-unavailable={!runAvailable || uiEditorRoute || undefined}
|
||||
className={mode === 'run' ? 'is-active' : ''}
|
||||
disabled={uiEditorRoute !== null}
|
||||
onClick={showRunView}
|
||||
onClick={() => {
|
||||
if (!runAvailable) {
|
||||
return;
|
||||
}
|
||||
showRunView();
|
||||
onPlay?.();
|
||||
}}
|
||||
>
|
||||
运行
|
||||
</button>
|
||||
</div>
|
||||
<div className="game-workbench-view-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="game-workbench-play-button"
|
||||
disabled={!runAvailable || !onPlay || uiEditorRoute !== null}
|
||||
onClick={() => {
|
||||
showRunView();
|
||||
onPlay?.();
|
||||
}}
|
||||
title={
|
||||
runAvailable ? '运行当前游戏' : '首个可运行原型完成后才能播放'
|
||||
}
|
||||
>
|
||||
<Play size={15} aria-hidden="true" />
|
||||
播放
|
||||
</button>
|
||||
{mode === 'run' ? (
|
||||
<button
|
||||
type="button"
|
||||
className="game-workbench-play-button"
|
||||
disabled={!runAvailable || !onPlay || uiEditorRoute !== null}
|
||||
onClick={() => {
|
||||
showRunView();
|
||||
onPlay?.();
|
||||
}}
|
||||
title={
|
||||
runAvailable
|
||||
? '运行当前游戏'
|
||||
: '首个可运行原型完成后才能播放'
|
||||
}
|
||||
>
|
||||
<Play size={15} aria-hidden="true" />
|
||||
播放
|
||||
</button>
|
||||
) : null}
|
||||
{mode === 'resources' &&
|
||||
!assetCanvasRoute &&
|
||||
!resourceEditorRoute &&
|
||||
@@ -4506,10 +4591,15 @@ export default function ProjectDevelopmentView({
|
||||
>
|
||||
{resourcePageCategories.map((category) => {
|
||||
const Icon = categoryIcons[category];
|
||||
const hasUnreadResources =
|
||||
unreadResourceCategories.has(category);
|
||||
return (
|
||||
<button
|
||||
key={category}
|
||||
type="button"
|
||||
aria-label={`${categoryLabels[category]}${
|
||||
hasUnreadResources ? ',有新资源' : ''
|
||||
}`}
|
||||
aria-current={
|
||||
category === activePageCategory
|
||||
? 'page'
|
||||
@@ -4525,7 +4615,15 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
>
|
||||
<Icon size={14} aria-hidden="true" />
|
||||
<strong>{categoryLabels[category]}</strong>
|
||||
<strong className="game-resource-outline-label">
|
||||
{categoryLabels[category]}
|
||||
{hasUnreadResources ? (
|
||||
<span
|
||||
className="game-resource-outline-unread"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
</strong>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -4787,29 +4885,13 @@ export default function ProjectDevelopmentView({
|
||||
<dd>{selectedResource.mediaType}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
) : (
|
||||
<p>暂停后选择资源可查看已登记信息</p>
|
||||
)}
|
||||
) : null}
|
||||
</section>
|
||||
<section aria-label="数值微调面板">
|
||||
<header>
|
||||
<SlidersHorizontal size={16} aria-hidden="true" />
|
||||
数值微调
|
||||
</header>
|
||||
<label>
|
||||
角色移动速度
|
||||
<input type="number" disabled placeholder="未载入" />
|
||||
</label>
|
||||
<label>
|
||||
跳跃高度
|
||||
<input type="number" disabled placeholder="未载入" />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
aria-label="新增微调数值项"
|
||||
placeholder="用自然语言新增微调项(尚未接入)"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
@@ -4819,11 +4901,6 @@ export default function ProjectDevelopmentView({
|
||||
{!uiEditorRoute ? (
|
||||
<aside className="game-workbench-chat" aria-label="陶泥儿 Agent 对话">
|
||||
<header>
|
||||
<img
|
||||
className="game-workbench-chat-avatar"
|
||||
src={PRODUCT_LOGO}
|
||||
alt="陶泥儿"
|
||||
/>
|
||||
<div className="game-workbench-chat-title">
|
||||
<strong>与陶泥儿的对话</strong>
|
||||
<small>
|
||||
|
||||
@@ -1335,7 +1335,7 @@ export function registerHomeProjectCreationTests() {
|
||||
expect(screen.getAllByText('今天有什么设计需要帮你整理')).toHaveLength(1);
|
||||
// 做方案走立项策划链路,建项按钮在这一档改叫「进入立项策划」。
|
||||
fireEvent.click(screen.getByRole('button', { name: '进入立项策划' }));
|
||||
expect(screen.getByText('请输入方案需求或上传资料')).not.toBeNull();
|
||||
expect(screen.queryByText('请输入方案需求或上传资料')).toBeNull();
|
||||
expect(invoke).not.toHaveBeenCalledWith(
|
||||
'create_automatic_local_game_project',
|
||||
);
|
||||
@@ -1510,12 +1510,13 @@ export function registerHomeProjectCreationTests() {
|
||||
});
|
||||
expect((createButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(screen.queryByLabelText('陶泥儿首页对话')).toBeNull();
|
||||
expect(screen.getByLabelText('最近项目').textContent).toContain(
|
||||
expect(screen.getByLabelText('最近项目').textContent).not.toContain(
|
||||
'选择一个项目继续创作',
|
||||
);
|
||||
expect(screen.getByLabelText('最近项目').textContent).not.toContain(
|
||||
'正在创建工作区',
|
||||
);
|
||||
expect(screen.queryByText('正在创建工作区')).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
rejectAutomaticProject?.(new Error('自动创建测试结束'));
|
||||
@@ -1524,7 +1525,7 @@ export function registerHomeProjectCreationTests() {
|
||||
await waitFor(() => {
|
||||
expect((createButton as HTMLButtonElement).disabled).toBe(false);
|
||||
});
|
||||
expect(screen.getAllByText('自动创建测试结束')).not.toHaveLength(0);
|
||||
expect(screen.queryByText('自动创建测试结束')).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -1387,6 +1387,174 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
expect(screen.queryByRole('button', { name: /放大.*内容/ })).toBeNull();
|
||||
});
|
||||
|
||||
it('marks newly added resources on inactive section tabs until the user opens them', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-resource-unread',
|
||||
'资源未读标识测试',
|
||||
);
|
||||
manifest.assets = [
|
||||
{
|
||||
id: 'unread-art-initial',
|
||||
kind: 'character',
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/initial.png',
|
||||
source: { kind: 'generated' },
|
||||
},
|
||||
];
|
||||
let layoutRevision = 0;
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'read_local_project_resource_graph') {
|
||||
return resourceGraphForInputs(args);
|
||||
}
|
||||
if (command === 'read_local_project_resource_canvas_layout') {
|
||||
return {
|
||||
schemaVersion: 'game-creator-resource-layout.v1',
|
||||
projectId: args?.expectedProjectId,
|
||||
mode: args?.mode,
|
||||
revision: layoutRevision,
|
||||
positions: [],
|
||||
updatedAt: layoutRevision,
|
||||
};
|
||||
}
|
||||
if (command === 'update_local_project_resource_canvas_layout') {
|
||||
layoutRevision += 1;
|
||||
return {
|
||||
status: 'updated',
|
||||
layout: {
|
||||
schemaVersion: 'game-creator-resource-layout.v1',
|
||||
projectId: args?.expectedProjectId,
|
||||
mode: args?.mode,
|
||||
revision: layoutRevision,
|
||||
positions: args?.positions,
|
||||
updatedAt: layoutRevision,
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
const viewProps = {
|
||||
projectName: manifest.name,
|
||||
projectPath: '/tmp/workbench-resource-unread',
|
||||
manifest,
|
||||
attachments: [],
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
supervisor: React.createElement('div', null, '项目总控'),
|
||||
onHomeOpen: vi.fn(),
|
||||
onProjectsOpen: vi.fn(),
|
||||
};
|
||||
const rendered = render(
|
||||
React.createElement(ProjectDevelopmentView, viewProps),
|
||||
);
|
||||
const outline = await screen.findByLabelText('资源栏目大纲');
|
||||
|
||||
expect(
|
||||
within(outline).queryByRole('button', { name: /有新资源/ }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
within(outline)
|
||||
.getByRole('button', { name: '美术资源' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
|
||||
const manifestWithCurrentArt = {
|
||||
...manifest,
|
||||
assets: [
|
||||
...manifest.assets,
|
||||
{
|
||||
id: 'unread-art-current',
|
||||
kind: 'character' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/current.png',
|
||||
source: { kind: 'generated' as const },
|
||||
},
|
||||
],
|
||||
};
|
||||
rendered.rerender(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
...viewProps,
|
||||
manifest: manifestWithCurrentArt,
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
within(outline).queryByRole('button', { name: /有新资源/ }),
|
||||
).toBeNull();
|
||||
|
||||
const manifestWithAudio = {
|
||||
...manifestWithCurrentArt,
|
||||
assets: [
|
||||
...manifestWithCurrentArt.assets,
|
||||
{
|
||||
id: 'unread-audio-new',
|
||||
kind: 'background-music' as const,
|
||||
mediaType: 'audio/mpeg',
|
||||
localPath: 'assets/new.mp3',
|
||||
source: { kind: 'generated' as const },
|
||||
},
|
||||
],
|
||||
};
|
||||
rendered.rerender(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
...viewProps,
|
||||
manifest: manifestWithAudio,
|
||||
}),
|
||||
);
|
||||
|
||||
const unreadAudioTab = await within(outline).findByRole('button', {
|
||||
name: '音乐音效,有新资源',
|
||||
});
|
||||
expect(
|
||||
unreadAudioTab.querySelector('.game-resource-outline-unread'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
within(outline).queryByRole('button', { name: '美术资源,有新资源' }),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.click(unreadAudioTab);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(outline)
|
||||
.getByRole('button', { name: '音乐音效' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
expect(
|
||||
within(outline).queryByRole('button', { name: /有新资源/ }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
const nextProjectManifest = createGameCreationAppManifest(
|
||||
'workbench-resource-unread-next',
|
||||
'下一个资源项目',
|
||||
);
|
||||
nextProjectManifest.assets = [
|
||||
{
|
||||
id: 'next-project-code',
|
||||
kind: 'game-code',
|
||||
mediaType: 'text/javascript',
|
||||
localPath: 'game/game.js',
|
||||
source: { kind: 'generated' },
|
||||
},
|
||||
];
|
||||
rendered.rerender(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
...viewProps,
|
||||
projectName: nextProjectManifest.name,
|
||||
projectPath: '/tmp/workbench-resource-unread-next',
|
||||
manifest: nextProjectManifest,
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(screen.getByLabelText('资源栏目大纲')).queryByRole('button', {
|
||||
name: /有新资源/,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the empty resource overview identical in both modes', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-empty-section-overview',
|
||||
@@ -4814,7 +4982,7 @@ export function registerUserSurfaceBoundaryTests() {
|
||||
}
|
||||
|
||||
export function registerProjectSupervisorSurfaceTests() {
|
||||
it('routes a top workbench play request into the existing game.run_local confirmation', async () => {
|
||||
it('runs a top workbench play request without a second confirmation', async () => {
|
||||
const projectPath = '/tmp/top-play-request';
|
||||
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
||||
projectPath,
|
||||
@@ -4832,6 +5000,19 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
recentRunStopReason: null,
|
||||
};
|
||||
}
|
||||
if (command === 'start_local_game_preview') {
|
||||
return {
|
||||
url: 'http://127.0.0.1:43127/',
|
||||
port: 43127,
|
||||
root: projectPath,
|
||||
};
|
||||
}
|
||||
if (command === 'get_local_game_manifest') {
|
||||
return createGameCreationAppManifest(
|
||||
'top-play-request',
|
||||
'顶部播放请求',
|
||||
);
|
||||
}
|
||||
return supervisorHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
@@ -4851,14 +5032,17 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText('game.run_local', {}, { timeout: 3_000 }),
|
||||
await screen.findByText(
|
||||
/运行通过,已载入客户端运行视图/,
|
||||
{},
|
||||
{ timeout: 3_000 },
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(handled).toHaveBeenCalledWith(7);
|
||||
expect(screen.getByText(`启动 ${projectPath}/game/`)).not.toBeNull();
|
||||
expect(invoke).not.toHaveBeenCalledWith(
|
||||
'start_local_game_preview',
|
||||
expect.anything(),
|
||||
);
|
||||
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
|
||||
projectPath,
|
||||
});
|
||||
expect(screen.queryByText('game.run_local')).toBeNull();
|
||||
});
|
||||
|
||||
it('starts a direct product preview without the legacy Canvas-only static smoke', async () => {
|
||||
@@ -4899,9 +5083,6 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('game.run_local')).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'运行通过,已载入客户端运行视图:http://127.0.0.1:43126/',
|
||||
|
||||
@@ -116,8 +116,10 @@
|
||||
- 资源总览的“资源依赖 / 资源类型”视图切换使用连通的分段按钮组,相邻选项共享边界并保持唯一选中语义。每个分段都必须有清晰的键盘焦点指示,焦点环不得被分段容器的圆角或 `overflow` 裁切。
|
||||
- 右侧 Supervisor 对话中,用户消息使用右对齐、最大宽度受限的主题暖色气泡,assistant 消息保持左对齐;消息换行不得产生水平溢出,执行过程卡继续占满消息区可用宽度。气泡正文在 light / dark 平台主题下均须满足 WCAG AA 普通文本 `4.5:1` 对比度。
|
||||
- 客户端正式产品仍只按最小 `1280×720` 横屏合同交付,并保留 `1280×800` 默认窗口与既有基线验收;更窄浏览器样式只负责不崩溃和开发兼容,不改成移动端创作工作台。
|
||||
- 工作台顶部播放按钮在桌面视窗中水平居中;资源管理视窗触发播放时直接切换到运行视窗并启动本地预览,不再弹出 `game.run_local` 二次确认。
|
||||
- 普通用户界面不默认展示内部错误码或本机绝对路径,也不展示 External Editor Base URL 或 Developer API Key;官方服务地址由客户端构建固定,远端编辑自动使用当前陶泥儿登录态。仅独立 standalone game-chat release 或显式高级自定义模式在隔离配置页维护 External v1 URL/Key,确需诊断的信息进入受控详情或开发模式。
|
||||
- 创建模式素材画布的“素材名称”是用户可编辑的正式输出名称;“资源用途”是 manifest subtype,不向普通用户开放自由文本。新增资源默认“普通游戏美术”,可从普通游戏美术、统一视觉规范、游戏界面原型、核心美术图集四项中选择。图片精修继承源名称和用途,不显示创建模式保存设置;候选图片只从选中图片的“设为最终图”提交。精修顶栏只保留返回、导入、定位当前最终图、撤销和重做,删除进入图片上下文工具栏,通用 AI 生成只保留给创建模式。图片输出统一使用 PNG;创建模式工具动作与保存设置分层展示,“保存到项目”在 `1280×800` 和窄容器中都必须完整可见。
|
||||
- 首页创作输入区与“最近项目”之间不展示共享项目状态文本,“最近项目”标题下也不追加解释性副标题;默认、成功、进行中或失败状态均不得在该位置形成文字行,项目管理页继续保留自己的状态反馈。
|
||||
|
||||
### 3.8 现有 Godot 项目
|
||||
|
||||
@@ -319,6 +321,7 @@ type UpdateProjectResourceCanvasLayoutResult =
|
||||
- 搜索或筛选只隐藏卡片,不删除、压缩或重排其坐标;清空搜索后恢复原位置。
|
||||
- 窗口尺寸变化只改变当前栏目的可视范围,不回写或裁切持久坐标,也不因资源 extent 或 resize 把已平移的 viewport 拉回内容边界。当前客户端继续以 `1280×800` 横屏合同验收。
|
||||
- 任一栏目出现资源后,资源管理固定使用 `设计文档 -> 美术资源 -> 音乐音效 -> 游戏代码 -> 项目版本` 五栏目分页画布;每个栏目按 `projectId + mode + category` 保留独立 viewport。普通 wheel 切换栏目,`Ctrl/Cmd + wheel` 以指针位置为锚点缩放当前无限画布,空白拖动只平移当前栏目;非空状态不提供分区高度、分区内部滚动或分区内容倍率。搜索和详情开关不得重置 viewport,项目、mode 或栏目切换只恢复各自会话状态,显式复位才重新适配当前栏目内容。
|
||||
- 首次载入项目中的既有资源不显示未读标识。当前会话内,非当前栏目出现稳定 ID 的新资源时,在对应栏目名称右上角显示红点;当前栏目新增资源不显示红点,用户通过点击、滚轮或程序跳转进入该栏目后立即清除。未读状态只属于当前前端会话,并按 `projectPath + projectId` 隔离,切换项目时清空,不写入 manifest、布局 sidecar 或后端。
|
||||
- 打开项目、切换 mode 或当前 mode 首次出现新资源时执行“读取 -> 协调 -> 必要时 CAS 写入”;dependency 模式必须先等待与当前 `projectPath + projectId + resource inputs` 匹配的 Rust 图进入 `ready` 或 `failed` 终态,等待期间不得创建 fallback、读取 sidecar、协调资源或入队保存。`failed` 只允许以空图降级初始化一次。项目或 mode 已切换后返回的旧异步结果必须丢弃。
|
||||
- 同一 `projectPath + projectId + mode` 的首次读取与资源集合协调必须分开:资源集合变化不得取消已经发出的读取或保存。当前 scope 内资源自动协调写入使用单写者 FIFO,任一时刻最多一个 CAS 在途,后一笔必须使用前一笔成功返回的 revision。切换项目或 mode 后,旧 scope 的在途请求不能阻塞新 scope 队列;前端放弃旧请求槽位并丢弃其迟到响应,后端继续依靠 `expectedProjectId + expectedRevision + 系统锁` 仲裁已发出的请求。
|
||||
- 自动协调 CAS 冲突时直接载入返回的最新布局;仍需协调时可以基于权威 revision 最多追加 `2` 次重试,持续跨窗口写入时不得无限自旋。当前提示只说明“布局已在其他窗口更新”,不得要求用户重新拖动。
|
||||
|
||||
@@ -578,6 +578,7 @@ game-project/
|
||||
- Tauri 命令固定为 `read_local_project_resource_canvas_layout` 与 `update_local_project_resource_canvas_layout`。写命令先只读确认有效 manifest,再通过持久 `.layout.lock` 入口获取句柄级跨窗口系统锁,并在锁内复核 projectId、重新读取当前 sidecar。Unix 使用 `flock`,Windows 使用不共享文件句柄;释放只通过句柄 Drop / 进程退出完成,不使用 mtime stale 回收,也不删除锁文件。其余写入继续复用安全路径、链接检查、容量上限、恢复副本与原子替换能力;不能只依赖 React 状态或进程内锁。
|
||||
- 前端从当前项目开发大组件中拆出纯布局模型与持久 Hook。默认布局、碰撞检查、资源增删协调和 section 边界由纯模型负责;读取、异步身份、CAS、错误回滚和冲突载入由 Hook 负责。Hook 以 `projectPath + projectId + mode` epoch 隔离异步结果,资源变化不取消首读或在途保存;自动协调写入经同一 FIFO 串行提交,每笔都使用最近一次成功 / 冲突响应的权威 revision。资源卡是普通可点击按钮,不绑定卡片级 Pointer 拖动处理器;指针移动不修改 CSS 坐标、不调用 SVG preview、不提交手动布局 CAS。
|
||||
- 新资源只在第一次进入某个 mode 时计算默认不重叠位置;全部现存坐标保持不变。搜索、筛选、窗口 resize 和 mode 切换不得重排或回写已有坐标,窄视图通过 section 画布范围与滚动访问,不裁切持久坐标。
|
||||
- 五栏目页签的未读红点由 `ProjectDevelopmentView` 在会话内比较各栏目稳定资源 ID 派生:首次快照只建立基线,只有非当前栏目后续出现新 ID 才加入未读集合;所有栏目切换路径统一以当前栏目变化清除对应未读,项目 scope 变化时重建基线并清空。该状态以 `projectPath + projectId` 隔离,不进入 manifest、布局 sidecar、localStorage、sessionStorage 或后端契约。
|
||||
- type 默认布局固定按 `subtype -> mediaType -> label -> id` 排序。manifest 资产的 subtype 使用 `asset.kind`,任务产物、导入附件和 Agent 文本成果使用稳定的来源 fallback;subtype 必须进入资源协调签名,不能因 MIME 相同而退化成按名称混排。
|
||||
- 自动协调保存失败时保留当前会话布局;CAS 冲突载入对方最新布局,需要继续协调时最多追加两次重试,持续跨窗口竞争时停止自旋。用户提示只说明“布局已在其他窗口更新”,不要求重新拖动。损坏、未知 schema、身份冲突、超限与链接文件失败关闭,不能用空布局覆盖原文件。
|
||||
- 本布局持久化切片不包含资源关系线、资源替换、聚焦态持久化、分区高度、分区内容倍率、整个画布平移、搜索 / 筛选条件、当前 mode,也不修改 `api-server` 或 SpacetimeDB。资源关系线、当前会话内中央聚焦、分区独立高度与内容倍率已在后续独立前端切片接入,都不改变本段 sidecar 合同;其余 P1 能力继续独立实施。
|
||||
|
||||
@@ -174,7 +174,7 @@ export function collectNpmWorkspaceErrors(rootDir) {
|
||||
);
|
||||
}
|
||||
const expectedWorkspaceVersion =
|
||||
workspacePath === 'apps/ai-game-creator-shell' ? '0.1.2' : '0.1.0';
|
||||
workspacePath === 'apps/ai-game-creator-shell' ? '0.1.4' : '0.1.0';
|
||||
if (manifest.version !== expectedWorkspaceVersion) {
|
||||
errors.push(
|
||||
`${manifestPath}: workspace version must be ${expectedWorkspaceVersion}`,
|
||||
|
||||
@@ -79,7 +79,7 @@ function createValidFixture() {
|
||||
name: workspaceNames[workspacePath],
|
||||
private: true,
|
||||
version:
|
||||
workspacePath === 'apps/ai-game-creator-shell' ? '0.1.2' : '0.1.0',
|
||||
workspacePath === 'apps/ai-game-creator-shell' ? '0.1.4' : '0.1.0',
|
||||
dependencies: localDependencies[workspacePath],
|
||||
};
|
||||
writeJson(rootDir, `${workspacePath}/package.json`, manifest);
|
||||
|
||||
Reference in New Issue
Block a user