fix: rotate short canvas suggestions

This commit is contained in:
2026-05-26 10:38:51 +08:00
parent 97f617197c
commit d01fdc5508
3 changed files with 59 additions and 21 deletions

View File

@@ -610,7 +610,7 @@
<tr><td><code>web/app/page.tsx</code></td><td>旧 React 单对话框生成台源码仍保留,便于以后回滚或抽能力;当前生产根域名已经由 <code>web/canvas-app/</code> 画布产物覆盖,不再把这个 React 首页作为默认首屏。该页面里的模式也已收敛为文生图、文生视频、图生视频;图生视频只显示“上传图片”,不把“首帧/首尾帧”作为用户入口。旧 TK 复刻工作台组件仍保留在 <code>web/components/ad-recreation-board.tsx</code>,但不再作为默认首页渲染。</td></tr>
<tr><td><code>web/canvas-app/</code></td><td>SKG 内部画布应用:从 <code>chatfire-AI/huobao-canvas</code> 交互逻辑改造而来。当前策略是“保留成熟画布能力,替换品牌/路由/API”Vue Flow 节点画布、项目列表、推荐词、AI 润色、自动执行、工作流模板、首帧/尾帧/参考图节点、图片/视频/LLM 配置节点、模型配置和批量下载都保留;可见品牌收敛为 SKG logo不展示上游注册链接或外部品牌。生产路径固定为根域名 <code>/</code>,内部路由用 <code>/p/:id?</code>;项目列表和画布 JSON 优先同步到服务端 Postgres浏览器本地存储只是缓存/导入来源;来源说明保存在 <code>THIRD_PARTY_NOTICES.md</code>,不展示给终端用户。</td></tr>
<tr><td><code>web/canvas-app/src/stores/projects.js</code></td><td>画布项目 Pinia store启动时先读本地 <code>localStorage["ai-canvas-projects"]</code> 作为缓存,再调用 <code>GET /canvas-projects</code> 拉服务端项目;如果发现本地旧项目,会调用 <code>POST /canvas-projects/import</code> 导入到当前登录用户。新建、重命名、画布节点变更、复制和删除会同步到 <code>/canvas-projects</code>,本地缓存只用于快速打开和网络异常兜底。</td></tr>
<tr><td><code>web/canvas-app/src/views/Canvas.vue</code></td><td>画布主交互:恢复上游底部 prompt composer、<code>AI 润色</code><code>自动执行</code>、推荐词、节点菜单、工作流面板、API/模型设置入口和批量下载入口。自动执行会调用 <code>useWorkflowOrchestrator</code> 分析提示词,创建文生图、图转视频、故事板、多角度分镜或绘本节点组;手动模式只创建文本节点,用户自行连接节点。</td></tr>
<tr><td><code>web/canvas-app/src/views/Canvas.vue</code></td><td>画布主交互:恢复上游底部 prompt composer、<code>AI 润色</code><code>自动执行</code>、推荐词、节点菜单、工作流面板、API/模型设置入口和批量下载入口。自动执行会调用 <code>useWorkflowOrchestrator</code> 分析提示词,创建文生图、图转视频、故事板、多角度分镜或绘本节点组;手动模式只创建文本节点,用户自行连接节点。底部推荐词为短词池4 个一组单行展示,刷新按钮只轮换下一组,不改变输入面板高度。</td></tr>
<tr><td><code>web/canvas-app/src/config/models.js</code></td><td>画布媒体模型和规格的前端白名单:图片只内置 <code>auto</code><code>gpt-image-2</code><code>gemini-3-pro-image-preview</code>,尺寸只内置 <code>auto</code><code>1024x1536</code><code>1024x1024</code><code>1536x1024</code>;视频只内置 <code>seedance</code> / <code>Seedance 2.0 Fast</code>,画幅和时长对齐后端 <code>/health</code> 能力边界。<code>useModelConfig.js</code> 和 Pinia 模型 store 会忽略浏览器本地自定义图片/视频模型,防止旧缓存把不可用模型带回生成下拉。</td></tr>
<tr><td><code>web/canvas-app/src/hooks/useApi.js</code></td><td>画布到本项目后端的适配层:不再读取浏览器 API Key而是使用当前登录会话 Cookie 调用 <code>/api</code>。文生图 / 图生图先创建轻量 creative job再调用 <code>/frames/0/generate</code>;文生视频 / 图生视频调用 <code>/storyboard/video</code> 并轮询 <code>/jobs/{id}</code>,完成后把图片或 mp4 URL 写回画布节点。<code>useChat</code> 已从 SKG 广告文案接口切到 <code>/prompt/polish</code>AI 润色显式使用 image/video prompt 模式LLM 节点使用通用 chat 模式,避免自动注入用户没有提到的 SKG 或营销语境;后端会判断原提示词是否有人物意图,无人物时禁止新增人物,有人物时才声明虚构 AI 角色。</td></tr>
<tr><td><code>web/scripts/sync-canvas-root.mjs</code></td><td>构建桥接脚本:在 <code>next build</code> 静态导出完成后,把 Vite 画布产物 <code>web/canvas-app/dist</code> 覆盖到 <code>web/out</code> 根目录,使 <code>https://marketing.skg.com</code> 登录后直接进入画布;旧 <code>web/scripts/sync-canvas-dist.mjs</code> 保留但不再由生产构建调用。</td></tr>
@@ -1243,6 +1243,18 @@ ProductRefStateItem {
<h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog">
<article class="change">
<header>
<h3>2026-05-26 · 推荐词刷新改为短词轮换</h3>
<span class="tag violet">Canvas</span>
<span class="tag rose">UX</span>
</header>
<div class="body">
<p><strong>问题:</strong>画布和首页推荐词是固定数组,旁边刷新按钮没有绑定事件;推荐词如果过长或换行,会把底部输入区撑高。</p>
<p><strong>改动:</strong><code>web/canvas-app/src/views/Canvas.vue</code><code>Home.vue</code> 改为 4 个一组的短推荐词池点击刷新按钮时切换到下一组推荐栏固定单行高度chip 设置最大宽度和截断,按钮加 <code>title</code> / <code>aria-label</code></p>
<p><strong>影响:</strong>用户可连续点刷新切换推荐词,推荐区不会因为文案长度换行或顶起 composer。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-26 · AI 润色按人物意图加安全词</h3>

View File

@@ -196,17 +196,23 @@
</div>
<!-- Quick suggestions | 快捷建议 -->
<div class="flex flex-wrap items-center justify-center gap-2 mt-2">
<span class="text-xs text-[var(--text-secondary)]">推荐</span>
<div class="flex flex-nowrap items-center justify-center gap-2 mt-2 h-7 overflow-hidden">
<span class="shrink-0 text-xs text-[var(--text-secondary)]">推荐</span>
<button
v-for="tag in suggestions"
:key="tag"
:key="`${suggestionPage}-${tag}`"
@click="chatInput = tag"
class="px-2 py-0.5 text-xs rounded-full bg-[var(--bg-secondary)]/80 border border-[var(--border-color)] hover:border-[var(--accent-color)] transition-colors"
:title="tag"
class="min-w-0 max-w-[118px] truncate px-2 py-0.5 text-xs rounded-full bg-[var(--bg-secondary)]/80 border border-[var(--border-color)] hover:border-[var(--accent-color)] transition-colors"
>
{{ tag }}
</button>
<button class="p-1 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
<button
@click="refreshSuggestions"
class="shrink-0 p-1 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
title="换一组推荐"
aria-label="换一组推荐"
>
<n-icon :size="14"><RefreshOutline /></n-icon>
</button>
</div>
@@ -437,13 +443,20 @@ const nodeTypeOptions = [
const inputPlaceholder = '你可以试着说"帮我生成一个二次元的卡通角色"'
// Quick suggestions | 快捷建议
const suggestions = [
'像个魔法森林',
'三只不同的小猫',
'生成多角度分镜',
'夏日田野环绕漫步'
const suggestionPage = ref(0)
const suggestionGroups = [
['魔法森林', '三只小猫', '多角度分镜', '夏日田野'],
['雨夜街摊', '产品特写', '水花慢镜', '极简桌面'],
['无人物街景', '夜市霓虹', '电商白底', '咖啡窗边'],
['插画封面', '厨房晨光', '3D 产品', '海边慢步']
]
const suggestions = computed(() => suggestionGroups[suggestionPage.value % suggestionGroups.length])
const refreshSuggestions = () => {
suggestionPage.value = (suggestionPage.value + 1) % suggestionGroups.length
}
// Add new node | 添加新节点
const addNewNode = async (type) => {
// Calculate viewport center position | 计算视口中心位置

View File

@@ -54,17 +54,23 @@
</div>
<!-- Quick suggestions | 快捷建议 -->
<div class="flex flex-wrap items-center justify-center gap-2 mt-4">
<span class="text-sm text-[var(--text-secondary)]">推荐</span>
<div class="flex flex-nowrap items-center justify-center gap-2 mt-4 h-8 overflow-hidden">
<span class="shrink-0 text-sm text-[var(--text-secondary)]">推荐</span>
<button
v-for="tag in suggestions"
:key="tag"
:key="`${suggestionPage}-${tag}`"
@click="inputText = tag"
class="px-3 py-1.5 text-sm rounded-full bg-[var(--bg-secondary)] border border-[var(--border-color)] hover:border-[var(--accent-color)] transition-colors"
:title="tag"
class="min-w-0 max-w-[132px] truncate px-3 py-1.5 text-sm rounded-full bg-[var(--bg-secondary)] border border-[var(--border-color)] hover:border-[var(--accent-color)] transition-colors"
>
{{ tag }}
</button>
<button class="p-1.5 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
<button
@click="refreshSuggestions"
class="shrink-0 p-1.5 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
title="换一组推荐"
aria-label="换一组推荐"
>
<n-icon :size="16"><RefreshOutline /></n-icon>
</button>
</div>
@@ -278,13 +284,20 @@ const renameValue = ref('')
const renameTargetId = ref(null)
// Suggestions tags | 建议标签
const suggestions = [
'雨中魔法森林',
'日式街面美食摄影',
'瀑布水流飞溅',
'雨天富声旁边花语'
const suggestionPage = ref(0)
const suggestionGroups = [
['魔法森林', '三只小猫', '多角度分镜', '夏日田野'],
['雨夜街摊', '产品特写', '水花慢镜', '极简桌面'],
['无人物街景', '夜市霓虹', '电商白底', '咖啡窗边'],
['插画封面', '厨房晨光', '3D 产品', '海边慢步']
]
const suggestions = computed(() => suggestionGroups[suggestionPage.value % suggestionGroups.length])
const refreshSuggestions = () => {
suggestionPage.value = (suggestionPage.value + 1) % suggestionGroups.length
}
// Format date | 格式化日期
const formatDate = (date) => {
if (!date) return ''