603 lines
34 KiB
Vue
603 lines
34 KiB
Vue
<script setup lang="ts">
|
||
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
||
import WorkspaceTools from '../../components/ui/WorkspaceTools.vue'
|
||
import {
|
||
NScrollbar,
|
||
NAlert,
|
||
NButton,
|
||
NCheckbox,
|
||
NCollapse,
|
||
NCollapseItem,
|
||
NInput,
|
||
NInputNumber,
|
||
NSelect,
|
||
NTag
|
||
} from 'naive-ui'
|
||
import { computed, ref, watch } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { RefreshCw, UserCheck } from '@lucide/vue'
|
||
import { AssetImage, DirectoryItem, EmptyState, StatusBadge } from '../../components/ui'
|
||
import { downloadText } from '../../lib/format'
|
||
import ConfirmAction from '../workflows/ConfirmAction.vue'
|
||
import { castingStatusLabel, currentAnchor } from './model'
|
||
import { useSubjectIdentity } from './useSubjectIdentity'
|
||
import IdentityEditor from './components/IdentityEditor.vue'
|
||
import IdentityGallery from './components/IdentityGallery.vue'
|
||
import IdentityImageDialog from './components/IdentityImageDialog.vue'
|
||
import CastingCandidateDialog from './components/CastingCandidateDialog.vue'
|
||
|
||
/** 主体身份工作区按正式主体聚合,不把同一主体的多个 Form 当成不同身份。 */
|
||
const route = useRoute()
|
||
const {
|
||
projectId,
|
||
operation,
|
||
selectedId,
|
||
subject,
|
||
subjects,
|
||
catalog,
|
||
castingQuery,
|
||
styleQuery,
|
||
detail,
|
||
identity,
|
||
images,
|
||
castingItem,
|
||
session,
|
||
concurrency,
|
||
candidateLimit,
|
||
force,
|
||
editorRevision,
|
||
dirty,
|
||
blocked,
|
||
castingBlocked,
|
||
detailBlocked,
|
||
hasStyle,
|
||
canGenerateText,
|
||
canGenerateImage,
|
||
canGenerateCandidate,
|
||
batchValid,
|
||
candidateLimitValid,
|
||
save,
|
||
generate,
|
||
generateProject,
|
||
generateCastingCandidates,
|
||
generateImage,
|
||
generateCastingCandidate,
|
||
setAnchor,
|
||
confirmCasting
|
||
} = useSubjectIdentity()
|
||
const search = ref('')
|
||
const module = ref('all')
|
||
const imageOpen = ref(false)
|
||
const castingOpen = ref(false)
|
||
const pendingSelection = ref('')
|
||
const anchor = computed(() => currentAnchor(images.value))
|
||
const casting = computed(() => {
|
||
const value = castingQuery.data.value
|
||
return value && Array.isArray(value.items) ? value : null
|
||
})
|
||
const filtered = computed(() =>
|
||
subjects.value.filter(
|
||
item =>
|
||
(module.value === 'all' || item.module === module.value) &&
|
||
`${item.name} ${item.ref}`.toLowerCase().includes(search.value.trim().toLowerCase())
|
||
)
|
||
)
|
||
|
||
/** 深链接仅在目标 ID 变化时选择,不因目录轮询把用户切回原主体。 */
|
||
const linkedSubjectId = computed(
|
||
() => subjects.value.find(item => item.id === route.query.subjectId || item.ref === route.query.subjectRef)?.id
|
||
)
|
||
watch(
|
||
linkedSubjectId,
|
||
id => {
|
||
if (id) select(id)
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
watch(selectedId, () => {
|
||
imageOpen.value = false
|
||
castingOpen.value = false
|
||
pendingSelection.value = ''
|
||
})
|
||
|
||
/** 切换主体前确认放弃当前草稿,避免误丢尚未保存的文本。 */
|
||
function select(id: string) {
|
||
if (id === selectedId.value) return
|
||
if (dirty.value) pendingSelection.value = id
|
||
else selectedId.value = id
|
||
}
|
||
|
||
/** 用户明确放弃草稿后切换,查询层会取消旧请求。 */
|
||
function discardAndSelect() {
|
||
if (!pendingSelection.value) return
|
||
dirty.value = false
|
||
selectedId.value = pendingSelection.value
|
||
}
|
||
|
||
/** 保存真实回执便于定位部分失败主体。 */
|
||
function exportReceipt() {
|
||
downloadText('subject-identities-receipt.json', JSON.stringify(session.value.receipt, null, 2), 'application/json')
|
||
}
|
||
|
||
/** 目录刷新同时读取正式形态和 Character Casting,避免无形态角色状态滞后。 */
|
||
function refreshDirectory() {
|
||
void Promise.all([catalog.refresh(), castingQuery.refresh()])
|
||
}
|
||
|
||
/** Character 使用确认选角事务;Scene / Prop 保留普通母版切换。 */
|
||
function chooseAnchor(imageId: string) {
|
||
if (subject.value?.module === 'character') void confirmCasting(imageId)
|
||
else void setAnchor(imageId)
|
||
}
|
||
/** 新回执自动展开,避免折叠区隐藏部分失败诊断。 */
|
||
const toolsOpen = ref(false)
|
||
const overview = ref<string[]>(['casting'])
|
||
watch(
|
||
() => session.value.receipt,
|
||
receipt => {
|
||
if (receipt) {
|
||
overview.value = ['casting']
|
||
toolsOpen.value = true
|
||
}
|
||
}
|
||
)
|
||
</script>
|
||
|
||
<template>
|
||
<WorkspacePage split compact
|
||
><template #header
|
||
><div class="workspace-toolbar">
|
||
<div class="toolbar-type-filter">
|
||
<NSelect
|
||
aria-label="身份主体类型"
|
||
v-model:value="module"
|
||
:options="[
|
||
{ label: String('全部类型'), value: 'all' },
|
||
{ label: String('人物'), value: 'character' },
|
||
{ label: String('场景'), value: 'scene' },
|
||
{ label: String('道具'), value: 'prop' }
|
||
]"
|
||
></NSelect>
|
||
</div>
|
||
<span class="toolbar-summary">共 {{ subjects.length }} 个主体</span>
|
||
<div class="toolbar-actions">
|
||
<WorkspaceTools
|
||
v-model:open="toolsOpen"
|
||
title="角色选角总览与批量操作"
|
||
:has-receipt="!!session.receipt"
|
||
>
|
||
<div class="identity-tools-intro flex flex-wrap items-end justify-between gap-4">
|
||
<div>
|
||
<h2 class="text-lg font-semibold">主体身份</h2>
|
||
<p class="mt-2 text-sm text-muted">
|
||
为同一个人物、场景或道具固定稳定特征,再生成不同造型。
|
||
</p>
|
||
</div>
|
||
<RouterLink :to="`/projects/${projectId}/subject-images`" class="text-button"
|
||
>下一步:形态图片 →</RouterLink
|
||
>
|
||
</div>
|
||
<div class="workspace-overview-panel">
|
||
<NCollapse v-model:expanded-names="overview"
|
||
><NCollapseItem name="casting" title="角色选角总览与批量操作"
|
||
><div class="overview-content">
|
||
<NAlert type="info" :show-icon="false" class="mt-5 text-xs">
|
||
Character 现在需要依次完成身份文本、选角候选、确认演员并锁定
|
||
Identity,之后首帧才能就绪。未明确人物背景时,AI
|
||
默认以中国人物作为选角基线;项目和角色的明确事实始终优先。场景和道具仍使用普通身份母版流程。
|
||
</NAlert>
|
||
<NAlert
|
||
v-if="styleQuery.data.value && !hasStyle"
|
||
type="info"
|
||
:show-icon="false"
|
||
class="mt-4"
|
||
>
|
||
先创建项目视觉风格,才能 AI 生成身份或身份图片。<RouterLink
|
||
:to="`/projects/${projectId}/visual-style`"
|
||
class="text-button ml-2"
|
||
>设置视觉风格 →</RouterLink
|
||
></NAlert
|
||
>
|
||
<section v-if="casting" class="panel my-5 p-5" aria-label="角色选角流程">
|
||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||
<div class="flex items-start gap-3">
|
||
<span class="production-step"><UserCheck :size="17" /></span>
|
||
<div>
|
||
<h3 class="text-sm font-semibold">角色选角</h3>
|
||
<p class="mt-2 text-xs leading-6 text-muted">
|
||
只处理 Character。每张候选都从 Identity 独立生成,不复用旧
|
||
Anchor;确认选择时才会同时切换 Anchor 并锁定 Identity。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<span class="font-mono text-xs text-muted"
|
||
>{{ casting.ready }} / {{ casting.total }} 已完成</span
|
||
>
|
||
</div>
|
||
<dl class="casting-stats mt-4">
|
||
<div>
|
||
<dt>缺身份文本</dt>
|
||
<dd>{{ casting.missingIdentity }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>缺候选图</dt>
|
||
<dd>{{ casting.missingAnchor }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>待确认演员</dt>
|
||
<dd>{{ casting.candidatePending }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>母版未锁定</dt>
|
||
<dd>{{ casting.unlocked }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>选角完成</dt>
|
||
<dd>{{ casting.ready }}</dd>
|
||
</div>
|
||
</dl>
|
||
<div class="mt-5 flex flex-wrap items-end gap-4 border-t border-line pt-4">
|
||
<label class="w-28">
|
||
<span class="field-label">并发数</span>
|
||
<NInputNumber
|
||
:disabled="operation.pending"
|
||
:value="typeof concurrency === 'number' ? concurrency : null"
|
||
@update:value="concurrency = $event ?? 0"
|
||
:min="1"
|
||
:step="1"
|
||
></NInputNumber>
|
||
</label>
|
||
<label class="w-28">
|
||
<span class="field-label">本批上限</span>
|
||
<NInputNumber
|
||
:disabled="operation.pending"
|
||
:value="
|
||
typeof candidateLimit === 'number' ? candidateLimit : null
|
||
"
|
||
@update:value="candidateLimit = $event ?? 0"
|
||
:min="1"
|
||
:step="1"
|
||
></NInputNumber>
|
||
</label>
|
||
<NCheckbox
|
||
v-model:checked="force"
|
||
:disabled="operation.pending"
|
||
class="flex gap-2 pb-2 text-xs"
|
||
>覆盖未锁定身份文本
|
||
</NCheckbox>
|
||
<ConfirmAction
|
||
:label="force ? '重生成角色身份文本' : '补齐角色身份文本'"
|
||
:disabled="
|
||
blocked || !hasStyle || !batchValid || dirty || !casting.total
|
||
"
|
||
acknowledgement
|
||
description="只生成 Character Identity 文本,不生成图片。已锁定角色始终跳过;覆盖文本不会自动更新已有候选图。"
|
||
@confirm="generateProject(true)"
|
||
/>
|
||
<ConfirmAction
|
||
label="生成选角候选"
|
||
:disabled="
|
||
castingBlocked ||
|
||
!hasStyle ||
|
||
!batchValid ||
|
||
!candidateLimitValid ||
|
||
!casting.missingAnchor
|
||
"
|
||
acknowledgement
|
||
description="最多按本批上限为缺少 Anchor 的 Character 调用 Seedream,各自独立生成一张停用的 primary 候选,不复用旧身份母版。已有候选、已有母版或已完成选角的角色不会重复处理。"
|
||
@confirm="generateCastingCandidates"
|
||
/>
|
||
</div>
|
||
<p
|
||
v-if="!batchValid || !candidateLimitValid"
|
||
class="mt-3 text-xs text-danger"
|
||
>
|
||
并发数与本批上限都必须是正整数。
|
||
</p>
|
||
<div class="casting-list" role="group" aria-label="角色选角进度">
|
||
<NButton
|
||
v-for="item in casting.items"
|
||
:key="item.subjectId"
|
||
@click="select(item.subjectId)"
|
||
class="casting-item"
|
||
:class="{ selected: selectedId === item.subjectId }"
|
||
:aria-pressed="selectedId === item.subjectId"
|
||
><span class="casting-item-identity">
|
||
<span class="casting-item-name">{{ item.subjectName }}</span>
|
||
<span class="casting-item-ref">{{
|
||
item.subjectRef
|
||
}}</span> </span
|
||
><StatusBadge
|
||
class="casting-item-status"
|
||
:status="item.status"
|
||
:label="castingStatusLabel(item.status)"
|
||
/></NButton>
|
||
</div>
|
||
</section>
|
||
<NCollapse class="panel mb-5 p-5"
|
||
><NCollapseItem name="details"
|
||
><template #header>高级:全部主体身份文本</template>
|
||
<p class="mt-3 text-xs leading-6 text-muted">
|
||
同时处理 Character、Scene 和
|
||
Prop;角色选角建议优先使用上方专用入口。已锁定身份始终跳过。
|
||
</p>
|
||
<ConfirmAction
|
||
class="mt-4"
|
||
:label="force ? '重生成全部未锁定身份' : '补齐全部主体身份文本'"
|
||
:disabled="
|
||
blocked || !hasStyle || !batchValid || dirty || !subjects.length
|
||
"
|
||
acknowledgement
|
||
description="生成全项目主体身份描述与提示词,不生成图片。覆盖不会自动更新旧身份图、形态图、首帧或视频。"
|
||
@confirm="generateProject(false)" /></NCollapseItem
|
||
></NCollapse>
|
||
<section
|
||
v-if="session.receipt"
|
||
class="panel mb-5 p-5"
|
||
aria-label="身份文本批量回执"
|
||
>
|
||
<div class="flex justify-between gap-3">
|
||
<h3 class="text-sm font-medium">
|
||
{{ session.receipt.title }} · 本次回执
|
||
</h3>
|
||
<NButton @click="exportReceipt" text size="small">导出诊断</NButton>
|
||
</div>
|
||
<p
|
||
v-if="session.receipt.kind === 'identities'"
|
||
class="mt-3 text-xs text-muted"
|
||
>
|
||
共 {{ session.receipt.result.total }} 个主体 · 目标
|
||
{{ session.receipt.result.targetCount }} · 生成
|
||
{{ session.receipt.result.generated }} · 跳过
|
||
{{ session.receipt.result.skipped }}(含锁定
|
||
{{ session.receipt.result.skippedLocked }})· 失败
|
||
{{ session.receipt.result.failed }}
|
||
</p>
|
||
<p v-else class="mt-3 text-xs text-muted">
|
||
共 {{ session.receipt.result.totalCharacters }} 个角色 · 缺候选
|
||
{{ session.receipt.result.missingAnchor }} · 本批目标
|
||
{{ session.receipt.result.targetCount }} · 已生成
|
||
{{ session.receipt.result.generated }} · 失败
|
||
{{ session.receipt.result.failed }}
|
||
</p>
|
||
<NAlert
|
||
v-if="session.receipt.result.failed"
|
||
role="alert"
|
||
type="error"
|
||
:show-icon="false"
|
||
class="mt-3"
|
||
>
|
||
本次存在部分失败,成功结果不会回滚。请检查逐角色原因后再重试。
|
||
</NAlert>
|
||
<ul class="mt-3 space-y-2 text-xs text-danger">
|
||
<li
|
||
v-for="failure in session.receipt.result.failures"
|
||
:key="failure.subjectId"
|
||
>
|
||
{{ failure.subjectRef }} · {{ failure.subjectId }}:{{
|
||
failure.error
|
||
}}
|
||
</li>
|
||
</ul>
|
||
<p class="mt-3 text-[11px] text-muted">
|
||
回执仅保留于当前浏览器会话;最终状态以刷新后的正式记录为准。
|
||
</p>
|
||
</section>
|
||
</div></NCollapseItem
|
||
></NCollapse
|
||
>
|
||
</div>
|
||
</WorkspaceTools>
|
||
</div>
|
||
</div></template
|
||
>
|
||
<NScrollbar
|
||
class="workspace-feedback"
|
||
v-if="catalog.error.value || styleQuery.error.value || castingQuery.error.value"
|
||
><NAlert role="alert" type="error" :show-icon="false"
|
||
>{{ catalog.error.value || styleQuery.error.value || castingQuery.error.value }}
|
||
当前相关操作已暂停,请刷新核对后端。
|
||
</NAlert></NScrollbar
|
||
>
|
||
<div v-if="subjects.length" class="identity-workspace">
|
||
<aside class="panel directory-panel identity-directory">
|
||
<div class="directory-heading">
|
||
<h3>
|
||
主体目录 <span>{{ filtered.length }} / {{ subjects.length }}</span>
|
||
</h3>
|
||
<NButton
|
||
:disabled="catalog.loading.value || castingQuery.loading.value"
|
||
@click="refreshDirectory"
|
||
quaternary
|
||
circle
|
||
size="small"
|
||
aria-label="刷新主体目录"
|
||
title="刷新主体目录"
|
||
><RefreshCw :size="14"
|
||
/></NButton>
|
||
</div>
|
||
<div class="directory-filters">
|
||
<NInput
|
||
placeholder="搜索主体或引用"
|
||
aria-label="搜索主体身份"
|
||
:input-props="{ 'aria-label': '搜索主体身份' }"
|
||
v-model:value="search"
|
||
clearable
|
||
></NInput>
|
||
</div>
|
||
<NScrollbar
|
||
class="identity-subject-list panel-scroll directory-scroll"
|
||
content-class="directory-list-content identity-subject-list-content"
|
||
x-scrollable
|
||
>
|
||
<DirectoryItem
|
||
v-for="item in filtered"
|
||
:key="item.id"
|
||
:aria-pressed="selectedId === item.id"
|
||
:active="selectedId === item.id"
|
||
@click="select(item.id)"
|
||
class="identity-subject-item"
|
||
>{{ item.name }}
|
||
<template #eyebrow>{{ item.ref }}</template>
|
||
<template #meta>
|
||
<span>{{ item.forms.length }} 个形态</span
|
||
><span
|
||
v-if="casting?.items.find(row => row.subjectId === item.id)"
|
||
class="directory-status"
|
||
>
|
||
{{
|
||
castingStatusLabel(casting.items.find(row => row.subjectId === item.id)!.status)
|
||
}}</span
|
||
>
|
||
</template></DirectoryItem
|
||
>
|
||
<p v-if="!filtered.length" class="py-6 text-xs text-muted">没有匹配主体,请调整筛选。</p>
|
||
</NScrollbar>
|
||
</aside>
|
||
<NScrollbar class="identity-detail panel-scroll min-w-0">
|
||
<NAlert v-if="pendingSelection" type="info" :show-icon="false" class="mb-4"
|
||
><p class="text-xs">当前主体有未保存修改,切换会丢弃草稿。</p>
|
||
<div class="mt-3 flex gap-3">
|
||
<NButton @click="discardAndSelect">放弃草稿并切换</NButton
|
||
><NButton @click="pendingSelection = ''" text size="small">继续编辑</NButton>
|
||
</div></NAlert
|
||
>
|
||
<section v-if="subject" class="panel p-5 sm:p-6">
|
||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||
<div>
|
||
<h3 class="font-semibold">
|
||
{{ subject.name }} <code class="subject-ref ml-2">{{ subject.ref }}</code>
|
||
</h3>
|
||
<p class="mt-2 text-xs text-muted">
|
||
<template v-if="subject.module === 'character' && castingItem">
|
||
选角状态:{{ castingStatusLabel(castingItem.status) }}
|
||
</template>
|
||
<template v-else>
|
||
{{
|
||
identity
|
||
? identity.isLocked
|
||
? '身份文本已锁定'
|
||
: '身份文本未锁定'
|
||
: '尚未创建身份'
|
||
}}
|
||
</template>
|
||
</p>
|
||
</div>
|
||
<NButton :disabled="detail.loading.value" @click="detail.refresh" text size="small">
|
||
刷新身份
|
||
</NButton>
|
||
</div>
|
||
<NAlert v-if="detail.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
|
||
>{{ detail.error.value }} 查询失败时不允许生成或覆盖。
|
||
</NAlert>
|
||
<p v-if="!detail.data.value && detail.loading.value" class="py-8 text-sm text-muted" role="status">
|
||
正在读取主体身份…
|
||
</p>
|
||
<template v-if="detail.data.value">
|
||
<div class="mt-4 flex flex-wrap gap-3">
|
||
<ConfirmAction
|
||
:label="identity ? 'AI 重新生成身份' : 'AI 生成身份'"
|
||
:disabled="!canGenerateText"
|
||
acknowledgement
|
||
description="调用文本模型生成稳定身份描述和提示词。旧图片不会自动重生成;已锁定身份需先解锁并保存。"
|
||
@confirm="generate"
|
||
/><NButton
|
||
v-if="subject.module === 'character'"
|
||
:disabled="!canGenerateCandidate"
|
||
@click="castingOpen = true"
|
||
>
|
||
生成选角候选</NButton
|
||
><NButton
|
||
v-if="subject.module !== 'character' || !!anchor"
|
||
:disabled="!canGenerateImage"
|
||
@click="imageOpen = true"
|
||
>{{ subject.module === 'character' ? '生成辅助身份图' : '生成身份参考图' }}</NButton
|
||
>
|
||
</div>
|
||
<p v-if="dirty" class="mt-3 text-xs text-muted">请先保存身份修改,再使用 AI 或生成图片。</p>
|
||
<IdentityEditor
|
||
:key="`${selectedId}:${editorRevision}`"
|
||
:identity="identity"
|
||
:module="subject.module"
|
||
:disabled="detailBlocked"
|
||
@save="save"
|
||
@dirty="dirty = $event"
|
||
/>
|
||
<div v-if="anchor" class="mt-5 flex items-center gap-4 border-t border-line pt-5">
|
||
<AssetImage
|
||
:src="anchor.imageUrl"
|
||
:alt="`${subject.name}当前身份母版`"
|
||
class="h-20 w-20 shrink-0 rounded"
|
||
/>
|
||
<div>
|
||
<p class="text-sm font-medium">当前身份母版</p>
|
||
<p class="mt-2 text-xs text-muted">
|
||
{{
|
||
subject.module === 'character'
|
||
? identity?.isLocked
|
||
? '演员身份已确认并锁定,可用于形态图与镜头首帧。'
|
||
: '已有演员母版但 Identity 尚未锁定,镜头首帧仍会被阻塞。'
|
||
: subject.module === 'prop'
|
||
? '道具形态生图暂不自动引用此母版。'
|
||
: '后续场景形态图会继承此图的空间结构。'
|
||
}}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<IdentityGallery
|
||
:key="selectedId"
|
||
:images="images"
|
||
:subject-name="subject.name"
|
||
:module="subject.module"
|
||
:identity-locked="!!identity?.isLocked"
|
||
:disabled="detailBlocked || (subject.module === 'character' && !!castingQuery.error.value)"
|
||
@anchor="chooseAnchor"
|
||
/>
|
||
</template>
|
||
<div class="mt-5 border-t border-line pt-5">
|
||
<h4 class="text-xs font-medium">此主体的形态</h4>
|
||
<div class="mt-3 flex flex-wrap gap-2">
|
||
<NTag v-for="form in subject.forms" :key="form.id" size="small" :bordered="false"
|
||
>{{ form.name }}{{ form.isDefault ? ' · 默认' : '' }}</NTag
|
||
>
|
||
</div>
|
||
<p v-if="!subject.forms.length" class="mt-3 text-xs text-muted">
|
||
此角色尚无正式形态,但可以先完成 Identity 与选角。
|
||
</p>
|
||
<RouterLink
|
||
:to="{ path: `/projects/${projectId}/subject-images`, query: { subjectRef: subject.ref } }"
|
||
class="text-button mt-3"
|
||
>查看此主体形态图片 →</RouterLink
|
||
>
|
||
</div>
|
||
</section>
|
||
</NScrollbar>
|
||
</div>
|
||
<p v-else-if="catalog.loading.value" class="py-8 text-sm text-muted" role="status">正在读取正式主体目录…</p>
|
||
<EmptyState
|
||
v-else-if="catalog.data.value"
|
||
title="还没有可展示的正式主体"
|
||
description="先完成剧本拆解与主体形态持久化,再来管理稳定身份。"
|
||
><RouterLink :to="`/projects/${projectId}/breakdown`" class="button button-secondary"
|
||
>前往剧本拆解</RouterLink
|
||
></EmptyState
|
||
>
|
||
<IdentityImageDialog
|
||
v-if="subject"
|
||
v-model:open="imageOpen"
|
||
:subject-id="subject.id"
|
||
:subject-name="subject.name"
|
||
:module="subject.module"
|
||
:images="images"
|
||
:disabled="!canGenerateImage"
|
||
@generate="generateImage" />
|
||
<CastingCandidateDialog
|
||
v-if="subject?.module === 'character'"
|
||
v-model:open="castingOpen"
|
||
:subject-id="subject.id"
|
||
:subject-name="subject.name"
|
||
:disabled="!canGenerateCandidate"
|
||
@generate="generateCastingCandidate"
|
||
/></WorkspacePage>
|
||
</template>
|