@@ -0,0 +1,107 @@
|
||||
# 抓大鹅 Match3D 领域规则与共享契约 Stage1 方案
|
||||
|
||||
日期:`2026-04-30`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文件承接 [MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md),只冻结 B1 + B2 开发范围:
|
||||
|
||||
1. 新增 `module-match3d` 纯领域 crate。
|
||||
2. 新增 Rust shared contracts。
|
||||
3. 新增 TypeScript shared contracts。
|
||||
|
||||
本阶段不实现 SpacetimeDB 表、procedure、`spacetime-client` 调用封装、`api-server` facade 和前端页面。
|
||||
|
||||
## 2. Stage1 边界
|
||||
|
||||
## 2.1 本阶段做
|
||||
|
||||
1. 领域层定义创作配置、作品草稿、作品 profile、运行态快照、物品、托盘、点击确认结果。
|
||||
2. 领域层提供纯函数:
|
||||
- 校验创作配置
|
||||
- 编译默认草稿
|
||||
- 校验发布字段
|
||||
- 按确定性 seed 生成初始运行态
|
||||
- 刷新 2D 可点击快照
|
||||
- 确认点击、入槽、三消、胜利、托盘满失败
|
||||
- 确认倒计时失败
|
||||
3. Rust / TypeScript shared contracts 提供前后端对齐的请求与响应 DTO。
|
||||
4. 运行态采用“前端即时反馈 + 后端权威确认”契约:
|
||||
- 前端可先播放点击、飞入、入槽、三消、腾格和胜负过渡。
|
||||
- 后端确认后返回权威快照。
|
||||
- 后端拒绝或快照版本不一致时,前端按权威快照回滚或校正。
|
||||
|
||||
## 2.2 本阶段不做
|
||||
|
||||
1. 不新增 SpacetimeDB 表。
|
||||
2. 不新增 SpacetimeDB procedure。
|
||||
3. 不生成新的 SpacetimeDB bindings。
|
||||
4. 不新增 `api-server` 路由。
|
||||
5. 不接入平台入口、结果页或运行态 UI。
|
||||
6. 不接入真实图片生成。
|
||||
7. 不做排行榜与后续关卡推荐。
|
||||
|
||||
## 3. 领域 crate 设计
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
server-rs/crates/module-match3d
|
||||
```
|
||||
|
||||
该 crate 是纯领域层,不读写数据库,不访问网络,不依赖浏览器或文件系统。
|
||||
|
||||
核心类型:
|
||||
|
||||
1. `Match3DCreatorConfig`
|
||||
2. `Match3DResultDraft`
|
||||
3. `Match3DWorkProfile`
|
||||
4. `Match3DRunSnapshot`
|
||||
5. `Match3DItemSnapshot`
|
||||
6. `Match3DTraySlot`
|
||||
7. `Match3DClickConfirmation`
|
||||
|
||||
核心函数:
|
||||
|
||||
1. `build_creator_config`
|
||||
2. `compile_result_draft`
|
||||
3. `validate_publish_requirements`
|
||||
4. `create_work_profile`
|
||||
5. `publish_work_profile`
|
||||
6. `start_run_with_seed_at`
|
||||
7. `confirm_click_at`
|
||||
8. `resolve_run_timer_at`
|
||||
|
||||
## 4. 即时反馈与权威确认
|
||||
|
||||
本阶段将点击处理明确拆成两层:
|
||||
|
||||
1. 前端即时反馈层
|
||||
- 读取后端快照中的 `boardVersion`、物品位置、层级、半径和 `clickable`。
|
||||
- 本地做命中检测和动画。
|
||||
- 立即表现飞入、入槽、三消和胜负过渡。
|
||||
|
||||
2. 后端权威确认层
|
||||
- 校验 `runId`、`itemInstanceId`、运行态状态和物品是否仍可点击。
|
||||
- 重新计算入槽、三消、托盘满失败和胜利。
|
||||
- 返回最新 `Match3DRunSnapshot`。
|
||||
- 用 `boardVersion` 帮前端识别是否需要校正。
|
||||
|
||||
`Flying` 只作为前端表现态,不要求后端逐帧落库。后端只确认物品是否已从 `InBoard` 进入 `InTray` 或 `Cleared`。
|
||||
|
||||
## 5. 生成规则 Stage1 口径
|
||||
|
||||
1. `clearCount` 必须是正整数。
|
||||
2. `totalItemCount = clearCount * 3`。
|
||||
3. 难度范围为 `1~10`。
|
||||
4. 首版内置 `10` 种 demo 视觉 key。
|
||||
5. 当 `clearCount > 10` 时,复用视觉 key,并保证每种物品数量仍为 `3` 的倍数。
|
||||
6. 初始布局使用确定性 seed 生成圆形空间内的 2D 坐标。
|
||||
7. 可点击判定只做 2D 近似:若物品被更高层物品完全覆盖,则不可点击;否则可点击。
|
||||
|
||||
## 6. 验收
|
||||
|
||||
1. `cargo test -p module-match3d` 通过。
|
||||
2. `cargo test -p shared-contracts match3d` 通过。
|
||||
3. `npm run check:encoding` 覆盖新增中文文档和新增源码。
|
||||
4. 本阶段不要求运行 `npm run api-server:maincloud`,因为未修改后端运行服务入口、SpacetimeDB 表或 `api-server` facade。
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。
|
||||
- [MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md](./MATCH3D_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-30.md):冻结抓大鹅 Match3D 首版 demo 的独立玩法域、表与 procedure、HTTP facade、前端即时反馈/后端权威确认协议,以及可并行开发包。
|
||||
- [MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md](./MATCH3D_DOMAIN_AND_CONTRACTS_STAGE1_2026-04-30.md):冻结抓大鹅 Match3D B1+B2 的纯领域规则 crate、Rust/TypeScript shared contracts,以及 Stage1 不触碰 SpacetimeDB 表和 api-server 的边界。
|
||||
- [PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md](./PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md):记录平台首页底部 dock 在手机浏览器地址栏展开时脱离可见区域的根因,以及 `100dvh`、固定底部锚点和安全区占位的修复口径。
|
||||
- [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md):记录 SpacetimeDB private 表迁移 JSON 导出/导入 procedure、迁移操作员授权、HTTP 413 分片导入、Jenkins 自动迁移回灌和导入脚本参数。
|
||||
- [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md):记录 `Genarrative-Database-Export` / `Genarrative-Database-Import` 两条 SCM-backed 数据库迁移流水线参数、默认 dry-run、token 边界和 `CHUNK_SIZE` 413 规避参数。
|
||||
|
||||
@@ -27,6 +27,7 @@ spacetime sql <db> "SELECT * FROM custom_world_gallery_entry"
|
||||
| RPG 运行时 | `story_session`, `story_event`, `npc_state`, `inventory_slot`, `battle_state`, `treasure_record`, `quest_record`, `quest_log`, `player_progression`, `chapter_progression` |
|
||||
| 世界创作 | `custom_world_profile`, `custom_world_session`, `custom_world_agent_session`, `custom_world_agent_message`, `custom_world_agent_operation`, `custom_world_draft_card`, `custom_world_gallery_entry` |
|
||||
| 拼图 | `puzzle_agent_session`, `puzzle_agent_message`, `puzzle_work_profile`, `puzzle_runtime_run` |
|
||||
| 抓大鹅 Match3D | `match3d_agent_session`, `match3d_agent_message`, `match3d_work_profile`, `match3d_runtime_run` |
|
||||
| 大鱼吃小鱼 | `big_fish_creation_session`, `big_fish_agent_message`, `big_fish_asset_slot`, `big_fish_runtime_run` |
|
||||
| 资产 | `asset_object`, `asset_entity_binding` |
|
||||
| AI 任务 | `ai_task`, `ai_task_stage`, `ai_text_chunk`, `ai_result_reference` |
|
||||
@@ -446,6 +447,53 @@ SELECT * FROM puzzle_runtime_run WHERE run_id = '<run_id>';
|
||||
SELECT * FROM puzzle_runtime_run WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
|
||||
```
|
||||
|
||||
## 抓大鹅 Match3D 表
|
||||
|
||||
### `match3d_agent_session`
|
||||
|
||||
- 作用:抓大鹅 Match3D 创作 Agent 会话表,保存种子、配置 JSON、草稿 JSON 和发布 profile 指针。
|
||||
- 结构:`session_id PK: String`, `owner_user_id: String`, `seed_text: String`, `current_turn: u32`, `progress_percent: u32`, `stage: String`, `config_json: String`, `draft_json: String`, `last_assistant_reply: String`, `published_profile_id: String`, `created_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 索引:`owner_user_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM match3d_agent_session WHERE session_id = '<session_id>';
|
||||
SELECT * FROM match3d_agent_session WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
|
||||
```
|
||||
|
||||
### `match3d_agent_message`
|
||||
|
||||
- 作用:抓大鹅 Match3D 创作 Agent 消息流水。
|
||||
- 结构:`message_id PK: String`, `session_id: String`, `role: String`, `kind: String`, `text: String`, `created_at: Timestamp`。
|
||||
- 索引:`session_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM match3d_agent_message WHERE session_id = '<session_id>' ORDER BY created_at ASC;
|
||||
```
|
||||
|
||||
### `match3d_work_profile`
|
||||
|
||||
- 作用:抓大鹅 Match3D 作品主表,保存作品基础信息、配置、发布状态和游玩次数。
|
||||
- 结构:`profile_id PK: String`, `owner_user_id: String`, `source_session_id: String`, `author_display_name: String`, `game_name: String`, `theme_text: String`, `summary_text: String`, `tags_json: String`, `cover_image_src: String`, `cover_asset_id: String`, `clear_count: u32`, `difficulty: u32`, `config_json: String`, `publication_status: String`, `play_count: u32`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`。
|
||||
- 索引:`owner_user_id`, `publication_status`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM match3d_work_profile WHERE profile_id = '<profile_id>';
|
||||
SELECT * FROM match3d_work_profile WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
|
||||
SELECT * FROM match3d_work_profile WHERE publication_status = 'Published';
|
||||
```
|
||||
|
||||
### `match3d_runtime_run`
|
||||
|
||||
- 作用:抓大鹅 Match3D 单局运行态表,保存权威快照、快照版本、胜负状态和成绩基础字段。
|
||||
- 结构:`run_id PK: String`, `owner_user_id: String`, `profile_id: String`, `status: String`, `snapshot_version: u32`, `started_at_ms: i64`, `duration_limit_ms: i64`, `finished_at_ms: i64`, `elapsed_ms: i64`, `clear_count: u32`, `total_item_count: u32`, `cleared_item_count: u32`, `failure_reason: String`, `snapshot_json: String`, `created_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 索引:`owner_user_id`, `profile_id`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM match3d_runtime_run WHERE run_id = '<run_id>';
|
||||
SELECT * FROM match3d_runtime_run WHERE owner_user_id = '<user_id>' ORDER BY updated_at DESC;
|
||||
SELECT * FROM match3d_runtime_run WHERE profile_id = '<profile_id>';
|
||||
```
|
||||
|
||||
## 大鱼吃小鱼表
|
||||
|
||||
### `big_fish_creation_session`
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
export type Match3DCreationStage =
|
||||
| 'collecting_config'
|
||||
| 'draft_ready'
|
||||
| 'ready_to_publish'
|
||||
| 'published'
|
||||
| string;
|
||||
|
||||
export type Match3DAgentMessageRole = 'user' | 'assistant' | 'system' | string;
|
||||
|
||||
export type Match3DAgentMessageKind =
|
||||
| 'chat'
|
||||
| 'summary'
|
||||
| 'action_result'
|
||||
| 'warning'
|
||||
| string;
|
||||
|
||||
export type Match3DAnchorStatus = 'confirmed' | 'missing' | 'inferred' | 'locked' | string;
|
||||
|
||||
export interface CreateMatch3DAgentSessionRequest {
|
||||
seedText?: string;
|
||||
themeText?: string;
|
||||
referenceImageSrc?: string | null;
|
||||
clearCount?: number;
|
||||
difficulty?: number;
|
||||
}
|
||||
|
||||
export type CreateMatch3DSessionRequest = CreateMatch3DAgentSessionRequest;
|
||||
|
||||
export interface SendMatch3DAgentMessageRequest {
|
||||
clientMessageId: string;
|
||||
text: string;
|
||||
quickFillRequested?: boolean;
|
||||
referenceImageSrc?: string | null;
|
||||
}
|
||||
|
||||
export type SendMatch3DMessageRequest = SendMatch3DAgentMessageRequest;
|
||||
|
||||
export interface ExecuteMatch3DAgentActionRequest {
|
||||
action: string;
|
||||
gameName?: string;
|
||||
summary?: string;
|
||||
tags?: string[];
|
||||
coverImageSrc?: string | null;
|
||||
clearCount?: number;
|
||||
difficulty?: number;
|
||||
}
|
||||
|
||||
export type ExecuteMatch3DActionRequest = ExecuteMatch3DAgentActionRequest;
|
||||
|
||||
export interface Match3DAnchorItemResponse {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
status: Match3DAnchorStatus;
|
||||
}
|
||||
|
||||
export interface Match3DAnchorPackResponse {
|
||||
theme: Match3DAnchorItemResponse;
|
||||
clearCount: Match3DAnchorItemResponse;
|
||||
difficulty: Match3DAnchorItemResponse;
|
||||
}
|
||||
|
||||
export interface Match3DCreatorConfig {
|
||||
themeText: string;
|
||||
referenceImageSrc?: string | null;
|
||||
clearCount: number;
|
||||
difficulty: number;
|
||||
}
|
||||
|
||||
export interface Match3DResultDraft {
|
||||
gameName: string;
|
||||
themeText: string;
|
||||
summaryText?: string;
|
||||
summary?: string;
|
||||
tags: string[];
|
||||
coverImageSrc?: string | null;
|
||||
referenceImageSrc?: string | null;
|
||||
clearCount: number;
|
||||
difficulty: number;
|
||||
totalItemCount?: number;
|
||||
publishReady?: boolean;
|
||||
blockers?: string[];
|
||||
}
|
||||
|
||||
export interface Match3DAgentMessage {
|
||||
id: string;
|
||||
role: Match3DAgentMessageRole;
|
||||
kind: Match3DAgentMessageKind;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type Match3DAgentMessageResponse = Match3DAgentMessage;
|
||||
|
||||
export interface Match3DAgentSessionSnapshot {
|
||||
sessionId: string;
|
||||
currentTurn: number;
|
||||
progressPercent: number;
|
||||
stage: Match3DCreationStage;
|
||||
anchorPack: Match3DAnchorPackResponse;
|
||||
config?: Match3DCreatorConfig | null;
|
||||
draft?: Match3DResultDraft | null;
|
||||
messages: Match3DAgentMessage[];
|
||||
lastAssistantReply?: string | null;
|
||||
publishedProfileId?: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Match3DAgentSessionResponse {
|
||||
session: Match3DAgentSessionSnapshot;
|
||||
}
|
||||
|
||||
export type Match3DSessionResponse = Match3DAgentSessionResponse;
|
||||
|
||||
export interface Match3DAgentActionResponse {
|
||||
session: Match3DAgentSessionSnapshot;
|
||||
}
|
||||
|
||||
export type Match3DActionResponse = Match3DAgentActionResponse;
|
||||
@@ -0,0 +1,125 @@
|
||||
export type Match3DRunStatus =
|
||||
| 'running'
|
||||
| 'won'
|
||||
| 'failed'
|
||||
| 'stopped'
|
||||
| 'Running'
|
||||
| 'Won'
|
||||
| 'Failed'
|
||||
| 'Stopped'
|
||||
| string;
|
||||
export type Match3DItemState =
|
||||
| 'in_board'
|
||||
| 'in_tray'
|
||||
| 'cleared'
|
||||
| 'InBoard'
|
||||
| 'Flying'
|
||||
| 'InTray'
|
||||
| 'Cleared'
|
||||
| string;
|
||||
export type Match3DFailureReason =
|
||||
| 'time_up'
|
||||
| 'tray_full'
|
||||
| 'TimeUp'
|
||||
| 'TrayFull'
|
||||
| string;
|
||||
export type Match3DClickRejectReason =
|
||||
| 'run_not_active'
|
||||
| 'snapshot_version_mismatch'
|
||||
| 'item_not_found'
|
||||
| 'item_not_in_board'
|
||||
| 'item_not_clickable'
|
||||
| 'tray_full'
|
||||
| string;
|
||||
|
||||
export type Match3DClickConfirmStatus =
|
||||
| 'Accepted'
|
||||
| 'RejectedNotClickable'
|
||||
| 'RejectedAlreadyMoved'
|
||||
| 'RejectedTrayFull'
|
||||
| 'VersionConflict'
|
||||
| 'RunFinished';
|
||||
|
||||
export interface StartMatch3DRunRequest {
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface Match3DClickItemRequest {
|
||||
runId?: string;
|
||||
itemInstanceId: string;
|
||||
clientActionId?: string;
|
||||
snapshotVersion?: number;
|
||||
clientSnapshotVersion: number;
|
||||
clientEventId: string;
|
||||
clickedAtMs: number;
|
||||
}
|
||||
|
||||
export type ClickMatch3DItemRequest = Match3DClickItemRequest;
|
||||
|
||||
export interface StopMatch3DRunRequest {
|
||||
clientActionId: string;
|
||||
}
|
||||
|
||||
export interface Match3DItemSnapshot {
|
||||
itemInstanceId: string;
|
||||
itemTypeId: string;
|
||||
visualKey: string;
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
layer: number;
|
||||
state: Match3DItemState;
|
||||
clickable: boolean;
|
||||
traySlotIndex?: number | null;
|
||||
}
|
||||
|
||||
export interface Match3DTraySlot {
|
||||
slotIndex: number;
|
||||
itemInstanceId?: string | null;
|
||||
itemTypeId?: string | null;
|
||||
visualKey?: string | null;
|
||||
}
|
||||
|
||||
export interface Match3DRunSnapshot {
|
||||
runId: string;
|
||||
profileId: string;
|
||||
ownerUserId?: string;
|
||||
status: Match3DRunStatus;
|
||||
snapshotVersion: number;
|
||||
startedAtMs: number;
|
||||
durationLimitMs: number;
|
||||
serverNowMs?: number;
|
||||
remainingMs: number;
|
||||
clearCount: number;
|
||||
totalItemCount: number;
|
||||
clearedItemCount: number;
|
||||
boardVersion?: number;
|
||||
items: Match3DItemSnapshot[];
|
||||
traySlots: Match3DTraySlot[];
|
||||
failureReason?: Match3DFailureReason | null;
|
||||
lastConfirmedActionId?: string | null;
|
||||
}
|
||||
|
||||
export interface Match3DClickConfirmation {
|
||||
accepted: boolean;
|
||||
rejectReason?: Match3DClickRejectReason | null;
|
||||
enteredSlotIndex?: number | null;
|
||||
clearedItemInstanceIds: string[];
|
||||
run: Match3DRunSnapshot;
|
||||
}
|
||||
|
||||
export interface Match3DClickItemResult {
|
||||
status: Match3DClickConfirmStatus;
|
||||
run: Match3DRunSnapshot;
|
||||
acceptedItemInstanceId?: string;
|
||||
clearedItemInstanceIds: string[];
|
||||
failureReason?: Match3DFailureReason | null;
|
||||
}
|
||||
|
||||
export interface Match3DRunResponse {
|
||||
run: Match3DRunSnapshot;
|
||||
}
|
||||
|
||||
export interface Match3DClickResponse {
|
||||
confirmation: Match3DClickConfirmation;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export type Match3DWorkPublicationStatus = 'draft' | 'published' | string;
|
||||
|
||||
export interface PutMatch3DWorkRequest {
|
||||
gameName: string;
|
||||
summary: string;
|
||||
tags: string[];
|
||||
coverImageSrc?: string | null;
|
||||
referenceImageSrc?: string | null;
|
||||
clearCount: number;
|
||||
difficulty: number;
|
||||
}
|
||||
|
||||
export interface Match3DWorkSummary {
|
||||
workId: string;
|
||||
profileId: string;
|
||||
ownerUserId: string;
|
||||
sourceSessionId?: string | null;
|
||||
gameName: string;
|
||||
themeText: string;
|
||||
summary: string;
|
||||
tags: string[];
|
||||
coverImageSrc?: string | null;
|
||||
referenceImageSrc?: string | null;
|
||||
clearCount: number;
|
||||
difficulty: number;
|
||||
publicationStatus: Match3DWorkPublicationStatus;
|
||||
playCount: number;
|
||||
updatedAt: string;
|
||||
publishedAt?: string | null;
|
||||
publishReady: boolean;
|
||||
}
|
||||
|
||||
export interface Match3DWorkProfile extends Match3DWorkSummary {}
|
||||
|
||||
export interface Match3DWorksResponse {
|
||||
items: Match3DWorkSummary[];
|
||||
}
|
||||
|
||||
export interface Match3DWorkDetailResponse {
|
||||
item: Match3DWorkProfile;
|
||||
}
|
||||
|
||||
export interface Match3DWorkMutationResponse {
|
||||
item: Match3DWorkProfile;
|
||||
}
|
||||
@@ -11,6 +11,9 @@ export * from './contracts/rpgCreationFixtures';
|
||||
export * from './contracts/rpgCreationPreview';
|
||||
export * from './contracts/rpgCreationResultView';
|
||||
export * from './contracts/rpgCreationWorkSummary';
|
||||
export * from './contracts/match3dAgent';
|
||||
export * from './contracts/match3dRuntime';
|
||||
export * from './contracts/match3dWorks';
|
||||
export * from './contracts/puzzleAgentActions';
|
||||
export * from './contracts/puzzleAgentDraft';
|
||||
export * from './contracts/puzzleAgentSession';
|
||||
|
||||
Generated
+9
@@ -1562,6 +1562,15 @@ dependencies = [
|
||||
"spacetimedb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "module-match3d"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"shared-kernel",
|
||||
"spacetimedb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "module-npc"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -15,6 +15,7 @@ members = [
|
||||
"crates/module-combat",
|
||||
"crates/module-inventory",
|
||||
"crates/module-custom-world",
|
||||
"crates/module-match3d",
|
||||
"crates/module-npc",
|
||||
"crates/module-puzzle",
|
||||
"crates/module-progression",
|
||||
@@ -52,4 +53,4 @@ incremental = true
|
||||
[profile.release]
|
||||
opt-level = 3 # 最大优化等级
|
||||
lto = "thin" # 启用 Thin LTO,平衡编译时间和性能
|
||||
codegen-units = 1 # 减少并行代码生成单元,提升优化但增加编译时间
|
||||
codegen-units = 1 # 减少并行代码生成单元,提升优化但增加编译时间
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "module-match3d"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
spacetime-types = ["dep:spacetimedb"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
shared-kernel = { path = "../shared-kernel" }
|
||||
spacetimedb = { workspace = true, optional = true }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,9 @@ pub mod big_fish;
|
||||
pub mod big_fish_works;
|
||||
pub mod creation_agent_document_input;
|
||||
pub mod llm;
|
||||
pub mod match3d_agent;
|
||||
pub mod match3d_runtime;
|
||||
pub mod match3d_works;
|
||||
pub mod puzzle_agent;
|
||||
pub mod puzzle_gallery;
|
||||
pub mod puzzle_runtime;
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateMatch3DAgentSessionRequest {
|
||||
#[serde(default)]
|
||||
pub seed_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub theme_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub reference_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub clear_count: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub difficulty: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendMatch3DAgentMessageRequest {
|
||||
pub client_message_id: String,
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub quick_fill_requested: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecuteMatch3DAgentActionRequest {
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub game_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub summary: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tags: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub clear_count: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub difficulty: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DCreatorConfigResponse {
|
||||
pub theme_text: String,
|
||||
#[serde(default)]
|
||||
pub reference_image_src: Option<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DResultDraftResponse {
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub summary: String,
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub reference_image_src: Option<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub publish_ready: bool,
|
||||
pub blockers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DAgentMessageResponse {
|
||||
pub id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DAgentSessionSnapshotResponse {
|
||||
pub session_id: String,
|
||||
pub current_turn: u32,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
#[serde(default)]
|
||||
pub config: Option<Match3DCreatorConfigResponse>,
|
||||
#[serde(default)]
|
||||
pub draft: Option<Match3DResultDraftResponse>,
|
||||
pub messages: Vec<Match3DAgentMessageResponse>,
|
||||
#[serde(default)]
|
||||
pub last_assistant_reply: Option<String>,
|
||||
#[serde(default)]
|
||||
pub published_profile_id: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DAgentSessionResponse {
|
||||
pub session: Match3DAgentSessionSnapshotResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DAgentActionResponse {
|
||||
pub session: Match3DAgentSessionSnapshotResponse,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn create_match3d_session_request_uses_camel_case() {
|
||||
let payload = serde_json::to_value(CreateMatch3DAgentSessionRequest {
|
||||
seed_text: Some("水果消除".to_string()),
|
||||
theme_text: Some("水果".to_string()),
|
||||
reference_image_src: Some("data:image/png;base64,abc".to_string()),
|
||||
clear_count: Some(4),
|
||||
difficulty: Some(3),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["seedText"], json!("水果消除"));
|
||||
assert_eq!(payload["themeText"], json!("水果"));
|
||||
assert_eq!(
|
||||
payload["referenceImageSrc"],
|
||||
json!("data:image/png;base64,abc")
|
||||
);
|
||||
assert_eq!(payload["clearCount"], json!(4));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StartMatch3DRunRequest {
|
||||
pub profile_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClickMatch3DItemRequest {
|
||||
pub item_instance_id: String,
|
||||
pub client_action_id: String,
|
||||
pub snapshot_version: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StopMatch3DRunRequest {
|
||||
pub client_action_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DItemSnapshotResponse {
|
||||
pub item_instance_id: String,
|
||||
pub item_type_id: String,
|
||||
pub visual_key: String,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub radius: f32,
|
||||
pub layer: u32,
|
||||
pub state: String,
|
||||
pub clickable: bool,
|
||||
#[serde(default)]
|
||||
pub tray_slot_index: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DTraySlotResponse {
|
||||
pub slot_index: u32,
|
||||
#[serde(default)]
|
||||
pub item_instance_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub item_type_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub visual_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DRunSnapshotResponse {
|
||||
pub run_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub status: String,
|
||||
pub started_at_ms: u64,
|
||||
pub duration_limit_ms: u64,
|
||||
pub remaining_ms: u64,
|
||||
pub clear_count: u32,
|
||||
pub total_item_count: u32,
|
||||
pub cleared_item_count: u32,
|
||||
pub board_version: u64,
|
||||
pub items: Vec<Match3DItemSnapshotResponse>,
|
||||
pub tray_slots: Vec<Match3DTraySlotResponse>,
|
||||
#[serde(default)]
|
||||
pub failure_reason: Option<String>,
|
||||
#[serde(default)]
|
||||
pub last_confirmed_action_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DClickConfirmationResponse {
|
||||
pub accepted: bool,
|
||||
#[serde(default)]
|
||||
pub reject_reason: Option<String>,
|
||||
#[serde(default)]
|
||||
pub entered_slot_index: Option<u32>,
|
||||
pub cleared_item_instance_ids: Vec<String>,
|
||||
pub run: Match3DRunSnapshotResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DRunResponse {
|
||||
pub run: Match3DRunSnapshotResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DClickResponse {
|
||||
pub confirmation: Match3DClickConfirmationResponse,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn click_match3d_item_request_uses_camel_case() {
|
||||
let payload = serde_json::to_value(ClickMatch3DItemRequest {
|
||||
item_instance_id: "item-1".to_string(),
|
||||
client_action_id: "action-1".to_string(),
|
||||
snapshot_version: 7,
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["itemInstanceId"], json!("item-1"));
|
||||
assert_eq!(payload["clientActionId"], json!("action-1"));
|
||||
assert_eq!(payload["snapshotVersion"], json!(7));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PutMatch3DWorkRequest {
|
||||
pub game_name: String,
|
||||
pub summary: String,
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub reference_image_src: Option<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DWorkSummaryResponse {
|
||||
pub work_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
#[serde(default)]
|
||||
pub source_session_id: Option<String>,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub summary: String,
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub reference_image_src: Option<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub publication_status: String,
|
||||
pub play_count: u32,
|
||||
pub updated_at: String,
|
||||
#[serde(default)]
|
||||
pub published_at: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DWorkProfileResponse {
|
||||
#[serde(flatten)]
|
||||
pub summary: Match3DWorkSummaryResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DWorksResponse {
|
||||
pub items: Vec<Match3DWorkSummaryResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DWorkDetailResponse {
|
||||
pub item: Match3DWorkProfileResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DWorkMutationResponse {
|
||||
pub item: Match3DWorkProfileResponse,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn match3d_work_request_uses_camel_case() {
|
||||
let payload = serde_json::to_value(PutMatch3DWorkRequest {
|
||||
game_name: "水果抓大鹅".to_string(),
|
||||
summary: "水果主题".to_string(),
|
||||
tags: vec!["水果".to_string()],
|
||||
cover_image_src: None,
|
||||
reference_image_src: None,
|
||||
clear_count: 4,
|
||||
difficulty: 5,
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(payload["gameName"], json!("水果抓大鹅"));
|
||||
assert_eq!(payload["clearCount"], json!(4));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ module-big-fish = { path = "../module-big-fish", default-features = false, featu
|
||||
module-combat = { path = "../module-combat", default-features = false, features = ["spacetime-types"] }
|
||||
module-inventory = { path = "../module-inventory", default-features = false, features = ["spacetime-types"] }
|
||||
module-custom-world = { path = "../module-custom-world", default-features = false, features = ["spacetime-types"] }
|
||||
module-match3d = { path = "../module-match3d", default-features = false }
|
||||
module-npc = { path = "../module-npc", default-features = false, features = ["spacetime-types"] }
|
||||
module-puzzle = { path = "../module-puzzle", default-features = false, features = ["spacetime-types"] }
|
||||
module-progression = { path = "../module-progression", default-features = false, features = ["spacetime-types"] }
|
||||
|
||||
@@ -31,6 +31,7 @@ mod auth;
|
||||
mod big_fish;
|
||||
mod domain_types;
|
||||
mod entry;
|
||||
mod match3d;
|
||||
mod migration;
|
||||
mod puzzle;
|
||||
mod runtime;
|
||||
@@ -41,6 +42,7 @@ pub use auth::*;
|
||||
pub use big_fish::*;
|
||||
pub use domain_types::*;
|
||||
pub use entry::*;
|
||||
pub use match3d::*;
|
||||
pub use migration::*;
|
||||
pub use runtime::*;
|
||||
|
||||
@@ -2856,7 +2858,9 @@ fn list_custom_world_profile_snapshots(
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn build_custom_world_profile_list_snapshot(row: &CustomWorldProfile) -> CustomWorldProfileSnapshot {
|
||||
fn build_custom_world_profile_list_snapshot(
|
||||
row: &CustomWorldProfile,
|
||||
) -> CustomWorldProfileSnapshot {
|
||||
let mut snapshot = build_custom_world_profile_snapshot(row);
|
||||
snapshot.profile_payload_json = build_custom_world_profile_list_payload_json(row);
|
||||
snapshot
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
use crate::*;
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = match3d_agent_session,
|
||||
index(accessor = by_match3d_agent_session_owner_user_id, btree(columns = [owner_user_id]))
|
||||
)]
|
||||
pub struct Match3DAgentSessionRow {
|
||||
#[primary_key]
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) seed_text: String,
|
||||
pub(crate) current_turn: u32,
|
||||
pub(crate) progress_percent: u32,
|
||||
pub(crate) stage: String,
|
||||
pub(crate) config_json: String,
|
||||
pub(crate) draft_json: String,
|
||||
pub(crate) last_assistant_reply: String,
|
||||
pub(crate) published_profile_id: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = match3d_agent_message,
|
||||
index(accessor = by_match3d_agent_message_session_id, btree(columns = [session_id]))
|
||||
)]
|
||||
pub struct Match3DAgentMessageRow {
|
||||
#[primary_key]
|
||||
pub(crate) message_id: String,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) role: String,
|
||||
pub(crate) kind: String,
|
||||
pub(crate) text: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = match3d_work_profile,
|
||||
index(accessor = by_match3d_work_owner_user_id, btree(columns = [owner_user_id])),
|
||||
index(accessor = by_match3d_work_publication_status, btree(columns = [publication_status]))
|
||||
)]
|
||||
pub struct Match3DWorkProfileRow {
|
||||
#[primary_key]
|
||||
pub(crate) profile_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) source_session_id: String,
|
||||
pub(crate) author_display_name: String,
|
||||
pub(crate) game_name: String,
|
||||
pub(crate) theme_text: String,
|
||||
pub(crate) summary_text: String,
|
||||
pub(crate) tags_json: String,
|
||||
pub(crate) cover_image_src: String,
|
||||
pub(crate) cover_asset_id: String,
|
||||
pub(crate) clear_count: u32,
|
||||
pub(crate) difficulty: u32,
|
||||
pub(crate) config_json: String,
|
||||
pub(crate) publication_status: String,
|
||||
pub(crate) play_count: u32,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
pub(crate) published_at: Option<Timestamp>,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = match3d_runtime_run,
|
||||
index(accessor = by_match3d_run_owner_user_id, btree(columns = [owner_user_id])),
|
||||
index(accessor = by_match3d_run_profile_id, btree(columns = [profile_id]))
|
||||
)]
|
||||
pub struct Match3DRuntimeRunRow {
|
||||
#[primary_key]
|
||||
pub(crate) run_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) profile_id: String,
|
||||
pub(crate) status: String,
|
||||
pub(crate) snapshot_version: u32,
|
||||
pub(crate) started_at_ms: i64,
|
||||
pub(crate) duration_limit_ms: i64,
|
||||
pub(crate) finished_at_ms: i64,
|
||||
pub(crate) elapsed_ms: i64,
|
||||
pub(crate) clear_count: u32,
|
||||
pub(crate) total_item_count: u32,
|
||||
pub(crate) cleared_item_count: u32,
|
||||
pub(crate) failure_reason: String,
|
||||
pub(crate) snapshot_json: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const MATCH3D_DEFAULT_DURATION_LIMIT_MS: i64 = 600_000;
|
||||
pub const MATCH3D_TRAY_SLOT_COUNT: u32 = 7;
|
||||
pub const MATCH3D_VISUAL_VARIANT_COUNT: u32 = 10;
|
||||
pub const MATCH3D_MIN_DIFFICULTY: u32 = 1;
|
||||
pub const MATCH3D_MAX_DIFFICULTY: u32 = 10;
|
||||
|
||||
pub const MATCH3D_STAGE_COLLECTING: &str = "Collecting";
|
||||
pub const MATCH3D_STAGE_READY_TO_COMPILE: &str = "ReadyToCompile";
|
||||
pub const MATCH3D_STAGE_DRAFT_COMPILED: &str = "DraftCompiled";
|
||||
pub const MATCH3D_STAGE_PUBLISHED: &str = "Published";
|
||||
|
||||
pub const MATCH3D_ROLE_USER: &str = "user";
|
||||
pub const MATCH3D_ROLE_ASSISTANT: &str = "assistant";
|
||||
pub const MATCH3D_KIND_TEXT: &str = "text";
|
||||
|
||||
pub const MATCH3D_PUBLICATION_DRAFT: &str = "Draft";
|
||||
pub const MATCH3D_PUBLICATION_PUBLISHED: &str = "Published";
|
||||
|
||||
pub const MATCH3D_RUN_RUNNING: &str = "Running";
|
||||
pub const MATCH3D_RUN_WON: &str = "Won";
|
||||
pub const MATCH3D_RUN_FAILED: &str = "Failed";
|
||||
pub const MATCH3D_RUN_STOPPED: &str = "Stopped";
|
||||
|
||||
pub const MATCH3D_FAILURE_TIME_UP: &str = "TimeUp";
|
||||
pub const MATCH3D_FAILURE_TRAY_FULL: &str = "TrayFull";
|
||||
|
||||
pub const MATCH3D_CLICK_ACCEPTED: &str = "Accepted";
|
||||
pub const MATCH3D_CLICK_REJECTED_NOT_CLICKABLE: &str = "RejectedNotClickable";
|
||||
pub const MATCH3D_CLICK_REJECTED_ALREADY_MOVED: &str = "RejectedAlreadyMoved";
|
||||
pub const MATCH3D_CLICK_REJECTED_TRAY_FULL: &str = "RejectedTrayFull";
|
||||
pub const MATCH3D_CLICK_VERSION_CONFLICT: &str = "VersionConflict";
|
||||
pub const MATCH3D_CLICK_RUN_FINISHED: &str = "RunFinished";
|
||||
|
||||
pub const MATCH3D_ITEM_IN_BOARD: &str = "InBoard";
|
||||
pub const MATCH3D_ITEM_IN_TRAY: &str = "InTray";
|
||||
pub const MATCH3D_ITEM_CLEARED: &str = "Cleared";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DAgentSessionCreateInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub seed_text: String,
|
||||
pub welcome_message_id: String,
|
||||
pub welcome_message_text: String,
|
||||
pub config_json: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DAgentSessionGetInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DAgentMessageSubmitInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub user_message_id: String,
|
||||
pub user_message_text: String,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DAgentMessageFinalizeInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub assistant_message_id: Option<String>,
|
||||
pub assistant_reply_text: Option<String>,
|
||||
pub config_json: Option<String>,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
pub updated_at_micros: i64,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DDraftCompileInput {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub author_display_name: String,
|
||||
pub game_name: Option<String>,
|
||||
pub summary_text: Option<String>,
|
||||
pub tags_json: Option<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub cover_asset_id: Option<String>,
|
||||
pub compiled_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DWorkUpdateInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub summary_text: String,
|
||||
pub tags_json: String,
|
||||
pub cover_image_src: String,
|
||||
pub cover_asset_id: String,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DWorkPublishInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub published_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DWorksListInput {
|
||||
pub owner_user_id: String,
|
||||
pub published_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DWorkGetInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DWorkDeleteInput {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DRunStartInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub started_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DRunGetInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DRunClickInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub item_instance_id: String,
|
||||
pub client_snapshot_version: u32,
|
||||
pub client_event_id: String,
|
||||
pub clicked_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DRunStopInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub stopped_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DRunRestartInput {
|
||||
pub source_run_id: String,
|
||||
pub next_run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub restarted_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DRunTimeUpInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub finished_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DAgentSessionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub session_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DWorkProcedureResult {
|
||||
pub ok: bool,
|
||||
pub work_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DWorksProcedureResult {
|
||||
pub ok: bool,
|
||||
pub items_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DRunProcedureResult {
|
||||
pub ok: bool,
|
||||
pub run_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DClickItemProcedureResult {
|
||||
pub ok: bool,
|
||||
pub status: String,
|
||||
pub run_json: Option<String>,
|
||||
pub accepted_item_instance_id: Option<String>,
|
||||
pub cleared_item_instance_ids: Vec<String>,
|
||||
pub failure_reason: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DCreatorConfigSnapshot {
|
||||
pub theme_text: String,
|
||||
pub reference_image_src: Option<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DAgentMessageSnapshot {
|
||||
pub message_id: String,
|
||||
pub session_id: String,
|
||||
pub role: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DDraftSnapshot {
|
||||
pub profile_id: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub summary_text: String,
|
||||
pub tags: Vec<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DAgentSessionSnapshot {
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub seed_text: String,
|
||||
pub current_turn: u32,
|
||||
pub progress_percent: u32,
|
||||
pub stage: String,
|
||||
pub config: Match3DCreatorConfigSnapshot,
|
||||
pub draft: Option<Match3DDraftSnapshot>,
|
||||
pub messages: Vec<Match3DAgentMessageSnapshot>,
|
||||
pub last_assistant_reply: String,
|
||||
pub published_profile_id: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DWorkSnapshot {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_session_id: String,
|
||||
pub author_display_name: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub summary_text: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: String,
|
||||
pub cover_asset_id: String,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub config: Match3DCreatorConfigSnapshot,
|
||||
pub publication_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DItemSnapshot {
|
||||
pub item_instance_id: String,
|
||||
pub item_type_id: String,
|
||||
pub visual_key: String,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub radius: f32,
|
||||
pub layer: u32,
|
||||
pub state: String,
|
||||
pub clickable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DTraySlotSnapshot {
|
||||
pub slot_index: u32,
|
||||
pub item_instance_id: Option<String>,
|
||||
pub item_type_id: Option<String>,
|
||||
pub visual_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match3DRunSnapshot {
|
||||
pub run_id: String,
|
||||
pub profile_id: String,
|
||||
pub status: String,
|
||||
pub snapshot_version: u32,
|
||||
pub started_at_ms: i64,
|
||||
pub duration_limit_ms: i64,
|
||||
pub server_now_ms: i64,
|
||||
pub remaining_ms: i64,
|
||||
pub clear_count: u32,
|
||||
pub total_item_count: u32,
|
||||
pub cleared_item_count: u32,
|
||||
pub tray_slots: Vec<Match3DTraySlotSnapshot>,
|
||||
pub items: Vec<Match3DItemSnapshot>,
|
||||
pub failure_reason: Option<String>,
|
||||
}
|
||||
@@ -4,6 +4,9 @@ use spacetimedb_lib::sats::de::serde::DeserializeWrapper;
|
||||
use spacetimedb_lib::sats::ser::serde::SerializeWrapper;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::match3d::tables::{
|
||||
match3d_agent_message, match3d_agent_session, match3d_runtime_run, match3d_work_profile,
|
||||
};
|
||||
use crate::puzzle::{
|
||||
puzzle_agent_message, puzzle_agent_session, puzzle_runtime_run, puzzle_work_profile,
|
||||
};
|
||||
@@ -187,6 +190,10 @@ macro_rules! migration_tables {
|
||||
puzzle_agent_message,
|
||||
puzzle_work_profile,
|
||||
puzzle_runtime_run,
|
||||
match3d_agent_session,
|
||||
match3d_agent_message,
|
||||
match3d_work_profile,
|
||||
match3d_runtime_run,
|
||||
big_fish_creation_session,
|
||||
big_fish_agent_message,
|
||||
big_fish_asset_slot
|
||||
|
||||
@@ -1261,8 +1261,9 @@ fn start_puzzle_run_tx(
|
||||
}
|
||||
let entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?;
|
||||
let started_at_ms = micros_to_millis(input.started_at_micros);
|
||||
let mut run = module_puzzle::start_run_at(input.run_id.clone(), &entry_profile, 0, started_at_ms)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut run =
|
||||
module_puzzle::start_run_at(input.run_id.clone(), &entry_profile, 0, started_at_ms)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let current_grid_size = run.current_grid_size;
|
||||
let current_profile_id = entry_profile.profile_id.clone();
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
@@ -1502,13 +1503,11 @@ fn use_puzzle_runtime_prop_tx(
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let current_run = deserialize_run(&row.snapshot_json)?;
|
||||
let next_run = match input.prop_kind.as_str() {
|
||||
"freezeTime" | "freeze_time" => {
|
||||
module_puzzle::apply_puzzle_freeze_time_at(
|
||||
¤t_run,
|
||||
micros_to_millis(input.used_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?
|
||||
}
|
||||
"freezeTime" | "freeze_time" => module_puzzle::apply_puzzle_freeze_time_at(
|
||||
¤t_run,
|
||||
micros_to_millis(input.used_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?,
|
||||
"hint" => module_puzzle::set_puzzle_run_paused_at(
|
||||
¤t_run,
|
||||
false,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import type {
|
||||
Match3DClickItemRequest,
|
||||
Match3DRunSnapshot,
|
||||
} from '../packages/shared/src/contracts/match3dRuntime';
|
||||
import { Match3DRuntimeShell } from './components/match3d-runtime';
|
||||
import {
|
||||
confirmLocalMatch3DClick,
|
||||
resolveLocalMatch3DTimer,
|
||||
startLocalMatch3DRun,
|
||||
} from './services/match3d-runtime';
|
||||
|
||||
function buildInitialRun() {
|
||||
return startLocalMatch3DRun(12);
|
||||
}
|
||||
|
||||
export default function Match3DPlaygroundApp() {
|
||||
const [run, setRun] = useState<Match3DRunSnapshot>(buildInitialRun);
|
||||
const authorityRunRef = useRef(run);
|
||||
|
||||
const syncRun = useCallback((nextRun: Match3DRunSnapshot) => {
|
||||
setRun(nextRun);
|
||||
}, []);
|
||||
|
||||
const handleClickItem = useCallback(async (payload: Match3DClickItemRequest) => {
|
||||
const result = await confirmLocalMatch3DClick(authorityRunRef.current, payload);
|
||||
authorityRunRef.current = result.run;
|
||||
setRun(result.run);
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
const handleRestart = useCallback(() => {
|
||||
const nextRun = buildInitialRun();
|
||||
authorityRunRef.current = nextRun;
|
||||
setRun(nextRun);
|
||||
}, []);
|
||||
|
||||
const handleExit = useCallback(() => {
|
||||
window.location.assign('/');
|
||||
}, []);
|
||||
|
||||
const handleTimeExpired = useCallback(() => {
|
||||
const nextRun = resolveLocalMatch3DTimer(authorityRunRef.current);
|
||||
authorityRunRef.current = nextRun;
|
||||
setRun(nextRun);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Match3DRuntimeShell
|
||||
run={run}
|
||||
onBack={handleExit}
|
||||
onRestart={handleRestart}
|
||||
onOptimisticRunChange={syncRun}
|
||||
onClickItem={handleClickItem}
|
||||
onTimeExpired={handleTimeExpired}
|
||||
error={null}
|
||||
isBusy={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowLeft, Paperclip, Send, Sparkles } from 'lucide-react';
|
||||
import { ArrowLeft, ImagePlus, Paperclip, Send, Sparkles, X } from 'lucide-react';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
@@ -75,15 +75,21 @@ type CreationAgentWorkspaceProps = {
|
||||
isBusy?: boolean;
|
||||
error?: string | null;
|
||||
quickActions?: CreationAgentQuickAction[];
|
||||
referenceImagePreviewSrc?: string | null;
|
||||
referenceImageLabel?: string | null;
|
||||
referenceImageError?: string | null;
|
||||
onBack: () => void;
|
||||
onSubmitText: (text: string, quickActionKey?: string) => void;
|
||||
onPrimaryAction: () => void;
|
||||
onQuickAction?: (action: CreationAgentQuickAction) => void;
|
||||
onReferenceImageChange?: (file: File) => Promise<void> | void;
|
||||
onClearReferenceImage?: () => void;
|
||||
};
|
||||
|
||||
const AUTO_SCROLL_FOLLOW_THRESHOLD_PX = 96;
|
||||
const DOCUMENT_INPUT_ACCEPT =
|
||||
'.txt,.md,.markdown,.csv,.json,text/plain,text/markdown,text/csv,application/json';
|
||||
const REFERENCE_IMAGE_INPUT_ACCEPT = 'image/png,image/jpeg,image/webp';
|
||||
|
||||
function uniqueRecommendedReplies(recommendedReplies: string[] = []) {
|
||||
return [
|
||||
@@ -290,19 +296,26 @@ export function CreationAgentWorkspace({
|
||||
isBusy = false,
|
||||
error = null,
|
||||
quickActions = [],
|
||||
referenceImagePreviewSrc = null,
|
||||
referenceImageLabel = null,
|
||||
referenceImageError = null,
|
||||
onBack,
|
||||
onSubmitText,
|
||||
onPrimaryAction,
|
||||
onQuickAction,
|
||||
onReferenceImageChange,
|
||||
onClearReferenceImage,
|
||||
}: CreationAgentWorkspaceProps) {
|
||||
const [draftText, setDraftText] = useState('');
|
||||
const [documentInputError, setDocumentInputError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [isParsingDocumentInput, setIsParsingDocumentInput] = useState(false);
|
||||
const [isReadingReferenceImage, setIsReadingReferenceImage] = useState(false);
|
||||
// 统一聊天区只在用户仍停留在底部附近时跟随新内容,避免流式回复持续抢走阅读位置。
|
||||
const messageListRef = useRef<HTMLDivElement | null>(null);
|
||||
const documentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const referenceImageInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const shouldAutoScrollRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -376,7 +389,7 @@ export function CreationAgentWorkspace({
|
||||
|
||||
const submit = () => {
|
||||
const text = draftText.trim();
|
||||
if (!text || isBusy || isParsingDocumentInput) {
|
||||
if (!text || isBusy || isParsingDocumentInput || isReadingReferenceImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -399,6 +412,10 @@ export function CreationAgentWorkspace({
|
||||
documentInputRef.current?.click();
|
||||
};
|
||||
|
||||
const openReferenceImagePicker = () => {
|
||||
referenceImageInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleDocumentInputChange = async (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
@@ -426,6 +443,25 @@ export function CreationAgentWorkspace({
|
||||
}
|
||||
};
|
||||
|
||||
const handleReferenceImageInputChange = async (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const file = event.target.files?.[0] ?? null;
|
||||
event.target.value = '';
|
||||
|
||||
if (!file || isBusy || isReadingReferenceImage || !onReferenceImageChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsReadingReferenceImage(true);
|
||||
|
||||
try {
|
||||
await onReferenceImageChange(file);
|
||||
} finally {
|
||||
setIsReadingReferenceImage(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-full min-h-0 w-full max-w-6xl flex-col gap-3 overflow-hidden px-1 sm:px-0">
|
||||
<div
|
||||
@@ -545,9 +581,36 @@ export function CreationAgentWorkspace({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{documentInputError || error ? (
|
||||
{referenceImagePreviewSrc ? (
|
||||
<div className="mx-4 mb-3 flex items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-2">
|
||||
<div className="h-12 w-12 shrink-0 overflow-hidden rounded-[0.9rem] bg-[var(--platform-track-fill)]">
|
||||
<img
|
||||
src={referenceImagePreviewSrc}
|
||||
alt="参考图"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 truncate text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
{referenceImageLabel || '已选择参考图'}
|
||||
</div>
|
||||
{onClearReferenceImage ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy || isReadingReferenceImage}
|
||||
onClick={onClearReferenceImage}
|
||||
className="platform-icon-button h-9 w-9"
|
||||
aria-label="移除参考图"
|
||||
title="移除参考图"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{documentInputError || referenceImageError || error ? (
|
||||
<div className="mx-4 mb-3 rounded-[1rem] border border-red-200/70 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{documentInputError || error}
|
||||
{documentInputError || referenceImageError || error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -560,6 +623,15 @@ export function CreationAgentWorkspace({
|
||||
className="hidden"
|
||||
onChange={handleDocumentInputChange}
|
||||
/>
|
||||
{onReferenceImageChange ? (
|
||||
<input
|
||||
ref={referenceImageInputRef}
|
||||
type="file"
|
||||
accept={REFERENCE_IMAGE_INPUT_ACCEPT}
|
||||
className="hidden"
|
||||
onChange={handleReferenceImageInputChange}
|
||||
/>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
@@ -575,9 +647,30 @@ export function CreationAgentWorkspace({
|
||||
className={`h-4 w-4 ${isParsingDocumentInput ? 'animate-pulse' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
{onReferenceImageChange ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
isReadingReferenceImage ? '正在读取参考图' : '上传参考图'
|
||||
}
|
||||
title={
|
||||
isReadingReferenceImage ? '正在读取参考图' : '上传参考图'
|
||||
}
|
||||
aria-busy={isReadingReferenceImage}
|
||||
disabled={isBusy || isReadingReferenceImage}
|
||||
onClick={openReferenceImagePicker}
|
||||
className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full text-[var(--platform-text-base)] hover:bg-black/5 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
<ImagePlus
|
||||
className={`h-4 w-4 ${isReadingReferenceImage ? 'animate-pulse' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
<textarea
|
||||
value={draftText}
|
||||
disabled={isBusy || isParsingDocumentInput}
|
||||
disabled={
|
||||
isBusy || isParsingDocumentInput || isReadingReferenceImage
|
||||
}
|
||||
rows={2}
|
||||
onChange={(event) => {
|
||||
setDraftText(event.target.value);
|
||||
@@ -595,7 +688,12 @@ export function CreationAgentWorkspace({
|
||||
<button
|
||||
type="button"
|
||||
aria-label="发送"
|
||||
disabled={isBusy || isParsingDocumentInput || !draftText.trim()}
|
||||
disabled={
|
||||
isBusy ||
|
||||
isParsingDocumentInput ||
|
||||
isReadingReferenceImage ||
|
||||
!draftText.trim()
|
||||
}
|
||||
onClick={submit}
|
||||
className={`inline-flex h-11 w-11 items-center justify-center rounded-full text-white disabled:cursor-not-allowed disabled:opacity-40 ${theme.userBubbleClass}`}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import type {
|
||||
ExecuteMatch3DActionRequest,
|
||||
Match3DAnchorItemResponse,
|
||||
Match3DAgentSessionSnapshot,
|
||||
SendMatch3DMessageRequest,
|
||||
} from '../../../packages/shared/src/contracts/match3dAgent';
|
||||
import {
|
||||
buildCreationAgentChatMessage,
|
||||
createCreationAgentChatQuickActions,
|
||||
createCreationAgentClientMessageId,
|
||||
resolveCreationAgentQuickActionMessage,
|
||||
} from '../../services/creation-agent';
|
||||
import {
|
||||
type CreationAgentAnchorView,
|
||||
type CreationAgentSessionView,
|
||||
type CreationAgentTheme,
|
||||
CreationAgentWorkspace,
|
||||
} from '../creation-agent';
|
||||
|
||||
type Match3DAgentWorkspaceProps = {
|
||||
session: Match3DAgentSessionSnapshot | null;
|
||||
streamingReplyText?: string;
|
||||
isStreamingReply?: boolean;
|
||||
isBusy?: boolean;
|
||||
error?: string | null;
|
||||
onBack: () => void;
|
||||
onSubmitMessage: (payload: SendMatch3DMessageRequest) => void;
|
||||
onExecuteAction: (payload: ExecuteMatch3DActionRequest) => void;
|
||||
};
|
||||
|
||||
type Match3DReferenceImageState = {
|
||||
src: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const MATCH3D_AGENT_THEME: CreationAgentTheme = {
|
||||
accentTextClass: 'text-lime-100/86',
|
||||
accentBgClass: 'bg-lime-200',
|
||||
accentButtonClass: 'bg-lime-200 shadow-emerald-950/20',
|
||||
userBubbleClass: 'bg-emerald-600 text-white',
|
||||
heroClass:
|
||||
'border border-lime-100/18 bg-[radial-gradient(circle_at_top_left,rgba(190,242,100,0.24),transparent_34%),radial-gradient(circle_at_bottom_right,rgba(251,146,60,0.2),transparent_32%),linear-gradient(135deg,rgba(20,83,45,0.96),rgba(39,39,42,0.96))]',
|
||||
anchorGridClass: 'grid gap-2 sm:grid-cols-3',
|
||||
};
|
||||
|
||||
const MATCH3D_QUICK_ACTIONS = [
|
||||
...createCreationAgentChatQuickActions(),
|
||||
{
|
||||
key: 'match3d-auto-config',
|
||||
label: '自动配置',
|
||||
},
|
||||
];
|
||||
|
||||
function readMatch3DReferenceImageAsDataUrl(file: File) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
reject(new Error('请选择图片文件。'));
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onerror = () => reject(new Error('参考图读取失败,请重试。'));
|
||||
reader.onload = () => resolve(String(reader.result || ''));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function mapMatch3DAnchor(
|
||||
anchor: Match3DAnchorItemResponse,
|
||||
): CreationAgentAnchorView {
|
||||
return {
|
||||
key: anchor.key,
|
||||
label: anchor.label,
|
||||
value: anchor.value,
|
||||
status: anchor.status,
|
||||
};
|
||||
}
|
||||
|
||||
function mapMatch3DSession(
|
||||
session: Match3DAgentSessionSnapshot,
|
||||
): CreationAgentSessionView {
|
||||
// 中文注释:抓大鹅 F1 只展示聊天与配置锚点,草稿结果交给后续结果页承接。
|
||||
const chatMessages = session.messages.filter(
|
||||
(message) =>
|
||||
message.kind === 'chat' ||
|
||||
message.kind === 'summary' ||
|
||||
message.kind === 'warning',
|
||||
);
|
||||
|
||||
return {
|
||||
sessionId: session.sessionId,
|
||||
title: null,
|
||||
assistantSummary: null,
|
||||
currentTurn: session.currentTurn,
|
||||
progressPercent: session.progressPercent,
|
||||
anchors: [
|
||||
session.anchorPack.theme,
|
||||
session.anchorPack.clearCount,
|
||||
session.anchorPack.difficulty,
|
||||
].map(mapMatch3DAnchor),
|
||||
messages: chatMessages,
|
||||
recommendedReplies: [],
|
||||
};
|
||||
}
|
||||
|
||||
function buildMatch3DChatPayload({
|
||||
text,
|
||||
quickFillRequested = false,
|
||||
referenceImageSrc,
|
||||
}: {
|
||||
text: string;
|
||||
quickFillRequested?: boolean;
|
||||
referenceImageSrc?: string | null;
|
||||
}) {
|
||||
return buildCreationAgentChatMessage<{
|
||||
referenceImageSrc?: string | null;
|
||||
}>({
|
||||
clientMessageId: createCreationAgentClientMessageId('match3d'),
|
||||
text,
|
||||
quickFillRequested,
|
||||
extraPayload: {
|
||||
referenceImageSrc: referenceImageSrc || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function Match3DAgentWorkspace({
|
||||
session,
|
||||
streamingReplyText = '',
|
||||
isStreamingReply = false,
|
||||
isBusy = false,
|
||||
error = null,
|
||||
onBack,
|
||||
onSubmitMessage,
|
||||
onExecuteAction,
|
||||
}: Match3DAgentWorkspaceProps) {
|
||||
const [referenceImage, setReferenceImage] =
|
||||
useState<Match3DReferenceImageState | null>(null);
|
||||
const [referenceImageError, setReferenceImageError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
return (
|
||||
<CreationAgentWorkspace
|
||||
session={session ? mapMatch3DSession(session) : null}
|
||||
theme={MATCH3D_AGENT_THEME}
|
||||
loadingText="正在准备抓大鹅共创工作区..."
|
||||
composerPlaceholder="题材、消除次数、难度..."
|
||||
primaryActionLabel="生成结果页"
|
||||
streamingReplyText={streamingReplyText}
|
||||
isStreamingReply={isStreamingReply}
|
||||
isBusy={isBusy}
|
||||
error={error}
|
||||
quickActions={MATCH3D_QUICK_ACTIONS}
|
||||
referenceImagePreviewSrc={referenceImage?.src ?? null}
|
||||
referenceImageLabel={referenceImage?.label ?? null}
|
||||
referenceImageError={referenceImageError}
|
||||
onBack={onBack}
|
||||
onSubmitText={(text) => {
|
||||
onSubmitMessage(
|
||||
buildMatch3DChatPayload({
|
||||
text,
|
||||
referenceImageSrc: referenceImage?.src ?? null,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onPrimaryAction={() => {
|
||||
onExecuteAction({ action: 'match3d_compile_draft' });
|
||||
}}
|
||||
onQuickAction={(action) => {
|
||||
const quickActionMessage =
|
||||
action.key === 'match3d-auto-config'
|
||||
? {
|
||||
text: '自动配置',
|
||||
quickFillRequested: true,
|
||||
}
|
||||
: resolveCreationAgentQuickActionMessage(
|
||||
action.key,
|
||||
'请总结一下当前抓大鹅设定。',
|
||||
);
|
||||
|
||||
onSubmitMessage(
|
||||
buildMatch3DChatPayload({
|
||||
...quickActionMessage,
|
||||
referenceImageSrc: referenceImage?.src ?? null,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onReferenceImageChange={async (file) => {
|
||||
try {
|
||||
const dataUrl = await readMatch3DReferenceImageAsDataUrl(file);
|
||||
setReferenceImage({
|
||||
src: dataUrl,
|
||||
label: file.name.trim() || '本地参考图',
|
||||
});
|
||||
setReferenceImageError(null);
|
||||
} catch (caughtError) {
|
||||
setReferenceImageError(
|
||||
caughtError instanceof Error
|
||||
? caughtError.message
|
||||
: '参考图读取失败,请重试。',
|
||||
);
|
||||
}
|
||||
}}
|
||||
onClearReferenceImage={() => {
|
||||
setReferenceImage(null);
|
||||
setReferenceImageError(null);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Match3DAgentWorkspace;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user