优化全能创作
This commit is contained in:
@@ -628,7 +628,8 @@ export const api = {
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 终止整理方案(planning)。成功 **200** `{conversation_id, agent_status: idle}`;
|
||||
* 终止整理方案(planning)。成功 **200** `{conversation_id, agent_status}`;
|
||||
* 闸门修订/推进中取消时多为 `awaiting_user`(已恢复 step_confirm),否则 `idle`。
|
||||
* 非 planning **409**。服务端放锁 + Redis 取消标,离开页面再进也生效。
|
||||
*/
|
||||
cancelCreationAgent(id: string) {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
}
|
||||
|
||||
.omni-session-shell {
|
||||
position: relative;
|
||||
width: min(980px, calc(100% - 42px));
|
||||
min-height: calc(100vh - var(--topbar-height));
|
||||
display: flex;
|
||||
@@ -162,6 +163,32 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
|
||||
.omni-reply-hint {
|
||||
margin: 8px 0 0;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed rgba(34, 42, 54, .12);
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.omni-reply-hint-label {
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
color: #3b5bdb;
|
||||
background: rgba(59, 91, 219, .08);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.omni-chat-row.is-user .omni-reply-hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.omni-chat-row.is-user .omni-chat-bubble {
|
||||
align-items: flex-end;
|
||||
border-color: var(--black);
|
||||
@@ -296,6 +323,11 @@
|
||||
.omni-video-plan-head span {
|
||||
color: #fff;
|
||||
background: var(--black);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-video-plan-head strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.omni-plan-section {
|
||||
@@ -310,7 +342,8 @@
|
||||
.omni-plan-section > strong {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.omni-plan-points {
|
||||
@@ -324,15 +357,16 @@
|
||||
border-radius: 9px;
|
||||
color: #525965;
|
||||
background: #f6f8fa;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.omni-plan-points b {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: #252b35;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.omni-plan-points i,
|
||||
@@ -364,15 +398,16 @@
|
||||
border-radius: 10px;
|
||||
color: #5b626d;
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.omni-plan-timeline b {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: var(--klein);
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.omni-plan-summary {
|
||||
@@ -383,14 +418,35 @@
|
||||
padding: 13px 17px;
|
||||
color: #444b56;
|
||||
background: #f6f8fb;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.omni-plan-summary b {
|
||||
color: var(--klein);
|
||||
}
|
||||
|
||||
/* 「最终输入」是方案交给出片模型前的最后一层信息,不能和辅助说明一样小。 */
|
||||
.omni-plan-summary .omni-plan-final-input {
|
||||
margin-left: auto;
|
||||
color: var(--accent-black);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.omni-plan-summary .omni-plan-final-input b {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.omni-plan-summary .omni-plan-final-input .omni-plan-point-body {
|
||||
display: inline;
|
||||
margin-left: 2px;
|
||||
color: var(--black-alpha-64);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.omni-prompt-file-card {
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr) auto;
|
||||
@@ -1301,9 +1357,8 @@
|
||||
box-sizing: border-box;
|
||||
width: min(760px, calc(100% - 44px));
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
margin: 0 0 24px 44px;
|
||||
padding: 14px 16px;
|
||||
@@ -1319,11 +1374,25 @@
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.omni-step-confirm-card.is-revising {
|
||||
border-color: rgba(0, 47, 167, .22);
|
||||
box-shadow: 0 12px 30px rgba(20, 27, 38, .065), 0 0 0 3px rgba(0, 47, 167, .08);
|
||||
}
|
||||
|
||||
.omni-step-confirm-top {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.omni-step-confirm-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
|
||||
.omni-step-confirm-copy strong {
|
||||
@@ -1342,6 +1411,7 @@
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.omni-step-confirm-actions button {
|
||||
@@ -1365,6 +1435,46 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 「我想改」卡片内反馈区:与卡片同宽,主路径不再依赖底部 composer */
|
||||
.omni-step-confirm-revise {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.omni-step-confirm-revise-input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 76px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(34, 42, 54, 0.14);
|
||||
border-radius: 10px;
|
||||
background: #f7f8fa;
|
||||
color: #222a36;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
resize: vertical;
|
||||
outline: 0;
|
||||
transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
|
||||
}
|
||||
|
||||
.omni-step-confirm-revise-input::placeholder {
|
||||
color: #8b919a;
|
||||
}
|
||||
|
||||
.omni-step-confirm-revise-input:focus {
|
||||
border-color: rgba(0, 47, 167, .42);
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 47, 167, .14);
|
||||
}
|
||||
|
||||
.omni-step-confirm-revise-input:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── 确认闸门(.omni-confirm-card)· 设计稿里是方案卡下面那条操作行,这里独立成卡。
|
||||
积分数字挂在按钮里 —— 用户按下去之前就该知道要花多少。 */
|
||||
.omni-confirm-card {
|
||||
@@ -1837,7 +1947,8 @@
|
||||
.omni-prompt-drawer-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 80;
|
||||
/* Prompt 是当前查看的内容,必须盖过右侧「资源」工作区。 */
|
||||
z-index: calc(var(--z-overlay) + 10);
|
||||
border: 0;
|
||||
background: rgba(16, 18, 24, .28);
|
||||
cursor: pointer;
|
||||
@@ -1848,7 +1959,7 @@
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
z-index: 81;
|
||||
z-index: calc(var(--z-overlay) + 15);
|
||||
width: min(560px, calc(100vw - 24px));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2119,3 +2230,390 @@
|
||||
color: #5c6570;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
/* ── 对话资源:固定入口 + 右侧工作区 ── */
|
||||
|
||||
.omni-assets-entry {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: 28px;
|
||||
z-index: 40;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
height: 36px;
|
||||
padding: 0 14px 0 12px;
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--surface);
|
||||
color: var(--black-alpha-56);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%);
|
||||
transition: background var(--t-base), border-color var(--t-base), color var(--t-base), opacity var(--t-base);
|
||||
}
|
||||
|
||||
.omni-assets-entry svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.omni-assets-entry:hover {
|
||||
border-color: var(--border-loud);
|
||||
background: var(--black-alpha-4);
|
||||
color: var(--accent-black);
|
||||
}
|
||||
|
||||
.omni-assets-entry.is-open {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.omni-assets-drawer {
|
||||
top: var(--topbar-height);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: min(440px, 36vw);
|
||||
max-width: none;
|
||||
border-left: 1px solid var(--border-faint);
|
||||
background: var(--surface);
|
||||
box-shadow: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.omni-assets-drawer-head {
|
||||
min-height: 64px;
|
||||
gap: 20px;
|
||||
padding: 14px 20px;
|
||||
}
|
||||
|
||||
.omni-assets-drawer-head > div:first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.omni-assets-drawer-head strong,
|
||||
.omni-assets-drawer-head small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.omni-assets-drawer-head strong {
|
||||
color: var(--accent-black);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.omni-assets-drawer-head small {
|
||||
margin-top: 2px;
|
||||
color: var(--black-alpha-48);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.omni-assets-panel-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.omni-assets-panel-tools .view-toggle {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.omni-assets-panel-tools .view-toggle button {
|
||||
width: 28px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.omni-assets-panel-tools .view-toggle svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.omni-assets-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 132px;
|
||||
height: 32px;
|
||||
gap: 6px;
|
||||
padding: 0 9px;
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
color: var(--black-alpha-48);
|
||||
background: var(--background-lighter);
|
||||
}
|
||||
|
||||
.omni-assets-search:focus-within {
|
||||
border-color: var(--black-alpha-24);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.omni-assets-search svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.omni-assets-search input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
color: var(--accent-black);
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-assets-search input::placeholder {
|
||||
color: var(--black-alpha-40);
|
||||
}
|
||||
|
||||
.omni-assets-drawer-head .x {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.omni-assets-drawer-body {
|
||||
padding: 24px 20px 32px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.omni-assets-empty {
|
||||
min-height: 220px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
justify-items: center;
|
||||
gap: 10px;
|
||||
color: var(--black-alpha-48);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.omni-assets-empty svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.omni-assets-empty p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.omni-assets-group + .omni-assets-group {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.omni-assets-group h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 12px;
|
||||
color: var(--accent-black);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.omni-assets-group h3 em {
|
||||
color: var(--black-alpha-48);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.omni-assets-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.omni-assets-grid.is-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.omni-assets-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--surface);
|
||||
color: var(--accent-black);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: background var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
|
||||
.omni-assets-card:hover:not(:disabled) {
|
||||
border-color: var(--border-loud);
|
||||
background: var(--background-lighter);
|
||||
}
|
||||
|
||||
.omni-assets-card:disabled {
|
||||
cursor: default;
|
||||
opacity: .72;
|
||||
}
|
||||
|
||||
.omni-assets-card.is-doc,
|
||||
.omni-assets-grid.is-list .omni-assets-card {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.omni-assets-thumb {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background: var(--background-lighter);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.omni-assets-grid.is-list .omni-assets-thumb {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
flex: 0 0 auto;
|
||||
aspect-ratio: auto;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.omni-assets-thumb.is-doc,
|
||||
.omni-assets-card.is-doc .omni-assets-thumb {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex: 0 0 auto;
|
||||
aspect-ratio: auto;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 6px;
|
||||
color: var(--heat);
|
||||
background: var(--heat-12);
|
||||
}
|
||||
|
||||
.omni-assets-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.omni-assets-thumb > i {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--black-alpha-48);
|
||||
}
|
||||
|
||||
.omni-assets-thumb > i svg,
|
||||
.omni-assets-thumb.is-doc svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.omni-assets-play {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(0, 0, 0, .24);
|
||||
color: var(--surface);
|
||||
}
|
||||
|
||||
.omni-assets-play svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.omni-assets-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
|
||||
.omni-assets-card.is-doc .omni-assets-meta,
|
||||
.omni-assets-grid.is-list .omni-assets-meta {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.omni-assets-meta strong,
|
||||
.omni-assets-meta small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.omni-assets-meta strong {
|
||||
color: var(--accent-black);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.omni-assets-meta small {
|
||||
color: var(--black-alpha-48);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@media (min-width: 1101px) {
|
||||
.omni-session-page.has-assets-panel {
|
||||
box-sizing: border-box;
|
||||
padding-right: min(440px, 36vw);
|
||||
}
|
||||
|
||||
.omni-session-page.has-assets-panel .omni-session-shell {
|
||||
width: min(760px, calc(100% - 42px));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.omni-assets-drawer {
|
||||
width: min(400px, calc(100vw - 16px));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.omni-assets-entry {
|
||||
right: 12px;
|
||||
width: 36px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.omni-assets-entry span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.omni-assets-drawer {
|
||||
width: calc(100vw - 8px);
|
||||
}
|
||||
|
||||
.omni-assets-drawer-head {
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.omni-assets-panel-tools {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.omni-assets-search {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.omni-assets-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,13 @@ import {
|
||||
Download,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
Grid2X2,
|
||||
Image,
|
||||
List,
|
||||
Play,
|
||||
Plus,
|
||||
Pencil,
|
||||
Search,
|
||||
Sparkles,
|
||||
Square,
|
||||
Upload,
|
||||
@@ -296,6 +299,87 @@ const AGENT_POLL_INTERVAL_MS = 1500;
|
||||
|
||||
// ────────────────────────────────────────────────────────── 卡片
|
||||
|
||||
type SessionAssetKind = "image" | "video" | "document";
|
||||
|
||||
type SessionAsset = {
|
||||
key: string;
|
||||
kind: SessionAssetKind;
|
||||
title: string;
|
||||
thumb?: string;
|
||||
src?: string;
|
||||
promptTitle?: string;
|
||||
promptBody?: string;
|
||||
};
|
||||
|
||||
/** 从本会话已加载 messages(+pinned_refs / 会话上传)聚合聊天资产,不另打 BE。 */
|
||||
function collectSessionAssets(
|
||||
messages: CreationMessage[],
|
||||
pinnedRefs: CreationRef[] = [],
|
||||
sessionUploads: CreationRef[] = [],
|
||||
): SessionAsset[] {
|
||||
const seen = new Set<string>();
|
||||
const out: SessionAsset[] = [];
|
||||
|
||||
const push = (item: SessionAsset) => {
|
||||
if (!item.key || seen.has(item.key)) return;
|
||||
seen.add(item.key);
|
||||
out.push(item);
|
||||
};
|
||||
|
||||
const addRef = (ref: CreationRef | null | undefined) => {
|
||||
if (!ref?.id) return;
|
||||
const cover = (ref.cover || "").trim();
|
||||
push({
|
||||
key: `ref:${ref.type}:${ref.id}`,
|
||||
kind: "image",
|
||||
title: shortRefName(ref.name) || REF_CHIP_LABEL[ref.type] || ref.name || "参考素材",
|
||||
thumb: cover || undefined,
|
||||
src: cover || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
for (const ref of pinnedRefs) addRef(ref);
|
||||
for (const ref of sessionUploads) addRef(ref);
|
||||
|
||||
for (const message of messages) {
|
||||
for (const ref of message.refs || []) addRef(ref);
|
||||
|
||||
if (message.kind === "result") {
|
||||
const assets = (message.payload?.assets as Array<Record<string, string>> | undefined) || [];
|
||||
assets.forEach((asset, index) => {
|
||||
const url = (asset.url || asset.cover || "").trim();
|
||||
const cover = (asset.cover || asset.url || "").trim();
|
||||
if (!url && !cover) return;
|
||||
const video = asset.type === "video";
|
||||
const id = asset.id || url || `${message.id}-${index}`;
|
||||
push({
|
||||
key: `result:${id}`,
|
||||
kind: video ? "video" : "image",
|
||||
title: video
|
||||
? (assets.length > 1 ? `生成视频 ${index + 1}` : "生成视频")
|
||||
: (assets.length > 1 ? `生成结果 ${index + 1}` : "生成图片"),
|
||||
thumb: cover || undefined,
|
||||
src: url || cover,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (message.kind === "prompt_file") {
|
||||
const title = String(message.payload?.title || "视频生成Prompt.md");
|
||||
const body = String(message.payload?.body || "");
|
||||
push({
|
||||
key: `prompt:${message.id || title}`,
|
||||
kind: "document",
|
||||
title,
|
||||
promptTitle: title,
|
||||
promptBody: body || "暂无内容",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function strategyField(payload: Record<string, unknown>, ...keys: string[]): string {
|
||||
for (const key of keys) {
|
||||
const value = payload[key];
|
||||
@@ -435,7 +519,7 @@ function PlanCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
,预计覆盖约 90% 时长
|
||||
</span>
|
||||
) : null}
|
||||
<span>
|
||||
<span className="omni-plan-final-input">
|
||||
最终输入:
|
||||
<b>
|
||||
Prompt.md + <em className="omni-plan-point-body">{String(payload.ref_count ?? 0)}</em> 组参考素材
|
||||
@@ -469,17 +553,18 @@ function PromptFileCard({
|
||||
);
|
||||
}
|
||||
|
||||
/** 视频 5 步闸门:策略/方案/Prompt 下方的「按这个继续 / 我想改」。 */
|
||||
/** 视频 5 步闸门:策略/方案/Prompt 下方的「按这个继续 / 我想改」。
|
||||
* 「我想改」在卡片内展开反馈输入,不再依赖底部 composer。 */
|
||||
function StepConfirmCard({
|
||||
message,
|
||||
disabled,
|
||||
onConfirm,
|
||||
onRevise,
|
||||
onReviseSubmit,
|
||||
}: {
|
||||
message: CreationMessage;
|
||||
disabled: boolean;
|
||||
onConfirm: () => void;
|
||||
onRevise: () => void;
|
||||
onReviseSubmit: (feedback: string) => void;
|
||||
}) {
|
||||
const submitted = Boolean(message.payload.submitted);
|
||||
const step = String(message.payload.step || "");
|
||||
@@ -487,41 +572,113 @@ function StepConfirmCard({
|
||||
const action = String(answers.step_action || "");
|
||||
const stepLabel =
|
||||
step === "strategy" ? "创作策略" : step === "plan" ? "视频方案" : step === "prompt" ? "出片 Prompt" : "这一步";
|
||||
const placeholder =
|
||||
step === "strategy"
|
||||
? "说说你想怎么改策略…"
|
||||
: step === "plan"
|
||||
? "说说你想怎么改方案…"
|
||||
: step === "prompt"
|
||||
? "说说你想怎么改 Prompt…"
|
||||
: "说说你想怎么改…";
|
||||
const [revising, setRevising] = useState(false);
|
||||
const [feedback, setFeedback] = useState("");
|
||||
const reviseRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!revising) return;
|
||||
const el = reviseRef.current;
|
||||
if (!el) return;
|
||||
el.focus();
|
||||
const len = el.value.length;
|
||||
el.setSelectionRange(len, len);
|
||||
}, [revising]);
|
||||
|
||||
const canSubmit = feedback.trim().length > 0;
|
||||
|
||||
return (
|
||||
<section className={`omni-step-confirm-card${submitted ? " is-submitted" : ""}`}>
|
||||
<div className="omni-step-confirm-copy">
|
||||
<strong>{submitted ? (action === "revise" ? `已收到对${stepLabel}的修改意见` : `已确认${stepLabel}`) : `请确认${stepLabel}`}</strong>
|
||||
<span>{submitted ? (action === "revise" ? "正在按你的反馈重写" : "继续下一步") : (message.text || "确认后继续;要改可以直接说。")}</span>
|
||||
</div>
|
||||
{submitted ? null : (
|
||||
<div className="omni-step-confirm-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="is-secondary"
|
||||
disabled={disabled}
|
||||
onMouseDown={(event) => {
|
||||
// 避免按钮抢焦点,否则 focus composer 会被立刻冲掉
|
||||
event.preventDefault();
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onRevise();
|
||||
}}
|
||||
>
|
||||
我想改
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onConfirm();
|
||||
}}
|
||||
>
|
||||
按这个继续
|
||||
</button>
|
||||
<section
|
||||
className={`omni-step-confirm-card${submitted ? " is-submitted" : ""}${revising && !submitted ? " is-revising" : ""}`}
|
||||
>
|
||||
<div className="omni-step-confirm-top">
|
||||
<div className="omni-step-confirm-copy">
|
||||
<strong>{submitted ? (action === "revise" ? `已收到对${stepLabel}的修改意见` : `已确认${stepLabel}`) : revising ? `修改${stepLabel}` : `请确认${stepLabel}`}</strong>
|
||||
<span>{submitted ? (action === "revise" ? "正在按你的反馈重写" : "继续下一步") : revising ? "写下你的想法,我们按这个重做这一步。" : (message.text || "确认后继续;要改可以直接说。")}</span>
|
||||
</div>
|
||||
)}
|
||||
{submitted || revising ? null : (
|
||||
<div className="omni-step-confirm-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="is-secondary"
|
||||
disabled={disabled}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setRevising(true);
|
||||
}}
|
||||
>
|
||||
我想改
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onConfirm();
|
||||
}}
|
||||
>
|
||||
按这个继续
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!submitted && revising ? (
|
||||
<div className="omni-step-confirm-revise">
|
||||
<textarea
|
||||
ref={reviseRef}
|
||||
className="omni-step-confirm-revise-input"
|
||||
rows={3}
|
||||
value={feedback}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
onChange={(event) => setFeedback(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
setRevising(false);
|
||||
setFeedback("");
|
||||
}
|
||||
if (event.key === "Enter" && (event.metaKey || event.ctrlKey) && canSubmit && !disabled) {
|
||||
event.preventDefault();
|
||||
onReviseSubmit(feedback.trim());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="omni-step-confirm-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="is-secondary"
|
||||
disabled={disabled}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setRevising(false);
|
||||
setFeedback("");
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled || !canSubmit}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
if (!canSubmit) return;
|
||||
onReviseSubmit(feedback.trim());
|
||||
}}
|
||||
>
|
||||
按这个重做
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -531,15 +688,12 @@ function ElicitCard({
|
||||
message,
|
||||
disabled,
|
||||
onSubmit,
|
||||
onStepRevise,
|
||||
}: {
|
||||
message: CreationMessage;
|
||||
disabled: boolean;
|
||||
/** answers 给模型读(人话),refs 给后端取卖点和参考图。**选素材必须两样都回**,
|
||||
只回名字的话同名素材会配错货。 */
|
||||
onSubmit: (answers: Record<string, string | string[]>, refs: CreationRef[]) => void;
|
||||
/** 步骤确认「我想改」:打开输入框让用户写反馈 */
|
||||
onStepRevise?: () => void;
|
||||
}) {
|
||||
const fields = ((message.payload.fields as CreationField[] | undefined) || []).filter(Boolean);
|
||||
const submitted = Boolean(message.payload.submitted);
|
||||
@@ -587,10 +741,7 @@ function ElicitCard({
|
||||
message={message}
|
||||
disabled={disabled}
|
||||
onConfirm={() => onSubmit({ step_action: "confirm" }, [])}
|
||||
onRevise={() => {
|
||||
if (onStepRevise) onStepRevise();
|
||||
else onSubmit({ step_action: "revise" }, []);
|
||||
}}
|
||||
onReviseSubmit={(feedback) => onSubmit({ step_action: "revise", feedback }, [])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1089,11 +1240,16 @@ function mergeMessagePreserveLocal(prevHit: CreationMessage | undefined, msg: Cr
|
||||
if (!prevHit) {
|
||||
return msg.clientKey === clientKey ? msg : { ...msg, clientKey };
|
||||
}
|
||||
// 开放中的追问/闸门:服务端尚未 submitted 时,保住本地已选选项与乐观提交
|
||||
// 开放中的追问:服务端尚未 submitted 时,保住本地已选选项与乐观提交。
|
||||
// step_confirm 除外 —— 取消修订时服务端会主动撤回 submitted,必须以服务端为准,
|
||||
// 否则会卡在「已收到…正在按你的反馈重写」。
|
||||
if (prevHit.kind === "elicit" && msg.kind === "elicit") {
|
||||
const prevPayload = prevHit.payload || {};
|
||||
const nextPayload = msg.payload || {};
|
||||
if (prevPayload.submitted && !nextPayload.submitted) {
|
||||
const isStepConfirm =
|
||||
prevPayload.interaction === "step_confirm"
|
||||
|| nextPayload.interaction === "step_confirm";
|
||||
if (!isStepConfirm && prevPayload.submitted && !nextPayload.submitted) {
|
||||
return {
|
||||
...msg,
|
||||
clientKey,
|
||||
@@ -1304,6 +1460,10 @@ export function OmniSessionPage({
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [stopping, setStopping] = useState(false);
|
||||
const [promptView, setPromptView] = useState<{ title: string; body: string } | null>(null);
|
||||
const [assetsOpen, setAssetsOpen] = useState(false);
|
||||
const [assetView, setAssetView] = useState<"grid" | "list">("grid");
|
||||
const [assetQuery, setAssetQuery] = useState("");
|
||||
const [assetPreview, setAssetPreview] = useState<{ src: string; kind: "image" | "video"; name: string } | null>(null);
|
||||
const [pendingUserId, setPendingUserId] = useState<string | null>(() => bootLocalIdRef.current);
|
||||
// App 传进来的 onNotify 是内联箭头,**每次 App 重渲染都是新身份**。
|
||||
// 直接放进 useEffect / useCallback 依赖会让整条会话被反复重拉、消息数组被反复替换
|
||||
@@ -1338,21 +1498,6 @@ export function OmniSessionPage({
|
||||
});
|
||||
}, []);
|
||||
|
||||
/** 「我想改」:露出底部输入框并聚焦,让用户写反馈(后端会把自由文本当 revise) */
|
||||
const beginStepRevise = useCallback(() => {
|
||||
setComposerHint("说说要改哪里…");
|
||||
const focusComposer = () => {
|
||||
const el = composerRef.current;
|
||||
if (!el) return;
|
||||
el.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
el.focus({ preventScroll: true });
|
||||
const len = el.value.length;
|
||||
el.setSelectionRange(len, len);
|
||||
};
|
||||
focusComposer();
|
||||
requestAnimationFrame(focusComposer);
|
||||
}, []);
|
||||
|
||||
const feedRef = useRef<HTMLElement>(null);
|
||||
const firstSentRef = useRef(false);
|
||||
const pendingUserIdRef = useRef<string | null>(bootLocalIdRef.current);
|
||||
@@ -1490,6 +1635,32 @@ export function OmniSessionPage({
|
||||
[messages]
|
||||
);
|
||||
const visibleMessages = useMemo(() => withoutLegacyGateArtifacts(messages), [messages]);
|
||||
const sessionAssets = useMemo(
|
||||
() => collectSessionAssets(messages, conversation?.pinned_refs || [], sessionUploads),
|
||||
[messages, conversation?.pinned_refs, sessionUploads],
|
||||
);
|
||||
const sessionAssetGroups = useMemo(() => {
|
||||
const groups: Array<{ key: SessionAssetKind; label: string; items: SessionAsset[] }> = [
|
||||
{ key: "image", label: "图片", items: [] },
|
||||
{ key: "video", label: "视频", items: [] },
|
||||
{ key: "document", label: "文档", items: [] },
|
||||
];
|
||||
for (const asset of sessionAssets) {
|
||||
const group = groups.find((item) => item.key === asset.kind);
|
||||
if (group) group.items.push(asset);
|
||||
}
|
||||
return groups.filter((group) => group.items.length > 0);
|
||||
}, [sessionAssets]);
|
||||
const visibleSessionAssetGroups = useMemo(() => {
|
||||
const query = assetQuery.trim().toLocaleLowerCase();
|
||||
if (!query) return sessionAssetGroups;
|
||||
return sessionAssetGroups
|
||||
.map((group) => ({
|
||||
...group,
|
||||
items: group.items.filter((item) => item.title.toLocaleLowerCase().includes(query)),
|
||||
}))
|
||||
.filter((group) => group.items.length > 0);
|
||||
}, [assetQuery, sessionAssetGroups]);
|
||||
messagesRef.current = messages;
|
||||
|
||||
// 出片在 worker 里跑。刷新后 GENERATING 还在库里,进来立刻拉一次再轮询,
|
||||
@@ -1783,7 +1954,7 @@ export function OmniSessionPage({
|
||||
setStopping(true);
|
||||
userCancelledRef.current = true;
|
||||
holdPlanningUntilRef.current = 0;
|
||||
// 先本地收态,再打 API —— 离开/重进靠服务端 cancel 标 + idle
|
||||
// 先本地收态,再打 API —— 闸门修订中取消后服务端会恢复 step_confirm
|
||||
streamingRef.current = false;
|
||||
setStreaming(false);
|
||||
setLiveText("");
|
||||
@@ -1792,28 +1963,40 @@ export function OmniSessionPage({
|
||||
setConversation((prev) =>
|
||||
prev ? { ...prev, agent_status: "idle" } : prev
|
||||
);
|
||||
const refreshAfterCancel = async () => {
|
||||
try {
|
||||
const detail = await api.getCreation(conversationId);
|
||||
// 直接采用服务端快照,避免 merge 把已撤回的 step_confirm 又乐观粘回 submitted
|
||||
setConversation(detail);
|
||||
setMessages(detail.messages || []);
|
||||
} catch {
|
||||
/* 恢复失败则等 poll / 用户刷新 */
|
||||
}
|
||||
};
|
||||
try {
|
||||
await api.cancelCreationAgent(conversationId);
|
||||
const result = await api.cancelCreationAgent(conversationId);
|
||||
notify("success", "已终止");
|
||||
if (result.agent_status) {
|
||||
setConversation((prev) =>
|
||||
prev ? { ...prev, agent_status: result.agent_status } : prev
|
||||
);
|
||||
}
|
||||
await refreshAfterCancel();
|
||||
} catch (error) {
|
||||
const status = (error as { status?: number }).status;
|
||||
if (status === 409) {
|
||||
// 已结束或竞态完成 —— 保持 idle,不刷错误
|
||||
// 已结束或竞态完成 —— 仍拉一次详情,可能已落新闸门确认
|
||||
notify("info", "已终止");
|
||||
await refreshAfterCancel();
|
||||
} else {
|
||||
userCancelledRef.current = false;
|
||||
notify("error", (error as Error).message || "终止失败");
|
||||
try {
|
||||
const detail = await api.getCreation(conversationId);
|
||||
mergeCreationDetail(detail);
|
||||
} catch {
|
||||
/* 恢复失败则等用户刷新 */
|
||||
}
|
||||
await refreshAfterCancel();
|
||||
}
|
||||
} finally {
|
||||
setStopping(false);
|
||||
}
|
||||
}, [stopping, isPlanning, conversationId, notify, mergeCreationDetail]);
|
||||
}, [stopping, isPlanning, conversationId, notify]);
|
||||
|
||||
const handleConfirm = async (message: CreationMessage, nextParams: Record<string, string>) => {
|
||||
if (confirming) return;
|
||||
@@ -1936,7 +2119,6 @@ export function OmniSessionPage({
|
||||
);
|
||||
void send({ kind: "elicit_answer", reply_to: message.id, answers, refs });
|
||||
}}
|
||||
onStepRevise={beginStepRevise}
|
||||
/>
|
||||
);
|
||||
case "strategy":
|
||||
@@ -2003,6 +2185,12 @@ export function OmniSessionPage({
|
||||
{(body || message.role !== "user") ? (
|
||||
<div className="omni-chat-bubble" aria-busy={pending || undefined}>
|
||||
{body ? <p className="omni-chat-text">{body}</p> : null}
|
||||
{message.role === "assistant" && typeof message.payload?.reply_hint === "string" && message.payload.reply_hint.trim() ? (
|
||||
<p className="omni-reply-hint">
|
||||
<span className="omni-reply-hint-label">下一步可以这样回</span>
|
||||
{String(message.payload.reply_hint)}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{message.role === "user" && !pending ? (
|
||||
@@ -2067,9 +2255,20 @@ export function OmniSessionPage({
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="page-view omni-session-page">
|
||||
<section className={`page-view omni-session-page${assetsOpen ? " has-assets-panel" : ""}`}>
|
||||
{topbarSlot ? createPortal(sessionHeader, topbarSlot) : sessionHeader}
|
||||
<div className="omni-session-shell">
|
||||
<button
|
||||
type="button"
|
||||
className={`omni-assets-entry${assetsOpen ? " is-open" : ""}`}
|
||||
aria-expanded={assetsOpen}
|
||||
aria-controls="omni-session-assets-drawer"
|
||||
title="查看对话资源"
|
||||
onClick={() => setAssetsOpen((open) => !open)}
|
||||
>
|
||||
<List />
|
||||
<span>查看对话资源</span>
|
||||
</button>
|
||||
|
||||
<main className="omni-session-feed" ref={feedRef} aria-live="polite">
|
||||
{visibleMessages.map(renderMessage)}
|
||||
@@ -2381,6 +2580,144 @@ export function OmniSessionPage({
|
||||
</aside>
|
||||
</>
|
||||
) : null}
|
||||
{assetsOpen ? (
|
||||
<aside
|
||||
id="omni-session-assets-drawer"
|
||||
className="drawer omni-assets-drawer show"
|
||||
role="dialog"
|
||||
aria-label="对话资源"
|
||||
>
|
||||
<header className="drawer-h omni-assets-drawer-head">
|
||||
<div>
|
||||
<strong>资源</strong>
|
||||
<small>{sessionAssets.length ? `本会话 · ${sessionAssets.length} 项` : "仅展示本会话素材"}</small>
|
||||
</div>
|
||||
<div className="omni-assets-panel-tools">
|
||||
<div className="view-toggle" aria-label="资源显示方式">
|
||||
<button
|
||||
type="button"
|
||||
className={assetView === "grid" ? "active" : ""}
|
||||
aria-label="网格显示"
|
||||
title="网格显示"
|
||||
onClick={() => setAssetView("grid")}
|
||||
>
|
||||
<Grid2X2 />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={assetView === "list" ? "active" : ""}
|
||||
aria-label="列表显示"
|
||||
title="列表显示"
|
||||
onClick={() => setAssetView("list")}
|
||||
>
|
||||
<List />
|
||||
</button>
|
||||
</div>
|
||||
<label className="omni-assets-search">
|
||||
<Search />
|
||||
<input
|
||||
value={assetQuery}
|
||||
onChange={(event) => setAssetQuery(event.target.value)}
|
||||
placeholder="搜索资源…"
|
||||
aria-label="搜索对话资源"
|
||||
/>
|
||||
</label>
|
||||
<button type="button" className="x" onClick={() => setAssetsOpen(false)} aria-label="关闭资源">
|
||||
<X />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="drawer-b omni-assets-drawer-body">
|
||||
{sessionAssets.length === 0 ? (
|
||||
<div className="omni-assets-empty" role="status">
|
||||
<FolderOpen />
|
||||
<p>本会话还没有素材</p>
|
||||
</div>
|
||||
) : visibleSessionAssetGroups.length === 0 ? (
|
||||
<div className="omni-assets-empty" role="status">
|
||||
<Search />
|
||||
<p>没有找到相关资源</p>
|
||||
</div>
|
||||
) : (
|
||||
visibleSessionAssetGroups.map((group) => (
|
||||
<section className="omni-assets-group" key={group.key}>
|
||||
<h3>
|
||||
{group.label}
|
||||
<em>{group.items.length}</em>
|
||||
</h3>
|
||||
<div className={`omni-assets-grid${group.key === "document" || assetView === "list" ? " is-list" : ""}`}>
|
||||
{group.items.map((asset) => {
|
||||
if (asset.kind === "document") {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="omni-assets-card is-doc"
|
||||
key={asset.key}
|
||||
onClick={() => {
|
||||
setPromptView({
|
||||
title: asset.promptTitle || asset.title,
|
||||
body: asset.promptBody || "暂无内容",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="omni-assets-thumb is-doc">
|
||||
<FileText />
|
||||
</span>
|
||||
<span className="omni-assets-meta">
|
||||
<strong>{asset.title}</strong>
|
||||
<small>文档 · 点击查看</small>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`omni-assets-card is-${asset.kind}`}
|
||||
key={asset.key}
|
||||
disabled={!asset.src}
|
||||
onClick={() => {
|
||||
if (!asset.src) return;
|
||||
setAssetPreview({
|
||||
src: asset.src,
|
||||
kind: asset.kind === "video" ? "video" : "image",
|
||||
name: asset.title,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="omni-assets-thumb">
|
||||
{asset.thumb ? (
|
||||
<img src={asset.thumb} alt="" />
|
||||
) : (
|
||||
<i>{asset.kind === "video" ? <Play /> : <Image />}</i>
|
||||
)}
|
||||
{asset.kind === "video" ? (
|
||||
<span className="omni-assets-play" aria-hidden="true">
|
||||
<Play />
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="omni-assets-meta">
|
||||
<strong>{asset.title}</strong>
|
||||
<small>{asset.kind === "video" ? "视频" : "图片"}</small>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
) : null}
|
||||
<MediaLightbox
|
||||
open={Boolean(assetPreview?.src)}
|
||||
src={assetPreview?.src || ""}
|
||||
kind={assetPreview?.kind}
|
||||
name={assetPreview?.name}
|
||||
close={() => setAssetPreview(null)}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user