Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f817e31366 | |||
| 0384d7f2a3 | |||
| f920630160 | |||
| c1266169d7 | |||
| 4530058648 | |||
| 2e52101b37 | |||
| 5bdeac20fb | |||
| bbc705c8d9 | |||
| 5b87594e36 | |||
| 796162de4d | |||
| aebe0ff827 |
@@ -0,0 +1,408 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>交互式对话框生成器</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#165DFF',
|
||||||
|
secondary: '#6B7280',
|
||||||
|
accent: '#F59E0B'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'system-ui', 'sans-serif']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style type="text/tailwindcss">
|
||||||
|
@layer utilities {
|
||||||
|
.dialog-item {
|
||||||
|
@apply p-3 border rounded-lg cursor-pointer transition-all hover:bg-primary/5 hover:border-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
.gen-dialog {
|
||||||
|
position: absolute;
|
||||||
|
min-width: 140px;
|
||||||
|
max-width: 280px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,0.12);
|
||||||
|
z-index: 10;
|
||||||
|
user-select: none;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.gen-dialog:hover { box-shadow: 0 6px 32px rgba(0,0,0,0.18); }
|
||||||
|
.gen-dialog.active-drag { z-index: 20; box-shadow: 0 8px 40px rgba(0,0,0,0.22); }
|
||||||
|
|
||||||
|
/* 样式1:白色圆角 */
|
||||||
|
.gen-dialog.style1 { background: rgba(255,255,255,0.97); border: 2px solid #CBD5E1; }
|
||||||
|
.gen-dialog.style1 .gen-confirm { background: #165DFF; }
|
||||||
|
.gen-dialog.style1 .gen-text { color: #1e293b; }
|
||||||
|
|
||||||
|
/* 样式2:蓝色气泡 */
|
||||||
|
.gen-dialog.style2 { background: rgba(22,93,255,0.95); border: 2px solid #4F8AFF; border-radius: 16px 16px 4px 16px; }
|
||||||
|
.gen-dialog.style2 .gen-confirm { background: #fff; color: #165DFF; }
|
||||||
|
.gen-dialog.style2 .gen-text { color: #fff; }
|
||||||
|
.gen-dialog.style2 .gen-text::placeholder { color: rgba(255,255,255,0.6); }
|
||||||
|
|
||||||
|
/* 样式3:黄色提示 */
|
||||||
|
.gen-dialog.style3 { background: rgba(255,247,237,0.97); border: 2px solid #F59E0B; }
|
||||||
|
.gen-dialog.style3 .gen-confirm { background: #F59E0B; }
|
||||||
|
.gen-dialog.style3 .gen-text { color: #92400e; }
|
||||||
|
.gen-dialog.style3 .gen-text::placeholder { color: rgba(146,64,14,0.4); }
|
||||||
|
|
||||||
|
/* 样式4:灰色简约 */
|
||||||
|
.gen-dialog.style4 { background: rgba(248,250,252,0.97); border: 2px solid #6B7280; border-radius: 4px; }
|
||||||
|
.gen-dialog.style4 .gen-confirm { background: #6B7280; }
|
||||||
|
.gen-dialog.style4 .gen-text { color: #1f2937; }
|
||||||
|
|
||||||
|
.gen-text {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
resize: none;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.gen-text::placeholder { color: rgba(0,0,0,0.3); }
|
||||||
|
|
||||||
|
.gen-confirm {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.gen-confirm:hover { filter: brightness(1.1); transform: translateY(-1px); }
|
||||||
|
.gen-confirm:active { transform: translateY(0); }
|
||||||
|
|
||||||
|
/* 已确认状态 */
|
||||||
|
.gen-dialog.confirmed .gen-text { cursor: default; }
|
||||||
|
.gen-dialog.confirmed .gen-confirm { display: none; }
|
||||||
|
.gen-dialog.confirmed .gen-edit-hint { display: inline-block; }
|
||||||
|
.gen-dialog:not(.confirmed) .gen-edit-hint { display: none; }
|
||||||
|
|
||||||
|
.gen-edit-hint {
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(0,0,0,0.3);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除按钮 */
|
||||||
|
.gen-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #EF4444;
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
.gen-dialog:hover .gen-delete { opacity: 1; }
|
||||||
|
|
||||||
|
#previewContainer { position: relative; }
|
||||||
|
#previewImage { width: 100%; height: 100%; background-size: contain; background-position: center; background-repeat: no-repeat; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 min-h-screen">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-center mb-8 text-gray-800">
|
||||||
|
交互式对话框生成器
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="flex flex-col lg:flex-row gap-6">
|
||||||
|
<!-- 左侧面板 -->
|
||||||
|
<div class="lg:w-1/3 bg-white rounded-xl shadow-md p-6">
|
||||||
|
<!-- 文件上传区域 -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-lg font-semibold mb-4 text-gray-700">
|
||||||
|
<i class="fa fa-upload mr-2 text-primary"></i>上传背景图片
|
||||||
|
</h2>
|
||||||
|
<div id="dropArea" class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-primary transition-colors">
|
||||||
|
<i class="fa fa-image text-4xl text-gray-400 mb-3"></i>
|
||||||
|
<p class="text-gray-500 mb-2">点击或拖拽图片到此处</p>
|
||||||
|
<p class="text-xs text-gray-400">支持 JPG、PNG、WEBP 格式</p>
|
||||||
|
<input type="file" id="fileInput" accept="image/*" class="hidden">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 对话框选择区域 -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold mb-4 text-gray-700">
|
||||||
|
<i class="fa fa-comment mr-2 text-primary"></i>点击添加对话框
|
||||||
|
</h2>
|
||||||
|
<p class="text-xs text-gray-400 mb-3">每点一次即在预览区新增一个对话框</p>
|
||||||
|
<div class="space-y-3" id="dialogList">
|
||||||
|
<div class="dialog-item" data-style="style1">
|
||||||
|
<span class="inline-block w-3 h-3 rounded bg-white border border-gray-300 mr-2 align-middle"></span>白色圆角对话框
|
||||||
|
</div>
|
||||||
|
<div class="dialog-item" data-style="style2">
|
||||||
|
<span class="inline-block w-3 h-3 rounded bg-blue-500 mr-2 align-middle"></span>蓝色气泡对话框
|
||||||
|
</div>
|
||||||
|
<div class="dialog-item" data-style="style3">
|
||||||
|
<span class="inline-block w-3 h-3 rounded bg-amber-400 mr-2 align-middle"></span>黄色提示对话框
|
||||||
|
</div>
|
||||||
|
<div class="dialog-item" data-style="style4">
|
||||||
|
<span class="inline-block w-3 h-3 rounded bg-gray-400 mr-2 align-middle"></span>灰色简约对话框
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<button id="clearBtn" class="w-full bg-gray-200 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-300 transition-all">
|
||||||
|
<i class="fa fa-trash mr-1"></i>清空全部对话框
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧预览面板 -->
|
||||||
|
<div class="lg:w-2/3 bg-white rounded-xl shadow-md p-6">
|
||||||
|
<h2 class="text-lg font-semibold mb-4 text-gray-700">
|
||||||
|
<i class="fa fa-eye mr-2 text-primary"></i>预览区域
|
||||||
|
</h2>
|
||||||
|
<div id="previewContainer" class="relative w-full h-[500px] border rounded-lg bg-gray-100 overflow-hidden">
|
||||||
|
<div id="previewImage"></div>
|
||||||
|
<div id="dialogContainer" class="absolute inset-0"></div>
|
||||||
|
<div id="emptyTip" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center text-gray-400 pointer-events-none">
|
||||||
|
<i class="fa fa-image text-5xl mb-3"></i>
|
||||||
|
<p>上传图片后开始编辑</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 mt-3">
|
||||||
|
<i class="fa fa-info-circle mr-1"></i>提示:对话框可拖动定位,输入文字后点确认即可渲染,双击已确认的框可重新编辑
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let dialogCount = 0;
|
||||||
|
|
||||||
|
const dropArea = document.getElementById('dropArea');
|
||||||
|
const fileInput = document.getElementById('fileInput');
|
||||||
|
const previewImage = document.getElementById('previewImage');
|
||||||
|
const emptyTip = document.getElementById('emptyTip');
|
||||||
|
const dialogList = document.getElementById('dialogList');
|
||||||
|
const dialogContainer = document.getElementById('dialogContainer');
|
||||||
|
const clearBtn = document.getElementById('clearBtn');
|
||||||
|
const previewContainer = document.getElementById('previewContainer');
|
||||||
|
|
||||||
|
// ===== 文件上传 =====
|
||||||
|
dropArea.addEventListener('click', () => fileInput.click());
|
||||||
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evt => {
|
||||||
|
dropArea.addEventListener(evt, e => { e.preventDefault(); e.stopPropagation(); });
|
||||||
|
});
|
||||||
|
dropArea.addEventListener('drop', e => handleFile(e.dataTransfer.files[0]));
|
||||||
|
fileInput.addEventListener('change', e => handleFile(e.target.files[0]));
|
||||||
|
|
||||||
|
function handleFile(file) {
|
||||||
|
if (!file || !file.type.startsWith('image/')) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
previewImage.style.backgroundImage = `url(${e.target.result})`;
|
||||||
|
emptyTip.classList.add('hidden');
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 点击添加对话框 =====
|
||||||
|
dialogList.addEventListener('click', e => {
|
||||||
|
const item = e.target.closest('.dialog-item');
|
||||||
|
if (!item) return;
|
||||||
|
createDialog(item.dataset.style);
|
||||||
|
});
|
||||||
|
|
||||||
|
function createDialog(style) {
|
||||||
|
dialogCount++;
|
||||||
|
const dialog = document.createElement('div');
|
||||||
|
dialog.className = `gen-dialog ${style}`;
|
||||||
|
dialog.dataset.style = style;
|
||||||
|
|
||||||
|
// 随机偏移避免完全重叠
|
||||||
|
const offsetX = 30 + (dialogCount * 25) % 200;
|
||||||
|
const offsetY = 30 + (dialogCount * 20) % 150;
|
||||||
|
dialog.style.left = offsetX + 'px';
|
||||||
|
dialog.style.top = offsetY + 'px';
|
||||||
|
dialog.style.padding = '12px 14px';
|
||||||
|
|
||||||
|
// 删除按钮
|
||||||
|
const delBtn = document.createElement('div');
|
||||||
|
delBtn.className = 'gen-delete';
|
||||||
|
delBtn.innerHTML = '<i class="fa fa-times" style="font-size:9px"></i>';
|
||||||
|
delBtn.addEventListener('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dialog.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 文本输入区
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.className = 'gen-text';
|
||||||
|
textarea.rows = 2;
|
||||||
|
textarea.placeholder = '输入文本...';
|
||||||
|
// 阻止拖动冲突
|
||||||
|
textarea.addEventListener('mousedown', e => e.stopPropagation());
|
||||||
|
textarea.addEventListener('touchstart', e => e.stopPropagation());
|
||||||
|
|
||||||
|
// 底部:确认按钮 + 编辑提示
|
||||||
|
const bottomRow = document.createElement('div');
|
||||||
|
bottomRow.style.cssText = 'display:flex; justify-content:flex-end; align-items:center;';
|
||||||
|
|
||||||
|
const editHint = document.createElement('span');
|
||||||
|
editHint.className = 'gen-edit-hint';
|
||||||
|
editHint.textContent = '双击编辑';
|
||||||
|
|
||||||
|
const confirmBtn = document.createElement('button');
|
||||||
|
confirmBtn.className = 'gen-confirm';
|
||||||
|
confirmBtn.innerHTML = '<i class="fa fa-check" style="font-size:10px"></i> 确认';
|
||||||
|
|
||||||
|
bottomRow.appendChild(editHint);
|
||||||
|
bottomRow.appendChild(confirmBtn);
|
||||||
|
|
||||||
|
dialog.appendChild(delBtn);
|
||||||
|
dialog.appendChild(textarea);
|
||||||
|
dialog.appendChild(bottomRow);
|
||||||
|
dialogContainer.appendChild(dialog);
|
||||||
|
|
||||||
|
// ===== 确认 =====
|
||||||
|
confirmBtn.addEventListener('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const text = textarea.value.trim();
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
// 把textarea换成纯文本展示
|
||||||
|
const textDisplay = document.createElement('div');
|
||||||
|
textDisplay.className = 'gen-text';
|
||||||
|
textDisplay.style.whiteSpace = 'pre-wrap';
|
||||||
|
textDisplay.textContent = text;
|
||||||
|
textarea.replaceWith(textDisplay);
|
||||||
|
|
||||||
|
dialog.classList.add('confirmed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 双击重新编辑 =====
|
||||||
|
dialog.addEventListener('dblclick', e => {
|
||||||
|
if (!dialog.classList.contains('confirmed')) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const textDisplay = dialog.querySelector('.gen-text');
|
||||||
|
const currentText = textDisplay.textContent;
|
||||||
|
|
||||||
|
const newTextarea = document.createElement('textarea');
|
||||||
|
newTextarea.className = 'gen-text';
|
||||||
|
newTextarea.rows = 2;
|
||||||
|
newTextarea.value = currentText;
|
||||||
|
newTextarea.addEventListener('mousedown', e => e.stopPropagation());
|
||||||
|
newTextarea.addEventListener('touchstart', e => e.stopPropagation());
|
||||||
|
|
||||||
|
textDisplay.replaceWith(newTextarea);
|
||||||
|
dialog.classList.remove('confirmed');
|
||||||
|
newTextarea.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 拖动 =====
|
||||||
|
bindDrag(dialog);
|
||||||
|
|
||||||
|
// 自动聚焦输入
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 拖动逻辑 =====
|
||||||
|
function bindDrag(element) {
|
||||||
|
let dragging = false, ox, oy;
|
||||||
|
|
||||||
|
element.addEventListener('mousedown', e => {
|
||||||
|
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'BUTTON' || e.target.closest('.gen-delete') || e.target.closest('.gen-confirm')) return;
|
||||||
|
dragging = true;
|
||||||
|
element.classList.add('active-drag');
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
ox = e.clientX - rect.left;
|
||||||
|
oy = e.clientY - rect.top;
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', e => {
|
||||||
|
if (!dragging) return;
|
||||||
|
const cr = dialogContainer.getBoundingClientRect();
|
||||||
|
let x = e.clientX - ox - cr.left;
|
||||||
|
let y = e.clientY - oy - cr.top;
|
||||||
|
x = Math.max(0, Math.min(x, cr.width - element.offsetWidth));
|
||||||
|
y = Math.max(0, Math.min(y, cr.height - element.offsetHeight));
|
||||||
|
element.style.left = x + 'px';
|
||||||
|
element.style.top = y + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
if (dragging) {
|
||||||
|
dragging = false;
|
||||||
|
element.classList.remove('active-drag');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 触摸支持
|
||||||
|
element.addEventListener('touchstart', e => {
|
||||||
|
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'BUTTON' || e.target.closest('.gen-delete') || e.target.closest('.gen-confirm')) return;
|
||||||
|
dragging = true;
|
||||||
|
element.classList.add('active-drag');
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const touch = e.touches[0];
|
||||||
|
ox = touch.clientX - rect.left;
|
||||||
|
oy = touch.clientY - rect.top;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
document.addEventListener('touchmove', e => {
|
||||||
|
if (!dragging) return;
|
||||||
|
const touch = e.touches[0];
|
||||||
|
const cr = dialogContainer.getBoundingClientRect();
|
||||||
|
let x = touch.clientX - ox - cr.left;
|
||||||
|
let y = touch.clientY - oy - cr.top;
|
||||||
|
x = Math.max(0, Math.min(x, cr.width - element.offsetWidth));
|
||||||
|
y = Math.max(0, Math.min(y, cr.height - element.offsetHeight));
|
||||||
|
element.style.left = x + 'px';
|
||||||
|
element.style.top = y + 'px';
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
document.addEventListener('touchend', () => {
|
||||||
|
if (dragging) {
|
||||||
|
dragging = false;
|
||||||
|
element.classList.remove('active-drag');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 清空 =====
|
||||||
|
clearBtn.addEventListener('click', () => {
|
||||||
|
dialogContainer.innerHTML = '';
|
||||||
|
dialogCount = 0;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -263,6 +263,7 @@ function App() {
|
|||||||
|
|
||||||
// Task store
|
// Task store
|
||||||
const tasks = useTaskStore((s) => s.tasks);
|
const tasks = useTaskStore((s) => s.tasks);
|
||||||
|
const setTasks = useTaskStore((s) => s.setTasks);
|
||||||
const appendTask = useTaskStore((s) => s.appendTask);
|
const appendTask = useTaskStore((s) => s.appendTask);
|
||||||
const mergeServerTasks = useTaskStore((s) => s.mergeServerTasks);
|
const mergeServerTasks = useTaskStore((s) => s.mergeServerTasks);
|
||||||
const clearTasks = useTaskStore((s) => s.clearTasks);
|
const clearTasks = useTaskStore((s) => s.clearTasks);
|
||||||
@@ -1042,6 +1043,8 @@ function App() {
|
|||||||
onOpenWorkbench={() => handleSetView("workbench")}
|
onOpenWorkbench={() => handleSetView("workbench")}
|
||||||
onOpenCommunity={() => handleSetView("community")}
|
onOpenCommunity={() => handleSetView("community")}
|
||||||
onDeleteProject={handleDeleteProject}
|
onDeleteProject={handleDeleteProject}
|
||||||
|
onOpenProject={handleOpenProject}
|
||||||
|
onRemoveWork={(task) => setTasks(tasks.filter((item) => item.id !== task.id))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "community":
|
case "community":
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function AppShell({
|
|||||||
const [navJustActivated, setNavJustActivated] = useState<WebViewKey | null>(null);
|
const [navJustActivated, setNavJustActivated] = useState<WebViewKey | null>(null);
|
||||||
const isAuthView = activeView === "login";
|
const isAuthView = activeView === "login";
|
||||||
const isImmersiveView = activeView === "agent" || activeView === "avatarConsole";
|
const isImmersiveView = activeView === "agent" || activeView === "avatarConsole";
|
||||||
const showFloatingNav = (!isAuthView || !!session) && !isImmersiveView && activeView !== "home";
|
const showFloatingNav = !isAuthView && !isImmersiveView && activeView !== "home";
|
||||||
const toolSurfaceViews = [
|
const toolSurfaceViews = [
|
||||||
"workbench",
|
"workbench",
|
||||||
"canvas",
|
"canvas",
|
||||||
@@ -102,9 +102,9 @@ function AppShell({
|
|||||||
"canvas",
|
"canvas",
|
||||||
"scriptTokens",
|
"scriptTokens",
|
||||||
"tokenUsage",
|
"tokenUsage",
|
||||||
"community",
|
|
||||||
"assets",
|
|
||||||
"more",
|
"more",
|
||||||
|
"assets",
|
||||||
|
"community",
|
||||||
];
|
];
|
||||||
return orderedKeys
|
return orderedKeys
|
||||||
.map((key) => navItems.find((item) => item.key === key))
|
.map((key) => navItems.find((item) => item.key === key))
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
SendOutlined,
|
SendOutlined,
|
||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import WorkspacePageShell from "../../components/WorkspacePageShell";
|
import WorkspacePageShell from "../../components/WorkspacePageShell";
|
||||||
import type { WebGenerationPreviewTask } from "../../types";
|
import type { WebGenerationPreviewTask } from "../../types";
|
||||||
|
|
||||||
@@ -72,6 +72,24 @@ const agentModes = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const agentModelOptions = [
|
||||||
|
{ id: "gemini-3.1-pro", label: "Gemini 3.1 Pro" },
|
||||||
|
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
||||||
|
{ id: "gpt-4o", label: "GPT-4o" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const thinkingSpeedOptions = [
|
||||||
|
{ id: "fast", label: "快速" },
|
||||||
|
{ id: "balanced", label: "均衡" },
|
||||||
|
{ id: "precise", label: "精细" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const thinkingDepthOptions = [
|
||||||
|
{ id: "concise", label: "简洁" },
|
||||||
|
{ id: "standard", label: "标准" },
|
||||||
|
{ id: "deep", label: "深度" },
|
||||||
|
];
|
||||||
|
|
||||||
const quickStarts = ["「新品发布」全链路运营", "「销售日报」自动分析", "「竞品监控」每周报告"];
|
const quickStarts = ["「新品发布」全链路运营", "「销售日报」自动分析", "「竞品监控」每周报告"];
|
||||||
|
|
||||||
function getTaskSourceLabel(task: WebGenerationPreviewTask): string | null {
|
function getTaskSourceLabel(task: WebGenerationPreviewTask): string | null {
|
||||||
@@ -93,6 +111,21 @@ function AgentPage({
|
|||||||
const [prompt, setPrompt] = useState("让 Omni Agent 帮我规划「新品发布会全流程」");
|
const [prompt, setPrompt] = useState("让 Omni Agent 帮我规划「新品发布会全流程」");
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
const [notice, setNotice] = useState("选择一个 Agent 模式,输入目标后即可开始。");
|
const [notice, setNotice] = useState("选择一个 Agent 模式,输入目标后即可开始。");
|
||||||
|
const [agentModel, setAgentModel] = useState(agentModelOptions[0].id);
|
||||||
|
const [thinkingSpeed, setThinkingSpeed] = useState(thinkingSpeedOptions[1].id);
|
||||||
|
const [thinkingDepth, setThinkingDepth] = useState(thinkingDepthOptions[1].id);
|
||||||
|
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
||||||
|
const controlsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (controlsRef.current && !controlsRef.current.contains(event.target as Node)) {
|
||||||
|
setActiveDropdown(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const selectedMode = agentModes.find((item) => item.id === activeMode) ?? agentModes[0];
|
const selectedMode = agentModes.find((item) => item.id === activeMode) ?? agentModes[0];
|
||||||
const recentTasks = tasks.slice(0, 3);
|
const recentTasks = tasks.slice(0, 3);
|
||||||
@@ -203,15 +236,85 @@ function AgentPage({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="agent-composer__footer">
|
<div className="agent-composer__footer">
|
||||||
<div className="agent-composer__controls" aria-label="输入设置">
|
<div className="agent-composer__controls" aria-label="输入设置" ref={controlsRef}>
|
||||||
<button type="button" className="agent-tool-icon" aria-label="上传附件">
|
<button type="button" className="agent-tool-icon" aria-label="上传附件">
|
||||||
<PaperClipOutlined />
|
<PaperClipOutlined />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="agent-tool-pill">
|
<div className="agent-tool-pills">
|
||||||
<ThunderboltOutlined />
|
<button
|
||||||
自动模式
|
type="button"
|
||||||
|
className={`agent-tool-pill${activeDropdown === "model" ? " is-open" : ""}`}
|
||||||
|
onClick={() => setActiveDropdown(activeDropdown === "model" ? null : "model")}
|
||||||
|
>
|
||||||
|
<RobotOutlined />
|
||||||
|
{agentModelOptions.find((m) => m.id === agentModel)?.label ?? "模型选择"}
|
||||||
<DownOutlined />
|
<DownOutlined />
|
||||||
</button>
|
</button>
|
||||||
|
{activeDropdown === "model" && (
|
||||||
|
<div className="agent-dropdown">
|
||||||
|
{agentModelOptions.map((m) => (
|
||||||
|
<button
|
||||||
|
key={m.id}
|
||||||
|
type="button"
|
||||||
|
className={`agent-dropdown__item${agentModel === m.id ? " is-active" : ""}`}
|
||||||
|
onClick={() => { setAgentModel(m.id); setActiveDropdown(null); }}
|
||||||
|
>
|
||||||
|
{m.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="agent-tool-pills">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`agent-tool-pill${activeDropdown === "speed" ? " is-open" : ""}`}
|
||||||
|
onClick={() => setActiveDropdown(activeDropdown === "speed" ? null : "speed")}
|
||||||
|
>
|
||||||
|
<ThunderboltOutlined />
|
||||||
|
{thinkingSpeedOptions.find((s) => s.id === thinkingSpeed)?.label ?? "思考速度"}
|
||||||
|
<DownOutlined />
|
||||||
|
</button>
|
||||||
|
{activeDropdown === "speed" && (
|
||||||
|
<div className="agent-dropdown">
|
||||||
|
{thinkingSpeedOptions.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.id}
|
||||||
|
type="button"
|
||||||
|
className={`agent-dropdown__item${thinkingSpeed === s.id ? " is-active" : ""}`}
|
||||||
|
onClick={() => { setThinkingSpeed(s.id); setActiveDropdown(null); }}
|
||||||
|
>
|
||||||
|
{s.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="agent-tool-pills">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`agent-tool-pill${activeDropdown === "depth" ? " is-open" : ""}`}
|
||||||
|
onClick={() => setActiveDropdown(activeDropdown === "depth" ? null : "depth")}
|
||||||
|
>
|
||||||
|
<ApartmentOutlined />
|
||||||
|
{thinkingDepthOptions.find((d) => d.id === thinkingDepth)?.label ?? "思考深度"}
|
||||||
|
<DownOutlined />
|
||||||
|
</button>
|
||||||
|
{activeDropdown === "depth" && (
|
||||||
|
<div className="agent-dropdown">
|
||||||
|
{thinkingDepthOptions.map((d) => (
|
||||||
|
<button
|
||||||
|
key={d.id}
|
||||||
|
type="button"
|
||||||
|
className={`agent-dropdown__item${thinkingDepth === d.id ? " is-active" : ""}`}
|
||||||
|
onClick={() => { setThinkingDepth(d.id); setActiveDropdown(null); }}
|
||||||
|
>
|
||||||
|
{d.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button type="button" className="agent-tool-icon" aria-label="工具集">
|
<button type="button" className="agent-tool-icon" aria-label="工具集">
|
||||||
<AppstoreOutlined />
|
<AppstoreOutlined />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -391,6 +391,8 @@ function CanvasPage({
|
|||||||
const canvasRef = useRef<HTMLElement>(null);
|
const canvasRef = useRef<HTMLElement>(null);
|
||||||
const videoGenerationInFlightRef = useRef(new Set<string>());
|
const videoGenerationInFlightRef = useRef(new Set<string>());
|
||||||
const canvasReferenceUploadPromisesRef = useRef(new Map<string, Promise<string | null>>());
|
const canvasReferenceUploadPromisesRef = useRef(new Map<string, Promise<string | null>>());
|
||||||
|
const canvasDragCounterRef = useRef(0);
|
||||||
|
const [isCanvasDragging, setIsCanvasDragging] = useState(false);
|
||||||
const suppressNextPaneClickRef = useRef(false);
|
const suppressNextPaneClickRef = useRef(false);
|
||||||
const canvasAutoSaveTimerRef = useRef<number | null>(null);
|
const canvasAutoSaveTimerRef = useRef<number | null>(null);
|
||||||
const canvasAutoSaveIdleHandleRef = useRef<number | null>(null);
|
const canvasAutoSaveIdleHandleRef = useRef<number | null>(null);
|
||||||
@@ -1280,7 +1282,7 @@ function CanvasPage({
|
|||||||
model: defaultVideoModel,
|
model: defaultVideoModel,
|
||||||
aspectRatio: "16:9",
|
aspectRatio: "16:9",
|
||||||
resolution: getDefaultVideoQuality(defaultVideoModel),
|
resolution: getDefaultVideoQuality(defaultVideoModel),
|
||||||
duration: "4",
|
duration: "5",
|
||||||
videoMode: "text2video",
|
videoMode: "text2video",
|
||||||
sourceTextNodeId: source.id,
|
sourceTextNodeId: source.id,
|
||||||
position: {
|
position: {
|
||||||
@@ -1304,7 +1306,7 @@ function CanvasPage({
|
|||||||
model: defaultVideoModel,
|
model: defaultVideoModel,
|
||||||
aspectRatio: "16:9",
|
aspectRatio: "16:9",
|
||||||
resolution: getDefaultVideoQuality(defaultVideoModel),
|
resolution: getDefaultVideoQuality(defaultVideoModel),
|
||||||
duration: "4",
|
duration: "5",
|
||||||
videoMode: "text2video",
|
videoMode: "text2video",
|
||||||
sourceTextNodeId: "",
|
sourceTextNodeId: "",
|
||||||
position,
|
position,
|
||||||
@@ -1360,7 +1362,7 @@ function CanvasPage({
|
|||||||
imageUrl = "",
|
imageUrl = "",
|
||||||
fileName = "本地图片",
|
fileName = "本地图片",
|
||||||
position = { x: 0, y: 0 },
|
position = { x: 0, y: 0 },
|
||||||
options?: { title?: string; sourceImageNodeId?: string }
|
options?: { title?: string; sourceImageNodeId?: string; sourceTextNodeId?: string }
|
||||||
) => {
|
) => {
|
||||||
const nodeNumber = imageNodeIdRef.current;
|
const nodeNumber = imageNodeIdRef.current;
|
||||||
imageNodeIdRef.current += 1;
|
imageNodeIdRef.current += 1;
|
||||||
@@ -1374,6 +1376,7 @@ function CanvasPage({
|
|||||||
imageSize: getDefaultImageQuality(fallbackVisibleImageModel),
|
imageSize: getDefaultImageQuality(fallbackVisibleImageModel),
|
||||||
fileName,
|
fileName,
|
||||||
sourceImageNodeId: options?.sourceImageNodeId,
|
sourceImageNodeId: options?.sourceImageNodeId,
|
||||||
|
sourceTextNodeId: options?.sourceTextNodeId,
|
||||||
position,
|
position,
|
||||||
size: createCanvasNodeSize("image"),
|
size: createCanvasNodeSize("image"),
|
||||||
};
|
};
|
||||||
@@ -1979,6 +1982,120 @@ function CanvasPage({
|
|||||||
setNodeMenu(null);
|
setNodeMenu(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Canvas drag-and-drop file upload ──────────────────────────────
|
||||||
|
const handleCanvasDragEnter = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
canvasDragCounterRef.current += 1;
|
||||||
|
if (canvasDragCounterRef.current === 1) {
|
||||||
|
setIsCanvasDragging(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCanvasDragLeave = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
canvasDragCounterRef.current -= 1;
|
||||||
|
if (canvasDragCounterRef.current <= 0) {
|
||||||
|
canvasDragCounterRef.current = 0;
|
||||||
|
setIsCanvasDragging(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCanvasDragOver = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCanvasDrop = useCallback(
|
||||||
|
(e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
canvasDragCounterRef.current = 0;
|
||||||
|
setIsCanvasDragging(false);
|
||||||
|
const files = Array.from(e.dataTransfer.files).filter(
|
||||||
|
(f) => f.type.startsWith("image/")
|
||||||
|
);
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
const rect = canvasRef.current?.getBoundingClientRect();
|
||||||
|
if (!rect) return;
|
||||||
|
const dropPosition = {
|
||||||
|
x: (e.clientX - rect.left - canvasViewport.x) / canvasViewport.zoom,
|
||||||
|
y: (e.clientY - rect.top - canvasViewport.y) / canvasViewport.zoom,
|
||||||
|
};
|
||||||
|
|
||||||
|
let offsetX = 0;
|
||||||
|
let offsetY = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
const imageUrl = URL.createObjectURL(file);
|
||||||
|
addImageNode(imageUrl, file.name, {
|
||||||
|
x: dropPosition.x + offsetX,
|
||||||
|
y: dropPosition.y + offsetY,
|
||||||
|
});
|
||||||
|
offsetX += 60;
|
||||||
|
offsetY += 60;
|
||||||
|
}
|
||||||
|
setContextMenu(null);
|
||||||
|
setNodeMenu(null);
|
||||||
|
},
|
||||||
|
[canvasViewport.x, canvasViewport.y, canvasViewport.zoom, addImageNode],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Text composer drag-and-drop ──────────────────────────────────
|
||||||
|
const [textComposerDragNodeId, setTextComposerDragNodeId] = useState<string | null>(null);
|
||||||
|
const textComposerDragCounterRef = useRef(0);
|
||||||
|
|
||||||
|
const handleTextComposerDragEnter = useCallback((_e: React.DragEvent, nodeId: string) => {
|
||||||
|
_e.preventDefault();
|
||||||
|
_e.stopPropagation();
|
||||||
|
textComposerDragCounterRef.current += 1;
|
||||||
|
if (textComposerDragCounterRef.current === 1) {
|
||||||
|
setTextComposerDragNodeId(nodeId);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTextComposerDragLeave = useCallback((_e: React.DragEvent) => {
|
||||||
|
_e.preventDefault();
|
||||||
|
_e.stopPropagation();
|
||||||
|
textComposerDragCounterRef.current -= 1;
|
||||||
|
if (textComposerDragCounterRef.current <= 0) {
|
||||||
|
textComposerDragCounterRef.current = 0;
|
||||||
|
setTextComposerDragNodeId(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTextComposerDragOver = useCallback((_e: React.DragEvent) => {
|
||||||
|
_e.preventDefault();
|
||||||
|
_e.stopPropagation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTextComposerDrop = useCallback(
|
||||||
|
(e: React.DragEvent, sourceNode: CanvasTextNode) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
textComposerDragCounterRef.current = 0;
|
||||||
|
setTextComposerDragNodeId(null);
|
||||||
|
const files = Array.from(e.dataTransfer.files).filter(
|
||||||
|
(f) => f.type.startsWith("image/")
|
||||||
|
);
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
let offsetX = 0;
|
||||||
|
let offsetY = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
const imageUrl = URL.createObjectURL(file);
|
||||||
|
addImageNode(imageUrl, file.name, {
|
||||||
|
x: sourceNode.position.x + sourceNode.size.width + 40 + offsetX,
|
||||||
|
y: sourceNode.position.y + offsetY,
|
||||||
|
}, { sourceTextNodeId: sourceNode.id });
|
||||||
|
offsetX += 60;
|
||||||
|
offsetY += 60;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addImageNode],
|
||||||
|
);
|
||||||
|
|
||||||
const activeTextNode = textNodeMenu
|
const activeTextNode = textNodeMenu
|
||||||
? textNodes.find((node) => node.id === textNodeMenu.nodeId) ?? null
|
? textNodes.find((node) => node.id === textNodeMenu.nodeId) ?? null
|
||||||
: null;
|
: null;
|
||||||
@@ -3552,7 +3669,7 @@ function CanvasPage({
|
|||||||
<WorkspacePageShell title="画布" fullWidth className="canvas-page page-motion">
|
<WorkspacePageShell title="画布" fullWidth className="canvas-page page-motion">
|
||||||
<div className={`studio-tool-layout studio-tool-layout--no-top studio-tool-layout--no-left studio-tool-layout--no-right studio-tool-layout--canvas${(shouldShowEmptyProjectState || isWaitingForProjects) ? " studio-tool-layout--canvas-empty" : ""}`}>
|
<div className={`studio-tool-layout studio-tool-layout--no-top studio-tool-layout--no-left studio-tool-layout--no-right studio-tool-layout--canvas${(shouldShowEmptyProjectState || isWaitingForProjects) ? " studio-tool-layout--canvas-empty" : ""}`}>
|
||||||
<section
|
<section
|
||||||
className={`studio-canvas${pendingLinkPort ? " is-linking" : ""}${(shouldShowEmptyProjectState || isWaitingForProjects) ? " is-empty-projects" : ""}`}
|
className={`studio-canvas${pendingLinkPort ? " is-linking" : ""}${isCanvasDragging ? " is-canvas-dragging" : ""}${(shouldShowEmptyProjectState || isWaitingForProjects) ? " is-empty-projects" : ""}`}
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
onAuxClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasAuxClick}
|
onAuxClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasAuxClick}
|
||||||
onContextMenu={(shouldShowEmptyProjectState || isWaitingForProjects) ? (event) => event.preventDefault() : handleCanvasContextMenu}
|
onContextMenu={(shouldShowEmptyProjectState || isWaitingForProjects) ? (event) => event.preventDefault() : handleCanvasContextMenu}
|
||||||
@@ -3560,6 +3677,10 @@ function CanvasPage({
|
|||||||
onDoubleClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDoubleClick}
|
onDoubleClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDoubleClick}
|
||||||
onMouseMove={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasMouseMove}
|
onMouseMove={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasMouseMove}
|
||||||
onWheel={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasWheel}
|
onWheel={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasWheel}
|
||||||
|
onDragEnter={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragEnter}
|
||||||
|
onDragOver={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragOver}
|
||||||
|
onDragLeave={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragLeave}
|
||||||
|
onDrop={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDrop}
|
||||||
style={{
|
style={{
|
||||||
"--canvas-bg-size": `${34 * canvasViewport.zoom}px`,
|
"--canvas-bg-size": `${34 * canvasViewport.zoom}px`,
|
||||||
"--canvas-bg-dot": `${1.35 * canvasViewport.zoom}px`,
|
"--canvas-bg-dot": `${1.35 * canvasViewport.zoom}px`,
|
||||||
@@ -4139,7 +4260,13 @@ function CanvasPage({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="studio-canvas-text-composer">
|
<div
|
||||||
|
className={`studio-canvas-text-composer${textComposerDragNodeId === textNode.id ? " is-drag-over" : ""}`}
|
||||||
|
onDragEnter={(e) => handleTextComposerDragEnter(e, textNode.id)}
|
||||||
|
onDragOver={handleTextComposerDragOver}
|
||||||
|
onDragLeave={handleTextComposerDragLeave}
|
||||||
|
onDrop={(e) => handleTextComposerDrop(e, textNode)}
|
||||||
|
>
|
||||||
<div className="studio-canvas-text-composer__input-wrap">
|
<div className="studio-canvas-text-composer__input-wrap">
|
||||||
<textarea
|
<textarea
|
||||||
value={textNode.prompt}
|
value={textNode.prompt}
|
||||||
|
|||||||
@@ -140,7 +140,6 @@ export const videoRatioOptions: CanvasOption[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const videoDurationOptions: CanvasOption[] = [
|
export const videoDurationOptions: CanvasOption[] = [
|
||||||
{ value: "4", label: "4s" },
|
|
||||||
{ value: "5", label: "5s" },
|
{ value: "5", label: "5s" },
|
||||||
{ value: "6", label: "6s" },
|
{ value: "6", label: "6s" },
|
||||||
{ value: "7", label: "7s" },
|
{ value: "7", label: "7s" },
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState, type DragEvent } from "react";
|
||||||
import StudioToolLayout from "../../components/StudioToolLayout";
|
import StudioToolLayout from "../../components/StudioToolLayout";
|
||||||
import type { WebImageWorkbenchTool, WebViewKey } from "../../types";
|
import type { WebImageWorkbenchTool, WebViewKey } from "../../types";
|
||||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||||
@@ -58,6 +58,7 @@ function CharacterMixPage({
|
|||||||
const [resultUrl, setResultUrl] = useState<string | null>(null);
|
const [resultUrl, setResultUrl] = useState<string | null>(null);
|
||||||
const abortRef = useRef(false);
|
const abortRef = useRef(false);
|
||||||
const taskIdRef = useRef<string | null>(null);
|
const taskIdRef = useRef<string | null>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -233,6 +234,32 @@ function CharacterMixPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent) => { e.preventDefault(); if (e.dataTransfer?.types?.includes("Files")) setIsDragging(true); };
|
||||||
|
const handleDragLeave = (e: DragEvent) => { e.preventDefault(); if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsDragging(false); };
|
||||||
|
const handleDrop = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
const file = e.dataTransfer?.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
if (file.type.startsWith("image/")) {
|
||||||
|
if (characterPreview) URL.revokeObjectURL(characterPreview);
|
||||||
|
setCharacterFile(file.name);
|
||||||
|
setCharacterPreview(URL.createObjectURL(file));
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => { if (typeof reader.result === "string") setCharacterDataUrl(reader.result); };
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
setNotice(`已选择人物图 ${file.name}`);
|
||||||
|
} else if (file.type.startsWith("video/")) {
|
||||||
|
if (videoPreview) URL.revokeObjectURL(videoPreview);
|
||||||
|
setVideoFile(file.name);
|
||||||
|
setVideoPreview(URL.createObjectURL(file));
|
||||||
|
const reader2 = new FileReader();
|
||||||
|
reader2.onload = () => { if (typeof reader2.result === "string") setVideoDataUrl(reader2.result); };
|
||||||
|
reader2.readAsDataURL(file);
|
||||||
|
setNotice(`已选择参考视频 ${file.name}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="image-workbench-page character-mix-page" aria-label="角色迁移">
|
<section className="image-workbench-page character-mix-page" aria-label="角色迁移">
|
||||||
<header className="image-workbench-topbar">
|
<header className="image-workbench-topbar">
|
||||||
@@ -292,7 +319,17 @@ function CharacterMixPage({
|
|||||||
<StudioToolLayout
|
<StudioToolLayout
|
||||||
noTop
|
noTop
|
||||||
leftPanel={
|
leftPanel={
|
||||||
<>
|
<div
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
style={{ position: "relative" }}
|
||||||
|
>
|
||||||
|
{isDragging ? (
|
||||||
|
<div style={{ position: "absolute", inset: 0, zIndex: 100, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,0.55)", border: "2px dashed var(--primary, #4a9eff)", borderRadius: 12, pointerEvents: "none" }}>
|
||||||
|
<span style={{ fontSize: 18, color: "#fff", fontWeight: 600 }}>释放文件以上传</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="studio-panel__section">
|
<div className="studio-panel__section">
|
||||||
<div className="studio-panel__section-head">
|
<div className="studio-panel__section-head">
|
||||||
<span className="studio-panel__section-title">人物图</span>
|
<span className="studio-panel__section-title">人物图</span>
|
||||||
@@ -370,7 +407,7 @@ function CharacterMixPage({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
}
|
}
|
||||||
canvas={
|
canvas={
|
||||||
isCreating ? (
|
isCreating ? (
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useCallback, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent, type TouchEvent as ReactTouchEvent } from "react";
|
import { useCallback, useEffect, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent, type TouchEvent as ReactTouchEvent } from "react";
|
||||||
|
import { ApartmentOutlined, DownOutlined, RobotOutlined, ThunderboltOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
type DialogStyle = "style1" | "style2" | "style3" | "style4";
|
type DialogStyle = "style1" | "style2" | "style3" | "style4";
|
||||||
|
type GenerationMode = "dialog" | "video";
|
||||||
|
|
||||||
interface DialogItem {
|
interface DialogItem {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -39,16 +41,68 @@ const textColorOptions = [
|
|||||||
{ value: "#00ff88", label: "绿色" },
|
{ value: "#00ff88", label: "绿色" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const dialogModelOptions = [
|
||||||
|
{ id: "gemini", label: "Gemini" },
|
||||||
|
{ id: "wanxian", label: "万相" },
|
||||||
|
{ id: "deepseek", label: "DeepSeek" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const thinkingSpeedOptions = [
|
||||||
|
{ id: "default", label: "默认" },
|
||||||
|
{ id: "high", label: "高" },
|
||||||
|
{ id: "ultra", label: "急速" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const thinkingDepthOptions = [
|
||||||
|
{ id: "default", label: "默认" },
|
||||||
|
{ id: "strong", label: "强" },
|
||||||
|
{ id: "extreme", label: "极限" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const videoDurationOptions = [
|
||||||
|
{ value: "5", label: "5s" },
|
||||||
|
{ value: "6", label: "6s" },
|
||||||
|
{ value: "7", label: "7s" },
|
||||||
|
{ value: "8", label: "8s" },
|
||||||
|
{ value: "9", label: "9s" },
|
||||||
|
{ value: "10", label: "10s" },
|
||||||
|
{ value: "11", label: "11s" },
|
||||||
|
{ value: "12", label: "12s" },
|
||||||
|
{ value: "13", label: "13s" },
|
||||||
|
{ value: "14", label: "14s" },
|
||||||
|
{ value: "15", label: "15s" },
|
||||||
|
];
|
||||||
|
|
||||||
function DialogGeneratorPage() {
|
function DialogGeneratorPage() {
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const previewRef = useRef<HTMLDivElement | null>(null);
|
const previewRef = useRef<HTMLDivElement | null>(null);
|
||||||
const dragRef = useRef<DragState | null>(null);
|
const dragRef = useRef<DragState | null>(null);
|
||||||
const nextIdRef = useRef(0);
|
const nextIdRef = useRef(0);
|
||||||
|
const controlsRef = useRef<HTMLDivElement>(null);
|
||||||
const [backgroundUrl, setBackgroundUrl] = useState("");
|
const [backgroundUrl, setBackgroundUrl] = useState("");
|
||||||
const [dialogs, setDialogs] = useState<DialogItem[]>([]);
|
const [dialogs, setDialogs] = useState<DialogItem[]>([]);
|
||||||
const [selectedTextColor, setSelectedTextColor] = useState(textColorOptions[0].value);
|
const [selectedTextColor, setSelectedTextColor] = useState(textColorOptions[0].value);
|
||||||
const [activeDragId, setActiveDragId] = useState<number | null>(null);
|
const [activeDragId, setActiveDragId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// ── Generation state ──
|
||||||
|
const [generationMode, setGenerationMode] = useState<GenerationMode>("dialog");
|
||||||
|
const [dialogModel, setDialogModel] = useState(dialogModelOptions[0].id);
|
||||||
|
const [thinkingSpeed, setThinkingSpeed] = useState(thinkingSpeedOptions[0].id);
|
||||||
|
const [thinkingDepth, setThinkingDepth] = useState(thinkingDepthOptions[0].id);
|
||||||
|
const [videoDuration, setVideoDuration] = useState(videoDurationOptions[0].value);
|
||||||
|
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
||||||
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (controlsRef.current && !controlsRef.current.contains(event.target as Node)) {
|
||||||
|
setActiveDropdown(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleFile = useCallback((file?: File | null) => {
|
const handleFile = useCallback((file?: File | null) => {
|
||||||
if (!file || !file.type.startsWith("image/")) return;
|
if (!file || !file.type.startsWith("image/")) return;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -194,6 +248,141 @@ function DialogGeneratorPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="dialog-generator-section">
|
||||||
|
<h2>生成设置</h2>
|
||||||
|
<div className="dialog-generator-mode-switch" role="radiogroup" aria-label="生成模式">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dialog-generator-mode${generationMode === "dialog" ? " is-active" : ""}`}
|
||||||
|
role="radio"
|
||||||
|
aria-checked={generationMode === "dialog"}
|
||||||
|
onClick={() => setGenerationMode("dialog")}
|
||||||
|
>
|
||||||
|
对话模式
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dialog-generator-mode${generationMode === "video" ? " is-active" : ""}`}
|
||||||
|
role="radio"
|
||||||
|
aria-checked={generationMode === "video"}
|
||||||
|
onClick={() => setGenerationMode("video")}
|
||||||
|
>
|
||||||
|
视频模式
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dialog-generator-controls" ref={controlsRef}>
|
||||||
|
{generationMode === "dialog" ? (
|
||||||
|
<>
|
||||||
|
<div className="dialog-generator-pills">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dialog-generator-pill${activeDropdown === "model" ? " is-open" : ""}`}
|
||||||
|
onClick={() => setActiveDropdown(activeDropdown === "model" ? null : "model")}
|
||||||
|
>
|
||||||
|
<RobotOutlined />
|
||||||
|
{dialogModelOptions.find((m) => m.id === dialogModel)?.label ?? "模型选择"}
|
||||||
|
<DownOutlined />
|
||||||
|
</button>
|
||||||
|
{activeDropdown === "model" && (
|
||||||
|
<div className="dialog-generator-dropdown">
|
||||||
|
{dialogModelOptions.map((m) => (
|
||||||
|
<button
|
||||||
|
key={m.id}
|
||||||
|
type="button"
|
||||||
|
className={`dialog-generator-dropdown__item${dialogModel === m.id ? " is-active" : ""}`}
|
||||||
|
onClick={() => { setDialogModel(m.id); setActiveDropdown(null); }}
|
||||||
|
>
|
||||||
|
{m.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="dialog-generator-pills">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dialog-generator-pill${activeDropdown === "speed" ? " is-open" : ""}`}
|
||||||
|
onClick={() => setActiveDropdown(activeDropdown === "speed" ? null : "speed")}
|
||||||
|
>
|
||||||
|
<ThunderboltOutlined />
|
||||||
|
{thinkingSpeedOptions.find((s) => s.id === thinkingSpeed)?.label ?? "思考速度"}
|
||||||
|
<DownOutlined />
|
||||||
|
</button>
|
||||||
|
{activeDropdown === "speed" && (
|
||||||
|
<div className="dialog-generator-dropdown">
|
||||||
|
{thinkingSpeedOptions.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.id}
|
||||||
|
type="button"
|
||||||
|
className={`dialog-generator-dropdown__item${thinkingSpeed === s.id ? " is-active" : ""}`}
|
||||||
|
onClick={() => { setThinkingSpeed(s.id); setActiveDropdown(null); }}
|
||||||
|
>
|
||||||
|
{s.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="dialog-generator-pills">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dialog-generator-pill${activeDropdown === "depth" ? " is-open" : ""}`}
|
||||||
|
onClick={() => setActiveDropdown(activeDropdown === "depth" ? null : "depth")}
|
||||||
|
>
|
||||||
|
<ApartmentOutlined />
|
||||||
|
{thinkingDepthOptions.find((d) => d.id === thinkingDepth)?.label ?? "思考深度"}
|
||||||
|
<DownOutlined />
|
||||||
|
</button>
|
||||||
|
{activeDropdown === "depth" && (
|
||||||
|
<div className="dialog-generator-dropdown">
|
||||||
|
{thinkingDepthOptions.map((d) => (
|
||||||
|
<button
|
||||||
|
key={d.id}
|
||||||
|
type="button"
|
||||||
|
className={`dialog-generator-dropdown__item${thinkingDepth === d.id ? " is-active" : ""}`}
|
||||||
|
onClick={() => { setThinkingDepth(d.id); setActiveDropdown(null); }}
|
||||||
|
>
|
||||||
|
{d.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="dialog-generator-duration">
|
||||||
|
<span className="dialog-generator-duration__label">视频时长</span>
|
||||||
|
<div className="dialog-generator-duration__options">
|
||||||
|
{videoDurationOptions.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
type="button"
|
||||||
|
className={`dialog-generator-duration__btn${videoDuration === opt.value ? " is-active" : ""}`}
|
||||||
|
onClick={() => setVideoDuration(opt.value)}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="dialog-generator-run"
|
||||||
|
disabled={isGenerating}
|
||||||
|
onClick={() => {
|
||||||
|
setIsGenerating(true);
|
||||||
|
// TODO: wire to actual generation API
|
||||||
|
setTimeout(() => setIsGenerating(false), 2000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isGenerating ? "生成中..." : "生成"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" className="dialog-generator-clear" onClick={() => setDialogs([])}>
|
<button type="button" className="dialog-generator-clear" onClick={() => setDialogs([])}>
|
||||||
清空全部文字
|
清空全部文字
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState, type DragEvent } from "react";
|
||||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||||
import { uploadAssetWithProgress } from "../../api/uploadWithProgress";
|
import { uploadAssetWithProgress } from "../../api/uploadWithProgress";
|
||||||
import { waitForTask } from "../../api/taskSubscription";
|
import { waitForTask } from "../../api/taskSubscription";
|
||||||
@@ -95,6 +95,7 @@ function DigitalHumanPage({
|
|||||||
const activeTaskIdRef = useRef(activeTaskId);
|
const activeTaskIdRef = useRef(activeTaskId);
|
||||||
activeTaskIdRef.current = activeTaskId;
|
activeTaskIdRef.current = activeTaskId;
|
||||||
const keepaliveRestoredRef = useRef(false);
|
const keepaliveRestoredRef = useRef(false);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -146,6 +147,28 @@ function DigitalHumanPage({
|
|||||||
setNotice("已取消");
|
setNotice("已取消");
|
||||||
}, [activeTaskId]);
|
}, [activeTaskId]);
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent) => { e.preventDefault(); if (e.dataTransfer?.types?.includes("Files")) setIsDragging(true); };
|
||||||
|
const handleDragLeave = (e: DragEvent) => { e.preventDefault(); if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsDragging(false); };
|
||||||
|
const handleDrop = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
const file = e.dataTransfer?.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
if (file.type.startsWith("image/")) {
|
||||||
|
if (imagePreview) URL.revokeObjectURL(imagePreview);
|
||||||
|
setImageName(file.name);
|
||||||
|
setImageFile(file);
|
||||||
|
setImagePreview(URL.createObjectURL(file));
|
||||||
|
setNotice(`已拖放参考图 ${file.name}`);
|
||||||
|
} else if (file.type.startsWith("audio/")) {
|
||||||
|
if (audioPreview) URL.revokeObjectURL(audioPreview);
|
||||||
|
setAudioName(file.name);
|
||||||
|
setAudioFile(file);
|
||||||
|
setAudioPreview(URL.createObjectURL(file));
|
||||||
|
setNotice(`已拖放音频 ${file.name}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDownloadResult = async () => {
|
const handleDownloadResult = async () => {
|
||||||
if (!resultVideoUrl || isDownloadingResult) return;
|
if (!resultVideoUrl || isDownloadingResult) return;
|
||||||
setIsDownloadingResult(true);
|
setIsDownloadingResult(true);
|
||||||
@@ -417,7 +440,17 @@ function DigitalHumanPage({
|
|||||||
<StudioToolLayout
|
<StudioToolLayout
|
||||||
noTop
|
noTop
|
||||||
leftPanel={
|
leftPanel={
|
||||||
<>
|
<div
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
style={{ position: "relative" }}
|
||||||
|
>
|
||||||
|
{isDragging ? (
|
||||||
|
<div style={{ position: "absolute", inset: 0, zIndex: 100, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,0.55)", border: "2px dashed var(--primary, #4a9eff)", borderRadius: 12, pointerEvents: "none" }}>
|
||||||
|
<span style={{ fontSize: 18, color: "#fff", fontWeight: 600 }}>释放文件以上传</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="studio-panel__section">
|
<div className="studio-panel__section">
|
||||||
<div className="studio-panel__section-head">
|
<div className="studio-panel__section-head">
|
||||||
<span className="studio-panel__section-title">参考人像</span>
|
<span className="studio-panel__section-title">参考人像</span>
|
||||||
@@ -490,7 +523,7 @@ function DigitalHumanPage({
|
|||||||
{audioPreview ? <audio src={audioPreview} controls className="studio-audio-preview" /> : null}
|
{audioPreview ? <audio src={audioPreview} controls className="studio-audio-preview" /> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
}
|
}
|
||||||
canvas={
|
canvas={
|
||||||
resultVideoUrl ? (
|
resultVideoUrl ? (
|
||||||
@@ -558,7 +591,7 @@ function DigitalHumanPage({
|
|||||||
</div>
|
</div>
|
||||||
<button type="button" className="studio-generate-btn" onClick={() => void handleCreateTask()} disabled={isCreating || !imageFile || !audioFile}>
|
<button type="button" className="studio-generate-btn" onClick={() => void handleCreateTask()} disabled={isCreating || !imageFile || !audioFile}>
|
||||||
<PlayCircleOutlined />
|
<PlayCircleOutlined />
|
||||||
{isCreating ? "生成中..." : "提交 wan2.2-s2v"}
|
{isCreating ? "生成中..." : "开始生成"}
|
||||||
</button>
|
</button>
|
||||||
{isCreating && (
|
{isCreating && (
|
||||||
<button type="button" className="studio-generate-btn digital-human-cancel-btn" onClick={handleCancel} aria-label="取消生成任务">
|
<button type="button" className="studio-generate-btn digital-human-cancel-btn" onClick={handleCancel} aria-label="取消生成任务">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -119,6 +119,7 @@ export default function EcommerceVideoWorkspace({
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [actionNotice, setActionNotice] = useState<string | null>(null);
|
const [actionNotice, setActionNotice] = useState<string | null>(null);
|
||||||
const [previewMedia, setPreviewMedia] = useState<{ url: string; type: "image" | "video" } | null>(null);
|
const [previewMedia, setPreviewMedia] = useState<{ url: string; type: "image" | "video" } | null>(null);
|
||||||
|
const [flowZoom, setFlowZoom] = useState(1);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
const renderAbortRef = useRef({ current: false });
|
const renderAbortRef = useRef({ current: false });
|
||||||
const setView = useAppStore((s) => s.setView);
|
const setView = useAppStore((s) => s.setView);
|
||||||
@@ -600,6 +601,12 @@ export default function EcommerceVideoWorkspace({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="ecom-video-flowbar__zoom">
|
||||||
|
<button type="button" onClick={() => setFlowZoom((z) => Math.max(0.25, z - 0.1))} disabled={flowZoom <= 0.25} aria-label="缩小">−</button>
|
||||||
|
<span>{Math.round(flowZoom * 100)}%</span>
|
||||||
|
<button type="button" onClick={() => setFlowZoom((z) => Math.min(2, z + 0.1))} disabled={flowZoom >= 2} aria-label="放大">+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="ecom-video-flowbar__actions">
|
<div className="ecom-video-flowbar__actions">
|
||||||
{onOpenHistory ? (
|
{onOpenHistory ? (
|
||||||
<button type="button" className="ecom-video-flow-action ecom-video-flow-action--ghost" onClick={onOpenHistory} title="生成记录">
|
<button type="button" className="ecom-video-flow-action ecom-video-flow-action--ghost" onClick={onOpenHistory} title="生成记录">
|
||||||
@@ -644,9 +651,10 @@ export default function EcommerceVideoWorkspace({
|
|||||||
|
|
||||||
{/* ── Flow canvas ──────────────────────────────────── */}
|
{/* ── Flow canvas ──────────────────────────────────── */}
|
||||||
<section className="ecom-video-flow-canvas" aria-label="视频分镜流程图">
|
<section className="ecom-video-flow-canvas" aria-label="视频分镜流程图">
|
||||||
|
<div style={{ zoom: flowZoom, flexShrink: 0, display: "flex", alignItems: "flex-start", justifyContent: "center", minWidth: "max-content" }}>
|
||||||
{!sourceImage ? (
|
{!sourceImage ? (
|
||||||
<div className="ecom-video-empty">
|
<div className="ecom-video-empty">
|
||||||
<span>上传商品图并点击“一键策划”开始</span>
|
<span>上传商品图并点击"一键策划"开始</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="ecom-video-tree">
|
<div className="ecom-video-tree">
|
||||||
@@ -762,6 +770,7 @@ export default function EcommerceVideoWorkspace({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ── Delivery dock ────────────────────────────── */}
|
{/* ── Delivery dock ────────────────────────────── */}
|
||||||
{primaryVideo ? (
|
{primaryVideo ? (
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import type { CSSProperties, ChangeEvent, DragEvent, MutableRefObject, RefObject } from "react";
|
import type { CSSProperties, ChangeEvent, DragEvent, MutableRefObject, RefObject } from "react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
@@ -118,6 +119,10 @@ interface EcommerceClonePanelProps {
|
|||||||
setOpenCloneBasicSelect: (value: CloneBasicSelectKey | null) => void;
|
setOpenCloneBasicSelect: (value: CloneBasicSelectKey | null) => void;
|
||||||
setCloneReferenceMode: (value: CloneReferenceMode) => void;
|
setCloneReferenceMode: (value: CloneReferenceMode) => void;
|
||||||
handleCloneReferenceUpload: (event: ChangeEvent<HTMLInputElement>) => void;
|
handleCloneReferenceUpload: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
isCloneReferenceDragging: boolean;
|
||||||
|
handleCloneReferenceDragOver: (event: DragEvent<HTMLButtonElement>) => void;
|
||||||
|
handleCloneReferenceDragLeave: (event: DragEvent<HTMLButtonElement>) => void;
|
||||||
|
handleCloneReferenceDrop: (event: DragEvent<HTMLButtonElement>) => void;
|
||||||
setCloneReplicateLevel: (value: CloneReplicateLevelKey) => void;
|
setCloneReplicateLevel: (value: CloneReplicateLevelKey) => void;
|
||||||
startCloneSetCountHold: (key: CloneSetCountKey, delta: -1 | 1, disabled: boolean) => void;
|
startCloneSetCountHold: (key: CloneSetCountKey, delta: -1 | 1, disabled: boolean) => void;
|
||||||
clearCloneSetCountHold: () => void;
|
clearCloneSetCountHold: () => void;
|
||||||
@@ -186,6 +191,10 @@ export default function EcommerceClonePanel({
|
|||||||
setOpenCloneBasicSelect,
|
setOpenCloneBasicSelect,
|
||||||
setCloneReferenceMode,
|
setCloneReferenceMode,
|
||||||
handleCloneReferenceUpload,
|
handleCloneReferenceUpload,
|
||||||
|
isCloneReferenceDragging,
|
||||||
|
handleCloneReferenceDragOver,
|
||||||
|
handleCloneReferenceDragLeave,
|
||||||
|
handleCloneReferenceDrop,
|
||||||
setCloneReplicateLevel,
|
setCloneReplicateLevel,
|
||||||
startCloneSetCountHold,
|
startCloneSetCountHold,
|
||||||
clearCloneSetCountHold,
|
clearCloneSetCountHold,
|
||||||
@@ -210,6 +219,14 @@ export default function EcommerceClonePanel({
|
|||||||
const videoOutfitRefRef = useRef<HTMLInputElement>(null);
|
const videoOutfitRefRef = useRef<HTMLInputElement>(null);
|
||||||
const [videoOutfitVideoUrl, setVideoOutfitVideoUrl] = useState<string | null>(null);
|
const [videoOutfitVideoUrl, setVideoOutfitVideoUrl] = useState<string | null>(null);
|
||||||
const [videoOutfitRefUrl, setVideoOutfitRefUrl] = useState<string | null>(null);
|
const [videoOutfitRefUrl, setVideoOutfitRefUrl] = useState<string | null>(null);
|
||||||
|
const [zoomImage, setZoomImage] = useState<{ src: string; x: number; y: number } | null>(null);
|
||||||
|
|
||||||
|
const handleFileMouseEnter = (src: string, event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
|
setZoomImage({ src, x: rect.left + rect.width / 2, y: rect.top });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileMouseLeave = () => setZoomImage(null);
|
||||||
|
|
||||||
const handleVideoOutfitVideoChange = () => {
|
const handleVideoOutfitVideoChange = () => {
|
||||||
const file = videoOutfitVideoRef.current?.files?.[0] || null;
|
const file = videoOutfitVideoRef.current?.files?.[0] || null;
|
||||||
@@ -383,23 +400,44 @@ export default function EcommerceClonePanel({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{cloneReferenceMode === "upload" ? (
|
{cloneReferenceMode === "upload" ? (
|
||||||
<button type="button" className="clone-ai-replicate-upload" onClick={() => cloneReferenceInputRef.current?.click()}>
|
<button
|
||||||
<span>
|
type="button"
|
||||||
<CloudUploadOutlined />
|
className={`clone-ai-replicate-upload${isCloneReferenceDragging ? " is-dragging" : ""}${cloneReferenceImages.length ? " has-files" : ""}`}
|
||||||
<span className="clone-ai-replicate-upload-text">添加图片</span>
|
onClick={() => cloneReferenceInputRef.current?.click()}
|
||||||
</span>
|
onDragOver={handleCloneReferenceDragOver}
|
||||||
<em>{cloneReferenceImages.length ? `已选 ${cloneReferenceImages.length}/${maxCloneReferenceImages}` : `最多 ${maxCloneReferenceImages} 张`}</em>
|
onDragLeave={handleCloneReferenceDragLeave}
|
||||||
|
onDrop={handleCloneReferenceDrop}
|
||||||
|
>
|
||||||
{cloneReferenceImages.length ? (
|
{cloneReferenceImages.length ? (
|
||||||
<div className="clone-ai-replicate-preview" aria-hidden="true">
|
<>
|
||||||
{cloneReferenceImages.slice(0, 4).map((item) => (
|
<div className="clone-ai-replicate-files">
|
||||||
<figure key={item.id}>
|
{cloneReferenceImages.map((item) => (
|
||||||
|
<figure
|
||||||
|
key={item.id}
|
||||||
|
className="clone-ai-replicate-file"
|
||||||
|
onMouseEnter={(e) => handleFileMouseEnter(item.src, e)}
|
||||||
|
onMouseLeave={handleFileMouseLeave}
|
||||||
|
>
|
||||||
<img src={item.src} alt="" />
|
<img src={item.src} alt="" />
|
||||||
<span className="uploaded-image-zoom">
|
|
||||||
<img src={item.src} alt="" />
|
|
||||||
</span>
|
|
||||||
</figure>
|
</figure>
|
||||||
))}
|
))}
|
||||||
{cloneReferenceImages.length > 4 ? <b>+{cloneReferenceImages.length - 4}</b> : null}
|
</div>
|
||||||
|
<span className="clone-ai-replicate-add-more">
|
||||||
|
<CloudUploadOutlined />
|
||||||
|
点击继续上传文件
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<CloudUploadOutlined />
|
||||||
|
<span className="clone-ai-replicate-upload-text">拖拽或点击上传参考图</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<em>{cloneReferenceImages.length ? `已选 ${cloneReferenceImages.length}/${maxCloneReferenceImages}` : `最多 ${maxCloneReferenceImages} 张`}</em>
|
||||||
|
{isCloneReferenceDragging ? (
|
||||||
|
<div className="clone-ai-replicate-upload-overlay">
|
||||||
|
<CloudUploadOutlined />
|
||||||
|
<span>释放文件以上传</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</button>
|
</button>
|
||||||
@@ -754,6 +792,18 @@ export default function EcommerceClonePanel({
|
|||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
{zoomImage
|
||||||
|
? createPortal(
|
||||||
|
<div
|
||||||
|
className="clone-ai-zoom-portal"
|
||||||
|
style={{ left: zoomImage.x, top: zoomImage.y } as CSSProperties}
|
||||||
|
onMouseLeave={handleFileMouseLeave}
|
||||||
|
>
|
||||||
|
<img src={zoomImage.src} alt="" />
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
: null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
TableOutlined,
|
TableOutlined,
|
||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState, type DragEvent } from "react";
|
||||||
import type { WebImageWorkbenchTool, WebViewKey } from "../../types";
|
import type { WebImageWorkbenchTool, WebViewKey } from "../../types";
|
||||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||||
import { waitForTask } from "../../api/taskSubscription";
|
import { waitForTask } from "../../api/taskSubscription";
|
||||||
@@ -138,6 +138,8 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
const [downloadingResultUrl, setDownloadingResultUrl] = useState<string | null>(null);
|
const [downloadingResultUrl, setDownloadingResultUrl] = useState<string | null>(null);
|
||||||
const [savingAssetResultUrl, setSavingAssetResultUrl] = useState<string | null>(null);
|
const [savingAssetResultUrl, setSavingAssetResultUrl] = useState<string | null>(null);
|
||||||
const [generationError, setGenerationError] = useState<string | null>(null);
|
const [generationError, setGenerationError] = useState<string | null>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [isCameraDragging, setIsCameraDragging] = useState(false);
|
||||||
const abortRef = useRef(false);
|
const abortRef = useRef(false);
|
||||||
const taskIdRef = useRef<string | null>(null);
|
const taskIdRef = useRef<string | null>(null);
|
||||||
const keepaliveRestoredRef = useRef(false);
|
const keepaliveRestoredRef = useRef(false);
|
||||||
@@ -229,6 +231,37 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleDragOver = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
const files = Array.from(event.dataTransfer.files).filter((f) => f.type.startsWith('image/'));
|
||||||
|
if (!files.length) return;
|
||||||
|
const selectedFiles = mode === 'blend' ? files : files.slice(0, 1);
|
||||||
|
selectedFiles.forEach((file) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
if (typeof reader.result !== 'string') return;
|
||||||
|
setReferenceImages((current) => (mode === 'blend' ? [...current, reader.result as string] : [reader.result as string]));
|
||||||
|
setStatus(mode === 'blend' ? `已追加 ${file.name}` : `已导入 ${file.name}`);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddUrl = () => {
|
const handleAddUrl = () => {
|
||||||
const nextUrl = imageUrlInput.trim();
|
const nextUrl = imageUrlInput.trim();
|
||||||
if (!nextUrl) return;
|
if (!nextUrl) return;
|
||||||
@@ -261,9 +294,15 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInpaintDrop = (event: React.DragEvent) => {
|
const [isInpaintDragging, setIsInpaintDragging] = useState(false);
|
||||||
event.preventDefault();
|
|
||||||
const file = Array.from(event.dataTransfer.files).find((f) => f.type.startsWith("image/"));
|
const handleInpaintDragOver = (e: DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes("Files")) setIsInpaintDragging(true); };
|
||||||
|
const handleInpaintDragLeave = (e: DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) setIsInpaintDragging(false); };
|
||||||
|
const handleInpaintDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsInpaintDragging(false);
|
||||||
|
const file = Array.from(e.dataTransfer.files).find((f) => f.type.startsWith("image/"));
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
@@ -302,7 +341,7 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!hasMask) {
|
if (!hasMask) {
|
||||||
setStatus("请先编辑遮罩,涂抹需要重绘的区域");
|
setStatus("请先编辑页面,涂抹需要重绘的区域");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (generating) return;
|
if (generating) return;
|
||||||
@@ -364,6 +403,33 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCameraDragOver = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsCameraDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCameraDragLeave = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsCameraDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCameraDrop = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsCameraDragging(false);
|
||||||
|
const file = Array.from(event.dataTransfer.files).find((f) => f.type.startsWith('image/'));
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
if (typeof reader.result !== 'string') return;
|
||||||
|
setCameraImage(reader.result);
|
||||||
|
setStatus(`已导入镜头参考图 ${file.name}`);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddCameraUrl = () => {
|
const handleAddCameraUrl = () => {
|
||||||
const nextUrl = cameraUrlInput.trim();
|
const nextUrl = cameraUrlInput.trim();
|
||||||
if (!nextUrl) return;
|
if (!nextUrl) return;
|
||||||
@@ -696,7 +762,13 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
accept="image/png,image/jpeg,image/webp"
|
accept="image/png,image/jpeg,image/webp"
|
||||||
onChange={handleInpaintFileChange}
|
onChange={handleInpaintFileChange}
|
||||||
/>
|
/>
|
||||||
<div className="image-workbench-upload-shell">
|
<div
|
||||||
|
className={`image-workbench-upload-shell${isInpaintDragging ? " is-dragging" : ""}`}
|
||||||
|
onDragOver={handleInpaintDragOver}
|
||||||
|
onDragLeave={handleInpaintDragLeave}
|
||||||
|
onDrop={handleInpaintDrop}
|
||||||
|
>
|
||||||
|
{isInpaintDragging ? <div className="image-workbench-upload-drop-overlay"><span>释放文件以上传</span></div> : null}
|
||||||
<button type="button" className="image-workbench-upload" onClick={() => inpaintFileInputRef.current?.click()}>
|
<button type="button" className="image-workbench-upload" onClick={() => inpaintFileInputRef.current?.click()}>
|
||||||
{inpaintImage ? <img src={inpaintImage} alt="" /> : <FileImageOutlined />}
|
{inpaintImage ? <img src={inpaintImage} alt="" /> : <FileImageOutlined />}
|
||||||
<strong>{inpaintImage ? "更换原图" : "选择图片"}</strong>
|
<strong>{inpaintImage ? "更换原图" : "选择图片"}</strong>
|
||||||
@@ -789,7 +861,7 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
<img src={inpaintResultImages[0]} alt="重绘结果" style={{ maxWidth: "95%", maxHeight: "95%", borderRadius: 8, objectFit: "contain" }} />
|
<img src={inpaintResultImages[0]} alt="重绘结果" style={{ maxWidth: "95%", maxHeight: "95%", borderRadius: 8, objectFit: "contain" }} />
|
||||||
<div className="image-workbench-inpaint-bottom-bar">
|
<div className="image-workbench-inpaint-bottom-bar">
|
||||||
<button type="button" className="image-workbench-inpaint-edit-btn" onClick={() => { setInpaintResultImages([]); setIsMaskEditing(true); setInpaintTool("brush"); setCanvasInitCounter((c) => c + 1); }}>
|
<button type="button" className="image-workbench-inpaint-edit-btn" onClick={() => { setInpaintResultImages([]); setIsMaskEditing(true); setInpaintTool("brush"); setCanvasInitCounter((c) => c + 1); }}>
|
||||||
<HighlightOutlined /> 重新编辑遮罩
|
<HighlightOutlined /> 重新编辑页面
|
||||||
</button>
|
</button>
|
||||||
{renderResultActions(inpaintResultImages[0], 0)}
|
{renderResultActions(inpaintResultImages[0], 0)}
|
||||||
{inpaintResultImages.length > 1 && (
|
{inpaintResultImages.length > 1 && (
|
||||||
@@ -845,7 +917,7 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
<div className="image-workbench-inpaint-bottom-bar">
|
<div className="image-workbench-inpaint-bottom-bar">
|
||||||
{!isMaskEditing && (
|
{!isMaskEditing && (
|
||||||
<button type="button" className="image-workbench-inpaint-edit-btn" onClick={() => { setInpaintTool("brush"); setIsMaskEditing(true); }}>
|
<button type="button" className="image-workbench-inpaint-edit-btn" onClick={() => { setInpaintTool("brush"); setIsMaskEditing(true); }}>
|
||||||
<HighlightOutlined /> {hasMask ? "重新编辑遮罩" : "编辑遮罩"}
|
<HighlightOutlined /> {hasMask ? "重新编辑页面" : "编辑页面"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<span className="image-workbench-inpaint-zoom-controls">
|
<span className="image-workbench-inpaint-zoom-controls">
|
||||||
@@ -858,11 +930,13 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="image-workbench-empty image-workbench-empty--button"
|
className={`image-workbench-empty image-workbench-empty--button${isInpaintDragging ? " is-dragging" : ""}`}
|
||||||
onClick={() => inpaintFileInputRef.current?.click()}
|
onClick={() => inpaintFileInputRef.current?.click()}
|
||||||
onDragOver={(e) => e.preventDefault()}
|
onDragOver={handleInpaintDragOver}
|
||||||
|
onDragLeave={handleInpaintDragLeave}
|
||||||
onDrop={handleInpaintDrop}
|
onDrop={handleInpaintDrop}
|
||||||
>
|
>
|
||||||
|
{isInpaintDragging ? <span className="image-workbench-upload-drop-overlay" style={{ borderRadius: "var(--radius-sm)" }}><span>释放文件以上传</span></span> : null}
|
||||||
<FileImageOutlined />
|
<FileImageOutlined />
|
||||||
<strong>拖拽或选择图片</strong>
|
<strong>拖拽或选择图片</strong>
|
||||||
<span>支持 PNG / JPG / WebP</span>
|
<span>支持 PNG / JPG / WebP</span>
|
||||||
@@ -870,36 +944,6 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside className="image-workbench-panel image-workbench-panel--right">
|
|
||||||
<section className="image-workbench-right-note">
|
|
||||||
<div className="image-workbench-section-title">
|
|
||||||
<h3>遮罩</h3>
|
|
||||||
<span>{isMaskEditing ? (inpaintTool === "eraser" ? "橡皮中" : "画笔中") : hasMask ? "已保存" : "待编辑"}</span>
|
|
||||||
</div>
|
|
||||||
<span>{inpaintImage ? (hasMask ? "遮罩区域已标记,可开始重绘。" : "点击画布上的「编辑遮罩」开始涂抹。") : "上传原图后可编辑遮罩"}</span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="image-workbench-right-note">
|
|
||||||
<div className="image-workbench-section-title">
|
|
||||||
<h3>结果</h3>
|
|
||||||
<span>{inpaintResultImages.length > 0 ? `${inpaintResultImages.length} 张` : "待生成"}</span>
|
|
||||||
</div>
|
|
||||||
{inpaintResultImages.length > 0 ? (
|
|
||||||
<div className="image-workbench-result-grid">
|
|
||||||
{inpaintResultImages.map((url, i) => (
|
|
||||||
<div key={url} className="image-workbench-result-card">
|
|
||||||
<a href={url} target="_blank" rel="noopener noreferrer" className="image-workbench-result-thumb">
|
|
||||||
<img src={url} alt={`重绘结果 ${i + 1}`} />
|
|
||||||
</a>
|
|
||||||
{renderResultActions(url, i)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span>重绘结果会显示在这里</span>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</main>
|
</main>
|
||||||
) : activeTool === "camera" ? (
|
) : activeTool === "camera" ? (
|
||||||
<main className="image-workbench-layout image-workbench-layout--camera">
|
<main className="image-workbench-layout image-workbench-layout--camera">
|
||||||
@@ -915,7 +959,13 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
accept="image/png,image/jpeg,image/webp"
|
accept="image/png,image/jpeg,image/webp"
|
||||||
onChange={handleCameraFileChange}
|
onChange={handleCameraFileChange}
|
||||||
/>
|
/>
|
||||||
<div className="image-workbench-upload-shell">
|
<div
|
||||||
|
className={`image-workbench-upload-shell${isCameraDragging ? " is-dragging" : ""}`}
|
||||||
|
onDragOver={handleCameraDragOver}
|
||||||
|
onDragLeave={handleCameraDragLeave}
|
||||||
|
onDrop={handleCameraDrop}
|
||||||
|
>
|
||||||
|
{isCameraDragging && <div className="image-workbench-upload-overlay">释放文件以上传</div>}
|
||||||
<button type="button" className="image-workbench-upload" onClick={() => cameraFileInputRef.current?.click()}>
|
<button type="button" className="image-workbench-upload" onClick={() => cameraFileInputRef.current?.click()}>
|
||||||
{cameraImage ? <img src={cameraImage} alt="" /> : <FileImageOutlined />}
|
{cameraImage ? <img src={cameraImage} alt="" /> : <FileImageOutlined />}
|
||||||
<strong>{cameraImage ? "更换参考图" : "导入参考图"}</strong>
|
<strong>{cameraImage ? "更换参考图" : "导入参考图"}</strong>
|
||||||
@@ -1194,7 +1244,13 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="image-workbench-upload-shell">
|
<div
|
||||||
|
className={`image-workbench-upload-shell${isDragging ? " is-dragging" : ""}`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
{isDragging && <div className="image-workbench-upload-overlay">释放文件以上传</div>}
|
||||||
<button type="button" className="image-workbench-upload" onClick={() => fileInputRef.current?.click()}>
|
<button type="button" className="image-workbench-upload" onClick={() => fileInputRef.current?.click()}>
|
||||||
{referenceImage ? <img src={referenceImage} alt="" /> : <FileImageOutlined />}
|
{referenceImage ? <img src={referenceImage} alt="" /> : <FileImageOutlined />}
|
||||||
<strong>{referenceImage ? "更换参考图" : "导入参考图"}</strong>
|
<strong>{referenceImage ? "更换参考图" : "导入参考图"}</strong>
|
||||||
@@ -1225,6 +1281,33 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="image-workbench-control-card">
|
||||||
|
<h3>输出</h3>
|
||||||
|
<span className="image-workbench-field-label">尺寸</span>
|
||||||
|
<div className="image-workbench-segmented">
|
||||||
|
{(["9:16", "16:9", "4:3", "3:4", "1:1"] as OutputSize[]).map((s) => (
|
||||||
|
<button key={s} type="button" className={outputSize === s ? "is-active" : ""} onClick={() => setOutputSize(s)}>
|
||||||
|
{s}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="image-workbench-count">
|
||||||
|
<span>数量</span>
|
||||||
|
<div>
|
||||||
|
{([1, 2, 3, 4] as OutputCount[]).map((count) => (
|
||||||
|
<button
|
||||||
|
key={count}
|
||||||
|
type="button"
|
||||||
|
className={outputCount === count ? "is-active" : ""}
|
||||||
|
onClick={() => setOutputCount(count)}
|
||||||
|
>
|
||||||
|
{count} 张
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="image-workbench-control-card image-workbench-prompt">
|
<section className="image-workbench-control-card image-workbench-prompt">
|
||||||
<h3>提示词</h3>
|
<h3>提示词</h3>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -1297,34 +1380,6 @@ function ImageWorkbenchPage({ initialTool = "workbench", onOpenMore, onSelectVie
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside className="image-workbench-panel image-workbench-panel--right">
|
|
||||||
<section className="image-workbench-control-card">
|
|
||||||
<h3>输出</h3>
|
|
||||||
<span className="image-workbench-field-label">尺寸</span>
|
|
||||||
<div className="image-workbench-segmented">
|
|
||||||
{(["9:16", "16:9", "4:3", "3:4", "1:1"] as OutputSize[]).map((s) => (
|
|
||||||
<button key={s} type="button" className={outputSize === s ? "is-active" : ""} onClick={() => setOutputSize(s)}>
|
|
||||||
{s}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="image-workbench-count">
|
|
||||||
<span>数量</span>
|
|
||||||
<div>
|
|
||||||
{([1, 2, 3, 4] as OutputCount[]).map((count) => (
|
|
||||||
<button
|
|
||||||
key={count}
|
|
||||||
type="button"
|
|
||||||
className={outputCount === count ? "is-active" : ""}
|
|
||||||
onClick={() => setOutputCount(count)}
|
|
||||||
>
|
|
||||||
{count} 张
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+151
-29
@@ -12,7 +12,7 @@ import {
|
|||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import type { ReactNode } from "react";
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import type { WebImageWorkbenchTool, WebViewKey } from "../../types";
|
import type { WebImageWorkbenchTool, WebViewKey } from "../../types";
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@ interface MoreTool {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
useCase: string;
|
||||||
|
tags: string[];
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
category: ToolCategory;
|
category: ToolCategory;
|
||||||
target?: WebViewKey;
|
target?: WebViewKey;
|
||||||
@@ -37,23 +39,77 @@ interface MoreTool {
|
|||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CompareScene =
|
||||||
|
| "workbench"
|
||||||
|
| "inpaint"
|
||||||
|
| "camera"
|
||||||
|
| "upscale"
|
||||||
|
| "watermark"
|
||||||
|
| "dialog"
|
||||||
|
| "subtitle"
|
||||||
|
| "digital-human"
|
||||||
|
| "character"
|
||||||
|
| "avatar";
|
||||||
|
|
||||||
|
const toolCompareScenes: Record<string, CompareScene> = {
|
||||||
|
workbench: "workbench",
|
||||||
|
inpaint: "inpaint",
|
||||||
|
camera: "camera",
|
||||||
|
upscale: "upscale",
|
||||||
|
watermarkRemoval: "watermark",
|
||||||
|
dialogGenerator: "dialog",
|
||||||
|
subtitleRemoval: "subtitle",
|
||||||
|
digitalHuman: "digital-human",
|
||||||
|
characterMix: "character",
|
||||||
|
avatarConsole: "avatar",
|
||||||
|
};
|
||||||
|
|
||||||
|
function ToolComparePanel({ scene }: { scene: CompareScene }) {
|
||||||
|
return (
|
||||||
|
<span className={`more-card__compare more-card__compare--${scene}`} aria-hidden="true">
|
||||||
|
<span className="more-card__compare-labels">
|
||||||
|
<span>Before</span>
|
||||||
|
<span>After</span>
|
||||||
|
</span>
|
||||||
|
<span className="more-card__compare-stage">
|
||||||
|
<span className="more-card__compare-side more-card__compare-side--before">
|
||||||
|
<span className="more-card__scene-subject" />
|
||||||
|
<span className="more-card__scene-detail" />
|
||||||
|
<span className="more-card__scene-overlay" />
|
||||||
|
</span>
|
||||||
|
<span className="more-card__compare-divider">
|
||||||
|
<span />
|
||||||
|
</span>
|
||||||
|
<span className="more-card__compare-side more-card__compare-side--after">
|
||||||
|
<span className="more-card__scene-subject" />
|
||||||
|
<span className="more-card__scene-detail" />
|
||||||
|
<span className="more-card__scene-overlay" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const tools: MoreTool[] = [
|
const tools: MoreTool[] = [
|
||||||
{ id: "workbench", title: "图片工作台", text: "融合、修复、局部增强", icon: <EditOutlined />, category: "image", imageTool: "workbench", ready: true, featured: true },
|
{ id: "workbench", title: "图片工作台", text: "融合、修复、局部增强", useCase: "适合商品图精修、创意合成和局部画面重做", tags: ["热门", "一站式", "商品图"], icon: <EditOutlined />, category: "image", imageTool: "workbench", ready: true, featured: true },
|
||||||
{ id: "inpaint", title: "局部重绘", text: "遮罩区域重新生成", icon: <HighlightOutlined />, category: "image", imageTool: "inpaint", ready: true },
|
{ id: "inpaint", title: "局部重绘", text: "修掉瑕疵、替换物体、重做局部画面", useCase: "适合快速处理商品瑕疵、人物细节和背景杂物", tags: ["新手推荐", "精修"], icon: <HighlightOutlined />, category: "image", imageTool: "inpaint", ready: true },
|
||||||
{ id: "camera", title: "镜头实验室", text: "角度、焦段和机位控制", icon: <CameraOutlined />, category: "image", imageTool: "camera", ready: true },
|
{ id: "camera", title: "镜头实验室", text: "快速生成俯拍、特写、广角等商业镜头", useCase: "适合做产品主图、种草图和不同机位方案", tags: ["电商常用", "镜头"], icon: <CameraOutlined />, category: "image", imageTool: "camera", ready: true },
|
||||||
{ id: "upscale", title: "分辨率提升", text: "图片与视频高清超分", icon: <ColumnWidthOutlined />, category: "image", target: "resolutionUpscale", ready: true },
|
{ id: "upscale", title: "分辨率提升", text: "把低清图片或视频提升到可交付质感", useCase: "适合修复旧素材、放大商品图和增强短视频清晰度", tags: ["高清", "交付前"], icon: <ColumnWidthOutlined />, category: "image", target: "resolutionUpscale", ready: true },
|
||||||
{ id: "watermarkRemoval", title: "去水印", text: "AI 智能去除图片水印和文字", icon: <DeleteOutlined />, category: "image", target: "watermarkRemoval", ready: true },
|
{ id: "watermarkRemoval", title: "去水印", text: "智能去除图片水印、文字和遮挡元素", useCase: "适合整理素材、清理参考图和恢复画面干净度", tags: ["素材清理", "高频"], icon: <DeleteOutlined />, category: "image", target: "watermarkRemoval", ready: true },
|
||||||
{ id: "dialogGenerator", title: "交互式对话框生成器", text: "上传背景图,添加可拖拽编辑的对话框", icon: <MessageOutlined />, category: "image", target: "dialogGenerator", ready: true },
|
{ id: "dialogGenerator", title: "交互式对话框生成器", text: "上传背景图,快速制作可拖拽编辑的对话框", useCase: "适合剧情海报、社媒截图和角色对白设计", tags: ["内容创作", "可编辑"], icon: <MessageOutlined />, category: "image", target: "dialogGenerator", ready: true },
|
||||||
{ id: "subtitleRemoval", title: "字幕去除", text: "AI 智能擦除视频字幕", icon: <DeleteOutlined />, category: "video", target: "subtitleRemoval", ready: true },
|
{ id: "subtitleRemoval", title: "字幕去除", text: "擦除视频字幕,让画面重新变干净", useCase: "适合二创前素材整理、短视频重剪和画面修复", tags: ["视频增强", "素材清理"], icon: <DeleteOutlined />, category: "video", target: "subtitleRemoval", ready: true },
|
||||||
{ id: "digitalHuman", title: "数字人", text: "参考人像与音频生成口播视频", icon: <CustomerServiceOutlined />, category: "video", target: "digitalHuman", ready: true, featured: true },
|
{ id: "digitalHuman", title: "数字人", text: "用一张人像和音频生成口播视频", useCase: "适合品牌讲解、课程口播和带货短视频", tags: ["热门", "口播", "视频"], icon: <CustomerServiceOutlined />, category: "video", target: "digitalHuman", ready: true, featured: true },
|
||||||
{ id: "characterMix", title: "角色迁移", text: "人物图迁移到参考视频动作", icon: <SwapOutlined />, category: "video", target: "characterMix", ready: true },
|
{ id: "characterMix", title: "角色迁移", text: "把人物图迁移到参考视频的动作里", useCase: "适合角色短片、动作复刻和虚拟人内容生产", tags: ["角色视频", "动作"], icon: <SwapOutlined />, category: "video", target: "characterMix", ready: true },
|
||||||
{ id: "avatarConsole", title: "数字人控制台", text: "形象、播报、互动与接入配置", icon: <DashboardOutlined />, category: "video", target: "avatarConsole", ready: true },
|
{ id: "avatarConsole", title: "数字人控制台", text: "管理形象、播报、互动与接入配置", useCase: "适合持续运营数字人、配置品牌形象和复用口播模板", tags: ["运营台", "企业"], icon: <DashboardOutlined />, category: "video", target: "avatarConsole", ready: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface FeaturedTool {
|
interface FeaturedTool {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
|
kicker: string;
|
||||||
|
steps: string[];
|
||||||
|
outcome: string;
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
imageTool?: WebImageWorkbenchTool;
|
imageTool?: WebImageWorkbenchTool;
|
||||||
target?: WebViewKey;
|
target?: WebViewKey;
|
||||||
@@ -65,7 +121,10 @@ const featuredTools: FeaturedTool[] = [
|
|||||||
{
|
{
|
||||||
id: "workbench",
|
id: "workbench",
|
||||||
title: "图片工作台",
|
title: "图片工作台",
|
||||||
desc: "融合、修复、局部增强 — 一站式图片创作",
|
desc: "从一张素材开始,完成精修、合成和二次创作。",
|
||||||
|
kicker: "图片精修工作流",
|
||||||
|
steps: ["上传素材", "局部修复", "高清导出"],
|
||||||
|
outcome: "适合商品图、海报图和创意视觉",
|
||||||
icon: <EditOutlined />,
|
icon: <EditOutlined />,
|
||||||
imageTool: "workbench",
|
imageTool: "workbench",
|
||||||
category: "image",
|
category: "image",
|
||||||
@@ -74,7 +133,10 @@ const featuredTools: FeaturedTool[] = [
|
|||||||
{
|
{
|
||||||
id: "digitalHuman",
|
id: "digitalHuman",
|
||||||
title: "数字人",
|
title: "数字人",
|
||||||
desc: "参考人像与音频,一键生成口播视频",
|
desc: "用参考人像和音频,快速生成可交付口播视频。",
|
||||||
|
kicker: "口播视频工作流",
|
||||||
|
steps: ["选择人像", "上传音频", "生成视频"],
|
||||||
|
outcome: "适合品牌讲解、课程和带货短视频",
|
||||||
icon: <CustomerServiceOutlined />,
|
icon: <CustomerServiceOutlined />,
|
||||||
target: "digitalHuman",
|
target: "digitalHuman",
|
||||||
category: "video",
|
category: "video",
|
||||||
@@ -136,6 +198,18 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
|
|||||||
}
|
}
|
||||||
}, [onOpenImageTool, onSelectView]);
|
}, [onOpenImageTool, onSelectView]);
|
||||||
|
|
||||||
|
const openFeaturedTool = useCallback((tool: FeaturedTool) => {
|
||||||
|
pushRecentToolId(tool.id);
|
||||||
|
setRecentIds(getRecentToolIds());
|
||||||
|
if (tool.imageTool && onOpenImageTool) {
|
||||||
|
onOpenImageTool(tool.imageTool);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tool.target && onSelectView) {
|
||||||
|
onSelectView(tool.target);
|
||||||
|
}
|
||||||
|
}, [onOpenImageTool, onSelectView]);
|
||||||
|
|
||||||
const filteredTools = tools.filter((t) => {
|
const filteredTools = tools.filter((t) => {
|
||||||
if (t.featured) return false;
|
if (t.featured) return false;
|
||||||
if (filter === "all") return true;
|
if (filter === "all") return true;
|
||||||
@@ -143,6 +217,13 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
|
|||||||
return t.category === filter;
|
return t.category === filter;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filterCounts: Record<FilterKey, number> = {
|
||||||
|
all: tools.filter((t) => !t.featured).length,
|
||||||
|
image: tools.filter((t) => !t.featured && t.category === "image").length,
|
||||||
|
video: tools.filter((t) => !t.featured && t.category === "video").length,
|
||||||
|
upcoming: tools.filter((t) => !t.featured && !t.ready).length,
|
||||||
|
};
|
||||||
|
|
||||||
const recentTools = recentIds
|
const recentTools = recentIds
|
||||||
.map((id) => tools.find((t) => t.id === id))
|
.map((id) => tools.find((t) => t.id === id))
|
||||||
.filter((t): t is MoreTool => Boolean(t) && (t?.ready ?? false));
|
.filter((t): t is MoreTool => Boolean(t) && (t?.ready ?? false));
|
||||||
@@ -153,19 +234,31 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<ToolCategory, MoreTool[]>);
|
}, {} as Record<ToolCategory, MoreTool[]>);
|
||||||
|
|
||||||
|
const activeFilterLabel = filters.find((item) => item.key === filter)?.label ?? "全部";
|
||||||
|
const hasGroupedTools = (["image", "video"] as ToolCategory[]).some((cat) => groupedTools[cat]?.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="more-page-v2">
|
<div className="more-page-v2">
|
||||||
<header className="more-page-v2__header">
|
<header className="more-page-v2__header">
|
||||||
|
<div className="more-page-v2__title-group">
|
||||||
|
<span className="more-page-v2__eyebrow">AI Tool Hub</span>
|
||||||
<h1>工具盒</h1>
|
<h1>工具盒</h1>
|
||||||
<nav className="more-page-v2__filters">
|
</div>
|
||||||
|
<div className="more-page-v2__header-meta" aria-label="工具盒概览">
|
||||||
|
<span>{tools.filter((tool) => tool.ready).length} 个可用工具</span>
|
||||||
|
<span>{featuredTools.length} 个核心入口</span>
|
||||||
|
</div>
|
||||||
|
<nav className="more-page-v2__filters" aria-label="工具分类筛选">
|
||||||
{filters.map((f) => (
|
{filters.map((f) => (
|
||||||
<button
|
<button
|
||||||
key={f.key}
|
key={f.key}
|
||||||
type="button"
|
type="button"
|
||||||
className={filter === f.key ? "is-active" : ""}
|
className={filter === f.key ? "is-active" : ""}
|
||||||
|
aria-pressed={filter === f.key}
|
||||||
onClick={() => setFilter(f.key)}
|
onClick={() => setFilter(f.key)}
|
||||||
>
|
>
|
||||||
{f.label}
|
<span>{f.label}</span>
|
||||||
|
<em>{filterCounts[f.key]}</em>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
@@ -176,6 +269,7 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
|
|||||||
<section className="more-page-v2__section">
|
<section className="more-page-v2__section">
|
||||||
<h2 className="more-page-v2__section-title">
|
<h2 className="more-page-v2__section-title">
|
||||||
<ClockCircleOutlined /> 最近使用
|
<ClockCircleOutlined /> 最近使用
|
||||||
|
<span>继续使用</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="more-page-v2__recent-row">
|
<div className="more-page-v2__recent-row">
|
||||||
{recentTools.map((tool) => (
|
{recentTools.map((tool) => (
|
||||||
@@ -183,10 +277,14 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
|
|||||||
key={tool.id}
|
key={tool.id}
|
||||||
type="button"
|
type="button"
|
||||||
className="more-card more-card--recent"
|
className="more-card more-card--recent"
|
||||||
|
aria-label={`打开最近使用工具:${tool.title}`}
|
||||||
onClick={() => openTool(tool)}
|
onClick={() => openTool(tool)}
|
||||||
>
|
>
|
||||||
<span className="more-card__icon">{tool.icon}</span>
|
<span className="more-card__icon">{tool.icon}</span>
|
||||||
|
<span className="more-card__recent-body">
|
||||||
<strong>{tool.title}</strong>
|
<strong>{tool.title}</strong>
|
||||||
|
<small>{categoryLabels[tool.category]}</small>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -204,23 +302,22 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
|
|||||||
key={tool.id}
|
key={tool.id}
|
||||||
type="button"
|
type="button"
|
||||||
className="more-card more-card--featured"
|
className="more-card more-card--featured"
|
||||||
style={{ "--card-gradient": tool.gradient } as React.CSSProperties}
|
style={{ "--card-gradient": tool.gradient } as CSSProperties}
|
||||||
onClick={() => {
|
aria-label={`打开核心工具:${tool.title},${tool.desc}`}
|
||||||
pushRecentToolId(tool.id);
|
onClick={() => openFeaturedTool(tool)}
|
||||||
setRecentIds(getRecentToolIds());
|
|
||||||
if (tool.imageTool && onOpenImageTool) {
|
|
||||||
onOpenImageTool(tool.imageTool);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (tool.target && onSelectView) {
|
|
||||||
onSelectView(tool.target);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span className="more-card__featured-icon">{tool.icon}</span>
|
<span className="more-card__featured-icon">{tool.icon}</span>
|
||||||
<div className="more-card__featured-body">
|
<div className="more-card__featured-body">
|
||||||
|
<span className="more-card__featured-kicker">{tool.kicker}</span>
|
||||||
<strong>{tool.title}</strong>
|
<strong>{tool.title}</strong>
|
||||||
<span>{tool.desc}</span>
|
<ToolComparePanel scene={toolCompareScenes[tool.id]} />
|
||||||
|
<span className="more-card__featured-desc">{tool.desc}</span>
|
||||||
|
<span className="more-card__steps" aria-hidden="true">
|
||||||
|
{tool.steps.map((step) => (
|
||||||
|
<span key={step}>{step}</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
<span className="more-card__outcome">{tool.outcome}</span>
|
||||||
<span className="more-card__cta">开始使用 →</span>
|
<span className="more-card__cta">开始使用 →</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -229,13 +326,14 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(["image", "video", "template"] as ToolCategory[]).map((cat) => {
|
{(["image", "video"] as ToolCategory[]).map((cat) => {
|
||||||
const group = groupedTools[cat];
|
const group = groupedTools[cat];
|
||||||
if (!group || group.length === 0) return null;
|
if (!group || group.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<section key={cat} className="more-page-v2__section">
|
<section key={cat} className="more-page-v2__section">
|
||||||
<h2 className="more-page-v2__section-title">
|
<h2 className="more-page-v2__section-title">
|
||||||
{categoryIcons[cat]} {categoryLabels[cat]}
|
{categoryIcons[cat]} {categoryLabels[cat]}
|
||||||
|
<span>{group.length}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="more-page-v2__grid">
|
<div className="more-page-v2__grid">
|
||||||
{group.map((tool) => (
|
{group.map((tool) => (
|
||||||
@@ -243,12 +341,21 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
|
|||||||
key={tool.id}
|
key={tool.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={`more-card${tool.ready ? " more-card--ready" : " more-card--pending"}`}
|
className={`more-card${tool.ready ? " more-card--ready" : " more-card--pending"}`}
|
||||||
|
aria-label={tool.ready ? `打开工具:${tool.title},${tool.text}` : `${tool.title}暂未开放`}
|
||||||
onClick={() => openTool(tool)}
|
onClick={() => openTool(tool)}
|
||||||
disabled={!tool.ready}
|
disabled={!tool.ready}
|
||||||
>
|
>
|
||||||
<span className="more-card__icon">{tool.icon}</span>
|
<span className="more-card__icon">{tool.icon}</span>
|
||||||
|
<span className="more-card__topline">
|
||||||
|
{tool.tags.slice(0, 2).map((tag) => (
|
||||||
|
<span key={tag}>{tag}</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
<strong>{tool.title}</strong>
|
<strong>{tool.title}</strong>
|
||||||
|
<ToolComparePanel scene={toolCompareScenes[tool.id]} />
|
||||||
<span className="more-card__desc">{tool.text}</span>
|
<span className="more-card__desc">{tool.text}</span>
|
||||||
|
<span className="more-card__use-case">{tool.useCase}</span>
|
||||||
|
<span className="more-card__action">打开工具 →</span>
|
||||||
{tool.badge && <span className="more-card__badge">{tool.badge}</span>}
|
{tool.badge && <span className="more-card__badge">{tool.badge}</span>}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -256,6 +363,21 @@ function MorePage({ onSelectView, onOpenImageTool }: MorePageProps) {
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{!hasGroupedTools && (
|
||||||
|
<section className="more-page-v2__section">
|
||||||
|
<div className="more-page-v2__empty">
|
||||||
|
<span className="more-page-v2__empty-icon">
|
||||||
|
<ClockCircleOutlined />
|
||||||
|
</span>
|
||||||
|
<strong>{activeFilterLabel}工具正在整理中</strong>
|
||||||
|
<p>当前分类暂无可展示入口,后续能力上线后会在这里集中呈现。</p>
|
||||||
|
<button type="button" className="more-page-v2__empty-action" onClick={() => setFilter("all")}>
|
||||||
|
查看全部可用工具
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
CheckCircleFilled,
|
CheckCircleFilled,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FileImageOutlined,
|
FileImageOutlined,
|
||||||
FolderOpenOutlined,
|
FolderOpenOutlined,
|
||||||
@@ -17,7 +18,8 @@ import {
|
|||||||
ShareAltOutlined,
|
ShareAltOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useEffect, useRef, useState, type ChangeEvent, type FormEvent } from "react";
|
import { useEffect, useRef, useState, type ChangeEvent, type FormEvent, type KeyboardEvent } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||||
import { assetClient } from "../../api/assetClient";
|
import { assetClient } from "../../api/assetClient";
|
||||||
import { communityClient, type ServerCommunityCase } from "../../api/communityClient";
|
import { communityClient, type ServerCommunityCase } from "../../api/communityClient";
|
||||||
@@ -26,6 +28,7 @@ import { isServerRequestError } from "../../api/serverConnection";
|
|||||||
import { ossAssets } from "../../data/ossAssets";
|
import { ossAssets } from "../../data/ossAssets";
|
||||||
import type { WebAuthMode, WebGenerationPreviewTask, WebProjectSummary, WebUsageSummary, WebUserSession } from "../../types";
|
import type { WebAuthMode, WebGenerationPreviewTask, WebProjectSummary, WebUsageSummary, WebUserSession } from "../../types";
|
||||||
import type { SavedAssetItem } from "../assets/localAssetStore";
|
import type { SavedAssetItem } from "../assets/localAssetStore";
|
||||||
|
import { downloadResultAsset } from "../workbench/workbenchDownload";
|
||||||
|
|
||||||
interface ProfilePageProps {
|
interface ProfilePageProps {
|
||||||
session: WebUserSession | null;
|
session: WebUserSession | null;
|
||||||
@@ -41,11 +44,16 @@ interface ProfilePageProps {
|
|||||||
onOpenWorkbench: () => void;
|
onOpenWorkbench: () => void;
|
||||||
onOpenCommunity: () => void;
|
onOpenCommunity: () => void;
|
||||||
onDeleteProject?: (project: WebProjectSummary) => void;
|
onDeleteProject?: (project: WebProjectSummary) => void;
|
||||||
|
onOpenProject?: (project: WebProjectSummary) => void;
|
||||||
|
onRemoveWork?: (task: WebGenerationPreviewTask) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthTab = "password" | "email" | "phone";
|
type AuthTab = "password" | "email" | "phone";
|
||||||
type ProfilePanel = "works" | "projects" | "assets" | "community";
|
type ProfilePanel = "works" | "projects" | "assets" | "community";
|
||||||
type AccountPanel = "credits" | "tasks";
|
type AccountPanel = "credits" | "tasks";
|
||||||
|
type ProfileDetailSelection =
|
||||||
|
| { kind: "work"; item: WebGenerationPreviewTask }
|
||||||
|
| { kind: "asset"; item: SavedAssetItem };
|
||||||
|
|
||||||
const PROFILE_LOCAL_STORAGE_PREFIX = "omniai-web-profile-ui";
|
const PROFILE_LOCAL_STORAGE_PREFIX = "omniai-web-profile-ui";
|
||||||
const AUTH_LOGO_URL = ossAssets.brand.logo;
|
const AUTH_LOGO_URL = ossAssets.brand.logo;
|
||||||
@@ -210,6 +218,8 @@ function ProfilePage({
|
|||||||
onOpenWorkbench,
|
onOpenWorkbench,
|
||||||
onOpenCommunity,
|
onOpenCommunity,
|
||||||
onDeleteProject,
|
onDeleteProject,
|
||||||
|
onOpenProject,
|
||||||
|
onRemoveWork,
|
||||||
}: ProfilePageProps) {
|
}: ProfilePageProps) {
|
||||||
const isLoggedIn = Boolean(session);
|
const isLoggedIn = Boolean(session);
|
||||||
const userId = session?.user.id;
|
const userId = session?.user.id;
|
||||||
@@ -253,6 +263,10 @@ function ProfilePage({
|
|||||||
const [bioEditBackup, setBioEditBackup] = useState("");
|
const [bioEditBackup, setBioEditBackup] = useState("");
|
||||||
const [bioStatusNotice, setBioStatusNotice] = useState<string | null>(null);
|
const [bioStatusNotice, setBioStatusNotice] = useState<string | null>(null);
|
||||||
const [bannerUrl, setBannerUrl] = useState(() => session?.user.backgroundUrl || readLocalProfileValue(userId, "background"));
|
const [bannerUrl, setBannerUrl] = useState(() => session?.user.backgroundUrl || readLocalProfileValue(userId, "background"));
|
||||||
|
const [detailSelection, setDetailSelection] = useState<ProfileDetailSelection | null>(null);
|
||||||
|
const [detailNotice, setDetailNotice] = useState<string | null>(null);
|
||||||
|
const [isDeletingDetail, setIsDeletingDetail] = useState(false);
|
||||||
|
const [isDownloadingDetail, setIsDownloadingDetail] = useState(false);
|
||||||
|
|
||||||
const completedTasks = tasks.filter((task) => task.status === "completed");
|
const completedTasks = tasks.filter((task) => task.status === "completed");
|
||||||
const visibleWorks = completedTasks.length ? completedTasks : tasks.slice(0, 6);
|
const visibleWorks = completedTasks.length ? completedTasks : tasks.slice(0, 6);
|
||||||
@@ -260,6 +274,30 @@ function ProfilePage({
|
|||||||
const packageLabel = session?.user.activePackages?.[0]?.name || "按量积分";
|
const packageLabel = session?.user.activePackages?.[0]?.name || "按量积分";
|
||||||
const avatarUrl = session?.user.avatarUrl || localAvatarUrl || null;
|
const avatarUrl = session?.user.avatarUrl || localAvatarUrl || null;
|
||||||
const displayedBio = profileBio.trim() || "这个人还没有填写个性签名";
|
const displayedBio = profileBio.trim() || "这个人还没有填写个性签名";
|
||||||
|
const activePanelTitle =
|
||||||
|
activePanel === "works"
|
||||||
|
? "代表作"
|
||||||
|
: activePanel === "projects"
|
||||||
|
? "服务器项目"
|
||||||
|
: activePanel === "assets"
|
||||||
|
? "我的资产"
|
||||||
|
: "社区审核";
|
||||||
|
const activePanelDescription =
|
||||||
|
activePanel === "works"
|
||||||
|
? "最近完成的高质量生成内容"
|
||||||
|
: activePanel === "projects"
|
||||||
|
? "云端同步的创作项目"
|
||||||
|
: activePanel === "assets"
|
||||||
|
? "可复用的图片、视频与素材"
|
||||||
|
: "已提交社区的案例状态";
|
||||||
|
const activePanelCount =
|
||||||
|
activePanel === "works"
|
||||||
|
? visibleWorks.length
|
||||||
|
: activePanel === "projects"
|
||||||
|
? projects.length
|
||||||
|
: activePanel === "assets"
|
||||||
|
? savedAssets.length
|
||||||
|
: communityCases.length;
|
||||||
const emailLooksValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
|
const emailLooksValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
|
||||||
const phoneLooksValid = /^1[3-9]\d{9}$/.test(phone.trim());
|
const phoneLooksValid = /^1[3-9]\d{9}$/.test(phone.trim());
|
||||||
const passwordLooksReady = password.length >= (mode === "register" ? 6 : 1);
|
const passwordLooksReady = password.length >= (mode === "register" ? 6 : 1);
|
||||||
@@ -611,6 +649,213 @@ function ProfilePage({
|
|||||||
setBioStatusNotice(null);
|
setBioStatusNotice(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInteractiveCardKeyDown = (event: KeyboardEvent<HTMLElement>, action: () => void) => {
|
||||||
|
if (event.target !== event.currentTarget) return;
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return;
|
||||||
|
event.preventDefault();
|
||||||
|
action();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDetailSelection = (selection: ProfileDetailSelection) => {
|
||||||
|
setDetailNotice(null);
|
||||||
|
setIsDeletingDetail(false);
|
||||||
|
setIsDownloadingDetail(false);
|
||||||
|
setDetailSelection(selection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDetailSelection = () => {
|
||||||
|
if (isDeletingDetail || isDownloadingDetail) return;
|
||||||
|
setDetailSelection(null);
|
||||||
|
setDetailNotice(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!detailSelection) return undefined;
|
||||||
|
const handleKeyDown = (event: globalThis.KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") closeDetailSelection();
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [detailSelection, isDeletingDetail, isDownloadingDetail]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!detailSelection || typeof document === "undefined") return undefined;
|
||||||
|
|
||||||
|
const { body, documentElement } = document;
|
||||||
|
const previousBodyOverflow = body.style.overflow;
|
||||||
|
const previousRootOverscroll = documentElement.style.overscrollBehavior;
|
||||||
|
|
||||||
|
body.style.overflow = "hidden";
|
||||||
|
documentElement.style.overscrollBehavior = "contain";
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
body.style.overflow = previousBodyOverflow;
|
||||||
|
documentElement.style.overscrollBehavior = previousRootOverscroll;
|
||||||
|
};
|
||||||
|
}, [detailSelection]);
|
||||||
|
|
||||||
|
const handleDownloadSelectedDetail = async () => {
|
||||||
|
if (!detailSelection || isDownloadingDetail) return;
|
||||||
|
|
||||||
|
const isWork = detailSelection.kind === "work";
|
||||||
|
const item = detailSelection.item;
|
||||||
|
const url = isWork ? item.outputUrl : item.imageUrl || item.url || "";
|
||||||
|
if (!url) {
|
||||||
|
setDetailNotice("暂无可下载的媒体文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVideo = isWork ? item.type === "video" : item.type === "video" || /\.(mp4|webm|mov|m4v)(\?|#|$)/i.test(url);
|
||||||
|
const taskId = isWork ? item.id : item.sourceTaskId || undefined;
|
||||||
|
const name = isWork ? item.title : item.name;
|
||||||
|
|
||||||
|
setIsDownloadingDetail(true);
|
||||||
|
setDetailNotice("正在准备下载...");
|
||||||
|
try {
|
||||||
|
const status = await downloadResultAsset(url, name, isVideo, taskId);
|
||||||
|
setDetailNotice(status === "saved" ? "已保存到本地" : "已开始保存到本地");
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DOMException && error.name === "AbortError") {
|
||||||
|
setDetailNotice("已取消下载");
|
||||||
|
} else {
|
||||||
|
setDetailNotice(error instanceof Error ? error.message : "下载失败,请稍后重试");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsDownloadingDetail(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteSelectedDetail = async () => {
|
||||||
|
if (!detailSelection || isDeletingDetail) return;
|
||||||
|
|
||||||
|
if (detailSelection.kind === "work") {
|
||||||
|
onRemoveWork?.(detailSelection.item);
|
||||||
|
setDetailNotice("已从当前代表作列表移除");
|
||||||
|
setDetailSelection(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeletingDetail(true);
|
||||||
|
setDetailNotice(null);
|
||||||
|
try {
|
||||||
|
await assetClient.delete(detailSelection.item.id, { cleanupUserData: true });
|
||||||
|
setSavedAssets((current) => current.filter((asset) => asset.id !== detailSelection.item.id));
|
||||||
|
setDetailSelection(null);
|
||||||
|
setAssetNotice(`已删除 ${detailSelection.item.name}`);
|
||||||
|
} catch (error) {
|
||||||
|
setDetailNotice(formatProfileLoadError(error, "资产删除失败"));
|
||||||
|
} finally {
|
||||||
|
setIsDeletingDetail(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDetailMedia = (url: string | null | undefined, type: "image" | "video" | "asset") => {
|
||||||
|
const mediaUrl = typeof url === "string" ? url.trim() : "";
|
||||||
|
const isVideoPreview = type === "video" || /\.(mp4|webm|mov|m4v)(\?|#|$)/i.test(mediaUrl);
|
||||||
|
|
||||||
|
if (!mediaUrl) {
|
||||||
|
return (
|
||||||
|
<div className="profile-page__detail-placeholder">
|
||||||
|
{type === "video" ? <PlayCircleOutlined /> : <FileImageOutlined />}
|
||||||
|
<span>暂无可预览内容</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isVideoPreview ? (
|
||||||
|
<video className="profile-page__detail-media" src={mediaUrl} controls playsInline />
|
||||||
|
) : (
|
||||||
|
<img className="profile-page__detail-media" src={mediaUrl} alt="" />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDetailModal = () => {
|
||||||
|
if (!detailSelection) return null;
|
||||||
|
const modalTarget = typeof document === "undefined" ? null : document.querySelector(".web-shell") || document.body;
|
||||||
|
if (!modalTarget) return null;
|
||||||
|
|
||||||
|
const isWork = detailSelection.kind === "work";
|
||||||
|
const title = isWork ? detailSelection.item.title : detailSelection.item.name;
|
||||||
|
const description = isWork ? detailSelection.item.prompt : detailSelection.item.description;
|
||||||
|
const mediaUrl = isWork ? detailSelection.item.outputUrl : detailSelection.item.imageUrl || detailSelection.item.url;
|
||||||
|
const mediaType = isWork
|
||||||
|
? detailSelection.item.type === "video" ? "video" : "image"
|
||||||
|
: detailSelection.item.type === "video" ? "video" : "asset";
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="profile-page__detail-overlay" role="dialog" aria-modal="true" aria-labelledby="profile-detail-title">
|
||||||
|
<button type="button" className="profile-page__detail-backdrop" aria-label="关闭详情" onClick={closeDetailSelection} />
|
||||||
|
<section className="profile-page__detail-panel">
|
||||||
|
<header className="profile-page__detail-head">
|
||||||
|
<div>
|
||||||
|
<span className="profile-page__detail-eyebrow">{isWork ? "代表作详情" : "资产详情"}</span>
|
||||||
|
<h2 id="profile-detail-title">{title}</h2>
|
||||||
|
</div>
|
||||||
|
<button type="button" className="profile-page__detail-close" aria-label="关闭详情" onClick={closeDetailSelection}>
|
||||||
|
<CloseOutlined />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="profile-page__detail-body">
|
||||||
|
<div className="profile-page__detail-preview">
|
||||||
|
{renderDetailMedia(mediaUrl, mediaType)}
|
||||||
|
</div>
|
||||||
|
<div className="profile-page__detail-info">
|
||||||
|
<p>{description || "暂无描述"}</p>
|
||||||
|
<dl>
|
||||||
|
<div>
|
||||||
|
<dt>{isWork ? "类型" : "资产类型"}</dt>
|
||||||
|
<dd>{isWork ? formatTaskType(detailSelection.item.type) : formatAssetType(detailSelection.item.type)}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>状态</dt>
|
||||||
|
<dd>{isWork ? formatTaskStatus(detailSelection.item.status) : formatAssetStatus(detailSelection.item.status)}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>{isWork ? "创建时间" : "更新时间"}</dt>
|
||||||
|
<dd>{formatProfileDate(isWork ? detailSelection.item.createdAt : detailSelection.item.updatedAt)}</dd>
|
||||||
|
</div>
|
||||||
|
{!isWork ? (
|
||||||
|
<div>
|
||||||
|
<dt>标签</dt>
|
||||||
|
<dd>{detailSelection.item.tags?.length ? detailSelection.item.tags.join(" / ") : "服务器素材"}</dd>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</dl>
|
||||||
|
{detailNotice ? <span className="profile-page__detail-notice">{detailNotice}</span> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="profile-page__detail-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="profile-page__detail-action profile-page__detail-action--primary"
|
||||||
|
onClick={() => void handleDownloadSelectedDetail()}
|
||||||
|
disabled={isDownloadingDetail}
|
||||||
|
>
|
||||||
|
<DownloadOutlined />
|
||||||
|
{isDownloadingDetail ? "下载中..." : "下载"}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="profile-page__detail-action profile-page__detail-action--secondary" onClick={onOpenWorkbench}>
|
||||||
|
<EditOutlined />
|
||||||
|
{isWork ? "继续编辑" : "使用素材"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="profile-page__detail-action profile-page__detail-action--danger"
|
||||||
|
onClick={() => void handleDeleteSelectedDetail()}
|
||||||
|
disabled={isDeletingDetail}
|
||||||
|
>
|
||||||
|
<DeleteOutlined />
|
||||||
|
{isDeletingDetail ? "删除中..." : isWork ? "移除代表作" : "删除资产"}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
</div>,
|
||||||
|
modalTarget,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderEmptyState = (text: string, actionLabel: string, action: () => void) => (
|
const renderEmptyState = (text: string, actionLabel: string, action: () => void) => (
|
||||||
<div className="profile-page__empty-state">
|
<div className="profile-page__empty-state">
|
||||||
<span className="profile-page__empty-mark" aria-hidden="true">
|
<span className="profile-page__empty-mark" aria-hidden="true">
|
||||||
@@ -656,7 +901,15 @@ function ProfilePage({
|
|||||||
<div className="profile-page__works-scroll">
|
<div className="profile-page__works-scroll">
|
||||||
<div className="profile-page__list-grid motion-stagger">
|
<div className="profile-page__list-grid motion-stagger">
|
||||||
{visibleWorks.map((task) => (
|
{visibleWorks.map((task) => (
|
||||||
<article key={task.id} className="profile-page__list-card profile-page__media-card">
|
<article
|
||||||
|
key={task.id}
|
||||||
|
className="profile-page__list-card profile-page__media-card profile-page__interactive-card"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`查看代表作 ${task.title}`}
|
||||||
|
onClick={() => openDetailSelection({ kind: "work", item: task })}
|
||||||
|
onKeyDown={(event) => handleInteractiveCardKeyDown(event, () => openDetailSelection({ kind: "work", item: task }))}
|
||||||
|
>
|
||||||
{renderCardPreview(task.outputUrl, task.type === "video" ? "video" : "image", formatTaskType(task.type))}
|
{renderCardPreview(task.outputUrl, task.type === "video" ? "video" : "image", formatTaskType(task.type))}
|
||||||
<div className="profile-page__list-card-body">
|
<div className="profile-page__list-card-body">
|
||||||
<div className="profile-page__list-card-head">
|
<div className="profile-page__list-card-head">
|
||||||
@@ -681,7 +934,17 @@ function ProfilePage({
|
|||||||
return projects.length ? (
|
return projects.length ? (
|
||||||
<div className="profile-page__list-grid motion-stagger">
|
<div className="profile-page__list-grid motion-stagger">
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<article key={project.id} className="profile-page__list-card profile-page__media-card">
|
<article
|
||||||
|
key={project.id}
|
||||||
|
className="profile-page__list-card profile-page__media-card profile-page__interactive-card"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`打开项目 ${project.name}`}
|
||||||
|
onClick={() => (onOpenProject ? onOpenProject(project) : onOpenWorkbench())}
|
||||||
|
onKeyDown={(event) =>
|
||||||
|
handleInteractiveCardKeyDown(event, () => (onOpenProject ? onOpenProject(project) : onOpenWorkbench()))
|
||||||
|
}
|
||||||
|
>
|
||||||
{renderCardPreview(project.thumbnailUrl, "project", "项目")}
|
{renderCardPreview(project.thumbnailUrl, "project", "项目")}
|
||||||
<div className="profile-page__list-card-body">
|
<div className="profile-page__list-card-body">
|
||||||
<div className="profile-page__list-card-head">
|
<div className="profile-page__list-card-head">
|
||||||
@@ -691,7 +954,10 @@ function ProfilePage({
|
|||||||
type="button"
|
type="button"
|
||||||
className="profile-page__delete-project"
|
className="profile-page__delete-project"
|
||||||
aria-label={`删除项目 ${project.name}`}
|
aria-label={`删除项目 ${project.name}`}
|
||||||
onClick={() => onDeleteProject(project)}
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onDeleteProject(project);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</button>
|
</button>
|
||||||
@@ -715,7 +981,15 @@ function ProfilePage({
|
|||||||
return savedAssets.length ? (
|
return savedAssets.length ? (
|
||||||
<div className="profile-page__list-grid">
|
<div className="profile-page__list-grid">
|
||||||
{savedAssets.map((asset) => (
|
{savedAssets.map((asset) => (
|
||||||
<article key={asset.id} className="profile-page__list-card profile-page__media-card">
|
<article
|
||||||
|
key={asset.id}
|
||||||
|
className="profile-page__list-card profile-page__media-card profile-page__interactive-card"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`查看资产 ${asset.name}`}
|
||||||
|
onClick={() => openDetailSelection({ kind: "asset", item: asset })}
|
||||||
|
onKeyDown={(event) => handleInteractiveCardKeyDown(event, () => openDetailSelection({ kind: "asset", item: asset }))}
|
||||||
|
>
|
||||||
{renderCardPreview(asset.imageUrl || asset.url, asset.type === "video" ? "video" : "asset", formatAssetType(asset.type))}
|
{renderCardPreview(asset.imageUrl || asset.url, asset.type === "video" ? "video" : "asset", formatAssetType(asset.type))}
|
||||||
<div className="profile-page__list-card-body">
|
<div className="profile-page__list-card-body">
|
||||||
<div className="profile-page__list-card-head">
|
<div className="profile-page__list-card-head">
|
||||||
@@ -765,9 +1039,9 @@ function ProfilePage({
|
|||||||
className={`profile-page__banner${bannerUrl ? " has-image" : ""}`}
|
className={`profile-page__banner${bannerUrl ? " has-image" : ""}`}
|
||||||
style={bannerUrl ? { backgroundImage: `url(${bannerUrl})` } : undefined}
|
style={bannerUrl ? { backgroundImage: `url(${bannerUrl})` } : undefined}
|
||||||
>
|
>
|
||||||
<button type="button" className="profile-page__banner-btn" onClick={() => bannerInputRef.current?.click()}>
|
<button type="button" className="profile-page__banner-btn" onClick={() => bannerInputRef.current?.click()} aria-label="更换背景">
|
||||||
<CameraOutlined />
|
<CameraOutlined />
|
||||||
更换背景
|
<span className="profile-page__banner-btn-label">更换背景</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="profile-page__banner-overlay" />
|
<div className="profile-page__banner-overlay" />
|
||||||
</header>
|
</header>
|
||||||
@@ -847,14 +1121,16 @@ function ProfilePage({
|
|||||||
className={accountPanel === "credits" ? "is-active" : ""}
|
className={accountPanel === "credits" ? "is-active" : ""}
|
||||||
onClick={() => setAccountPanel("credits")}
|
onClick={() => setAccountPanel("credits")}
|
||||||
>
|
>
|
||||||
积分 {(totalBalance / 100).toFixed(2)}
|
<span>可用积分</span>
|
||||||
|
<strong>{(totalBalance / 100).toFixed(2)}</strong>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={accountPanel === "tasks" ? "is-active" : ""}
|
className={accountPanel === "tasks" ? "is-active" : ""}
|
||||||
onClick={() => setAccountPanel("tasks")}
|
onClick={() => setAccountPanel("tasks")}
|
||||||
>
|
>
|
||||||
任务 {tasks.length}
|
<span>生成任务</span>
|
||||||
|
<strong>{tasks.length}</strong>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="profile-page__account-summary">
|
<div className="profile-page__account-summary">
|
||||||
@@ -863,6 +1139,7 @@ function ProfilePage({
|
|||||||
<span className="profile-page__account-summary-main">
|
<span className="profile-page__account-summary-main">
|
||||||
<small>当前账号</small>
|
<small>当前账号</small>
|
||||||
<strong>{displayName}</strong>
|
<strong>{displayName}</strong>
|
||||||
|
<em>{packageLabel}</em>
|
||||||
</span>
|
</span>
|
||||||
<span className="profile-page__account-summary-metric">
|
<span className="profile-page__account-summary-metric">
|
||||||
<small>积分剩余</small>
|
<small>积分剩余</small>
|
||||||
@@ -874,6 +1151,7 @@ function ProfilePage({
|
|||||||
<span className="profile-page__account-summary-main">
|
<span className="profile-page__account-summary-main">
|
||||||
<small>任务概览</small>
|
<small>任务概览</small>
|
||||||
<strong>{tasks.length} 个任务</strong>
|
<strong>{tasks.length} 个任务</strong>
|
||||||
|
<em>{completedTasks.length} 个已完成</em>
|
||||||
</span>
|
</span>
|
||||||
<span className="profile-page__account-summary-metric">
|
<span className="profile-page__account-summary-metric">
|
||||||
<small>已完成</small>
|
<small>已完成</small>
|
||||||
@@ -884,6 +1162,7 @@ function ProfilePage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="profile-page__actions">
|
||||||
<button type="button" className="profile-page__share-btn profile-page__share-btn--plan">
|
<button type="button" className="profile-page__share-btn profile-page__share-btn--plan">
|
||||||
<ShareAltOutlined />
|
<ShareAltOutlined />
|
||||||
{packageLabel}
|
{packageLabel}
|
||||||
@@ -901,38 +1180,36 @@ function ProfilePage({
|
|||||||
<LockOutlined />
|
<LockOutlined />
|
||||||
退出登录
|
退出登录
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="profile-page__main">
|
<main className="profile-page__main">
|
||||||
<div className="profile-page__main-tabs">
|
<div className="profile-page__main-tabs">
|
||||||
<button type="button" className={activePanel === "works" ? "is-active" : ""} onClick={() => setActivePanel("works")}>
|
<button type="button" className={activePanel === "works" ? "is-active" : ""} onClick={() => setActivePanel("works")}>
|
||||||
我的作品
|
<span>我的作品</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={activePanel === "projects" ? "is-active" : ""} onClick={() => setActivePanel("projects")}>
|
<button type="button" className={activePanel === "projects" ? "is-active" : ""} onClick={() => setActivePanel("projects")}>
|
||||||
我的项目
|
<span>我的项目</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={activePanel === "assets" ? "is-active" : ""} onClick={() => setActivePanel("assets")}>
|
<button type="button" className={activePanel === "assets" ? "is-active" : ""} onClick={() => setActivePanel("assets")}>
|
||||||
我的资产
|
<span>我的资产</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={activePanel === "community" ? "is-active" : ""} onClick={() => setActivePanel("community")}>
|
<button type="button" className={activePanel === "community" ? "is-active" : ""} onClick={() => setActivePanel("community")}>
|
||||||
社区发布
|
<span>社区发布</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="profile-page__section">
|
<div className="profile-page__section">
|
||||||
<span className="profile-page__section-label">
|
<div className="profile-page__section-head">
|
||||||
{activePanel === "works"
|
<span className="profile-page__section-label">{activePanelTitle}</span>
|
||||||
? "代表作"
|
<span className="profile-page__section-desc">{activePanelDescription}</span>
|
||||||
: activePanel === "projects"
|
<span className="profile-page__section-meta">{activePanelCount} 项</span>
|
||||||
? "服务器项目"
|
</div>
|
||||||
: activePanel === "assets"
|
|
||||||
? "我的资产"
|
|
||||||
: "社区审核"}
|
|
||||||
</span>
|
|
||||||
{renderActivePanel()}
|
{renderActivePanel()}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
{renderDetailModal()}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState, type DragEvent } from "react";
|
||||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||||
import { waitForTask } from "../../api/taskSubscription";
|
import { waitForTask } from "../../api/taskSubscription";
|
||||||
import { saveToolTaskState, loadToolTaskState, clearToolTaskState } from "../workbench/toolKeepalive";
|
import { saveToolTaskState, loadToolTaskState, clearToolTaskState } from "../workbench/toolKeepalive";
|
||||||
@@ -87,6 +87,7 @@ function ResolutionUpscalePage({
|
|||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
const [isSavingAsset, setIsSavingAsset] = useState(false);
|
const [isSavingAsset, setIsSavingAsset] = useState(false);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const activeTaskIdRef = useRef(activeTaskId);
|
const activeTaskIdRef = useRef(activeTaskId);
|
||||||
activeTaskIdRef.current = activeTaskId;
|
activeTaskIdRef.current = activeTaskId;
|
||||||
const keepaliveRestoredRef = useRef(false);
|
const keepaliveRestoredRef = useRef(false);
|
||||||
@@ -164,6 +165,24 @@ function ResolutionUpscalePage({
|
|||||||
event.currentTarget.value = "";
|
event.currentTarget.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processDroppedFile = (file: File) => {
|
||||||
|
if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview);
|
||||||
|
setSourceName(file.name);
|
||||||
|
setSourceFile(file);
|
||||||
|
setSourceUrl("");
|
||||||
|
setSourcePreview(URL.createObjectURL(file));
|
||||||
|
setResultPreview("");
|
||||||
|
setSourceDimensions(null);
|
||||||
|
setVideoViewMode("source");
|
||||||
|
setActiveTaskId("");
|
||||||
|
setTaskProgress(0);
|
||||||
|
setStatus(`已导入 ${file.name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes("Files")) setIsDragging(true); };
|
||||||
|
const handleDragLeave = (e: DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) setIsDragging(false); };
|
||||||
|
const handleDrop = (e: DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file) processDroppedFile(file); };
|
||||||
|
|
||||||
const handleImportUrl = () => {
|
const handleImportUrl = () => {
|
||||||
const normalizedUrl = sourceUrl.trim();
|
const normalizedUrl = sourceUrl.trim();
|
||||||
if (!/^https?:\/\//i.test(normalizedUrl)) {
|
if (!/^https?:\/\//i.test(normalizedUrl)) {
|
||||||
@@ -405,7 +424,13 @@ function ResolutionUpscalePage({
|
|||||||
accept={mode === "image" ? "image/png,image/jpeg,image/webp" : "video/mp4,video/quicktime,video/webm,video/*"}
|
accept={mode === "image" ? "image/png,image/jpeg,image/webp" : "video/mp4,video/quicktime,video/webm,video/*"}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
<div className="image-workbench-upload-shell">
|
<div
|
||||||
|
className={`image-workbench-upload-shell${isDragging ? " is-dragging" : ""}`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
{isDragging ? <div className="image-workbench-upload-drop-overlay"><span>释放文件以上传</span></div> : null}
|
||||||
<button type="button" className="image-workbench-upload" onClick={() => fileInputRef.current?.click()}>
|
<button type="button" className="image-workbench-upload" onClick={() => fileInputRef.current?.click()}>
|
||||||
{sourcePreview && mode === "image" ? <img src={sourcePreview} alt="" /> : <FileImageOutlined />}
|
{sourcePreview && mode === "image" ? <img src={sourcePreview} alt="" /> : <FileImageOutlined />}
|
||||||
<strong>{sourceName || (mode === "image" ? "选择图片" : "选择视频")}</strong>
|
<strong>{sourceName || (mode === "image" ? "选择图片" : "选择视频")}</strong>
|
||||||
@@ -574,11 +599,13 @@ function ResolutionUpscalePage({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<button type="button" className="image-workbench-empty image-workbench-empty--button" onClick={() => fileInputRef.current?.click()}>
|
<div className="studio-canvas-ghost">
|
||||||
<ColumnWidthOutlined />
|
<div className="studio-canvas-ghost__icon">
|
||||||
<strong>{mode === "image" ? "拖拽或选择图片" : "拖拽或选择视频"}</strong>
|
<ThunderboltOutlined />
|
||||||
<span>{mode === "image" ? "支持 PNG / JPG / WebP" : "支持 MP4 / MOV / WebM"}</span>
|
</div>
|
||||||
</button>
|
<div className="studio-canvas-ghost__title">{mode === "image" ? "拖拽或选择图片" : "拖拽或选择视频"}</div>
|
||||||
|
<div className="studio-canvas-ghost__hint">{mode === "image" ? "支持 PNG / JPG / WebP" : "支持 MP4 / MOV / WebM"}</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
CheckCircleFilled,
|
CheckCircleFilled,
|
||||||
|
CloseOutlined,
|
||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
@@ -8,7 +9,7 @@ import {
|
|||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useEffect, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
|
import { useEffect, useRef, useState, type ChangeEvent, type DragEvent, type KeyboardEvent } from "react";
|
||||||
import { evaluateScript } from "../../api/scriptEvalClient";
|
import { evaluateScript } from "../../api/scriptEvalClient";
|
||||||
import { buildApiUrl, getStoredToken } from "../../api/serverConnection";
|
import { buildApiUrl, getStoredToken } from "../../api/serverConnection";
|
||||||
import { useSessionStore } from "../../stores";
|
import { useSessionStore } from "../../stores";
|
||||||
@@ -302,6 +303,7 @@ function ScriptTokensPage() {
|
|||||||
const [animatedScore, setAnimatedScore] = useState(0);
|
const [animatedScore, setAnimatedScore] = useState(0);
|
||||||
const [activeHistoryIndex, setActiveHistoryIndex] = useState<number>(0);
|
const [activeHistoryIndex, setActiveHistoryIndex] = useState<number>(0);
|
||||||
const [history, setHistory] = useState<HistoryEntry[]>(loadHistory);
|
const [history, setHistory] = useState<HistoryEntry[]>(loadHistory);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const scoreFrameRef = useRef<number | null>(null);
|
const scoreFrameRef = useRef<number | null>(null);
|
||||||
|
|
||||||
@@ -324,9 +326,7 @@ function ScriptTokensPage() {
|
|||||||
return () => { if (scoreFrameRef.current) cancelAnimationFrame(scoreFrameRef.current); };
|
return () => { if (scoreFrameRef.current) cancelAnimationFrame(scoreFrameRef.current); };
|
||||||
}, [result]);
|
}, [result]);
|
||||||
|
|
||||||
const handleFileUpload = async (event: ChangeEvent<HTMLInputElement>) => {
|
const processUploadedFile = async (file: File) => {
|
||||||
const file = event.target.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
const ext = getFileExtension(file.name);
|
const ext = getFileExtension(file.name);
|
||||||
const readable = isReadableTextFile(file, ext);
|
const readable = isReadableTextFile(file, ext);
|
||||||
setUploadedFile({ name: file.name, size: file.size });
|
setUploadedFile({ name: file.name, size: file.size });
|
||||||
@@ -356,6 +356,12 @@ function ScriptTokensPage() {
|
|||||||
} else {
|
} else {
|
||||||
setScript(`[已上传文件:${file.name}]\n\n暂不支持解析 ${ext ? ext.toUpperCase() : "未知"} 格式,请上传常见文本类文件。`);
|
setScript(`[已上传文件:${file.name}]\n\n暂不支持解析 ${ext ? ext.toUpperCase() : "未知"} 格式,请上传常见文本类文件。`);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
await processUploadedFile(file);
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -456,6 +462,30 @@ function ScriptTokensPage() {
|
|||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.dataTransfer.types.includes("Files")) {
|
||||||
|
setIsDragging(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.currentTarget === event.target || !event.currentTarget.contains(event.relatedTarget as Node)) {
|
||||||
|
setIsDragging(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
const file = event.dataTransfer.files[0];
|
||||||
|
if (file) processUploadedFile(file);
|
||||||
|
};
|
||||||
|
|
||||||
const grade = result ? getGrade(result.totalScore) : null;
|
const grade = result ? getGrade(result.totalScore) : null;
|
||||||
const beatPct = result ? (result.totalScore >= 95 ? 97 : result.totalScore >= 88 ? 92 : result.totalScore >= 80 ? 85 : 72) : 0;
|
const beatPct = result ? (result.totalScore >= 95 ? 97 : result.totalScore >= 88 ? 92 : result.totalScore >= 80 ? 85 : 72) : 0;
|
||||||
const compactTitle = uploadedFile?.name?.replace(/\.[^.]+$/, "") ?? "剧本评测";
|
const compactTitle = uploadedFile?.name?.replace(/\.[^.]+$/, "") ?? "剧本评测";
|
||||||
@@ -472,14 +502,31 @@ function ScriptTokensPage() {
|
|||||||
<div className="script-eval-v5-lp-section">
|
<div className="script-eval-v5-lp-section">
|
||||||
<div className="script-eval-v5-lp-label">上传剧本</div>
|
<div className="script-eval-v5-lp-label">上传剧本</div>
|
||||||
<div
|
<div
|
||||||
className="script-eval-v5-upload-zone"
|
className={`script-eval-v5-upload-zone${isDragging ? " is-dragging" : ""}`}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
onKeyDown={uploadKeyDown}
|
onKeyDown={uploadKeyDown}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
|
{isDragging ? (
|
||||||
|
<div className="script-eval-v5-upload-drop-overlay">
|
||||||
|
<UploadOutlined />
|
||||||
|
<span>释放文件以上传</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{uploadedFile ? (
|
{uploadedFile ? (
|
||||||
<div className="script-eval-v5-upload-done is-show">
|
<div className="script-eval-v5-upload-done is-show">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="script-eval-v5-upload-delete"
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleReset(); }}
|
||||||
|
aria-label="删除文件"
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</button>
|
||||||
<CheckCircleFilled />
|
<CheckCircleFilled />
|
||||||
<span className="script-eval-v5-uf-meta">
|
<span className="script-eval-v5-uf-meta">
|
||||||
<span className="script-eval-v5-uf-name">{uploadedFile.name}</span>
|
<span className="script-eval-v5-uf-name">{uploadedFile.name}</span>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
SwapOutlined,
|
SwapOutlined,
|
||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react";
|
import { useCallback, useEffect, useRef, useState, type CSSProperties, type DragEvent } from "react";
|
||||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||||
import { waitForTask } from "../../api/taskSubscription";
|
import { waitForTask } from "../../api/taskSubscription";
|
||||||
import { saveToolTaskState, loadToolTaskState, clearToolTaskState } from "../workbench/toolKeepalive";
|
import { saveToolTaskState, loadToolTaskState, clearToolTaskState } from "../workbench/toolKeepalive";
|
||||||
@@ -73,6 +73,7 @@ function SubtitleRemovalPage({
|
|||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
const [isSavingAsset, setIsSavingAsset] = useState(false);
|
const [isSavingAsset, setIsSavingAsset] = useState(false);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const activeTaskIdRef = useRef(activeTaskId);
|
const activeTaskIdRef = useRef(activeTaskId);
|
||||||
activeTaskIdRef.current = activeTaskId;
|
activeTaskIdRef.current = activeTaskId;
|
||||||
const keepaliveRestoredRef = useRef(false);
|
const keepaliveRestoredRef = useRef(false);
|
||||||
@@ -125,10 +126,7 @@ function SubtitleRemovalPage({
|
|||||||
event.currentTarget.value = "";
|
event.currentTarget.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileDrop = (event: React.DragEvent) => {
|
const processDroppedFile = (file: File) => {
|
||||||
event.preventDefault();
|
|
||||||
const file = Array.from(event.dataTransfer.files).find((f) => f.type.startsWith("video/"));
|
|
||||||
if (!file) return;
|
|
||||||
if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview);
|
if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview);
|
||||||
setSourceName(file.name);
|
setSourceName(file.name);
|
||||||
setSourceFile(file);
|
setSourceFile(file);
|
||||||
@@ -140,6 +138,10 @@ function SubtitleRemovalPage({
|
|||||||
setStatus(`已导入 ${file.name}`);
|
setStatus(`已导入 ${file.name}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes("Files")) setIsDragging(true); };
|
||||||
|
const handleDragLeave = (e: DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) setIsDragging(false); };
|
||||||
|
const handleFileDrop = (e: DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const file = Array.from(e.dataTransfer.files).find((f) => f.type.startsWith("video/")); if (file) processDroppedFile(file); };
|
||||||
|
|
||||||
const handleImportUrl = () => {
|
const handleImportUrl = () => {
|
||||||
const normalized = sourceUrl.trim();
|
const normalized = sourceUrl.trim();
|
||||||
if (!/^https?:\/\//i.test(normalized)) {
|
if (!/^https?:\/\//i.test(normalized)) {
|
||||||
@@ -341,7 +343,13 @@ function SubtitleRemovalPage({
|
|||||||
accept="video/mp4"
|
accept="video/mp4"
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
<div className="image-workbench-upload-shell" onDragOver={(e) => e.preventDefault()} onDrop={handleFileDrop}>
|
<div
|
||||||
|
className={`image-workbench-upload-shell${isDragging ? " is-dragging" : ""}`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleFileDrop}
|
||||||
|
>
|
||||||
|
{isDragging ? <div className="image-workbench-upload-drop-overlay"><span>释放文件以上传</span></div> : null}
|
||||||
<button type="button" className="image-workbench-upload" onClick={() => fileInputRef.current?.click()}>
|
<button type="button" className="image-workbench-upload" onClick={() => fileInputRef.current?.click()}>
|
||||||
<FileImageOutlined />
|
<FileImageOutlined />
|
||||||
<strong>{sourceName || "拖拽或选择视频"}</strong>
|
<strong>{sourceName || "拖拽或选择视频"}</strong>
|
||||||
@@ -435,9 +443,17 @@ function SubtitleRemovalPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="image-workbench-empty-canvas">
|
<div
|
||||||
<DeleteOutlined style={{ fontSize: 48, opacity: 0.2 }} />
|
className="studio-canvas-ghost"
|
||||||
<p>上传视频后在此预览</p>
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleFileDrop}
|
||||||
|
>
|
||||||
|
<div className="studio-canvas-ghost__icon">
|
||||||
|
<VideoCameraOutlined />
|
||||||
|
</div>
|
||||||
|
<div className="studio-canvas-ghost__title">拖拽或选择视频</div>
|
||||||
|
<div className="studio-canvas-ghost__hint">仅支持 MP4,最大 1GB,最高 1080P</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ import {
|
|||||||
type WorkbenchKeepaliveTask,
|
type WorkbenchKeepaliveTask,
|
||||||
MODE_META,
|
MODE_META,
|
||||||
MODE_OPTIONS,
|
MODE_OPTIONS,
|
||||||
|
CHAT_MODEL_OPTIONS,
|
||||||
|
THINKING_SPEED_OPTIONS,
|
||||||
|
THINKING_DEPTH_OPTIONS,
|
||||||
IMAGE_MODEL_OPTIONS,
|
IMAGE_MODEL_OPTIONS,
|
||||||
VIDEO_MODEL_OPTIONS,
|
VIDEO_MODEL_OPTIONS,
|
||||||
RATIO_OPTIONS,
|
RATIO_OPTIONS,
|
||||||
@@ -337,9 +340,13 @@ function WorkbenchPage({
|
|||||||
const [videoModel, setVideoModel] = useState(VIDEO_MODEL_OPTIONS[0].value);
|
const [videoModel, setVideoModel] = useState(VIDEO_MODEL_OPTIONS[0].value);
|
||||||
const [videoFrameMode, setVideoFrameMode] = useState("omni");
|
const [videoFrameMode, setVideoFrameMode] = useState("omni");
|
||||||
const [videoRatio, setVideoRatio] = useState("16:9");
|
const [videoRatio, setVideoRatio] = useState("16:9");
|
||||||
const [videoDuration, setVideoDuration] = useState("4");
|
const [videoDuration, setVideoDuration] = useState("5");
|
||||||
const [videoQuality, setVideoQuality] = useState(() => getDefaultVideoQuality(VIDEO_MODEL_OPTIONS[0].value));
|
const [videoQuality, setVideoQuality] = useState(() => getDefaultVideoQuality(VIDEO_MODEL_OPTIONS[0].value));
|
||||||
|
|
||||||
|
const [chatModel, setChatModel] = useState(CHAT_MODEL_OPTIONS[0].value);
|
||||||
|
const [thinkingSpeed, setThinkingSpeed] = useState(THINKING_SPEED_OPTIONS[0].value);
|
||||||
|
const [thinkingDepth, setThinkingDepth] = useState(THINKING_DEPTH_OPTIONS[0].value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
@@ -395,13 +402,13 @@ function WorkbenchPage({
|
|||||||
const referenceCount = referenceItems.length;
|
const referenceCount = referenceItems.length;
|
||||||
const activeVideoModelValue = toHappyHorseDisplayModel(videoModel);
|
const activeVideoModelValue = toHappyHorseDisplayModel(videoModel);
|
||||||
const activeModelValue =
|
const activeModelValue =
|
||||||
activeMode === "image" ? imageModel : activeMode === "video" ? activeVideoModelValue : CHAT_MODEL;
|
activeMode === "image" ? imageModel : activeMode === "video" ? activeVideoModelValue : chatModel;
|
||||||
const activeModel =
|
const activeModel =
|
||||||
activeMode === "image"
|
activeMode === "image"
|
||||||
? imageModelOptions.find((item) => item.value === imageModel)?.label || imageModel
|
? imageModelOptions.find((item) => item.value === imageModel)?.label || imageModel
|
||||||
: activeMode === "video"
|
: activeMode === "video"
|
||||||
? videoModelOptions.find((item) => item.value === activeVideoModelValue)?.label || activeVideoModelValue
|
? videoModelOptions.find((item) => item.value === activeVideoModelValue)?.label || activeVideoModelValue
|
||||||
: "OmniChat";
|
: CHAT_MODEL_OPTIONS.find((item) => item.value === chatModel)?.label || chatModel;
|
||||||
const conversationRecords = useMemo<WebProjectSummary[]>(
|
const conversationRecords = useMemo<WebProjectSummary[]>(
|
||||||
() =>
|
() =>
|
||||||
conversations.map((conversation) => ({
|
conversations.map((conversation) => ({
|
||||||
@@ -2720,6 +2727,46 @@ function WorkbenchPage({
|
|||||||
ariaLabel="工作台模式"
|
ariaLabel="工作台模式"
|
||||||
direction={dropdownDirection}
|
direction={dropdownDirection}
|
||||||
/>
|
/>
|
||||||
|
{activeMode === "chat" && (
|
||||||
|
<>
|
||||||
|
<SelectChip
|
||||||
|
chipId="chat-model"
|
||||||
|
value={chatModel}
|
||||||
|
options={CHAT_MODEL_OPTIONS}
|
||||||
|
disabled={disabled}
|
||||||
|
isOpen={toolbarMenuId === "chat-model"}
|
||||||
|
onToggle={() => toggleToolbarMenu("chat-model")}
|
||||||
|
onClose={closeToolbarMenus}
|
||||||
|
onChange={setChatModel}
|
||||||
|
ariaLabel="对话模型"
|
||||||
|
direction={dropdownDirection}
|
||||||
|
/>
|
||||||
|
<SelectChip
|
||||||
|
chipId="chat-speed"
|
||||||
|
value={thinkingSpeed}
|
||||||
|
options={THINKING_SPEED_OPTIONS}
|
||||||
|
disabled={disabled}
|
||||||
|
isOpen={toolbarMenuId === "chat-speed"}
|
||||||
|
onToggle={() => toggleToolbarMenu("chat-speed")}
|
||||||
|
onClose={closeToolbarMenus}
|
||||||
|
onChange={setThinkingSpeed}
|
||||||
|
ariaLabel="思考速度"
|
||||||
|
direction={dropdownDirection}
|
||||||
|
/>
|
||||||
|
<SelectChip
|
||||||
|
chipId="chat-depth"
|
||||||
|
value={thinkingDepth}
|
||||||
|
options={THINKING_DEPTH_OPTIONS}
|
||||||
|
disabled={disabled}
|
||||||
|
isOpen={toolbarMenuId === "chat-depth"}
|
||||||
|
onToggle={() => toggleToolbarMenu("chat-depth")}
|
||||||
|
onClose={closeToolbarMenus}
|
||||||
|
onChange={setThinkingDepth}
|
||||||
|
ariaLabel="思考深度"
|
||||||
|
direction={dropdownDirection}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{activeMode === "image" && (
|
{activeMode === "image" && (
|
||||||
<>
|
<>
|
||||||
<SelectChip
|
<SelectChip
|
||||||
|
|||||||
@@ -3,24 +3,6 @@ import type { ReactNode } from "react";
|
|||||||
import type { WorkbenchOption, WorkbenchFieldGroup } from "./workbenchConstants";
|
import type { WorkbenchOption, WorkbenchFieldGroup } from "./workbenchConstants";
|
||||||
import { getRatioOptionClassName, getSettingsGridColumnsClassName } from "./workbenchReferenceUtils";
|
import { getRatioOptionClassName, getSettingsGridColumnsClassName } from "./workbenchReferenceUtils";
|
||||||
|
|
||||||
const VIDEO_MODEL_ICON_URLS = {
|
|
||||||
happyHorse: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/HappyHorse.svg",
|
|
||||||
pixverse: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/Pixverse.svg",
|
|
||||||
vidu: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/viduQ3.svg",
|
|
||||||
wanxiang: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/wan.svg",
|
|
||||||
kling: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/kling.svg",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
function getVideoModelIconUrl(option: WorkbenchOption): string | null {
|
|
||||||
const text = `${option.value} ${option.label}`.toLowerCase();
|
|
||||||
if (text.includes("happyhorse")) return VIDEO_MODEL_ICON_URLS.happyHorse;
|
|
||||||
if (text.includes("pixverse")) return VIDEO_MODEL_ICON_URLS.pixverse;
|
|
||||||
if (text.includes("vidu")) return VIDEO_MODEL_ICON_URLS.vidu;
|
|
||||||
if (text.includes("wan") || text.includes("万相")) return VIDEO_MODEL_ICON_URLS.wanxiang;
|
|
||||||
if (text.includes("kling") || text.includes("可灵")) return VIDEO_MODEL_ICON_URLS.kling;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SelectChip({
|
export function SelectChip({
|
||||||
chipId,
|
chipId,
|
||||||
value,
|
value,
|
||||||
@@ -74,7 +56,6 @@ export function SelectChip({
|
|||||||
>
|
>
|
||||||
{options.map((option, index) => {
|
{options.map((option, index) => {
|
||||||
const active = option.value === value;
|
const active = option.value === value;
|
||||||
const iconUrl = chipId === "video-model" ? getVideoModelIconUrl(option) : null;
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={option.value}
|
key={option.value}
|
||||||
@@ -90,11 +71,6 @@ export function SelectChip({
|
|||||||
>
|
>
|
||||||
<span className="ai-workbench-select-chip__option-label">
|
<span className="ai-workbench-select-chip__option-label">
|
||||||
<span className="ai-workbench-select-chip__option-dot" aria-hidden="true" />
|
<span className="ai-workbench-select-chip__option-dot" aria-hidden="true" />
|
||||||
{iconUrl ? (
|
|
||||||
<span className="ai-workbench-select-chip__option-icon" aria-hidden="true">
|
|
||||||
<img src={iconUrl} alt="" loading="lazy" />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<span className="ai-workbench-select-chip__option-copy">
|
<span className="ai-workbench-select-chip__option-copy">
|
||||||
<span className="ai-workbench-select-chip__option-title">
|
<span className="ai-workbench-select-chip__option-title">
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import type { ReactNode } from "react";
|
|||||||
export type WorkbenchMode = "chat" | "image" | "video";
|
export type WorkbenchMode = "chat" | "image" | "video";
|
||||||
export type ToolbarMenuId =
|
export type ToolbarMenuId =
|
||||||
| "studio-mode"
|
| "studio-mode"
|
||||||
|
| "chat-model"
|
||||||
|
| "chat-speed"
|
||||||
|
| "chat-depth"
|
||||||
| "image-model"
|
| "image-model"
|
||||||
| "image-settings"
|
| "image-settings"
|
||||||
| "image-grid-mode"
|
| "image-grid-mode"
|
||||||
@@ -138,6 +141,24 @@ export const REFERENCE_IMAGE_INITIAL_QUALITY = 0.84;
|
|||||||
export const REFERENCE_IMAGE_MIN_QUALITY = 0.62;
|
export const REFERENCE_IMAGE_MIN_QUALITY = 0.62;
|
||||||
export const CHAT_MODEL = "gemini-3.1-pro";
|
export const CHAT_MODEL = "gemini-3.1-pro";
|
||||||
|
|
||||||
|
export const CHAT_MODEL_OPTIONS: WorkbenchOption[] = [
|
||||||
|
{ value: "gemini", label: "Gemini" },
|
||||||
|
{ value: "wanxian", label: "万相" },
|
||||||
|
{ value: "deepseek", label: "DeepSeek" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const THINKING_SPEED_OPTIONS: WorkbenchOption[] = [
|
||||||
|
{ value: "default", label: "默认" },
|
||||||
|
{ value: "high", label: "高" },
|
||||||
|
{ value: "ultra", label: "急速" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const THINKING_DEPTH_OPTIONS: WorkbenchOption[] = [
|
||||||
|
{ value: "default", label: "默认" },
|
||||||
|
{ value: "strong", label: "强" },
|
||||||
|
{ value: "extreme", label: "极限" },
|
||||||
|
];
|
||||||
|
|
||||||
export const CHAT_NATURAL_SYSTEM_PROMPT = [
|
export const CHAT_NATURAL_SYSTEM_PROMPT = [
|
||||||
"你是 OmniAI 的创作协作助手,像一个正在一起工作的同伴一样说话。",
|
"你是 OmniAI 的创作协作助手,像一个正在一起工作的同伴一样说话。",
|
||||||
`默认使用自然、简洁的中文,不要官腔,不要机械套话,不要频繁使用“首先、其次、最后”这种模板。`,
|
`默认使用自然、简洁的中文,不要官腔,不要机械套话,不要频繁使用“首先、其次、最后”这种模板。`,
|
||||||
@@ -210,13 +231,13 @@ export const MODE_OPTIONS: WorkbenchOption[] = (Object.keys(MODE_META) as Workbe
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const IMAGE_MODEL_OPTIONS: WorkbenchOption[] = [
|
export const IMAGE_MODEL_OPTIONS: WorkbenchOption[] = [
|
||||||
{ value: "wan2.7-image-pro", label: "wan 2.7 Pro 4K · 0.20 积分" },
|
{ value: "wan2.7-image-pro", label: "wan 2.7 Pro 4K" },
|
||||||
{ value: "wan2.7-image", label: "wan 2.7 · 0.20 积分" },
|
{ value: "wan2.7-image", label: "wan 2.7" },
|
||||||
{ value: "gpt-image-2", label: "GPT-Image-2 · 0.20 积分" },
|
{ value: "gpt-image-2", label: "GPT-Image-2" },
|
||||||
{ value: "gpt-image-2-vip", label: "GPT-Image-2 VIP · 0.20 积分" },
|
{ value: "gpt-image-2-vip", label: "GPT-Image-2 VIP" },
|
||||||
{ value: "nano-banana-pro", label: "Nano Banana Pro · 0.20 积分" },
|
{ value: "nano-banana-pro", label: "Nano Banana Pro" },
|
||||||
{ value: "nano-banana-2", label: "Nano Banana 2 · 0.20 积分" },
|
{ value: "nano-banana-2", label: "Nano Banana 2" },
|
||||||
{ value: "nano-banana-fast", label: "Nano Banana · 0.20 积分" },
|
{ value: "nano-banana-fast", label: "Nano Banana" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const VIDEO_MODEL_OPTIONS: WorkbenchOption[] = ENTERPRISE_VIDEO_MODEL_OPTIONS.map((option) => ({ ...option }));
|
export const VIDEO_MODEL_OPTIONS: WorkbenchOption[] = ENTERPRISE_VIDEO_MODEL_OPTIONS.map((option) => ({ ...option }));
|
||||||
@@ -249,7 +270,6 @@ export const VIDEO_FRAME_OPTIONS: WorkbenchOption[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const VIDEO_DURATION_OPTIONS: WorkbenchOption[] = [
|
export const VIDEO_DURATION_OPTIONS: WorkbenchOption[] = [
|
||||||
{ value: "4", label: "4s" },
|
|
||||||
{ value: "5", label: "5s" },
|
{ value: "5", label: "5s" },
|
||||||
{ value: "6", label: "6s" },
|
{ value: "6", label: "6s" },
|
||||||
{ value: "7", label: "7s" },
|
{ value: "7", label: "7s" },
|
||||||
|
|||||||
@@ -1,13 +1 @@
|
|||||||
/* Agent page rules move here as they are retired from legacy-pages.css. */
|
/* Agent page rules move here as they are retired from legacy-pages.css. */
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.agent-page {
|
|
||||||
padding: 16px 16px 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.agent-page {
|
|
||||||
padding: 12px 10px 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -243,24 +243,3 @@
|
|||||||
max-height: calc(100vh - 190px);
|
max-height: calc(100vh - 190px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.asset-preview-modal {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-preview-modal__panel {
|
|
||||||
width: calc(100vw - 16px);
|
|
||||||
max-height: calc(100vh - 16px);
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-preview-modal__body {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-preview-modal__body img,
|
|
||||||
.asset-preview-modal__body video {
|
|
||||||
max-height: calc(100vh - 160px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4211,30 +4211,3 @@
|
|||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.avatar-console-page {
|
|
||||||
height: auto;
|
|
||||||
min-height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-console-main {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding-bottom: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-console-scroll {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-console-toolbar {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-console-video-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+37
-91
@@ -723,6 +723,43 @@
|
|||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Canvas drag-and-drop visual feedback ─────────────────────────── */
|
||||||
|
.studio-canvas.is-canvas-dragging::after {
|
||||||
|
content: "释放以上传图片";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(33, 242, 154, 0.12);
|
||||||
|
border: 3px dashed #21f29a;
|
||||||
|
color: #111;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-text-composer.is-drag-over {
|
||||||
|
outline: 2px dashed #21f29a;
|
||||||
|
outline-offset: 2px;
|
||||||
|
background: rgba(33, 242, 154, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-canvas-text-composer.is-drag-over::after {
|
||||||
|
content: "释放图片以创建节点";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10;
|
||||||
|
color: #333;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Tool Modal Overlay */
|
/* Tool Modal Overlay */
|
||||||
.studio-canvas-tool-modal-overlay {
|
.studio-canvas-tool-modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -910,94 +947,3 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════════
|
|
||||||
Responsive: Canvas
|
|
||||||
Breakpoints: 900px / 560px
|
|
||||||
═══════════════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.studio-canvas-page {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-canvas-toolbar {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
gap: 6px;
|
|
||||||
overflow-x: auto;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--border-weak);
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-canvas-toolbar__group {
|
|
||||||
flex-direction: row;
|
|
||||||
flex-shrink: 0;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-canvas-toolbar button {
|
|
||||||
min-width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-canvas-main {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-canvas-node {
|
|
||||||
min-width: 140px;
|
|
||||||
min-height: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.studio-canvas-page {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-canvas-toolbar {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
padding: 6px 8px;
|
|
||||||
gap: 4px;
|
|
||||||
border-bottom: 1px solid var(--border-weak);
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-canvas-toolbar button {
|
|
||||||
min-width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-canvas-main {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-canvas-node {
|
|
||||||
min-width: 110px;
|
|
||||||
min-height: 48px;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-canvas-project-bar {
|
|
||||||
right: 8px;
|
|
||||||
left: 8px;
|
|
||||||
bottom: 80px;
|
|
||||||
gap: 4px;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-canvas-project-bar__name-form {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -117,52 +117,3 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════════
|
|
||||||
Responsive: Community
|
|
||||||
Breakpoints: 900px / 560px
|
|
||||||
═══════════════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.community-page {
|
|
||||||
padding: 12px 16px 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-carousel__slide--video {
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-carousel__video {
|
|
||||||
max-height: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-case-card__preview {
|
|
||||||
min-height: 140px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.community-page {
|
|
||||||
padding: 10px 10px 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-carousel__slide--video {
|
|
||||||
min-height: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-carousel__video {
|
|
||||||
max-height: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-case-card__preview {
|
|
||||||
min-height: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-case-empty {
|
|
||||||
padding: 32px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-case-empty__icon {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -553,6 +553,209 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Generation controls ── */
|
||||||
|
.dialog-generator-mode-switch {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-mode {
|
||||||
|
min-height: 42px;
|
||||||
|
border: 1px solid rgba(0, 255, 136, 0.16);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
color: #9aa1b8;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
background 180ms ease,
|
||||||
|
color 180ms ease,
|
||||||
|
transform 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-mode:hover {
|
||||||
|
border-color: rgba(0, 255, 136, 0.32);
|
||||||
|
color: #dce3ed;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-mode.is-active {
|
||||||
|
border-color: rgba(0, 255, 136, 0.42);
|
||||||
|
background: rgba(0, 255, 136, 0.08);
|
||||||
|
color: #00ff88;
|
||||||
|
box-shadow: 0 0 16px rgba(0, 255, 136, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-pills {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 38px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
color: #dce3ed;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 750;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
background 180ms ease,
|
||||||
|
color 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-pill:hover {
|
||||||
|
border-color: rgba(0, 255, 136, 0.28);
|
||||||
|
color: #f6f8fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-pill.is-open {
|
||||||
|
border-color: rgba(0, 255, 136, 0.38);
|
||||||
|
background: rgba(0, 255, 136, 0.08);
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-pill .anticon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 30;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
min-width: 148px;
|
||||||
|
border: 1px solid rgba(0, 255, 136, 0.18);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(10, 16, 26, 0.96);
|
||||||
|
box-shadow:
|
||||||
|
0 12px 36px rgba(0, 0, 0, 0.42),
|
||||||
|
0 0 0 1px rgba(0, 255, 136, 0.06);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
padding: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-dropdown__item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: #bcc4d6;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 9px 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
transition:
|
||||||
|
background 120ms ease,
|
||||||
|
color 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-dropdown__item:hover {
|
||||||
|
background: rgba(0, 255, 136, 0.08);
|
||||||
|
color: #e8eaef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-dropdown__item.is-active {
|
||||||
|
background: rgba(0, 255, 136, 0.12);
|
||||||
|
color: #00ff88;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Video duration ── */
|
||||||
|
.dialog-generator-duration {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-duration__label {
|
||||||
|
color: #9aa1b8;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-duration__options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-duration__btn {
|
||||||
|
min-width: 42px;
|
||||||
|
min-height: 34px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
color: #9aa1b8;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 750;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
background 180ms ease,
|
||||||
|
color 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-duration__btn:hover {
|
||||||
|
border-color: rgba(0, 255, 136, 0.28);
|
||||||
|
color: #dce3ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-duration__btn.is-active {
|
||||||
|
border-color: rgba(0, 255, 136, 0.42);
|
||||||
|
background: rgba(0, 255, 136, 0.12);
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Generate button ── */
|
||||||
|
.dialog-generator-run {
|
||||||
|
min-height: 48px;
|
||||||
|
border: 1px solid rgba(0, 255, 136, 0.28);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 255, 136, 0.14) 0%, rgba(34, 240, 192, 0.08) 100%);
|
||||||
|
color: #00ff88;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
background 180ms ease,
|
||||||
|
transform 180ms ease,
|
||||||
|
box-shadow 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-run:hover:not(:disabled) {
|
||||||
|
border-color: rgba(0, 255, 136, 0.5);
|
||||||
|
background: linear-gradient(135deg, rgba(0, 255, 136, 0.2) 0%, rgba(34, 240, 192, 0.12) 100%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 255, 136, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-generator-run:disabled {
|
||||||
|
opacity: 0.52;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.dialog-generator-shell {
|
.dialog-generator-shell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -6,9 +6,13 @@
|
|||||||
align-content: initial;
|
align-content: initial;
|
||||||
justify-items: initial;
|
justify-items: initial;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
background: #0e1014;
|
background: #0e1014;
|
||||||
scrollbar-color: #353b45 #0e1014;
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.product-clone-page[data-tool="clone"] .product-clone-preview--video::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecom-video-workspace {
|
.ecom-video-workspace {
|
||||||
@@ -20,6 +24,11 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0e1014;
|
background: #0e1014;
|
||||||
color: #e5ebf4;
|
color: #e5ebf4;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.ecom-video-workspace::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecom-video-flowbar {
|
.ecom-video-flowbar {
|
||||||
@@ -112,6 +121,42 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Flowbar zoom controls ─────────────────────────── */
|
||||||
|
.ecom-video-flowbar__zoom {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.ecom-video-flowbar__zoom button {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border: 1px solid #2c3038;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #1a1d24;
|
||||||
|
color: #8890a0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
transition: border-color 150ms ease, color 150ms ease;
|
||||||
|
}
|
||||||
|
.ecom-video-flowbar__zoom button:hover:not(:disabled) {
|
||||||
|
border-color: #00ff88;
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
.ecom-video-flowbar__zoom button:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.ecom-video-flowbar__zoom span {
|
||||||
|
min-width: 38px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6a7282;
|
||||||
|
}
|
||||||
|
|
||||||
.ecom-video-flowbar__error {
|
.ecom-video-flowbar__error {
|
||||||
max-width: min(260px, 28vw);
|
max-width: min(260px, 28vw);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -181,8 +226,13 @@
|
|||||||
background: #101318;
|
background: #101318;
|
||||||
padding: 32px 40px;
|
padding: 32px 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.ecom-video-flow-canvas::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecom-video-flow-map {
|
.ecom-video-flow-map {
|
||||||
@@ -418,22 +468,23 @@
|
|||||||
width: 38px;
|
width: 38px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border: 1px solid #3d3020;
|
border: 1px solid #00cc6a;
|
||||||
border-radius: 8px;
|
border-radius: 9px;
|
||||||
background: #15181f;
|
background: #00ff88;
|
||||||
color: #ffe1ad;
|
color: #06130d;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
transform 150ms ease,
|
transform 150ms ease,
|
||||||
border-color 150ms ease,
|
filter 150ms ease,
|
||||||
background-color 150ms ease;
|
box-shadow 150ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecom-video-flow-dock button:hover {
|
.ecom-video-flow-dock button:hover {
|
||||||
border-color: #4d3a1a;
|
filter: brightness(1.08);
|
||||||
background: #241c12;
|
box-shadow: 0 2px 12px rgba(0, 255, 136, 0.25);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecom-video-flow-notice {
|
.ecom-video-flow-notice {
|
||||||
|
|||||||
+160
-488
@@ -990,8 +990,8 @@
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 20px 18px;
|
padding: 20px 18px;
|
||||||
scrollbar-color: #3a3f49 #15171c;
|
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #3a3f49 #15171c;
|
||||||
transition:
|
transition:
|
||||||
opacity 360ms ease,
|
opacity 360ms ease,
|
||||||
transform var(--clone-settings-motion-duration) var(--clone-settings-motion-ease);
|
transform var(--clone-settings-motion-duration) var(--clone-settings-motion-ease);
|
||||||
@@ -1166,25 +1166,6 @@
|
|||||||
background: #202c28;
|
background: #202c28;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone.is-full {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone.is-full strong {
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
color: #aeb8c4;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone.is-full:hover {
|
|
||||||
border-color: rgba(255, 255, 255, 0.16);
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 50% 0%, rgba(var(--ecm-accent-rgb), 0.09), transparent 58%),
|
|
||||||
var(--ecm-inset);
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:active {
|
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:active {
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
@@ -1302,27 +1283,6 @@
|
|||||||
transform: scale(0.92);
|
transform: scale(0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-card,
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone,
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-files {
|
|
||||||
position: relative;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone {
|
|
||||||
z-index: 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-uploaded-file:hover),
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-uploaded-file:focus-within) {
|
|
||||||
z-index: 90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file:hover,
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file:focus-within {
|
|
||||||
z-index: 95;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-settings-section {
|
.product-clone-page[data-tool="clone"] .clone-ai-settings-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -1581,12 +1541,11 @@
|
|||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-panel {
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex: 0 0 272px;
|
flex: 0 0 auto;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
gap: 9px;
|
gap: 9px;
|
||||||
height: 272px;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
border: 1px solid #303540;
|
border: 1px solid #303540;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: #1c1f26;
|
background: #1c1f26;
|
||||||
@@ -1648,7 +1607,7 @@
|
|||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload {
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 78px;
|
min-height: 96px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
@@ -1657,7 +1616,7 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #20242c;
|
background: #20242c;
|
||||||
color: #eef2f6;
|
color: #eef2f6;
|
||||||
padding: 8px;
|
padding: 16px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
border-color 160ms ease,
|
border-color 160ms ease,
|
||||||
@@ -1665,15 +1624,52 @@
|
|||||||
transform 160ms ease;
|
transform 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload.has-files {
|
||||||
|
min-height: 120px;
|
||||||
|
place-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload:hover {
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload:hover {
|
||||||
border-color: #00ff88;
|
border-color: #00ff88;
|
||||||
background: #202c28;
|
background: #202c28;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload.is-dragging {
|
||||||
|
border-color: #00ff88;
|
||||||
|
background: #1a2e24;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload:active {
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload:active {
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(0, 255, 136, 0.08);
|
||||||
|
color: #00ff88;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload-overlay .anticon {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload-overlay span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload > span {
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload > span {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
grid-template-columns: auto minmax(0, max-content);
|
grid-template-columns: auto minmax(0, max-content);
|
||||||
@@ -1716,75 +1712,86 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-preview {
|
/* ── Reference image file grid (inside upload button) ── */
|
||||||
position: absolute;
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-files {
|
||||||
inset: 6px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
|
||||||
grid-auto-columns: minmax(0, 56px);
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
border-radius: 10px;
|
width: 100%;
|
||||||
background: #20242c;
|
overflow: visible;
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transform: scale(0.98);
|
|
||||||
transition:
|
|
||||||
opacity 160ms ease,
|
|
||||||
transform 160ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload:hover .clone-ai-replicate-preview,
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-file {
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload:focus-visible .clone-ai-replicate-preview {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-preview figure {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
width: 56px;
|
aspect-ratio: 1;
|
||||||
height: 52px;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-preview figure > img {
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-file > img {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid #3a4555;
|
border: 1px solid #3a4555;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
background: #111720;
|
background: #111720;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-preview figure:only-child {
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-file > img:hover {
|
||||||
width: min(170px, 100%);
|
border-color: #00ff88;
|
||||||
height: 52px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-preview figure:only-child > img {
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-add-more {
|
||||||
object-fit: contain;
|
display: inline-flex;
|
||||||
}
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-preview b {
|
gap: 5px;
|
||||||
display: grid;
|
justify-self: center;
|
||||||
width: 42px;
|
width: fit-content;
|
||||||
height: 42px;
|
max-width: calc(100% - 8px);
|
||||||
place-items: center;
|
height: 28px;
|
||||||
border: 1px solid #3a4555;
|
min-width: 0;
|
||||||
border-radius: 999px;
|
border-radius: 7px;
|
||||||
background: #151b24;
|
background: #2b3039;
|
||||||
color: #eef2f6;
|
color: #9aa4b4;
|
||||||
|
padding: 0 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 900;
|
font-weight: 750;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-add-more .anticon {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #5d84bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Portal-based zoom preview (avoids overflow clipping) ── */
|
||||||
|
.clone-ai-zoom-portal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
width: min(280px, 52vw);
|
||||||
|
max-height: 340px;
|
||||||
|
border: 1px solid #3a4555;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #101115;
|
||||||
|
padding: 8px;
|
||||||
|
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
transform: translate(-50%, calc(-100% - 12px));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clone-ai-zoom-portal img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 324px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-replicate-link input {
|
.product-clone-page[data-tool="clone"] .clone-ai-replicate-link input {
|
||||||
@@ -2785,12 +2792,18 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow-y: auto;
|
||||||
align-content: center;
|
overflow-x: hidden;
|
||||||
|
align-content: safe center;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
gap: 22px;
|
gap: 22px;
|
||||||
background: #101115;
|
background: #101115;
|
||||||
padding: 92px 46px 142px;
|
padding: 92px 46px 142px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-preview::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-preview-header {
|
.product-clone-page[data-tool="clone"] .clone-ai-preview-header {
|
||||||
@@ -2821,6 +2834,50 @@
|
|||||||
color: #00ff88;
|
color: #00ff88;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Preview zoom controls ─────────────────────────── */
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-preview-zoom {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-preview-zoom button {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid #2c3038;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #1b1d23;
|
||||||
|
color: #a0a8b8;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
transition: border-color 150ms ease, color 150ms ease;
|
||||||
|
}
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-preview-zoom button:hover:not(:disabled) {
|
||||||
|
border-color: #00ff88;
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-preview-zoom button:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-preview-zoom span {
|
||||||
|
min-width: 42px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #758096;
|
||||||
|
}
|
||||||
|
.product-clone-page[data-tool="clone"] .clone-ai-preview-zoom-wrap {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-empty-state {
|
.product-clone-page[data-tool="clone"] .clone-ai-empty-state {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: min(100%, 600px);
|
width: min(100%, 600px);
|
||||||
@@ -2928,7 +2985,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-main-result {
|
.product-clone-page[data-tool="clone"] .clone-ai-main-result {
|
||||||
height: 440px;
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-result-grid {
|
.product-clone-page[data-tool="clone"] .clone-ai-result-grid {
|
||||||
@@ -2938,12 +2996,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-result-grid button {
|
.product-clone-page[data-tool="clone"] .clone-ai-result-grid button {
|
||||||
height: 210px;
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-result-grid button:first-child {
|
.product-clone-page[data-tool="clone"] .clone-ai-result-grid button:first-child {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
height: 240px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-main-result span,
|
.product-clone-page[data-tool="clone"] .clone-ai-main-result span,
|
||||||
@@ -3514,7 +3572,7 @@
|
|||||||
|
|
||||||
.uploaded-image-zoom {
|
.uploaded-image-zoom {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 220;
|
z-index: 70;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: calc(100% + 10px);
|
bottom: calc(100% + 10px);
|
||||||
display: block;
|
display: block;
|
||||||
@@ -3535,18 +3593,6 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file .uploaded-image-zoom {
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(100% + 12px);
|
|
||||||
width: min(240px, 58vw);
|
|
||||||
transform: translate(0, 8px) scale(0.96);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file:hover .uploaded-image-zoom,
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file:focus-within .uploaded-image-zoom {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploaded-image-zoom img {
|
.uploaded-image-zoom img {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -3562,240 +3608,13 @@
|
|||||||
.product-set-thumb:focus-within .uploaded-image-zoom,
|
.product-set-thumb:focus-within .uploaded-image-zoom,
|
||||||
.product-clone-uploaded-thumb:hover .uploaded-image-zoom,
|
.product-clone-uploaded-thumb:hover .uploaded-image-zoom,
|
||||||
.product-clone-uploaded-thumb:focus-within .uploaded-image-zoom,
|
.product-clone-uploaded-thumb:focus-within .uploaded-image-zoom,
|
||||||
.clone-ai-replicate-preview figure:hover .uploaded-image-zoom,
|
.clone-ai-replicate-file:hover .uploaded-image-zoom,
|
||||||
.clone-ai-replicate-preview figure:focus-within .uploaded-image-zoom {
|
.clone-ai-replicate-file:focus-within .uploaded-image-zoom {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translate(-50%, 0) scale(1);
|
transform: translate(-50%, 0) scale(1);
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) {
|
|
||||||
align-content: start;
|
|
||||||
justify-items: stretch;
|
|
||||||
gap: 10px;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 12px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) .clone-ai-upload-main {
|
|
||||||
grid-template-columns: 34px minmax(0, 1fr) auto;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: start;
|
|
||||||
gap: 4px 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) .clone-ai-upload-icon {
|
|
||||||
grid-row: span 2;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) .clone-ai-upload-title {
|
|
||||||
min-width: 0;
|
|
||||||
color: #c9d2dd;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) strong {
|
|
||||||
grid-row: span 2;
|
|
||||||
min-width: 96px;
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) .clone-ai-upload-hint {
|
|
||||||
grid-column: 2;
|
|
||||||
min-width: 0;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview-wrap {
|
|
||||||
display: grid;
|
|
||||||
gap: 7px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 126px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid rgba(var(--ecm-accent-rgb), 0.2);
|
|
||||||
border-radius: 10px;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 48%),
|
|
||||||
rgba(8, 10, 12, 0.56);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview img {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 126px;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 0 2px;
|
|
||||||
color: #eef2f6;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta span {
|
|
||||||
display: grid;
|
|
||||||
min-width: 0;
|
|
||||||
gap: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta b,
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta em {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: 260px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta b {
|
|
||||||
color: var(--ecm-accent);
|
|
||||||
font-size: 10px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta em {
|
|
||||||
color: #aeb8c4;
|
|
||||||
font-size: 10px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-files {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 7px;
|
|
||||||
margin-top: 0;
|
|
||||||
padding-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-stack {
|
|
||||||
display: grid;
|
|
||||||
gap: 7px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-head {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
color: #768292;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-head span {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-head b {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
padding: 2px 7px;
|
|
||||||
border: 1px solid rgba(var(--ecm-accent-rgb), 0.18);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(var(--ecm-accent-rgb), 0.07);
|
|
||||||
color: var(--ecm-accent);
|
|
||||||
font-size: 10px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file {
|
|
||||||
width: 46px;
|
|
||||||
height: 46px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-color: rgba(255, 255, 255, 0.12);
|
|
||||||
border-radius: 9px;
|
|
||||||
background: rgba(255, 255, 255, 0.035);
|
|
||||||
transition:
|
|
||||||
border-color 160ms ease,
|
|
||||||
box-shadow 160ms ease,
|
|
||||||
transform 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file > button:not(.clone-ai-uploaded-file__thumb) {
|
|
||||||
top: 2px;
|
|
||||||
right: 2px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background: rgba(8, 10, 12, 0.82);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file.is-active {
|
|
||||||
border-color: rgba(var(--ecm-accent-rgb), 0.86);
|
|
||||||
box-shadow: 0 0 0 2px rgba(var(--ecm-accent-rgb), 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file .clone-ai-uploaded-file__thumb {
|
|
||||||
position: static;
|
|
||||||
inset: auto;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
border-radius: inherit;
|
|
||||||
background: transparent;
|
|
||||||
color: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: inherit;
|
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file .clone-ai-uploaded-file__thumb img {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file__thumb span {
|
|
||||||
position: absolute;
|
|
||||||
left: 3px;
|
|
||||||
bottom: 3px;
|
|
||||||
min-width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(8, 10, 12, 0.76);
|
|
||||||
color: #eef2f6;
|
|
||||||
font-size: 9px;
|
|
||||||
font-weight: 900;
|
|
||||||
line-height: 14px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file:hover {
|
|
||||||
border-color: rgba(var(--ecm-accent-rgb), 0.62);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes image-mention-menu-rise {
|
@keyframes image-mention-menu-rise {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -8661,147 +8480,6 @@
|
|||||||
.clone-ai-adwizard__risk.is-low { color: #52c41a; }
|
.clone-ai-adwizard__risk.is-low { color: #52c41a; }
|
||||||
.clone-ai-adwizard__risk.is-medium { color: #faad14; }
|
.clone-ai-adwizard__risk.is-medium { color: #faad14; }
|
||||||
.clone-ai-adwizard__risk.is-high { color: #ff4d4f; }
|
.clone-ai-adwizard__risk.is-high { color: #ff4d4f; }
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════════
|
|
||||||
Responsive: Ecommerce Tools
|
|
||||||
Breakpoints: 1180px / 900px / 560px
|
|
||||||
═══════════════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
/* ── 1180px: Narrower panels ── */
|
|
||||||
@media (max-width: 1180px) {
|
|
||||||
.product-clone-page[data-tool="clone"] > .product-clone-shell {
|
|
||||||
grid-template-columns: minmax(360px, 440px) minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
.product-clone-page[data-tool="set"] > .product-clone-shell {
|
|
||||||
grid-template-columns: minmax(320px, 420px) minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── 900px: Tablet — stacked layout ── */
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
/* All tools: collapse shell to single column */
|
|
||||||
.product-clone-page > .product-clone-shell,
|
|
||||||
.product-clone-page[data-tool="clone"] > .product-clone-shell,
|
|
||||||
.product-clone-page[data-tool="set"] > .product-clone-shell {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rail becomes horizontal tab bar */
|
|
||||||
.product-clone-rail {
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 4px;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
padding: 8px 12px;
|
|
||||||
overflow-x: auto;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--border-weak, #2a3032);
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-rail button {
|
|
||||||
flex-shrink: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Panel: full-width, reduced height */
|
|
||||||
.product-clone-panel {
|
|
||||||
width: 100%;
|
|
||||||
max-height: 45vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .product-clone-panel {
|
|
||||||
--clone-settings-panel-width: 100%;
|
|
||||||
max-height: 45vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Preview: independent scroll area */
|
|
||||||
.product-clone-preview {
|
|
||||||
min-height: 40vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 20px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="set"] .product-clone-preview {
|
|
||||||
padding: 20px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Collapse toggle tweak */
|
|
||||||
.product-clone-page[data-tool="clone"] .product-clone-panel {
|
|
||||||
transition: max-height 320ms ease, opacity 320ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page.is-settings-collapsed .product-clone-panel {
|
|
||||||
max-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── 560px: Phone — further compression ── */
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.product-clone-page {
|
|
||||||
grid-template-rows: 44px minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-rail {
|
|
||||||
padding: 6px 8px;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-rail button {
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 11px;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-panel {
|
|
||||||
max-height: 38vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .product-clone-panel {
|
|
||||||
max-height: 38vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="set"] .product-clone-panel__scroll {
|
|
||||||
padding: 20px 14px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="set"] :is(.product-set-upload-section, .product-set-settings-section) {
|
|
||||||
padding: 18px;
|
|
||||||
border-radius: 14px;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="set"] .product-set-upload {
|
|
||||||
min-height: 180px;
|
|
||||||
padding: 16px;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="set"] .product-set-upload-title {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="set"] .product-set-upload strong {
|
|
||||||
height: 42px;
|
|
||||||
min-width: 140px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Preview full-width */
|
|
||||||
.product-clone-preview {
|
|
||||||
padding: 12px 10px !important;
|
|
||||||
min-height: 50vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.clone-ai-adwizard__issues { margin: 0; padding-left: 16px; font-size: 12px; display: flex; flex-direction: column; gap: 4px; }
|
.clone-ai-adwizard__issues { margin: 0; padding-left: 16px; font-size: 12px; display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
|
||||||
/* ===== Ecommerce Template Apple Carousel ===== */
|
/* ===== Ecommerce Template Apple Carousel ===== */
|
||||||
@@ -9183,13 +8861,6 @@
|
|||||||
box-shadow: 0 10px 28px rgba(var(--ecm-accent-rgb), 0.18);
|
box-shadow: 0 10px 28px rgba(var(--ecm-accent-rgb), 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone.is-full strong {
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
color: #aeb8c4;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-clone-page[data-tool="clone"] :is(.clone-ai-generate:hover:not(:disabled), .clone-ai-send-button:hover:not(:disabled)) {
|
.product-clone-page[data-tool="clone"] :is(.clone-ai-generate:hover:not(:disabled), .clone-ai-send-button:hover:not(:disabled)) {
|
||||||
filter: brightness(1.03);
|
filter: brightness(1.03);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
@@ -9885,3 +9556,4 @@
|
|||||||
min-height: calc(100% - 59px);
|
min-height: calc(100% - 59px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,14 +216,14 @@
|
|||||||
|
|
||||||
.image-workbench-layout {
|
.image-workbench-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 280px 1fr 220px;
|
grid-template-columns: 280px 1fr;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-workbench-layout--inpaint {
|
.image-workbench-layout--inpaint {
|
||||||
grid-template-columns: 260px 1fr 240px;
|
grid-template-columns: 260px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-workbench-layout--camera {
|
.image-workbench-layout--camera {
|
||||||
@@ -278,6 +278,27 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-workbench-upload-shell.is-dragging {
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
outline: 2px dashed var(--accent);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-workbench-upload-drop-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: rgba(var(--accent-rgb), 0.08);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.image-workbench-upload {
|
.image-workbench-upload {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1776,28 +1797,3 @@ textarea.image-workbench-prompt {
|
|||||||
.image-workbench-result-card:nth-child(2) { animation-delay: 0.08s; }
|
.image-workbench-result-card:nth-child(2) { animation-delay: 0.08s; }
|
||||||
.image-workbench-result-card:nth-child(3) { animation-delay: 0.16s; }
|
.image-workbench-result-card:nth-child(3) { animation-delay: 0.16s; }
|
||||||
.image-workbench-result-card:nth-child(4) { animation-delay: 0.24s; }
|
.image-workbench-result-card:nth-child(4) { animation-delay: 0.24s; }
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.image-workbench-page {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
.image-workbench-toolbar {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.image-workbench-page {
|
|
||||||
padding: 8px 8px 80px;
|
|
||||||
}
|
|
||||||
.image-workbench-toolbar {
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.image-workbench-toolbar button {
|
|
||||||
min-width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14618,6 +14618,55 @@
|
|||||||
background: #ddf5e2;
|
background: #ddf5e2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-tool-pill.is-open {
|
||||||
|
background: #ddf5e2;
|
||||||
|
box-shadow: 1px 1px 0 #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tool-pills {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 120;
|
||||||
|
min-width: 160px;
|
||||||
|
background: #fff;
|
||||||
|
border: 3px solid #111;
|
||||||
|
box-shadow: 3px 3px 0 #111;
|
||||||
|
border-radius: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown__item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
color: #111;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown__item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown__item:hover {
|
||||||
|
background: #ddf5e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-dropdown__item.is-active {
|
||||||
|
background: #c8f0d6;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.agent-run-button {
|
.agent-run-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1286,23 +1286,3 @@ textarea.image-workbench-prompt,
|
|||||||
justify-items: start;
|
justify-items: start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.token-usage-page {
|
|
||||||
padding: 0 8px 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-usage-page .script-token-page__scroll,
|
|
||||||
.script-token-page__scroll {
|
|
||||||
padding-inline: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-member-card {
|
|
||||||
padding: 14px;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-member-card__head {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+875
-95
File diff suppressed because it is too large
Load Diff
+434
-15
@@ -2,32 +2,451 @@
|
|||||||
|
|
||||||
/* ── 代表作滚动容器:固定3列,刚好显示9个(3行),超出可滚动,隐藏滚动条 ── */
|
/* ── 代表作滚动容器:固定3列,刚好显示9个(3行),超出可滚动,隐藏滚动条 ── */
|
||||||
.profile-page__works-scroll {
|
.profile-page__works-scroll {
|
||||||
max-height: 390px;
|
max-height: 390px; /* 3行卡片:3 × 120(min-height) + 2 × 10(gap) = 380px,留10px余量 */
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none; /* Firefox */
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none; /* IE/Edge */
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-page__works-scroll::-webkit-scrollbar {
|
.profile-page__works-scroll::-webkit-scrollbar {
|
||||||
display: none;
|
display: none; /* Chrome/Safari/Edge */
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-page__works-scroll .profile-page__list-grid {
|
.profile-page__works-scroll .profile-page__list-grid {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr); /* 固定3列,刚好3×3=9个可见 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
/* Dashboard uses natural page scrolling instead of a nested works scroller. */
|
||||||
.profile-page,
|
.profile-page--dashboard .profile-page__works-scroll {
|
||||||
.auth-page,
|
max-height: none;
|
||||||
.settings-page {
|
overflow: visible;
|
||||||
padding: 16px 16px 80px;
|
scrollbar-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__interactive-card {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__interactive-card:focus-visible {
|
||||||
|
outline: 2px solid color-mix(in srgb, var(--accent) 72%, transparent);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 120;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: clamp(18px, 3vw, 32px);
|
||||||
|
isolation: isolate;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.62);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-panel {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
gap: 16px;
|
||||||
|
width: min(1080px, calc(100vw - 48px));
|
||||||
|
max-height: min(820px, calc(100dvh - 48px));
|
||||||
|
overflow: hidden;
|
||||||
|
padding: clamp(16px, 2vw, 22px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 18px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.018)),
|
||||||
|
color-mix(in srgb, var(--bg-panel) 94%, #000);
|
||||||
|
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
min-width: 0;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-head h2 {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 5px 0 0;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: clamp(18px, 2vw, 25px);
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-eyebrow {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-close {
|
||||||
|
display: inline-grid;
|
||||||
|
flex: 0 0 38px;
|
||||||
|
place-items: center;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-close:hover,
|
||||||
|
.profile-page__detail-close:focus-visible {
|
||||||
|
border-color: rgba(255, 255, 255, 0.22);
|
||||||
|
background: rgba(255, 255, 255, 0.075);
|
||||||
|
color: var(--text);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.42fr) minmax(300px, 0.78fr);
|
||||||
|
gap: 18px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-preview {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
min-height: min(500px, 54dvh);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(var(--accent-rgb), 0.055), transparent 58%),
|
||||||
|
rgba(255, 255, 255, 0.024);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-media {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: min(640px, 56dvh);
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
video.profile-page__detail-media {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 320px;
|
||||||
|
background: #050607;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-placeholder {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-placeholder .anticon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(var(--accent-rgb), 0.08);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info {
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info p {
|
||||||
|
margin: 0;
|
||||||
|
max-height: min(230px, 28dvh);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 13px 14px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 13px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
word-break: break-word;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info dl {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info dl div {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 13px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info dt {
|
||||||
|
color: var(--text-soft);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 760;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info dd {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-notice {
|
||||||
|
display: inline-flex;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid rgba(243, 170, 38, 0.28);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(243, 170, 38, 0.08);
|
||||||
|
color: var(--warning);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: stretch;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 1px solid rgba(var(--accent-rgb), 0.36);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(var(--accent-rgb), 0.11);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 750;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-action--primary {
|
||||||
|
justify-self: start;
|
||||||
|
min-width: 132px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-action--secondary,
|
||||||
|
.profile-page__detail-action--danger {
|
||||||
|
min-width: 118px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-action:hover,
|
||||||
|
.profile-page__detail-action:focus-visible {
|
||||||
|
border-color: rgba(var(--accent-rgb), 0.54);
|
||||||
|
background: rgba(var(--accent-rgb), 0.16);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-action--danger {
|
||||||
|
border-color: rgba(255, 90, 95, 0.24);
|
||||||
|
background: rgba(255, 90, 95, 0.08);
|
||||||
|
color: #ffadb0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-action--danger:hover,
|
||||||
|
.profile-page__detail-action--danger:focus-visible {
|
||||||
|
border-color: rgba(255, 90, 95, 0.42);
|
||||||
|
background: rgba(255, 90, 95, 0.13);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-action:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.profile-page__detail-overlay {
|
||||||
|
align-items: end;
|
||||||
|
padding: 10px 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-panel {
|
||||||
|
width: 100%;
|
||||||
|
max-height: calc(100dvh - 10px);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-preview {
|
||||||
|
align-items: start;
|
||||||
|
min-height: 260px;
|
||||||
|
max-height: 42dvh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-media {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
video.profile-page__detail-media {
|
||||||
|
align-self: center;
|
||||||
|
height: auto;
|
||||||
|
min-height: 220px;
|
||||||
|
max-height: 42dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info {
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info p {
|
||||||
|
min-height: 96px;
|
||||||
|
max-height: 30dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info dl {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info dl div {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info dt {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info dd {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-action--primary {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-action--secondary,
|
||||||
|
.profile-page__detail-action--danger {
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 420px) {
|
||||||
.profile-page,
|
.profile-page__detail-overlay {
|
||||||
.auth-page,
|
padding: 0;
|
||||||
.settings-page {
|
}
|
||||||
padding: 12px 10px 80px;
|
|
||||||
|
.profile-page__detail-panel {
|
||||||
|
max-height: 94dvh;
|
||||||
|
padding: 14px;
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-left: 0;
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-head {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-head h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-close {
|
||||||
|
flex-basis: 36px;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info dl {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info p {
|
||||||
|
min-height: 108px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-preview {
|
||||||
|
min-height: 240px;
|
||||||
|
max-height: 40dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-info dl {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-actions {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-action--primary {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-page__detail-action {
|
||||||
|
min-height: 42px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,40 +164,3 @@
|
|||||||
.provider-health-table tr:hover td {
|
.provider-health-table tr:hover td {
|
||||||
background: var(--surface-elevated);
|
background: var(--surface-elevated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.provider-health-page__inner {
|
|
||||||
padding: 16px;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-health-grid {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.provider-health-page__inner {
|
|
||||||
padding: 12px 12px 80px;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-health-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-health-toolbar {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-health-table {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-health-table th,
|
|
||||||
.provider-health-table td {
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -142,6 +142,7 @@
|
|||||||
|
|
||||||
/* Upload zone */
|
/* Upload zone */
|
||||||
.script-eval-v5-upload-zone {
|
.script-eval-v5-upload-zone {
|
||||||
|
position: relative;
|
||||||
border: 2px dashed var(--v5-border2);
|
border: 2px dashed var(--v5-border2);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 22px 18px;
|
padding: 22px 18px;
|
||||||
@@ -155,6 +156,37 @@
|
|||||||
background: var(--v5-green-deep);
|
background: var(--v5-green-deep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.script-eval-v5-upload-zone.is-dragging {
|
||||||
|
border-color: var(--v5-green);
|
||||||
|
border-style: solid;
|
||||||
|
background: var(--v5-green-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-eval-v5-upload-drop-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(0, 255, 136, 0.06);
|
||||||
|
color: var(--v5-green);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-eval-v5-upload-drop-overlay .anticon {
|
||||||
|
font-size: 40px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-eval-v5-upload-drop-overlay span {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.script-eval-v5-upload-icon {
|
.script-eval-v5-upload-icon {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: 38px;
|
font-size: 38px;
|
||||||
@@ -195,10 +227,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.script-eval-v5-upload-done {
|
.script-eval-v5-upload-done {
|
||||||
|
position: relative;
|
||||||
display: none;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 12px 14px;
|
padding: 12px 28px 12px 14px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--v5-green-deep);
|
background: var(--v5-green-deep);
|
||||||
border: 1px solid var(--v5-green-border);
|
border: 1px solid var(--v5-green-border);
|
||||||
@@ -208,6 +241,30 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.script-eval-v5-upload-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
color: var(--v5-text3);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-eval-v5-upload-delete:hover {
|
||||||
|
background: rgba(255, 77, 103, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.script-eval-v5-upload-done .anticon {
|
.script-eval-v5-upload-done .anticon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--v5-green);
|
color: var(--v5-green);
|
||||||
@@ -218,7 +275,7 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--v5-green);
|
color: var(--v5-green);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
flex: 1;
|
max-width: 16ch;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -2805,7 +2862,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
max-width: 8em;
|
max-width: 16em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.script-eval-v5-uf-size {
|
.script-eval-v5-uf-size {
|
||||||
|
|||||||
@@ -474,24 +474,3 @@
|
|||||||
width: min(100%, 430px);
|
width: min(100%, 430px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.size-template-page {
|
|
||||||
padding: 12px 10px 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-template-main-frame {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-template-spec-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-template-preview-note,
|
|
||||||
.size-template-detail-card {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -307,9 +307,10 @@
|
|||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: rgba(var(--accent-rgb), 0.13);
|
background: rgba(var(--accent-rgb), 0.22);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
|
box-shadow: 0 0 20px rgba(var(--accent-rgb), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-canvas-ghost__title {
|
.studio-canvas-ghost__title {
|
||||||
@@ -674,19 +675,3 @@
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.studio-tool-layout {
|
|
||||||
grid-template-rows: 40px auto minmax(200px, 1fr) 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-tool-layout__toolstrip {
|
|
||||||
padding: 6px 8px;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-panel {
|
|
||||||
max-height: 240px;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -133,15 +133,3 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.subtitle-removal-page {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.subtitle-removal-page {
|
|
||||||
padding: 8px 8px 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -440,137 +440,3 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════════
|
|
||||||
Responsive: Workbench Launch & Active State
|
|
||||||
Breakpoints: 900px / 560px
|
|
||||||
═══════════════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
/* ── 900px: Tablet — launch state ── */
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.wb-home {
|
|
||||||
padding: 36px 16px 80px;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-home__title {
|
|
||||||
font-size: clamp(24px, 6vw, 36px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-home__composer {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-home__suggestions {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-showcase {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-showcase__grid {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Active state: message thread */
|
|
||||||
.ai-workbench-content-scroll {
|
|
||||||
width: 100%;
|
|
||||||
padding: 16px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-chat-message-stack {
|
|
||||||
max-width: 86%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-chat-message-bubble {
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Composer in active state */
|
|
||||||
.wb-composer {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 0 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── 560px: Phone — full-width compact ── */
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.wb-home {
|
|
||||||
padding: 24px 10px 72px;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-home__title {
|
|
||||||
font-size: clamp(20px, 7vw, 28px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-home__glow {
|
|
||||||
width: 240px;
|
|
||||||
height: 120px;
|
|
||||||
top: -40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-showcase__grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-suggestion-chip {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 7px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Active state: messages fill screen */
|
|
||||||
.ai-workbench-content-scroll {
|
|
||||||
padding: 12px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-workbench-thread-shell {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-chat-message-list {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-chat-message-stack {
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-chat-message-bubble {
|
|
||||||
padding: 8px 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-chat-message-bubble--user {
|
|
||||||
border-radius: 12px 12px 4px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-chat-message-bubble--ai {
|
|
||||||
border-radius: 12px 12px 12px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-chat-avatar {
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
flex: 0 0 26px;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-chat-message-row {
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Composer at bottom */
|
|
||||||
.wb-composer {
|
|
||||||
padding: 6px 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-chat-scroll-actions {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -896,21 +896,6 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1180px) {
|
|
||||||
.brand-lockup__tone {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.creator-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.member-button {
|
|
||||||
max-width: 148px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.web-shell {
|
.web-shell {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -977,7 +962,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 640px: Narrower topbar adjustments ── */
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.brand-lockup__name {
|
.brand-lockup__name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -997,33 +981,3 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 560px: Phone-sized compact layout ── */
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.web-topbar {
|
|
||||||
flex: 0 0 44px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-lockup {
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-lockup__mark {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-lockup__name {
|
|
||||||
font-size: 13px;
|
|
||||||
max-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-button,
|
|
||||||
.icon-button {
|
|
||||||
min-width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+1906
-93
File diff suppressed because it is too large
Load Diff
@@ -84,14 +84,3 @@
|
|||||||
--danger: var(--error);
|
--danger: var(--error);
|
||||||
--shadow-soft: var(--shadow-panel);
|
--shadow-soft: var(--shadow-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════════
|
|
||||||
Responsive Breakpoints (standardized)
|
|
||||||
Reference-only — CSS custom properties cannot be used
|
|
||||||
inside @media queries, but all pages should align to
|
|
||||||
these three breakpoints for consistency.
|
|
||||||
|
|
||||||
Phone : 560px (max-width — portrait / landscape)
|
|
||||||
Tablet : 900px (max-width — tablet / small desktop)
|
|
||||||
Desktop: 1180px (max-width — large desktop)
|
|
||||||
═══════════════════════════════════════════════════════ */
|
|
||||||
|
|||||||
@@ -8,27 +8,27 @@ export const ENTERPRISE_WANXIANG_I2V_MODEL = "wan2.7-i2v";
|
|||||||
export const ENTERPRISE_VIDEO_MODEL_OPTIONS = [
|
export const ENTERPRISE_VIDEO_MODEL_OPTIONS = [
|
||||||
{
|
{
|
||||||
value: HAPPY_HORSE_UI_MODEL,
|
value: HAPPY_HORSE_UI_MODEL,
|
||||||
label: "HappyHorse 1.0 · 0.72 积分/秒起",
|
label: "HappyHorse 1.0",
|
||||||
description: "自动匹配文生视频、首帧图生视频或参考图生视频",
|
description: "自动匹配文生视频、首帧图生视频或参考图生视频",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: VIDU_UI_MODEL,
|
value: VIDU_UI_MODEL,
|
||||||
label: "Vidu Q3 Turbo · 0.40 积分/秒起",
|
label: "Vidu Q3 Turbo",
|
||||||
description: "自动匹配文生视频或图生视频,支持16秒",
|
description: "自动匹配文生视频或图生视频,支持16秒",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: PIXVERSE_UI_MODEL,
|
value: PIXVERSE_UI_MODEL,
|
||||||
label: "PixVerse V6 · 0.40 积分/秒起",
|
label: "PixVerse V6",
|
||||||
description: "自动匹配文生视频或图生视频,擅长动作特效",
|
description: "自动匹配文生视频或图生视频,擅长动作特效",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: ENTERPRISE_WANXIANG_I2V_MODEL,
|
value: ENTERPRISE_WANXIANG_I2V_MODEL,
|
||||||
label: "万相 图生视频 · 0.60 积分/秒起",
|
label: "万相 图生视频",
|
||||||
description: "图生视频模型,支持首帧图驱动",
|
description: "图生视频模型,支持首帧图驱动",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: ENTERPRISE_KLING_MODEL,
|
value: ENTERPRISE_KLING_MODEL,
|
||||||
label: "Kling V3 Omni · 0.60 积分/秒起",
|
label: "Kling V3 Omni",
|
||||||
description: "支持文生视频、图生视频及多模态参考生成",
|
description: "支持文生视频、图生视频及多模态参考生成",
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user