Enforce Genarrative play-type SOP and update docs

Rewrite Genarrative play-type integration guidance across .codex and .hermes to define a platform-level SOP: default to form/image workbench, unify single-image asset slots (CreativeImageInputPanel), standardize series-material sheet->cut->transparent->OSS pipeline, and forbid copying legacy chat/agent workflows as the default. Add decision-log entry freezing the SOP and a pitfalls note warning against direct reuse of old play tools. Update CONTEXT.md and docs/README.md, add a new PRD file, and apply related small server-side changes (module-auth, spacetime-client mappers and runtime) to align back-end code with the new contracts and flows.
This commit is contained in:
2026-05-20 12:12:00 +08:00
parent f370539a6f
commit 3931442249
123 changed files with 15514 additions and 3419 deletions

View File

@@ -34,13 +34,19 @@ export type CreativeImageInputPanelProps = {
className?: string;
disabled?: boolean;
isSubmitting?: boolean;
mainImageMode?: 'edit' | 'preview';
canRemoveMainImage?: boolean;
canToggleAiRedraw?: boolean;
uploadedImageSrc: string;
uploadedImageAlt: string;
uploadedImageRefreshKey?: string | number | null;
mainImageMeta?: ReactNode;
mainImageInputId: string;
mainImageAccept?: string;
promptTextareaId: string;
prompt: string;
promptLabel: string;
promptAriaLabel?: string;
promptRows?: number;
aiRedraw: boolean;
promptReferenceImages: CreativeImageInputReferenceImage[];
@@ -69,13 +75,19 @@ export function CreativeImageInputPanel({
className = '',
disabled = false,
isSubmitting = false,
mainImageMode = 'edit',
canRemoveMainImage = true,
canToggleAiRedraw = true,
uploadedImageSrc,
uploadedImageAlt,
uploadedImageRefreshKey = null,
mainImageMeta = null,
mainImageInputId,
mainImageAccept = DEFAULT_IMAGE_ACCEPT,
promptTextareaId,
prompt,
promptLabel,
promptAriaLabel,
promptRows = 2,
aiRedraw,
promptReferenceImages,
@@ -100,9 +112,10 @@ export function CreativeImageInputPanel({
useState<CreativeImageInputReferenceImage | null>(null);
const [isRemoveImageConfirmOpen, setIsRemoveImageConfirmOpen] =
useState(false);
const showPrompt = !uploadedImageSrc || aiRedraw;
const showPrompt = mainImageMode === 'preview' || !uploadedImageSrc || aiRedraw;
const promptReferenceUploadDisabled =
disabled || promptReferenceImages.length >= promptReferenceLimit;
const canEditMainImage = mainImageMode === 'edit';
useEffect(() => {
if (uploadedImageSrc) {
@@ -144,33 +157,40 @@ export function CreativeImageInputPanel({
</div>
<div className="creative-image-input-panel__image-frame puzzle-image-card-frame flex min-h-0 flex-1 items-center justify-center">
<div className="creative-image-input-panel__image-card puzzle-image-upload-card relative aspect-square h-full max-h-full max-w-full overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/90 shadow-[0_12px_28px_rgba(15,23,42,0.08)] transition lg:h-auto lg:w-full">
<input
id={mainImageInputId}
type="file"
accept={mainImageAccept}
disabled={disabled}
aria-label={labels.uploadImage}
onChange={(event) => {
const file = event.currentTarget.files?.[0] ?? null;
event.currentTarget.value = '';
if (file) {
onMainImageFileSelect(file);
}
}}
className="sr-only"
/>
<label
htmlFor={mainImageInputId}
className={`absolute inset-0 z-0 cursor-pointer ${disabled ? 'cursor-not-allowed' : ''}`}
title={uploadedImageSrc ? labels.replaceImage : labels.uploadImage}
>
<span className="sr-only">
{uploadedImageSrc ? labels.replaceImage : labels.uploadImage}
</span>
</label>
{canEditMainImage ? (
<>
<input
id={mainImageInputId}
type="file"
accept={mainImageAccept}
disabled={disabled}
aria-label={labels.uploadImage}
onChange={(event) => {
const file = event.currentTarget.files?.[0] ?? null;
event.currentTarget.value = '';
if (file) {
onMainImageFileSelect(file);
}
}}
className="sr-only"
/>
<label
htmlFor={mainImageInputId}
className={`absolute inset-0 z-0 cursor-pointer ${disabled ? 'cursor-not-allowed' : ''}`}
title={
uploadedImageSrc ? labels.replaceImage : labels.uploadImage
}
>
<span className="sr-only">
{uploadedImageSrc ? labels.replaceImage : labels.uploadImage}
</span>
</label>
</>
) : null}
{uploadedImageSrc ? (
<ResolvedAssetImage
src={uploadedImageSrc}
refreshKey={uploadedImageRefreshKey}
alt={uploadedImageAlt}
className="pointer-events-none absolute inset-0 h-full w-full object-cover"
/>
@@ -182,7 +202,7 @@ export function CreativeImageInputPanel({
</span>
)}
<div className="pointer-events-none absolute inset-0 z-[1] bg-[linear-gradient(180deg,rgba(255,255,255,0.12)_0%,rgba(255,255,255,0.04)_42%,rgba(255,255,255,0.18)_100%)]" />
{onHistoryClick ? (
{canEditMainImage && onHistoryClick ? (
<button
type="button"
disabled={disabled}
@@ -197,7 +217,7 @@ export function CreativeImageInputPanel({
<span></span>
</button>
) : null}
{uploadedImageSrc ? (
{canEditMainImage && uploadedImageSrc && canToggleAiRedraw ? (
<label className="absolute bottom-3 left-3 z-10 inline-flex cursor-pointer items-center gap-2 rounded-full border border-white/80 bg-white/94 px-3 py-2 text-xs font-black text-[var(--platform-text-strong)] shadow-sm backdrop-blur">
<span>AI重绘</span>
<input
@@ -223,7 +243,7 @@ export function CreativeImageInputPanel({
</span>
</label>
) : null}
{uploadedImageSrc ? (
{canEditMainImage && uploadedImageSrc && canRemoveMainImage ? (
<button
type="button"
disabled={disabled}
@@ -234,7 +254,7 @@ export function CreativeImageInputPanel({
>
<Trash2 className="h-4 w-4" />
</button>
) : (
) : canEditMainImage && !uploadedImageSrc ? (
<label
htmlFor={mainImageInputId}
className={`absolute bottom-9 left-1/2 z-10 -translate-x-1/2 whitespace-nowrap text-center text-sm font-black text-[var(--platform-text-strong)] drop-shadow-[0_1px_0_rgba(255,255,255,0.82)] transition hover:text-[#ff4056] sm:bottom-10 ${
@@ -245,9 +265,10 @@ export function CreativeImageInputPanel({
>
{labels.emptyImageHint}
</label>
)}
) : null}
</div>
</div>
{mainImageMeta ? <div className="mt-3 shrink-0">{mainImageMeta}</div> : null}
</div>
{showPrompt ? (
@@ -267,7 +288,7 @@ export function CreativeImageInputPanel({
placeholder=""
onChange={(event) => onPromptChange(event.target.value)}
className="h-[6rem] min-h-[6rem] w-full resize-none rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 pb-14 text-base leading-6 text-[var(--platform-text-strong)] outline-none placeholder:text-zinc-400 sm:h-[7.5rem] sm:min-h-[7.5rem] lg:h-[9.25rem] lg:min-h-[9.25rem]"
aria-label={promptLabel}
aria-label={promptAriaLabel ?? promptLabel}
/>
{imageModelPicker}
{!uploadedImageSrc && onPromptReferenceFilesSelect ? (