+
+ {isDragging ?
释放文件以上传
: null}
)
) : (
-
+
+
+
+
+
{mode === "image" ? "拖拽或选择图片" : "拖拽或选择视频"}
+
{mode === "image" ? "支持 PNG / JPG / WebP" : "支持 MP4 / MOV / WebM"}
+
)}
diff --git a/src/features/script-tokens/ScriptTokensPage.tsx b/src/features/script-tokens/ScriptTokensPage.tsx
index c59cbb0..8b8daaa 100644
--- a/src/features/script-tokens/ScriptTokensPage.tsx
+++ b/src/features/script-tokens/ScriptTokensPage.tsx
@@ -1,6 +1,7 @@
import {
BarChartOutlined,
CheckCircleFilled,
+ CloseOutlined,
CopyOutlined,
DownloadOutlined,
FileTextOutlined,
@@ -8,7 +9,7 @@ import {
ThunderboltOutlined,
UploadOutlined,
} 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 { buildApiUrl, getStoredToken } from "../../api/serverConnection";
import { useSessionStore } from "../../stores";
@@ -239,6 +240,7 @@ function ScriptTokensPage() {
const [animatedScore, setAnimatedScore] = useState(0);
const [activeHistoryIndex, setActiveHistoryIndex] = useState
(0);
const [history, setHistory] = useState(loadHistory);
+ const [isDragging, setIsDragging] = useState(false);
const fileInputRef = useRef(null);
const scoreFrameRef = useRef(null);
@@ -261,9 +263,7 @@ function ScriptTokensPage() {
return () => { if (scoreFrameRef.current) cancelAnimationFrame(scoreFrameRef.current); };
}, [result]);
- const handleFileUpload = async (event: ChangeEvent) => {
- const file = event.target.files?.[0];
- if (!file) return;
+ const processUploadedFile = async (file: File) => {
const ext = getFileExtension(file.name);
const readable = isReadableTextFile(file, ext);
setUploadedFile({ name: file.name, size: file.size });
@@ -293,6 +293,12 @@ function ScriptTokensPage() {
} else {
setScript(`[已上传文件:${file.name}]\n\n暂不支持解析 ${ext ? ext.toUpperCase() : "未知"} 格式,请上传常见文本类文件。`);
}
+ };
+
+ const handleFileUpload = async (event: ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (!file) return;
+ await processUploadedFile(file);
event.target.value = "";
};
@@ -393,6 +399,30 @@ function ScriptTokensPage() {
fileInputRef.current?.click();
};
+ const handleDragOver = (event: DragEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (event.dataTransfer.types.includes("Files")) {
+ setIsDragging(true);
+ }
+ };
+
+ const handleDragLeave = (event: DragEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (event.currentTarget === event.target || !event.currentTarget.contains(event.relatedTarget as Node)) {
+ setIsDragging(false);
+ }
+ };
+
+ const handleDrop = (event: DragEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ setIsDragging(false);
+ const file = event.dataTransfer.files[0];
+ if (file) processUploadedFile(file);
+ };
+
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 compactTitle = uploadedFile?.name?.replace(/\.[^.]+$/, "") ?? "剧本评测";
@@ -409,14 +439,31 @@ function ScriptTokensPage() {
上传剧本
fileInputRef.current?.click()}
onKeyDown={uploadKeyDown}
+ onDragOver={handleDragOver}
+ onDragLeave={handleDragLeave}
+ onDrop={handleDrop}
>
+ {isDragging ? (
+
+
+ 释放文件以上传
+
+ ) : null}
{uploadedFile ? (
+
{uploadedFile.name}
diff --git a/src/features/subtitle-removal/SubtitleRemovalPage.tsx b/src/features/subtitle-removal/SubtitleRemovalPage.tsx
index cc11450..5f719fe 100644
--- a/src/features/subtitle-removal/SubtitleRemovalPage.tsx
+++ b/src/features/subtitle-removal/SubtitleRemovalPage.tsx
@@ -12,7 +12,7 @@ import {
SwapOutlined,
VideoCameraOutlined,
} 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 { waitForTask } from "../../api/taskSubscription";
import { saveToolTaskState, loadToolTaskState, clearToolTaskState } from "../workbench/toolKeepalive";
@@ -73,6 +73,7 @@ function SubtitleRemovalPage({
const [isProcessing, setIsProcessing] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const [isSavingAsset, setIsSavingAsset] = useState(false);
+ const [isDragging, setIsDragging] = useState(false);
const activeTaskIdRef = useRef(activeTaskId);
activeTaskIdRef.current = activeTaskId;
const keepaliveRestoredRef = useRef(false);
@@ -125,10 +126,7 @@ function SubtitleRemovalPage({
event.currentTarget.value = "";
};
- const handleFileDrop = (event: React.DragEvent) => {
- event.preventDefault();
- const file = Array.from(event.dataTransfer.files).find((f) => f.type.startsWith("video/"));
- if (!file) return;
+ const processDroppedFile = (file: File) => {
if (sourcePreview.startsWith("blob:")) URL.revokeObjectURL(sourcePreview);
setSourceName(file.name);
setSourceFile(file);
@@ -140,6 +138,10 @@ function SubtitleRemovalPage({
setStatus(`已导入 ${file.name}`);
};
+ const handleDragOver = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes("Files")) setIsDragging(true); };
+ const handleDragLeave = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) setIsDragging(false); };
+ const handleFileDrop = (e: DragEvent) => { 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 normalized = sourceUrl.trim();
if (!/^https?:\/\//i.test(normalized)) {
@@ -341,7 +343,13 @@ function SubtitleRemovalPage({
accept="video/mp4"
onChange={handleFileChange}
/>
- e.preventDefault()} onDrop={handleFileDrop}>
+
+ {isDragging ?
释放文件以上传
: null}
) : (
-
-
-
上传视频后在此预览
+
+
+
+
+
拖拽或选择视频
+
仅支持 MP4,最大 1GB,最高 1080P
)}
diff --git a/src/features/workbench/WorkbenchPage.tsx b/src/features/workbench/WorkbenchPage.tsx
index 140c0e9..e89f964 100644
--- a/src/features/workbench/WorkbenchPage.tsx
+++ b/src/features/workbench/WorkbenchPage.tsx
@@ -99,6 +99,9 @@ import {
type WorkbenchKeepaliveTask,
MODE_META,
MODE_OPTIONS,
+ CHAT_MODEL_OPTIONS,
+ THINKING_SPEED_OPTIONS,
+ THINKING_DEPTH_OPTIONS,
IMAGE_MODEL_OPTIONS,
VIDEO_MODEL_OPTIONS,
RATIO_OPTIONS,
@@ -330,9 +333,13 @@ function WorkbenchPage({
const [videoModel, setVideoModel] = useState(VIDEO_MODEL_OPTIONS[0].value);
const [videoFrameMode, setVideoFrameMode] = useState("omni");
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 [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(() => {
let cancelled = false;
@@ -388,13 +395,13 @@ function WorkbenchPage({
const referenceCount = referenceItems.length;
const activeVideoModelValue = toHappyHorseDisplayModel(videoModel);
const activeModelValue =
- activeMode === "image" ? imageModel : activeMode === "video" ? activeVideoModelValue : CHAT_MODEL;
+ activeMode === "image" ? imageModel : activeMode === "video" ? activeVideoModelValue : chatModel;
const activeModel =
activeMode === "image"
? imageModelOptions.find((item) => item.value === imageModel)?.label || imageModel
: activeMode === "video"
? videoModelOptions.find((item) => item.value === activeVideoModelValue)?.label || activeVideoModelValue
- : "OmniChat";
+ : CHAT_MODEL_OPTIONS.find((item) => item.value === chatModel)?.label || chatModel;
const conversationRecords = useMemo
(
() =>
conversations.map((conversation) => ({
@@ -2648,6 +2655,46 @@ function WorkbenchPage({
ariaLabel="工作台模式"
direction={dropdownDirection}
/>
+ {activeMode === "chat" && (
+ <>
+ toggleToolbarMenu("chat-model")}
+ onClose={closeToolbarMenus}
+ onChange={setChatModel}
+ ariaLabel="对话模型"
+ direction={dropdownDirection}
+ />
+ toggleToolbarMenu("chat-speed")}
+ onClose={closeToolbarMenus}
+ onChange={setThinkingSpeed}
+ ariaLabel="思考速度"
+ direction={dropdownDirection}
+ />
+ toggleToolbarMenu("chat-depth")}
+ onClose={closeToolbarMenus}
+ onChange={setThinkingDepth}
+ ariaLabel="思考深度"
+ direction={dropdownDirection}
+ />
+ >
+ )}
{activeMode === "image" && (
<>
span {
display: inline-grid;
grid-template-columns: auto minmax(0, max-content);
@@ -1676,75 +1712,86 @@
font-weight: 800;
}
-.product-clone-page[data-tool="clone"] .clone-ai-replicate-preview {
- position: absolute;
- inset: 6px;
+/* ── Reference image file grid (inside upload button) ── */
+.product-clone-page[data-tool="clone"] .clone-ai-replicate-files {
display: grid;
- grid-auto-flow: column;
- grid-auto-columns: minmax(0, 56px);
- align-items: center;
- justify-content: center;
+ grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
gap: 6px;
- border-radius: 10px;
- background: #20242c;
- opacity: 0;
- pointer-events: none;
- transform: scale(0.98);
- transition:
- opacity 160ms ease,
- transform 160ms ease;
+ width: 100%;
+ overflow: visible;
}
-.product-clone-page[data-tool="clone"] .clone-ai-replicate-upload:hover .clone-ai-replicate-preview,
-.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 {
+.product-clone-page[data-tool="clone"] .clone-ai-replicate-file {
position: relative;
display: block;
- width: 56px;
- height: 52px;
+ aspect-ratio: 1;
min-width: 0;
overflow: visible;
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;
width: 100%;
height: 100%;
min-width: 0;
- overflow: hidden;
border: 1px solid #3a4555;
- border-radius: 8px;
+ border-radius: 6px;
background: #111720;
object-fit: cover;
}
-.product-clone-page[data-tool="clone"] .clone-ai-replicate-preview figure:only-child {
- width: min(170px, 100%);
- height: 52px;
+.product-clone-page[data-tool="clone"] .clone-ai-replicate-file > img:hover {
+ border-color: #00ff88;
}
-.product-clone-page[data-tool="clone"] .clone-ai-replicate-preview figure:only-child > img {
- object-fit: contain;
-}
-
-.product-clone-page[data-tool="clone"] .clone-ai-replicate-preview b {
- display: grid;
- width: 42px;
- height: 42px;
- place-items: center;
- border: 1px solid #3a4555;
- border-radius: 999px;
- background: #151b24;
- color: #eef2f6;
+.product-clone-page[data-tool="clone"] .clone-ai-replicate-add-more {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ justify-self: center;
+ width: fit-content;
+ max-width: calc(100% - 8px);
+ height: 28px;
+ min-width: 0;
+ border-radius: 7px;
+ background: #2b3039;
+ color: #9aa4b4;
+ padding: 0 10px;
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 {
@@ -3510,8 +3557,8 @@
.product-set-thumb:focus-within .uploaded-image-zoom,
.product-clone-uploaded-thumb:hover .uploaded-image-zoom,
.product-clone-uploaded-thumb:focus-within .uploaded-image-zoom,
-.clone-ai-replicate-preview figure:hover .uploaded-image-zoom,
-.clone-ai-replicate-preview figure:focus-within .uploaded-image-zoom {
+.clone-ai-replicate-file:hover .uploaded-image-zoom,
+.clone-ai-replicate-file:focus-within .uploaded-image-zoom {
opacity: 1;
transform: translate(-50%, 0) scale(1);
visibility: visible;
@@ -9458,3 +9505,4 @@
min-height: calc(100% - 59px);
}
}
+
diff --git a/src/styles/pages/image-workbench.css b/src/styles/pages/image-workbench.css
index 20948ea..0b854ab 100644
--- a/src/styles/pages/image-workbench.css
+++ b/src/styles/pages/image-workbench.css
@@ -216,14 +216,14 @@
.image-workbench-layout {
display: grid;
- grid-template-columns: 280px 1fr 220px;
+ grid-template-columns: 280px 1fr;
flex: 1;
min-height: 0;
overflow: hidden;
}
.image-workbench-layout--inpaint {
- grid-template-columns: 260px 1fr 240px;
+ grid-template-columns: 260px 1fr;
}
.image-workbench-layout--camera {
@@ -278,6 +278,27 @@
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 {
display: flex;
flex-direction: column;
diff --git a/src/styles/pages/legacy-pages.css b/src/styles/pages/legacy-pages.css
index 3a7585d..07c635e 100644
--- a/src/styles/pages/legacy-pages.css
+++ b/src/styles/pages/legacy-pages.css
@@ -14618,6 +14618,55 @@
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 {
display: flex;
align-items: center;
diff --git a/src/styles/pages/script-tokens-v5.css b/src/styles/pages/script-tokens-v5.css
index 666a3c0..d452e74 100644
--- a/src/styles/pages/script-tokens-v5.css
+++ b/src/styles/pages/script-tokens-v5.css
@@ -142,6 +142,7 @@
/* Upload zone */
.script-eval-v5-upload-zone {
+ position: relative;
border: 2px dashed var(--v5-border2);
border-radius: 12px;
padding: 22px 18px;
@@ -155,6 +156,37 @@
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 {
margin-bottom: 10px;
font-size: 38px;
@@ -195,10 +227,11 @@
}
.script-eval-v5-upload-done {
+ position: relative;
display: none;
align-items: center;
gap: 10px;
- padding: 12px 14px;
+ padding: 12px 28px 12px 14px;
border-radius: 8px;
background: var(--v5-green-deep);
border: 1px solid var(--v5-green-border);
@@ -208,6 +241,30 @@
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 {
font-size: 16px;
color: var(--v5-green);
@@ -218,7 +275,7 @@
font-size: 13px;
color: var(--v5-green);
font-weight: 600;
- flex: 1;
+ max-width: 16ch;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -2805,7 +2862,7 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- max-width: 8em;
+ max-width: 16em;
}
.script-eval-v5-uf-size {
@@ -3425,8 +3482,6 @@
font-size: 13px;
}
}
-<<<<<<< HEAD
-=======
/* Script review left panel overflow guard: keep actions available while history remains scrollable. */
.script-eval-v5-left {
@@ -3935,4 +3990,3 @@
.script-eval-v5.is-ready .script-eval-v5-status-dot {
box-shadow: none;
}
->>>>>>> c1c4086383ddd7c1c8c152c2d5a97a4f432fa260
diff --git a/src/styles/pages/studio-layout.css b/src/styles/pages/studio-layout.css
index 0bfdbb2..8ca62a1 100644
--- a/src/styles/pages/studio-layout.css
+++ b/src/styles/pages/studio-layout.css
@@ -307,9 +307,10 @@
width: 56px;
height: 56px;
border-radius: var(--radius-sm);
- background: rgba(var(--accent-rgb), 0.13);
+ background: rgba(var(--accent-rgb), 0.22);
color: var(--accent);
font-size: 26px;
+ box-shadow: 0 0 20px rgba(var(--accent-rgb), 0.08);
}
.studio-canvas-ghost__title {