Merge branch 'codex/backend-rewrite-spacetimedb' of http://82.157.175.59:3000/GenarrativeAI/Genarrative into codex/backend-rewrite-spacetimedb
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/* @vitest-environment jsdom */
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@@ -81,11 +81,12 @@ test('big fish workspace submits quick keyword fill request after two turns', as
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '补充剩余关键字' }));
|
||||
await user.click(screen.getByRole('button', { name: '补充剩余设定' }));
|
||||
|
||||
expect(onSubmitMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: '请补充剩余关键字。',
|
||||
text: '请补充剩余设定。',
|
||||
quickFillRequested: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -100,5 +101,5 @@ test('big fish workspace hides keyword fill before two turns', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: '补充剩余关键字' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '补充剩余设定' })).toBeNull();
|
||||
});
|
||||
|
||||
@@ -4,7 +4,12 @@ import type {
|
||||
ExecuteBigFishActionRequest,
|
||||
SendBigFishMessageRequest,
|
||||
} from '../../../packages/shared/src/contracts/bigFish';
|
||||
import { createCreationAgentClientMessageId } from '../../services/creation-agent';
|
||||
import {
|
||||
buildCreationAgentChatMessage,
|
||||
createCreationAgentChatQuickActions,
|
||||
createCreationAgentClientMessageId,
|
||||
resolveCreationAgentQuickActionMessage,
|
||||
} from '../../services/creation-agent';
|
||||
import {
|
||||
type CreationAgentAnchorView,
|
||||
type CreationAgentSessionView,
|
||||
@@ -84,35 +89,30 @@ export function BigFishAgentWorkspace({
|
||||
isStreamingReply={Boolean(streamingReplyText)}
|
||||
isBusy={isBusy}
|
||||
error={error}
|
||||
quickActions={[
|
||||
{
|
||||
key: 'summarize',
|
||||
label: '总结当前设定',
|
||||
},
|
||||
{
|
||||
key: 'quickFill',
|
||||
label: '补充剩余关键字',
|
||||
minTurn: 2,
|
||||
},
|
||||
]}
|
||||
quickActions={createCreationAgentChatQuickActions()}
|
||||
onBack={onBack}
|
||||
onSubmitText={(text) => {
|
||||
onSubmitMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('big-fish'),
|
||||
text,
|
||||
});
|
||||
onSubmitMessage(
|
||||
buildCreationAgentChatMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('big-fish'),
|
||||
text,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onPrimaryAction={() => {
|
||||
onExecuteAction({ action: 'big_fish_compile_draft' });
|
||||
}}
|
||||
onQuickAction={(action) => {
|
||||
onSubmitMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('big-fish'),
|
||||
text:
|
||||
action.key === 'quickFill'
|
||||
? '请补充剩余关键字。'
|
||||
: '请总结一下当前已经成形的大鱼吃小鱼设定。',
|
||||
});
|
||||
const quickActionMessage = resolveCreationAgentQuickActionMessage(
|
||||
action.key,
|
||||
'请总结一下当前已经成形的大鱼吃小鱼设定。',
|
||||
);
|
||||
onSubmitMessage(
|
||||
buildCreationAgentChatMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('big-fish'),
|
||||
...quickActionMessage,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ArrowLeft, Loader2 } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, type PointerEvent } from 'react';
|
||||
|
||||
import type {
|
||||
BigFishAssetSlotResponse,
|
||||
@@ -9,6 +9,12 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/bigFish';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
|
||||
type TouchOrigin = {
|
||||
pointerId: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
type BigFishRuntimeShellProps = {
|
||||
run: BigFishRuntimeSnapshotResponse | null;
|
||||
assetSlots?: BigFishAssetSlotResponse[];
|
||||
@@ -34,6 +40,20 @@ function normalizeVector(x: number, y: number) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDirectionFromOrigin(
|
||||
origin: TouchOrigin,
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
) {
|
||||
const deadZone = 12;
|
||||
const deltaX = clientX - origin.x;
|
||||
const deltaY = clientY - origin.y;
|
||||
if (Math.hypot(deltaX, deltaY) < deadZone) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
return normalizeVector(deltaX, deltaY);
|
||||
}
|
||||
|
||||
function projectEntity(
|
||||
entity: BigFishRuntimeEntityResponse,
|
||||
run: BigFishRuntimeSnapshotResponse,
|
||||
@@ -152,7 +172,8 @@ export function BigFishRuntimeShell({
|
||||
onBack,
|
||||
onSubmitInput,
|
||||
}: BigFishRuntimeShellProps) {
|
||||
const padRef = useRef<HTMLDivElement | null>(null);
|
||||
const stageRef = useRef<HTMLDivElement | null>(null);
|
||||
const [touchOrigin, setTouchOrigin] = useState<TouchOrigin | null>(null);
|
||||
const [stick, setStick] = useState({ x: 0, y: 0 });
|
||||
const stickRef = useRef(stick);
|
||||
|
||||
@@ -163,7 +184,7 @@ export function BigFishRuntimeShell({
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
const current = stickRef.current;
|
||||
// 即使摇杆静止也持续回传当前输入,让后端持续推进刷怪、清理与胜负裁决。
|
||||
// 即使没有方向输入也持续回传当前状态,让后端持续推进刷怪、清理与胜负裁决。
|
||||
onSubmitInput(current);
|
||||
}, 220);
|
||||
|
||||
@@ -172,20 +193,39 @@ export function BigFishRuntimeShell({
|
||||
};
|
||||
}, [onSubmitInput]);
|
||||
|
||||
const updateStickFromPointer = (clientX: number, clientY: number) => {
|
||||
const pad = padRef.current;
|
||||
if (!pad) {
|
||||
const submitDirection = (direction: SubmitBigFishInputRequest) => {
|
||||
setStick(direction);
|
||||
onSubmitInput(direction);
|
||||
};
|
||||
|
||||
const beginTouchControl = (event: PointerEvent<HTMLDivElement>) => {
|
||||
if (event.target instanceof HTMLElement && event.target.closest('button')) {
|
||||
return;
|
||||
}
|
||||
const rect = pad.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
const vector = normalizeVector(
|
||||
(clientX - centerX) / (rect.width / 2),
|
||||
(clientY - centerY) / (rect.height / 2),
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
setTouchOrigin({
|
||||
pointerId: event.pointerId,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
submitDirection({ x: 0, y: 0 });
|
||||
};
|
||||
|
||||
const updateTouchControl = (event: PointerEvent<HTMLDivElement>) => {
|
||||
if (!touchOrigin || touchOrigin.pointerId !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
submitDirection(
|
||||
resolveDirectionFromOrigin(touchOrigin, event.clientX, event.clientY),
|
||||
);
|
||||
setStick(vector);
|
||||
onSubmitInput(vector);
|
||||
};
|
||||
|
||||
const endTouchControl = (event: PointerEvent<HTMLDivElement>) => {
|
||||
if (!touchOrigin || touchOrigin.pointerId !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
setTouchOrigin(null);
|
||||
submitDirection({ x: 0, y: 0 });
|
||||
};
|
||||
|
||||
if (!run) {
|
||||
@@ -206,7 +246,14 @@ export function BigFishRuntimeShell({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex justify-center bg-slate-950 text-white">
|
||||
<div className="relative h-full w-full max-w-[430px] overflow-hidden bg-[radial-gradient(circle_at_50%_20%,rgba(34,211,238,0.2),transparent_28%),radial-gradient(circle_at_20%_80%,rgba(16,185,129,0.18),transparent_26%),linear-gradient(180deg,#082f49,#020617)]">
|
||||
<div
|
||||
ref={stageRef}
|
||||
className="relative h-full w-full max-w-[430px] touch-none overflow-hidden bg-[radial-gradient(circle_at_50%_20%,rgba(34,211,238,0.2),transparent_28%),radial-gradient(circle_at_20%_80%,rgba(16,185,129,0.18),transparent_26%),linear-gradient(180deg,#082f49,#020617)]"
|
||||
onPointerDown={beginTouchControl}
|
||||
onPointerMove={updateTouchControl}
|
||||
onPointerUp={endTouchControl}
|
||||
onPointerCancel={endTouchControl}
|
||||
>
|
||||
{backgroundAsset ? (
|
||||
<ResolvedAssetImage
|
||||
src={backgroundAsset}
|
||||
@@ -251,40 +298,7 @@ export function BigFishRuntimeShell({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-6 left-4 z-30">
|
||||
<div
|
||||
ref={padRef}
|
||||
role="presentation"
|
||||
className="relative h-28 w-28 rounded-full border border-white/18 bg-black/24 backdrop-blur"
|
||||
onPointerDown={(event) => {
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
updateStickFromPointer(event.clientX, event.clientY);
|
||||
}}
|
||||
onPointerMove={(event) => {
|
||||
if (event.buttons <= 0) {
|
||||
return;
|
||||
}
|
||||
updateStickFromPointer(event.clientX, event.clientY);
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
setStick({ x: 0, y: 0 });
|
||||
onSubmitInput({ x: 0, y: 0 });
|
||||
}}
|
||||
onPointerCancel={() => {
|
||||
setStick({ x: 0, y: 0 });
|
||||
onSubmitInput({ x: 0, y: 0 });
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-1/2 top-1/2 h-11 w-11 -translate-x-1/2 -translate-y-1/2 rounded-full bg-cyan-200 shadow-lg shadow-cyan-950/30"
|
||||
style={{
|
||||
transform: `translate(calc(-50% + ${stick.x * 34}px), calc(-50% + ${stick.y * 34}px))`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-6 right-4 z-30 max-w-[13rem] space-y-2 text-right text-xs text-white/72">
|
||||
<div className="pointer-events-none absolute bottom-6 right-4 z-30 max-w-[13rem] space-y-2 text-right text-xs text-white/72">
|
||||
{isBusy ? <div>同步中...</div> : null}
|
||||
{error ? <div className="text-rose-200">{error}</div> : null}
|
||||
{run.eventLog.slice(-3).map((event) => (
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/* @vitest-environment jsdom */
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import { createCreationAgentChatQuickActions } from '../../services/creation-agent';
|
||||
|
||||
import {
|
||||
type CreationAgentTheme,
|
||||
CreationAgentWorkspace,
|
||||
@@ -243,17 +245,7 @@ test('creation agent workspace shows primary and progress actions at completed p
|
||||
loadingText="正在准备"
|
||||
composerPlaceholder="输入消息"
|
||||
primaryActionLabel="生成结果页"
|
||||
quickActions={[
|
||||
{
|
||||
key: 'summarize',
|
||||
label: '总结当前设定',
|
||||
},
|
||||
{
|
||||
key: 'quickFill',
|
||||
label: '补全剩余设定',
|
||||
minTurn: 2,
|
||||
},
|
||||
]}
|
||||
quickActions={createCreationAgentChatQuickActions()}
|
||||
onBack={() => {}}
|
||||
onSubmitText={() => {}}
|
||||
onPrimaryAction={() => {}}
|
||||
@@ -262,7 +254,7 @@ test('creation agent workspace shows primary and progress actions at completed p
|
||||
|
||||
expect(screen.getByRole('button', { name: '生成结果页' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '总结当前设定' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '补全剩余设定' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '补充剩余设定' })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('creation agent workspace hides hero copy area when title and summary are absent', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* @vitest-environment jsdom */
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@@ -108,11 +108,11 @@ test('workspace enables quick fill after at least two turns and submits quick fi
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '补全剩余设定' }));
|
||||
await user.click(screen.getByRole('button', { name: '补充剩余设定' }));
|
||||
|
||||
expect(onSubmitMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: '请补全剩余设定。',
|
||||
text: '请补充剩余设定。',
|
||||
quickFillRequested: true,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -5,8 +5,11 @@ import type {
|
||||
SendCustomWorldAgentMessageRequest,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import {
|
||||
buildCreationAgentChatMessage,
|
||||
createCreationAgentChatQuickActions,
|
||||
createCreationAgentClientMessageId,
|
||||
isCreationAgentOperationBusy,
|
||||
resolveCreationAgentQuickActionMessage,
|
||||
} from '../../services/creation-agent';
|
||||
import {
|
||||
type CreationAgentAnchorView,
|
||||
@@ -166,13 +169,17 @@ export function CustomWorldAgentWorkspace({
|
||||
isCreationAgentOperationBusy(activeOperation) || isStreamingReply;
|
||||
|
||||
const submitMessage = (text: string, quickFillRequested = false) => {
|
||||
onSubmitMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('custom-world'),
|
||||
text,
|
||||
quickFillRequested,
|
||||
focusCardId: null,
|
||||
selectedCardIds: [],
|
||||
});
|
||||
onSubmitMessage(
|
||||
buildCreationAgentChatMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('custom-world'),
|
||||
text,
|
||||
quickFillRequested,
|
||||
extraPayload: {
|
||||
focusCardId: null,
|
||||
selectedCardIds: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -186,17 +193,7 @@ export function CustomWorldAgentWorkspace({
|
||||
streamingReplyText={streamingReplyText}
|
||||
isStreamingReply={isStreamingReply}
|
||||
isBusy={isBusy}
|
||||
quickActions={[
|
||||
{
|
||||
key: 'summarize',
|
||||
label: '总结当前设定',
|
||||
},
|
||||
{
|
||||
key: 'quickFill',
|
||||
label: '补全剩余设定',
|
||||
minTurn: 2,
|
||||
},
|
||||
]}
|
||||
quickActions={createCreationAgentChatQuickActions()}
|
||||
onBack={onBack}
|
||||
onSubmitText={(text) => {
|
||||
submitMessage(text);
|
||||
@@ -207,12 +204,14 @@ export function CustomWorldAgentWorkspace({
|
||||
});
|
||||
}}
|
||||
onQuickAction={(action) => {
|
||||
if (action.key === 'quickFill') {
|
||||
submitMessage('请补全剩余设定。', true);
|
||||
return;
|
||||
}
|
||||
|
||||
submitMessage('请总结一下当前已经成形的世界设定。');
|
||||
const quickActionMessage = resolveCreationAgentQuickActionMessage(
|
||||
action.key,
|
||||
'请总结一下当前已经成形的世界设定。',
|
||||
);
|
||||
submitMessage(
|
||||
quickActionMessage.text,
|
||||
quickActionMessage.quickFillRequested,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* @vitest-environment jsdom */
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@@ -80,11 +80,12 @@ test('puzzle workspace submits quick keyword fill request after two turns', asyn
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '补充剩余关键字' }));
|
||||
await user.click(screen.getByRole('button', { name: '补充剩余设定' }));
|
||||
|
||||
expect(onSubmitMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: '请补充剩余关键字。',
|
||||
text: '请补充剩余设定。',
|
||||
quickFillRequested: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -99,5 +100,5 @@ test('puzzle workspace hides keyword fill before two turns', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: '补充剩余关键字' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '补充剩余设定' })).toBeNull();
|
||||
});
|
||||
|
||||
@@ -6,7 +6,12 @@ import type {
|
||||
PuzzleAgentSessionSnapshot,
|
||||
SendPuzzleAgentMessageRequest,
|
||||
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import { createCreationAgentClientMessageId } from '../../services/creation-agent';
|
||||
import {
|
||||
buildCreationAgentChatMessage,
|
||||
createCreationAgentChatQuickActions,
|
||||
createCreationAgentClientMessageId,
|
||||
resolveCreationAgentQuickActionMessage,
|
||||
} from '../../services/creation-agent';
|
||||
import {
|
||||
type CreationAgentOperationView,
|
||||
type CreationAgentSessionView,
|
||||
@@ -100,35 +105,30 @@ export function PuzzleAgentWorkspace({
|
||||
isStreamingReply={Boolean(streamingReplyText)}
|
||||
isBusy={isBusy}
|
||||
error={error}
|
||||
quickActions={[
|
||||
{
|
||||
key: 'summarize',
|
||||
label: '总结当前设定',
|
||||
},
|
||||
{
|
||||
key: 'quickFill',
|
||||
label: '补充剩余关键字',
|
||||
minTurn: 2,
|
||||
},
|
||||
]}
|
||||
quickActions={createCreationAgentChatQuickActions()}
|
||||
onBack={onBack}
|
||||
onSubmitText={(text) => {
|
||||
onSubmitMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('puzzle'),
|
||||
text,
|
||||
});
|
||||
onSubmitMessage(
|
||||
buildCreationAgentChatMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('puzzle'),
|
||||
text,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onPrimaryAction={() => {
|
||||
onExecuteAction({ action: 'compile_puzzle_draft' });
|
||||
}}
|
||||
onQuickAction={(action) => {
|
||||
onSubmitMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('puzzle'),
|
||||
text:
|
||||
action.key === 'quickFill'
|
||||
? '请补充剩余关键字。'
|
||||
: '请总结一下当前已经成形的拼图设定。',
|
||||
});
|
||||
const quickActionMessage = resolveCreationAgentQuickActionMessage(
|
||||
action.key,
|
||||
'请总结一下当前已经成形的拼图设定。',
|
||||
);
|
||||
onSubmitMessage(
|
||||
buildCreationAgentChatMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('puzzle'),
|
||||
...quickActionMessage,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user