fix: make canvas the root generation experience
This commit is contained in:
@@ -55,8 +55,8 @@ const props = defineProps({
|
||||
|
||||
// Image role options | 图片角色选项
|
||||
const imageRoleOptions = [
|
||||
{ label: '首帧', key: 'first_frame_image' },
|
||||
{ label: '尾帧', key: 'last_frame_image' },
|
||||
{ label: '图片', key: 'first_frame_image' },
|
||||
{ label: '结束图', key: 'last_frame_image' },
|
||||
{ label: '参考图', key: 'input_reference' }
|
||||
]
|
||||
|
||||
@@ -66,7 +66,7 @@ const currentRole = computed(() => props.data?.imageRole || 'first_frame_image')
|
||||
// Current role label | 当前角色标签
|
||||
const currentRoleLabel = computed(() => {
|
||||
const option = imageRoleOptions.find(o => o.key === currentRole.value)
|
||||
return option?.label || '首帧'
|
||||
return option?.label || '图片'
|
||||
})
|
||||
|
||||
// Calculate bezier path | 计算贝塞尔路径
|
||||
@@ -95,7 +95,7 @@ const edgeStyle = computed(() => ({
|
||||
|
||||
// Handle role selection | 处理角色选择
|
||||
const handleRoleSelect = (role) => {
|
||||
// If selecting first_frame or last_frame, ensure uniqueness | 如果选择首帧或尾帧,确保唯一性
|
||||
// Keep endpoint image roles unique when advanced users edit edge roles.
|
||||
if (role === 'first_frame_image' || role === 'last_frame_image') {
|
||||
// Find other edges connected to the same target with the same role | 查找连接到同一目标且具有相同角色的其他边
|
||||
const sameTargetEdges = edges.value.filter(edge =>
|
||||
|
||||
@@ -932,7 +932,7 @@ const handleVideoGen = () => {
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'left',
|
||||
type: 'imageRole',
|
||||
data: { imageRole: 'first_frame_image' } // Default to first frame | 默认首帧
|
||||
data: { imageRole: 'first_frame_image' } // Default reference image | 默认参考图
|
||||
})
|
||||
|
||||
// Connect text node to config node | 连接文本节点到配置节点
|
||||
|
||||
@@ -82,16 +82,8 @@
|
||||
提示词 {{ connectedPrompt ? '✓' : '○' }}
|
||||
</span>
|
||||
<span class="px-2 py-0.5 rounded-full"
|
||||
:class="imagesByRole.firstFrame ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800'">
|
||||
首帧 {{ imagesByRole.firstFrame ? '✓' : '○' }}
|
||||
</span>
|
||||
<span class="px-2 py-0.5 rounded-full"
|
||||
:class="imagesByRole.lastFrame ? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800'">
|
||||
尾帧 {{ imagesByRole.lastFrame ? '✓' : '○' }}
|
||||
</span>
|
||||
<span class="px-2 py-0.5 rounded-full"
|
||||
:class="imagesByRole.referenceImages.length > 0 ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800'">
|
||||
参考图 {{ imagesByRole.referenceImages.length > 0 ? `✓ ${imagesByRole.referenceImages.length}` : '○' }}
|
||||
:class="connectedImages.length > 0 ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800'">
|
||||
图片 {{ connectedImages.length > 0 ? `✓ ${connectedImages.length}` : '○' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -195,7 +187,7 @@ const connectedImages = computed(() => {
|
||||
edgeId: edge.id,
|
||||
url: sourceNode.data.url,
|
||||
base64: sourceNode.data.base64,
|
||||
role: edge.data?.imageRole || 'first_frame_image' // Default to first frame | 默认首帧
|
||||
role: edge.data?.imageRole || 'first_frame_image' // Default reference image | 默认参考图
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -385,12 +377,12 @@ const handleGenerate = async () => {
|
||||
params.prompt = prompt
|
||||
}
|
||||
|
||||
// Add first frame image | 添加首帧图片
|
||||
// Add primary reference image | 添加主参考图
|
||||
if (first_frame_image) {
|
||||
params.first_frame_image = first_frame_image
|
||||
}
|
||||
|
||||
// Add last frame image | 添加尾帧图片
|
||||
// Add optional ending reference image | 添加可选结束参考图
|
||||
if (last_frame_image) {
|
||||
params.last_frame_image = last_frame_image
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const routes = [
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory('/canvas/'),
|
||||
history: createWebHistory('/'),
|
||||
routes
|
||||
})
|
||||
|
||||
|
||||
@@ -164,22 +164,15 @@
|
||||
v-if="needsFirstFrame"
|
||||
class="px-3 py-1.5 text-xs rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)] text-[var(--text-secondary)] cursor-pointer hover:text-[var(--text-primary)]"
|
||||
>
|
||||
{{ firstFrameFile ? `首帧 · ${firstFrameFile.name}` : '上传首帧' }}
|
||||
{{ firstFrameFile ? `图片 · ${firstFrameFile.name}` : '上传图片' }}
|
||||
<input type="file" accept="image/*" class="hidden" @change="event => handleFrameFile('first', event)" />
|
||||
</label>
|
||||
<label
|
||||
v-if="needsLastFrame"
|
||||
class="px-3 py-1.5 text-xs rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)] text-[var(--text-secondary)] cursor-pointer hover:text-[var(--text-primary)]"
|
||||
>
|
||||
{{ lastFrameFile ? `尾帧 · ${lastFrameFile.name}` : '上传尾帧' }}
|
||||
<input type="file" accept="image/*" class="hidden" @change="event => handleFrameFile('last', event)" />
|
||||
</label>
|
||||
<button
|
||||
v-if="firstFrameFile || lastFrameFile"
|
||||
v-if="firstFrameFile"
|
||||
@click="clearFrameFiles"
|
||||
class="px-2 py-1.5 text-xs rounded-lg text-[var(--text-secondary)] hover:bg-[var(--bg-secondary)]"
|
||||
>
|
||||
清空帧
|
||||
清空图片
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
@@ -194,10 +187,7 @@
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span v-if="firstFramePreview" class="h-8 w-8 overflow-hidden rounded-md border border-[var(--border-color)] bg-[var(--bg-secondary)]">
|
||||
<img :src="firstFramePreview" alt="首帧" class="h-full w-full object-cover" />
|
||||
</span>
|
||||
<span v-if="lastFramePreview" class="h-8 w-8 overflow-hidden rounded-md border border-[var(--border-color)] bg-[var(--bg-secondary)]">
|
||||
<img :src="lastFramePreview" alt="尾帧" class="h-full w-full object-cover" />
|
||||
<img :src="firstFramePreview" alt="参考图片" class="h-full w-full object-cover" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -370,22 +360,17 @@ const showGrid = ref(true)
|
||||
const isProcessing = ref(false)
|
||||
|
||||
const creationModes = [
|
||||
{ id: 'text-video', label: '文生视频' },
|
||||
{ id: 'text-image', label: '文生图' },
|
||||
{ id: 'first-frame-video', label: '首帧生视频' },
|
||||
{ id: 'first-last-frame-video', label: '首尾帧生视频' }
|
||||
{ id: 'text-video', label: '文生视频' },
|
||||
{ id: 'image-video', label: '图生视频' }
|
||||
]
|
||||
const creationMode = ref('text-video')
|
||||
const creationMode = ref('text-image')
|
||||
const firstFrameFile = ref(null)
|
||||
const lastFrameFile = ref(null)
|
||||
const firstFramePreview = ref('')
|
||||
const lastFramePreview = ref('')
|
||||
const needsFirstFrame = computed(() => creationMode.value === 'first-frame-video' || creationMode.value === 'first-last-frame-video')
|
||||
const needsLastFrame = computed(() => creationMode.value === 'first-last-frame-video')
|
||||
const needsFirstFrame = computed(() => creationMode.value === 'image-video')
|
||||
const canSubmit = computed(() => {
|
||||
if (!chatInput.value.trim()) return false
|
||||
if (needsFirstFrame.value && !firstFrameFile.value) return false
|
||||
if (needsLastFrame.value && !lastFrameFile.value) return false
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -443,18 +428,14 @@ const nodeTypeOptions = [
|
||||
// Input placeholder | 输入占位符
|
||||
const inputPlaceholder = computed(() => {
|
||||
if (creationMode.value === 'text-image') return '写清楚画面、主体、构图、光线、比例和 SKG 产品露出方式'
|
||||
if (creationMode.value === 'first-frame-video') return '上传首帧后,写人物动作、镜头运动、产品细节保持和视频节奏'
|
||||
if (creationMode.value === 'first-last-frame-video') return '上传首帧和尾帧后,写中间如何过渡、动作节奏和产品细节保持'
|
||||
if (creationMode.value === 'image-video') return '上传图片后,写人物动作、镜头运动、产品细节保持和视频节奏'
|
||||
return '写清楚画面、动作、镜头、产品出现方式、视频比例和时长'
|
||||
})
|
||||
|
||||
const setCreationMode = (mode) => {
|
||||
creationMode.value = mode
|
||||
if (mode === 'text-video' || mode === 'text-image') {
|
||||
if (mode !== 'image-video') {
|
||||
clearFrameFiles()
|
||||
} else if (mode === 'first-frame-video') {
|
||||
lastFrameFile.value = null
|
||||
lastFramePreview.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,20 +454,13 @@ const handleFrameFile = (slot, event) => {
|
||||
if (firstFramePreview.value) URL.revokeObjectURL(firstFramePreview.value)
|
||||
firstFrameFile.value = file
|
||||
firstFramePreview.value = url
|
||||
} else {
|
||||
if (lastFramePreview.value) URL.revokeObjectURL(lastFramePreview.value)
|
||||
lastFrameFile.value = file
|
||||
lastFramePreview.value = url
|
||||
}
|
||||
}
|
||||
|
||||
const clearFrameFiles = () => {
|
||||
if (firstFramePreview.value) URL.revokeObjectURL(firstFramePreview.value)
|
||||
if (lastFramePreview.value) URL.revokeObjectURL(lastFramePreview.value)
|
||||
firstFrameFile.value = null
|
||||
lastFrameFile.value = null
|
||||
firstFramePreview.value = ''
|
||||
lastFramePreview.value = ''
|
||||
}
|
||||
|
||||
// Add new node | 添加新节点
|
||||
@@ -575,7 +549,7 @@ const onConnect = (params) => {
|
||||
addEdge({
|
||||
...params,
|
||||
type: 'imageRole',
|
||||
data: { imageRole: 'first_frame_image' } // Default to first frame | 默认首帧
|
||||
data: { imageRole: 'first_frame_image' } // Default reference image | 默认参考图
|
||||
})
|
||||
} else if (sourceNode?.type === 'text' && targetNode?.type === 'imageConfig') {
|
||||
// Use promptOrder edge type | 使用提示词顺序边类型
|
||||
@@ -773,25 +747,15 @@ const sendMessage = async () => {
|
||||
const firstId = addNode('image', { x: baseX, y: baseY + 160 }, {
|
||||
url: dataUrl,
|
||||
base64: dataUrl,
|
||||
label: '首帧'
|
||||
label: '参考图'
|
||||
})
|
||||
imageNodeIds.push({ id: firstId, role: 'first_frame_image' })
|
||||
promptY = baseY - 140
|
||||
updateNode(textNodeId, { zIndex: 5 })
|
||||
}
|
||||
|
||||
if (needsLastFrame.value && lastFrameFile.value) {
|
||||
const dataUrl = await fileToDataUrl(lastFrameFile.value)
|
||||
const lastId = addNode('image', { x: baseX, y: baseY + 440 }, {
|
||||
url: dataUrl,
|
||||
base64: dataUrl,
|
||||
label: '尾帧'
|
||||
})
|
||||
imageNodeIds.push({ id: lastId, role: 'last_frame_image' })
|
||||
}
|
||||
|
||||
const videoConfigNodeId = addNode('videoConfig', { x: videoX, y: promptY }, {
|
||||
label: creationMode.value === 'text-video' ? '文生视频' : '生视频',
|
||||
label: creationMode.value === 'text-video' ? '文生视频' : '图生视频',
|
||||
autoExecute: true
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: '/canvas/',
|
||||
base: '/',
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user