@@ -44,3 +44,15 @@
|
|||||||
3. 创作 Tab 默认显示拼图创作表单内容,且不显示旧“Hi, 朋友”、输入框或智能创作快捷按钮。
|
3. 创作 Tab 默认显示拼图创作表单内容,且不显示旧“Hi, 朋友”、输入框或智能创作快捷按钮。
|
||||||
4. 隐藏的智能创作类型不出现在模板 Tab、旧选择弹层和创作 Hub 卡片中。
|
4. 隐藏的智能创作类型不出现在模板 Tab、旧选择弹层和创作 Hub 卡片中。
|
||||||
5. 草稿页返回创作页后仍回到同一模板入口,并可保留拼图表单草稿内容。
|
5. 草稿页返回创作页后仍回到同一模板入口,并可保留拼图表单草稿内容。
|
||||||
|
|
||||||
|
## 5. 嵌入式表单 UI 细节
|
||||||
|
|
||||||
|
2026-05-10 补充:抓大鹅与视觉小说作为创作 Tab 内嵌表单时,风格类横滑选择器应统一使用浅底卡片、柔和玫瑰色选中态和小圆点确认标记。不要使用大面积黑色渐变、黑底胶囊标签或高饱和红色外框,以免在输入框下方误读为错误提示。
|
||||||
|
|
||||||
|
嵌入式表单控件保持以下口径:
|
||||||
|
|
||||||
|
1. 大文本输入框使用白底、低饱和边框和轻量 focus ring。
|
||||||
|
2. 风格选择区作为独立浅色分组承载横滑卡片,移动端只横向滚动,不挤压生成按钮。
|
||||||
|
3. 风格卡标签使用浅底胶囊,保证图片仍是主体。
|
||||||
|
4. 难度等分段选项可以使用主品牌色,但选中态需要降低阴影和饱和度。
|
||||||
|
5. UI 中不补充玩法规则说明文案,保持创作入口清爽。
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
生成页步骤固定为:
|
生成页步骤固定为:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
生成物品名称 -> 生成素材图 -> 切割独立图片 -> 生成 3D 模型 -> 上传图片与模型资产 -> 写入草稿页
|
生成游戏名称 -> 生成物品名称 -> 生成素材图 -> 切割独立图片 -> 上传图片资产 -> 写入草稿页
|
||||||
```
|
```
|
||||||
|
|
||||||
生成页只展示题材和物品数量,不展示玩法规则说明。
|
生成页只展示题材和物品数量,不展示玩法规则说明。
|
||||||
@@ -32,15 +32,17 @@
|
|||||||
|
|
||||||
1. 读取 session config。
|
1. 读取 session config。
|
||||||
2. 将本次 MVP 的 `clearCount` 固定为 `3`,并同步用于草稿编译。
|
2. 将本次 MVP 的 `clearCount` 固定为 `3`,并同步用于草稿编译。
|
||||||
3. 调用文本模型生成 `3` 个题材下的短物品名称。
|
3. 基于入口页题材设定文本调用文本模型生成作品元信息。模型固定请求 `gpt-4o`,只返回 JSON,其中 `gameName` 为 4 到 12 个中文字符的游戏名称,`tags` 为 3 到 6 个中文短标签;`summary` 首版必须保持空字符串,结果页 `作品描述` 默认留给用户填写。
|
||||||
4. 调用项目当前图片链路 VectorEngine `gpt-image-2-all` 生成一张 `1:1` 素材图,提示词必须合入入口页选择的 `assetStylePrompt`。历史 `nanobanana2` 图片选项当前按项目统一决策回落到 VectorEngine,不重新接入 APIMart 图片网关。
|
4. 调用文本模型生成 `3` 个题材下的短物品名称。
|
||||||
5. 将素材图按 `n*n` 网格切割成独立图片。当前 `3` 件物品使用 `2*2` 网格,取前 `3` 格。
|
5. 调用项目当前图片链路 VectorEngine `gpt-image-2-all` 生成一张 `1:1` 素材图,提示词必须合入入口页选择的 `assetStylePrompt`。历史 `nanobanana2` 图片选项当前按项目统一决策回落到 VectorEngine,不重新接入 APIMart 图片网关。
|
||||||
6. 将素材图和每张独立图片上传到 OSS,其中独立图片作为 Rodin 图生模型参考图。
|
6. 将素材图按 `n*n` 网格切割成独立图片。当前 `3` 件物品使用 `2*2` 网格,取前 `3` 格。
|
||||||
7. 对每张独立图片调用 Hyper3D Rodin Gen-2 图生模型,等待任务完成,读取 GLB 下载文件并转存到 `generated-match3d-assets`。
|
7. 将素材图和每张独立图片上传到 OSS,其中独立图片作为草稿页素材预览和后续 Rodin 图生模型参考图。
|
||||||
8. 调用现有 SpacetimeDB compile procedure 写入草稿,并把本次生成的独立物品图片与模型文件引用序列化写入 `match3d_work_profile.generated_item_assets_json`。这一步对标拼图的 `save_puzzle_generated_images`:生成资产不能只挂在本次 HTTP response 上,否则退出结果页后从草稿架读取 `getMatch3DWorkDetail` 会丢失素材列表。
|
8. 调用现有 SpacetimeDB compile procedure 写入草稿,并把本次生成的独立物品图片列表序列化写入 `match3d_work_profile.generated_item_assets_json`。这一步对标拼图的 `save_puzzle_generated_images`:生成资产不能只挂在本次 HTTP response 上,否则退出结果页后从草稿架读取 `getMatch3DWorkDetail` 会丢失素材列表。
|
||||||
9. 在 HTTP 返回的 draft/profile DTO 中附带本次生成的素材资产预览信息,模型生成成功时状态为 `model_ready`;后续重进草稿页时从 work profile 的持久化 `generatedItemAssets` 恢复同一批素材。
|
9. 在 HTTP 返回的 draft/profile DTO 中附带本次生成的素材资产预览信息,独立图片状态为 `image_ready`,模型字段保持为空;后续重进草稿页时从 work profile 的持久化 `generatedItemAssets` 恢复同一批素材。
|
||||||
|
|
||||||
草稿生成阶段会调用 Hyper3D Rodin,并等待 GLB 模型文件可下载;不得把上游下载 URL 直接写入 Match3D profile,必须先转存 OSS,再保存 `/generated-match3d-assets/...` 引用。结果页 `3D素材` Tab 的重新生成按钮仍可手动触发 Rodin 任务,但正式持久化仍以后端草稿生成链路写入的模型文件为准。
|
若文本模型不可用或返回无法解析,后端必须降级为 `{themeText}抓大鹅` 与本地标签兜底,不阻断素材生成;但描述仍保持空字符串。
|
||||||
|
|
||||||
|
草稿生成阶段不调用 Hyper3D Rodin,不等待 `subscriptionKey`,也不下载模型文件;Rodin 生成只在结果页 `3D素材` Tab 由用户手动触发。手动生成得到的上游下载 URL 仍不得直接写入 Match3D profile,后续正式资产绑定以独立技术方案为准。
|
||||||
|
|
||||||
## 4. 图片提示词
|
## 4. 图片提示词
|
||||||
|
|
||||||
@@ -82,12 +84,11 @@ generated-match3d-assets
|
|||||||
```text
|
```text
|
||||||
generated-match3d-assets/{sessionId}/{profileId}/material-sheet/{taskId}/sheet.png
|
generated-match3d-assets/{sessionId}/{profileId}/material-sheet/{taskId}/sheet.png
|
||||||
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/image.png
|
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/image.png
|
||||||
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/model/{taskUuid}/model.glb
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`itemSlug` 必须带 `itemId` 前缀,例如 `match3d-item-1-item`。中文物品名清洗后可能都退回 `item`,不能只用物品名做路径,否则多张切割图会写到同一个 object key,导致草稿页预览图全部一致。
|
`itemSlug` 必须带 `itemId` 前缀,例如 `match3d-item-1-item`。中文物品名清洗后可能都退回 `item`,不能只用物品名做路径,否则多张切割图会写到同一个 object key,导致草稿页预览图全部一致。
|
||||||
|
|
||||||
HTTP DTO 同时返回 `imageSrc`、`imageObjectKey`、`modelSrc`、`modelObjectKey`、`modelFileName`、`taskUuid`、`subscriptionKey` 和 `status = model_ready`。前端模型预览不得直接请求裸 `/generated-match3d-assets/...` 路径;需要通过 `/api/assets/read-bytes` 读取模型字节,转成 Blob URL 后交给 Three.js GLTFLoader 加载,以绕开私有 bucket CORS。
|
HTTP DTO 同时返回 `imageSrc`、`imageObjectKey`、空的 `modelSrc`、空的 `modelObjectKey` 和 `status = image_ready`。前端预览图片继续走 `ResolvedAssetImage` 换签;后续手动生成的模型文件也必须通过 `useResolvedAssetReadUrl` / `/api/assets/read-url` 换签后打开,不直接请求裸 `/generated-match3d-assets/...` 路径。
|
||||||
|
|
||||||
## 6. 自动保存与草稿恢复
|
## 6. 自动保存与草稿恢复
|
||||||
|
|
||||||
@@ -101,6 +102,13 @@ HTTP DTO 同时返回 `imageSrc`、`imageObjectKey`、`modelSrc`、`modelObjectK
|
|||||||
|
|
||||||
因此 `map_match3d_work_summary_response` / `map_match3d_work_profile_response` 需要从 work profile snapshot 反序列化 `generated_item_assets_json` 并输出 `generatedItemAssets`。前端 `Match3DResultView` 仍保持现有优先级:本次生成流程内有 `draft.generatedItemAssets` 时用 draft;从草稿架重进没有 draft 时,用 `profile.generatedItemAssets`;两者都没有才回退到默认 3D 素材占位。
|
因此 `map_match3d_work_summary_response` / `map_match3d_work_profile_response` 需要从 work profile snapshot 反序列化 `generated_item_assets_json` 并输出 `generatedItemAssets`。前端 `Match3DResultView` 仍保持现有优先级:本次生成流程内有 `draft.generatedItemAssets` 时用 draft;从草稿架重进没有 draft 时,用 `profile.generatedItemAssets`;两者都没有才回退到默认 3D 素材占位。
|
||||||
|
|
||||||
|
结果页 `作品信息` Tab 字段命名对齐拼图草稿:
|
||||||
|
|
||||||
|
1. `作品名称` 对应 Match3D `gameName`。
|
||||||
|
2. `作品描述` 对应 Match3D `summary`,草稿生成默认空。
|
||||||
|
3. `作品标签` 对应 Match3D `tags`,可由 AI 首次生成并允许用户继续编辑。
|
||||||
|
4. 封面图与作品名称不再拆成左右两个大模块;封面只作为同一 Tab 内的可选上传入口,避免和作品基础信息割裂。
|
||||||
|
|
||||||
`3D素材` 详情页只保留:
|
`3D素材` 详情页只保留:
|
||||||
|
|
||||||
1. 模型预览区:优先加载 `modelSrc` 对应 GLB,支持拖动旋转;没有模型时展示空预览。
|
1. 模型预览区:优先加载 `modelSrc` 对应 GLB,支持拖动旋转;没有模型时展示空预览。
|
||||||
@@ -127,4 +135,4 @@ cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
|
|||||||
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
|
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
真实草稿生成需要本地私密环境配置 `VECTOR_ENGINE_API_KEY`、`HYPER3D_API_KEY` 和 OSS 访问变量。后端改动后使用 `npm run api-server` 启动,并检查 `/healthz`。
|
真实草稿生成需要本地私密环境配置 `VECTOR_ENGINE_API_KEY` 和 OSS 访问变量;`HYPER3D_API_KEY` 只在结果页手动生成 3D 模型时需要。后端改动后使用 `npm run api-server` 启动,并检查 `/healthz`。
|
||||||
|
|||||||
@@ -1805,6 +1805,220 @@ async fn generate_match3d_material_sheet(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn generate_match3d_rodin_model_asset(
|
||||||
|
state: &AppState,
|
||||||
|
owner_user_id: &str,
|
||||||
|
session_id: &str,
|
||||||
|
profile_id: &str,
|
||||||
|
item_slug: &str,
|
||||||
|
item_name: &str,
|
||||||
|
config: &Match3DConfigJson,
|
||||||
|
image_bytes: Vec<u8>,
|
||||||
|
generated_at_micros: i64,
|
||||||
|
) -> Result<Match3DRodinModelAsset, AppError> {
|
||||||
|
let image_data_url = build_match3d_png_data_url(&image_bytes);
|
||||||
|
let submit_response = submit_image_to_model(
|
||||||
|
state,
|
||||||
|
hyper3d_contract::Hyper3dImageToModelRequest {
|
||||||
|
image_data_urls: vec![image_data_url],
|
||||||
|
image_urls: Vec::new(),
|
||||||
|
prompt: Some(build_match3d_rodin_model_prompt(config, item_name)),
|
||||||
|
condition_mode: Some("concat".to_string()),
|
||||||
|
seed: None,
|
||||||
|
geometry_file_format: Some("glb".to_string()),
|
||||||
|
material: Some("PBR".to_string()),
|
||||||
|
quality: Some("medium".to_string()),
|
||||||
|
mesh_mode: Some("Quad".to_string()),
|
||||||
|
addons: Vec::new(),
|
||||||
|
bbox_condition: None,
|
||||||
|
preview_render: Some(true),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
wait_for_match3d_rodin_model(
|
||||||
|
state,
|
||||||
|
submit_response.subscription_key.as_str(),
|
||||||
|
item_name,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let download_response = query_downloads(
|
||||||
|
state,
|
||||||
|
hyper3d_contract::Hyper3dDownloadRequest {
|
||||||
|
task_uuid: submit_response.task_uuid.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let model_file = select_match3d_glb_download(
|
||||||
|
&download_response.files,
|
||||||
|
submit_response.task_uuid.as_str(),
|
||||||
|
item_name,
|
||||||
|
)?;
|
||||||
|
let downloaded_model = download_match3d_rodin_model(model_file).await?;
|
||||||
|
let uploaded_model = persist_match3d_generated_bytes(
|
||||||
|
state,
|
||||||
|
owner_user_id,
|
||||||
|
session_id,
|
||||||
|
profile_id,
|
||||||
|
&[
|
||||||
|
"items",
|
||||||
|
item_slug,
|
||||||
|
"model",
|
||||||
|
submit_response.task_uuid.as_str(),
|
||||||
|
],
|
||||||
|
downloaded_model.file_name.as_str(),
|
||||||
|
downloaded_model.content_type.as_str(),
|
||||||
|
downloaded_model.bytes,
|
||||||
|
"match3d_item_model",
|
||||||
|
Some(submit_response.task_uuid.as_str()),
|
||||||
|
generated_at_micros,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Match3DRodinModelAsset {
|
||||||
|
task_uuid: submit_response.task_uuid,
|
||||||
|
subscription_key: submit_response.subscription_key,
|
||||||
|
model_file_name: downloaded_model.file_name,
|
||||||
|
upload: uploaded_model,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_match3d_png_data_url(image_bytes: &[u8]) -> String {
|
||||||
|
format!(
|
||||||
|
"data:image/png;base64,{}",
|
||||||
|
BASE64_STANDARD.encode(image_bytes)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_match3d_rodin_model_prompt(config: &Match3DConfigJson, item_name: &str) -> String {
|
||||||
|
let style_clause = resolve_match3d_asset_style_prompt(config)
|
||||||
|
.map(|prompt| format!("画风遵循:{prompt}。"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
format!(
|
||||||
|
"{theme}题材抓大鹅游戏物件:{item_name}。{style_clause}生成单个完整游戏 3D 模型,主体清晰,低面数,PBR 材质,适合移动端实时渲染,不要文字、底座、场景和额外物体。",
|
||||||
|
theme = config.theme_text,
|
||||||
|
style_clause = style_clause,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_match3d_rodin_model(
|
||||||
|
state: &AppState,
|
||||||
|
subscription_key: &str,
|
||||||
|
item_name: &str,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
for attempt in 0..MATCH3D_RODIN_STATUS_MAX_ATTEMPTS {
|
||||||
|
let status_response = query_task_status(
|
||||||
|
state,
|
||||||
|
hyper3d_contract::Hyper3dTaskStatusRequest {
|
||||||
|
subscription_key: subscription_key.to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
match status_response.status.as_str() {
|
||||||
|
"done" => return Ok(()),
|
||||||
|
"failed" => {
|
||||||
|
let message = status_response
|
||||||
|
.jobs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|job| job.message.as_deref())
|
||||||
|
.map(str::trim)
|
||||||
|
.find(|value| !value.is_empty())
|
||||||
|
.unwrap_or("Rodin 模型生成失败");
|
||||||
|
return Err(match3d_bad_gateway(format!(
|
||||||
|
"{item_name} 3D 模型生成失败:{message}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempt + 1 < MATCH3D_RODIN_STATUS_MAX_ATTEMPTS {
|
||||||
|
tokio::time::sleep(Duration::from_millis(
|
||||||
|
MATCH3D_RODIN_STATUS_POLL_INTERVAL_MS,
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(match3d_bad_gateway(format!(
|
||||||
|
"{item_name} 3D 模型生成超时,请稍后重试"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_match3d_glb_download<'a>(
|
||||||
|
files: &'a [hyper3d_contract::Hyper3dDownloadFilePayload],
|
||||||
|
task_uuid: &str,
|
||||||
|
item_name: &str,
|
||||||
|
) -> Result<&'a hyper3d_contract::Hyper3dDownloadFilePayload, AppError> {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.find(|file| {
|
||||||
|
file.name.to_ascii_lowercase().ends_with(".glb")
|
||||||
|
|| file.url.to_ascii_lowercase().split('?').next().unwrap_or("").ends_with(".glb")
|
||||||
|
})
|
||||||
|
.or_else(|| files.first())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
match3d_bad_gateway(format!(
|
||||||
|
"{item_name} 3D 模型已完成但未返回可下载模型文件:{task_uuid}"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn download_match3d_rodin_model(
|
||||||
|
file: &hyper3d_contract::Hyper3dDownloadFilePayload,
|
||||||
|
) -> Result<Match3DDownloadedModel, AppError> {
|
||||||
|
let response = reqwest::Client::new()
|
||||||
|
.get(file.url.as_str())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|error| match3d_bad_gateway(format!("下载 Rodin 模型失败:{error}")))?;
|
||||||
|
let status = response.status();
|
||||||
|
let content_type = response
|
||||||
|
.headers()
|
||||||
|
.get(header::CONTENT_TYPE)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.unwrap_or("model/gltf-binary")
|
||||||
|
.to_string();
|
||||||
|
let bytes = response
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|error| match3d_bad_gateway(format!("读取 Rodin 模型内容失败:{error}")))?;
|
||||||
|
if !status.is_success() {
|
||||||
|
return Err(match3d_bad_gateway(format!(
|
||||||
|
"下载 Rodin 模型失败:HTTP {}",
|
||||||
|
status.as_u16()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if bytes.is_empty() || bytes.len() > MATCH3D_RODIN_MAX_MODEL_BYTES {
|
||||||
|
return Err(match3d_bad_gateway("Rodin 模型内容为空或超过大小上限"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Match3DDownloadedModel {
|
||||||
|
bytes: bytes.to_vec(),
|
||||||
|
file_name: normalize_match3d_model_file_name(file.name.as_str()),
|
||||||
|
content_type: normalize_match3d_model_content_type(content_type.as_str()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_match3d_model_file_name(raw: &str) -> String {
|
||||||
|
let trimmed = raw.trim().rsplit('/').next().unwrap_or(raw).trim();
|
||||||
|
let without_query = trimmed.split('?').next().unwrap_or(trimmed).trim();
|
||||||
|
let sanitized = sanitize_match3d_asset_segment(without_query, "model");
|
||||||
|
if sanitized.to_ascii_lowercase().ends_with(".glb") {
|
||||||
|
sanitized
|
||||||
|
} else {
|
||||||
|
"model.glb".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_match3d_model_content_type(raw: &str) -> String {
|
||||||
|
let normalized = raw.split(';').next().unwrap_or(raw).trim().to_lowercase();
|
||||||
|
if normalized == "model/gltf-binary" || normalized == "application/octet-stream" {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
"model/gltf-binary".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn build_match3d_material_sheet_prompt(
|
fn build_match3d_material_sheet_prompt(
|
||||||
config: &Match3DConfigJson,
|
config: &Match3DConfigJson,
|
||||||
item_names: &[String],
|
item_names: &[String],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
// This was generated using spacetimedb cli version 2.2.0 (commit eb11e2f5c41dce6979715ad407996270d61329f6).
|
// This was generated using spacetimedb cli version 2.1.0 (commit 6981f48b4bc1a71c8dd9bdfe5a2c343f6370243d).
|
||||||
|
|
||||||
#![allow(unused, clippy::all)]
|
#![allow(unused, clippy::all)]
|
||||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub struct ProfileInviteCode {
|
|||||||
pub updated_at: __sdk::Timestamp,
|
pub updated_at: __sdk::Timestamp,
|
||||||
pub starts_at: Option<__sdk::Timestamp>,
|
pub starts_at: Option<__sdk::Timestamp>,
|
||||||
pub expires_at: Option<__sdk::Timestamp>,
|
pub expires_at: Option<__sdk::Timestamp>,
|
||||||
|
pub granted_user_tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for ProfileInviteCode {
|
impl __sdk::InModule for ProfileInviteCode {
|
||||||
@@ -31,6 +32,7 @@ pub struct ProfileInviteCodeCols {
|
|||||||
pub updated_at: __sdk::__query_builder::Col<ProfileInviteCode, __sdk::Timestamp>,
|
pub updated_at: __sdk::__query_builder::Col<ProfileInviteCode, __sdk::Timestamp>,
|
||||||
pub starts_at: __sdk::__query_builder::Col<ProfileInviteCode, Option<__sdk::Timestamp>>,
|
pub starts_at: __sdk::__query_builder::Col<ProfileInviteCode, Option<__sdk::Timestamp>>,
|
||||||
pub expires_at: __sdk::__query_builder::Col<ProfileInviteCode, Option<__sdk::Timestamp>>,
|
pub expires_at: __sdk::__query_builder::Col<ProfileInviteCode, Option<__sdk::Timestamp>>,
|
||||||
|
pub granted_user_tags: __sdk::__query_builder::Col<ProfileInviteCode, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for ProfileInviteCode {
|
impl __sdk::__query_builder::HasCols for ProfileInviteCode {
|
||||||
@@ -44,6 +46,7 @@ impl __sdk::__query_builder::HasCols for ProfileInviteCode {
|
|||||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||||
starts_at: __sdk::__query_builder::Col::new(table_name, "starts_at"),
|
starts_at: __sdk::__query_builder::Col::new(table_name, "starts_at"),
|
||||||
expires_at: __sdk::__query_builder::Col::new(table_name, "expires_at"),
|
expires_at: __sdk::__query_builder::Col::new(table_name, "expires_at"),
|
||||||
|
granted_user_tags: __sdk::__query_builder::Col::new(table_name, "granted_user_tags"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ pub trait record_tracking_event_and_return {
|
|||||||
input: RuntimeTrackingEventInput,
|
input: RuntimeTrackingEventInput,
|
||||||
|
|
||||||
__callback: impl FnOnce(
|
__callback: impl FnOnce(
|
||||||
&super::ProcedureEventContext,
|
&super::ProcedureEventContext,
|
||||||
Result<RuntimeTrackingEventProcedureResult, __sdk::InternalError>,
|
Result<RuntimeTrackingEventProcedureResult, __sdk::InternalError>,
|
||||||
) + Send
|
) + Send
|
||||||
+ 'static,
|
+ 'static,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,10 +44,10 @@ impl record_tracking_event_and_return for super::RemoteProcedures {
|
|||||||
input: RuntimeTrackingEventInput,
|
input: RuntimeTrackingEventInput,
|
||||||
|
|
||||||
__callback: impl FnOnce(
|
__callback: impl FnOnce(
|
||||||
&super::ProcedureEventContext,
|
&super::ProcedureEventContext,
|
||||||
Result<RuntimeTrackingEventProcedureResult, __sdk::InternalError>,
|
Result<RuntimeTrackingEventProcedureResult, __sdk::InternalError>,
|
||||||
) + Send
|
) + Send
|
||||||
+ 'static,
|
+ 'static,
|
||||||
) {
|
) {
|
||||||
self.imp
|
self.imp
|
||||||
.invoke_procedure_with_callback::<_, RuntimeTrackingEventProcedureResult>(
|
.invoke_procedure_with_callback::<_, RuntimeTrackingEventProcedureResult>(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub struct RuntimeProfileInviteCodeAdminUpsertInput {
|
|||||||
pub admin_user_id: String,
|
pub admin_user_id: String,
|
||||||
pub invite_code: String,
|
pub invite_code: String,
|
||||||
pub metadata_json: String,
|
pub metadata_json: String,
|
||||||
|
pub granted_user_tags: Vec<String>,
|
||||||
pub starts_at_micros: Option<i64>,
|
pub starts_at_micros: Option<i64>,
|
||||||
pub expires_at_micros: Option<i64>,
|
pub expires_at_micros: Option<i64>,
|
||||||
pub updated_at_micros: i64,
|
pub updated_at_micros: i64,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub struct RuntimeProfileInviteCodeSnapshot {
|
|||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
pub invite_code: String,
|
pub invite_code: String,
|
||||||
pub metadata_json: String,
|
pub metadata_json: String,
|
||||||
|
pub granted_user_tags: Vec<String>,
|
||||||
pub starts_at_micros: Option<i64>,
|
pub starts_at_micros: Option<i64>,
|
||||||
pub expires_at_micros: Option<i64>,
|
pub expires_at_micros: Option<i64>,
|
||||||
pub created_at_micros: i64,
|
pub created_at_micros: i64,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub struct UserAccount {
|
|||||||
pub password_hash: String,
|
pub password_hash: String,
|
||||||
pub password_login_enabled: bool,
|
pub password_login_enabled: bool,
|
||||||
pub token_version: u64,
|
pub token_version: u64,
|
||||||
|
pub user_tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for UserAccount {
|
impl __sdk::InModule for UserAccount {
|
||||||
@@ -43,6 +44,7 @@ pub struct UserAccountCols {
|
|||||||
pub password_hash: __sdk::__query_builder::Col<UserAccount, String>,
|
pub password_hash: __sdk::__query_builder::Col<UserAccount, String>,
|
||||||
pub password_login_enabled: __sdk::__query_builder::Col<UserAccount, bool>,
|
pub password_login_enabled: __sdk::__query_builder::Col<UserAccount, bool>,
|
||||||
pub token_version: __sdk::__query_builder::Col<UserAccount, u64>,
|
pub token_version: __sdk::__query_builder::Col<UserAccount, u64>,
|
||||||
|
pub user_tags: __sdk::__query_builder::Col<UserAccount, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for UserAccount {
|
impl __sdk::__query_builder::HasCols for UserAccount {
|
||||||
@@ -68,6 +70,7 @@ impl __sdk::__query_builder::HasCols for UserAccount {
|
|||||||
"password_login_enabled",
|
"password_login_enabled",
|
||||||
),
|
),
|
||||||
token_version: __sdk::__query_builder::Col::new(table_name, "token_version"),
|
token_version: __sdk::__query_builder::Col::new(table_name, "token_version"),
|
||||||
|
user_tags: __sdk::__query_builder::Col::new(table_name, "user_tags"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ pub struct UserAccount {
|
|||||||
pub(crate) password_hash: String,
|
pub(crate) password_hash: String,
|
||||||
pub(crate) password_login_enabled: bool,
|
pub(crate) password_login_enabled: bool,
|
||||||
pub(crate) token_version: u64,
|
pub(crate) token_version: u64,
|
||||||
#[default(Vec::<String>::new())]
|
|
||||||
pub(crate) user_tags: Vec<String>,
|
pub(crate) user_tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,14 @@ use module_puzzle::{
|
|||||||
tag_similarity_score,
|
tag_similarity_score,
|
||||||
};
|
};
|
||||||
use module_runtime::RuntimeProfileWalletLedgerSourceType;
|
use module_runtime::RuntimeProfileWalletLedgerSourceType;
|
||||||
|
use module_runtime::visible_runtime_profile_user_tags;
|
||||||
use serde_json::from_str as json_from_str;
|
use serde_json::from_str as json_from_str;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use serde_json::to_string as json_to_string;
|
use serde_json::to_string as json_to_string;
|
||||||
use spacetimedb::{ProcedureContext, SpacetimeType, Table, Timestamp, TxContext};
|
use spacetimedb::{ProcedureContext, SpacetimeType, Table, Timestamp, TxContext};
|
||||||
|
|
||||||
|
use crate::auth::user_account;
|
||||||
|
|
||||||
const PUZZLE_POINT_INCENTIVE_DEFAULT_U64: u64 = 0;
|
const PUZZLE_POINT_INCENTIVE_DEFAULT_U64: u64 = 0;
|
||||||
|
|
||||||
/// 拼图 Agent session 真相表。
|
/// 拼图 Agent session 真相表。
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ pub struct ProfileInviteCode {
|
|||||||
pub(crate) starts_at: Option<Timestamp>,
|
pub(crate) starts_at: Option<Timestamp>,
|
||||||
#[default(None::<Timestamp>)]
|
#[default(None::<Timestamp>)]
|
||||||
pub(crate) expires_at: Option<Timestamp>,
|
pub(crate) expires_at: Option<Timestamp>,
|
||||||
#[default(Vec::<String>::new())]
|
|
||||||
pub(crate) granted_user_tags: Vec<String>,
|
pub(crate) granted_user_tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Loader2, Sparkles, WandSparkles, X } from 'lucide-react';
|
import { Loader2, Plus, Sparkles, WandSparkles, X } from 'lucide-react';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -325,7 +325,7 @@ export function Match3DAgentWorkspace({
|
|||||||
|
|
||||||
<section className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
<section className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] gap-2 sm:gap-3 lg:grid-cols-[minmax(0,1.1fr)_minmax(16rem,0.9fr)] lg:grid-rows-1 ${isBusy ? 'opacity-55' : ''}`}
|
className={`grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] gap-3 lg:grid-cols-[minmax(0,1.1fr)_minmax(16rem,0.9fr)] lg:grid-rows-1 ${isBusy ? 'opacity-55' : ''}`}
|
||||||
>
|
>
|
||||||
<label className="block min-h-0">
|
<label className="block min-h-0">
|
||||||
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
||||||
@@ -342,18 +342,18 @@ export function Match3DAgentWorkspace({
|
|||||||
themeText: event.target.value,
|
themeText: event.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
className="h-full min-h-[7.75rem] w-full resize-none rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none sm:min-h-[9rem] lg:min-h-[14rem]"
|
className="h-full min-h-[7.75rem] w-full resize-none rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100 sm:min-h-[9rem] lg:min-h-[14rem]"
|
||||||
aria-label="想做一个什么题材的抓大鹅?"
|
aria-label="想做一个什么题材的抓大鹅?"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="flex min-h-0 flex-col gap-2 overflow-hidden">
|
<div className="flex min-h-0 flex-col gap-2 overflow-hidden">
|
||||||
<div className="min-h-0">
|
<div className="min-h-0 rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/52 p-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.78)]">
|
||||||
<div className="mb-1.5 text-sm font-black text-[var(--platform-text-strong)]">
|
<div className="mb-1.5 text-sm font-black text-[var(--platform-text-strong)]">
|
||||||
3D素材风格
|
3D素材风格
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex snap-x gap-2 overflow-x-auto overscroll-x-contain pb-1 scrollbar-hide touch-pan-x [-webkit-overflow-scrolling:touch]"
|
className="flex snap-x gap-2.5 overflow-x-auto overscroll-x-contain pb-0.5 scrollbar-hide touch-pan-x [-webkit-overflow-scrolling:touch]"
|
||||||
aria-label="3D素材风格"
|
aria-label="3D素材风格"
|
||||||
>
|
>
|
||||||
{MATCH3D_ASSET_STYLE_OPTIONS.map((option) => {
|
{MATCH3D_ASSET_STYLE_OPTIONS.map((option) => {
|
||||||
@@ -374,10 +374,10 @@ export function Match3DAgentWorkspace({
|
|||||||
assetStyleId: option.id,
|
assetStyleId: option.id,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
className={`relative h-[4.45rem] w-[5.2rem] shrink-0 snap-start overflow-hidden rounded-[0.9rem] border p-0 text-left transition sm:h-[5.2rem] sm:w-[6.1rem] ${
|
className={`group relative h-[4.9rem] w-[5.85rem] shrink-0 snap-start overflow-hidden rounded-[0.95rem] border p-0 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-200 sm:h-[5.45rem] sm:w-[6.4rem] ${
|
||||||
selected
|
selected
|
||||||
? 'border-[#ff4056] ring-1 ring-inset ring-[#ff4056]'
|
? 'border-rose-300 bg-white shadow-[0_8px_18px_rgba(190,18,60,0.10)] ring-2 ring-rose-100'
|
||||||
: 'border-[var(--platform-subpanel-border)]'
|
: 'border-[var(--platform-subpanel-border)] bg-white/70 hover:border-rose-200 hover:bg-white/95'
|
||||||
} ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
} ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||||
aria-pressed={selected}
|
aria-pressed={selected}
|
||||||
aria-label={option.label}
|
aria-label={option.label}
|
||||||
@@ -386,19 +386,30 @@ export function Match3DAgentWorkspace({
|
|||||||
<img
|
<img
|
||||||
src={option.imageSrc}
|
src={option.imageSrc}
|
||||||
alt=""
|
alt=""
|
||||||
className="absolute inset-0 h-full w-full object-cover"
|
className="absolute inset-0 h-full w-full object-cover transition duration-200 group-hover:scale-[1.03]"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="absolute inset-0 bg-[linear-gradient(135deg,rgba(255,255,255,0.98),rgba(255,240,244,0.9))]" />
|
<span className="absolute inset-0 bg-[radial-gradient(circle_at_32%_24%,rgba(255,255,255,0.98),transparent_30%),linear-gradient(135deg,rgba(255,247,250,0.98),rgba(255,236,241,0.92))]" />
|
||||||
)}
|
)}
|
||||||
<span className="absolute inset-0 bg-[linear-gradient(180deg,rgba(3,7,18,0.02)_0%,rgba(3,7,18,0.1)_42%,rgba(3,7,18,0.82)_100%)]" />
|
<span className="absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.02)_0%,rgba(255,255,255,0.18)_44%,rgba(255,255,255,0.82)_100%)]" />
|
||||||
|
{selected ? (
|
||||||
|
<span className="absolute right-1.5 top-1.5 h-2.5 w-2.5 rounded-full bg-rose-400 shadow-[0_0_0_3px_rgba(255,255,255,0.84)]" />
|
||||||
|
) : null}
|
||||||
{isCustom ? (
|
{isCustom ? (
|
||||||
<span className="absolute inset-0 flex items-center justify-center text-2xl font-black text-[var(--platform-text-strong)]">
|
<span className="absolute inset-0 flex items-center justify-center text-rose-500">
|
||||||
+
|
<span className="grid h-8 w-8 place-items-center rounded-full bg-white/82 shadow-[0_6px_18px_rgba(190,18,60,0.12)]">
|
||||||
|
<Plus className="h-5 w-5" />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<span className="absolute inset-x-2 bottom-1.5 truncate rounded-full bg-black/26 px-1.5 py-0.5 text-center text-[11px] font-black text-white [text-shadow:0_1px_6px_rgba(0,0,0,0.9)]">
|
<span
|
||||||
|
className={`absolute inset-x-2 bottom-1.5 truncate rounded-full px-1.5 py-0.5 text-center text-[11px] font-black shadow-[0_3px_10px_rgba(15,23,42,0.10)] ${
|
||||||
|
selected
|
||||||
|
? 'bg-rose-50/95 text-rose-700'
|
||||||
|
: 'bg-white/88 text-[var(--platform-text-strong)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -407,7 +418,7 @@ export function Match3DAgentWorkspace({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shrink-0">
|
<div className="shrink-0 rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/44 p-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.7)]">
|
||||||
<div className="mb-1.5 text-sm font-black text-[var(--platform-text-strong)]">
|
<div className="mb-1.5 text-sm font-black text-[var(--platform-text-strong)]">
|
||||||
难度
|
难度
|
||||||
</div>
|
</div>
|
||||||
@@ -426,10 +437,10 @@ export function Match3DAgentWorkspace({
|
|||||||
difficultyOptionId: option.id,
|
difficultyOptionId: option.id,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
className={`min-h-10 rounded-[0.85rem] border px-2 text-sm font-black transition sm:min-h-11 ${
|
className={`min-h-10 rounded-[0.85rem] border px-2 text-sm font-black transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-200 sm:min-h-11 ${
|
||||||
selected
|
selected
|
||||||
? 'border-[#ff4056] bg-[#ff4056] text-white shadow-[0_8px_18px_rgba(255,64,86,0.18)]'
|
? 'border-[#ff7890] bg-[linear-gradient(180deg,#ff7890_0%,#ff4f6a_100%)] text-white shadow-[0_8px_18px_rgba(244,63,94,0.16)]'
|
||||||
: 'border-[var(--platform-subpanel-border)] bg-white/88 text-[var(--platform-text-strong)]'
|
: 'border-[var(--platform-subpanel-border)] bg-white/76 text-[var(--platform-text-strong)] hover:border-rose-200 hover:bg-white'
|
||||||
} ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
} ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||||
aria-pressed={selected}
|
aria-pressed={selected}
|
||||||
>
|
>
|
||||||
@@ -502,7 +513,7 @@ export function Match3DAgentWorkspace({
|
|||||||
value={draftCustomStylePrompt}
|
value={draftCustomStylePrompt}
|
||||||
onChange={(event) => setDraftCustomStylePrompt(event.target.value)}
|
onChange={(event) => setDraftCustomStylePrompt(event.target.value)}
|
||||||
rows={4}
|
rows={4}
|
||||||
className="mt-4 h-[7.5rem] w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none"
|
className="mt-4 h-[7.5rem] w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
||||||
aria-label="自定义3D素材风格描述"
|
aria-label="自定义3D素材风格描述"
|
||||||
/>
|
/>
|
||||||
<div className="mt-5 grid grid-cols-2 gap-3">
|
<div className="mt-5 grid grid-cols-2 gap-3">
|
||||||
|
|||||||
@@ -151,16 +151,24 @@ function VisualNovelStyleButton({
|
|||||||
aria-pressed={active}
|
aria-pressed={active}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`relative h-[4.45rem] w-[5.2rem] shrink-0 snap-start overflow-hidden rounded-[0.9rem] border p-0 text-left transition sm:h-[5.2rem] sm:w-[6.1rem] ${
|
className={`group relative h-[4.9rem] w-[5.85rem] shrink-0 snap-start overflow-hidden rounded-[0.95rem] border p-0 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-200 sm:h-[5.45rem] sm:w-[6.4rem] ${
|
||||||
active
|
active
|
||||||
? 'border-[#ff4056] ring-1 ring-inset ring-[#ff4056]'
|
? 'border-rose-300 bg-white shadow-[0_8px_18px_rgba(190,18,60,0.10)] ring-2 ring-rose-100'
|
||||||
: 'border-[var(--platform-subpanel-border)]'
|
: 'border-[var(--platform-subpanel-border)] bg-white/70 hover:border-rose-200 hover:bg-white/95'
|
||||||
} ${disabled ? 'cursor-not-allowed opacity-55' : ''}`}
|
} ${disabled ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||||
>
|
>
|
||||||
<span className="absolute inset-0 bg-[linear-gradient(135deg,rgba(255,255,255,0.98),rgba(244,247,255,0.9))]" />
|
<span className="absolute inset-0 bg-[radial-gradient(circle_at_32%_24%,rgba(255,255,255,0.98),transparent_30%),linear-gradient(135deg,rgba(255,247,250,0.98),rgba(255,236,241,0.92))]" />
|
||||||
<span className="absolute inset-0 bg-[radial-gradient(circle_at_30%_20%,rgba(255,255,255,0.95),transparent_28%),linear-gradient(135deg,rgba(255,64,86,0.18),rgba(56,189,248,0.18))]" />
|
<span className="absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.02)_0%,rgba(255,255,255,0.18)_44%,rgba(255,255,255,0.82)_100%)]" />
|
||||||
<span className="absolute inset-0 bg-[linear-gradient(180deg,rgba(3,7,18,0.02)_0%,rgba(3,7,18,0.1)_42%,rgba(3,7,18,0.82)_100%)]" />
|
{active ? (
|
||||||
<span className="absolute inset-x-2 bottom-1.5 truncate rounded-full bg-black/26 px-1.5 py-0.5 text-center text-[11px] font-black text-white [text-shadow:0_1px_6px_rgba(0,0,0,0.9)]">
|
<span className="absolute right-1.5 top-1.5 h-2.5 w-2.5 rounded-full bg-rose-400 shadow-[0_0_0_3px_rgba(255,255,255,0.84)]" />
|
||||||
|
) : null}
|
||||||
|
<span
|
||||||
|
className={`absolute inset-x-2 bottom-1.5 truncate rounded-full px-1.5 py-0.5 text-center text-[11px] font-black shadow-[0_3px_10px_rgba(15,23,42,0.10)] ${
|
||||||
|
active
|
||||||
|
? 'bg-rose-50/95 text-rose-700'
|
||||||
|
: 'bg-white/88 text-[var(--platform-text-strong)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -255,7 +263,7 @@ export function VisualNovelAgentWorkspace({
|
|||||||
|
|
||||||
<section className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
<section className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] gap-2 sm:gap-3 lg:grid-cols-[minmax(0,1.1fr)_minmax(16rem,0.9fr)] lg:grid-rows-1 ${isBusy ? 'opacity-55' : ''}`}
|
className={`grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] gap-3 lg:grid-cols-[minmax(0,1.1fr)_minmax(16rem,0.9fr)] lg:grid-rows-1 ${isBusy ? 'opacity-55' : ''}`}
|
||||||
>
|
>
|
||||||
<label className="block min-h-0">
|
<label className="block min-h-0">
|
||||||
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
||||||
@@ -272,18 +280,18 @@ export function VisualNovelAgentWorkspace({
|
|||||||
ideaText: event.target.value,
|
ideaText: event.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
className="h-full min-h-[7.75rem] w-full resize-none rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none sm:min-h-[9rem] lg:min-h-[14rem]"
|
className="h-full min-h-[7.75rem] w-full resize-none rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100 sm:min-h-[9rem] lg:min-h-[14rem]"
|
||||||
aria-label="一句话创作"
|
aria-label="一句话创作"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="flex min-h-0 flex-col gap-2 overflow-hidden">
|
<div className="flex min-h-0 flex-col gap-2 overflow-hidden">
|
||||||
<div className="min-h-0">
|
<div className="min-h-0 rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/52 p-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.78)]">
|
||||||
<div className="mb-1.5 text-sm font-black text-[var(--platform-text-strong)]">
|
<div className="mb-1.5 text-sm font-black text-[var(--platform-text-strong)]">
|
||||||
视觉画风
|
视觉画风
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex snap-x gap-2 overflow-x-auto overscroll-x-contain pb-1 scrollbar-hide touch-pan-x [-webkit-overflow-scrolling:touch]"
|
className="flex snap-x gap-2.5 overflow-x-auto overscroll-x-contain pb-0.5 scrollbar-hide touch-pan-x [-webkit-overflow-scrolling:touch]"
|
||||||
aria-label="视觉画风"
|
aria-label="视觉画风"
|
||||||
>
|
>
|
||||||
{VISUAL_NOVEL_STYLE_OPTIONS.map((option) => (
|
{VISUAL_NOVEL_STYLE_OPTIONS.map((option) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user