Compare commits
6 Commits
e351e93200
...
30536ad15f
| Author | SHA1 | Date | |
|---|---|---|---|
| 30536ad15f | |||
| e78cc05299 | |||
| b88be66e7f | |||
| 1a9196a63a | |||
| 4dfcb6fc8a | |||
| 60d5cd2edf |
+7
-1
@@ -374,6 +374,7 @@ function App() {
|
||||
})));
|
||||
|
||||
const [ecommerceEverMounted, setEcommerceEverMounted] = useState(false);
|
||||
const [workbenchResetToken, setWorkbenchResetToken] = useState(0);
|
||||
const isEcommerceActive = activeView === "ecommerce" || activeView === "ecommerceHub";
|
||||
useEffect(() => {
|
||||
if (isEcommerceActive && !ecommerceEverMounted) setEcommerceEverMounted(true);
|
||||
@@ -459,6 +460,9 @@ function App() {
|
||||
);
|
||||
|
||||
const handleSetView = useCallback((view: WebViewKey) => {
|
||||
if (view === "workbench" && Boolean(session)) {
|
||||
setWorkbenchResetToken((token) => token + 1);
|
||||
}
|
||||
window.location.hash = `/${view}`;
|
||||
setView(view);
|
||||
if (view !== "login") {
|
||||
@@ -467,7 +471,7 @@ function App() {
|
||||
if (isWorkspaceView(view)) {
|
||||
setWorkspaceExpanded(true);
|
||||
}
|
||||
}, [setView, setWorkspaceExpanded]);
|
||||
}, [session, setView, setWorkspaceExpanded]);
|
||||
|
||||
const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => {
|
||||
clearAllUserStorage();
|
||||
@@ -1313,11 +1317,13 @@ function App() {
|
||||
case "workbench":
|
||||
return (
|
||||
<WorkbenchPage
|
||||
key={`workbench-${workbenchResetToken}`}
|
||||
isAuthenticated={Boolean(session)}
|
||||
session={session}
|
||||
onRequireLogin={handleRequireTaskLogin}
|
||||
onOpenResultInCanvas={handleOpenResultInCanvas}
|
||||
onRefreshUsage={refreshUsage}
|
||||
resetToken={workbenchResetToken}
|
||||
/>
|
||||
);
|
||||
case "home":
|
||||
|
||||
@@ -38,9 +38,14 @@ function normalizeModelOption(raw: unknown): ModelCapabilityOption | null {
|
||||
const enabled = raw.enabled === undefined ? status !== "maintenance" && status !== "disabled" : Boolean(raw.enabled);
|
||||
if (!enabled) return null;
|
||||
|
||||
const label = toStringValue(raw.label ?? raw.displayName ?? raw.display_name ?? raw.name, value);
|
||||
|
||||
return {
|
||||
value,
|
||||
label: toStringValue(raw.label ?? raw.displayName ?? raw.display_name ?? raw.name, value),
|
||||
label:
|
||||
value === "wan2.7-image-pro"
|
||||
? label.replace(/\s*4k\b/i, "").trim() || "wan 2.7 Pro"
|
||||
: label,
|
||||
description: toStringValue(raw.description) || undefined,
|
||||
badge: toStringValue(raw.badge) || undefined,
|
||||
enabled,
|
||||
|
||||
@@ -81,7 +81,9 @@ import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
|
||||
import { calculateEnterpriseVideoCredits, ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
|
||||
import {
|
||||
getImageQualityOptions,
|
||||
getImageQualityOptionsForContext,
|
||||
getDefaultImageQuality,
|
||||
getDefaultImageQualityForContext,
|
||||
getVideoQualityOptions,
|
||||
getDefaultVideoQuality,
|
||||
getVideoQualityLabel,
|
||||
@@ -200,6 +202,7 @@ interface WorkbenchPageProps {
|
||||
onRequireLogin: (input: CreatePreviewTaskInput) => void;
|
||||
onOpenResultInCanvas?: (payload: import("./workbenchConstants").WorkbenchResultActionPayload) => void;
|
||||
onRefreshUsage?: () => void;
|
||||
resetToken?: number;
|
||||
}
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────
|
||||
@@ -231,6 +234,7 @@ function WorkbenchPage({
|
||||
onRequireLogin,
|
||||
onOpenResultInCanvas,
|
||||
onRefreshUsage,
|
||||
resetToken,
|
||||
}: WorkbenchPageProps) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const referenceInputRef = useRef<HTMLInputElement | null>(null);
|
||||
@@ -249,10 +253,11 @@ function WorkbenchPage({
|
||||
const activeConversationIdRef = useRef<number | null>(null);
|
||||
const messagesRef = useRef<ChatMessage[]>([]);
|
||||
const conversationMessagesCacheRef = useRef<Map<number, ChatMessage[]>>(new Map());
|
||||
const skipConversationAutoSelectRef = useRef(false);
|
||||
const skipConversationAutoSelectRef = useRef(Boolean(resetToken));
|
||||
const keepaliveTasksRef = useRef<Record<string, WorkbenchKeepaliveTask>>(readStoredKeepaliveTasks());
|
||||
const taskAbortControllersRef = useRef<Map<string, AbortController>>(new Map());
|
||||
const lastScrollTopRef = useRef(0);
|
||||
const scrollActionHintTimerRef = useRef<number | null>(null);
|
||||
const shouldFollowNewMessagesRef = useRef(true);
|
||||
const pendingScrollToLatestRef = useRef(true);
|
||||
const genTracker = useGenerationTasks({ sourceView: "workbench" });
|
||||
@@ -261,7 +266,7 @@ function WorkbenchPage({
|
||||
|
||||
const [activeMode, setActiveMode] = useState<WorkbenchMode>("video");
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [messages, setMessages] = useState<ChatMessage[]>(() => readStoredMessages());
|
||||
const [messages, setMessages] = useState<ChatMessage[]>(() => (resetToken ? [] : readStoredMessages()));
|
||||
const [promptHistory, setPromptHistory] = useState<string[]>(() => readStoredPromptHistory());
|
||||
const [toolbarMenuId, setToolbarMenuId] = useState<ToolbarMenuId>(null);
|
||||
const [referenceItems, setReferenceItems] = useState<ReferenceItem[]>([]);
|
||||
@@ -284,7 +289,7 @@ function WorkbenchPage({
|
||||
const [projectError, setProjectError] = useState<string | null>(null);
|
||||
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
|
||||
const [activeConversationId, setActiveConversationId] = useState<number | null>(() =>
|
||||
readStoredActiveConversationId(readStoredMessages()),
|
||||
resetToken ? null : readStoredActiveConversationId(readStoredMessages()),
|
||||
);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
||||
const [deleteDialog, setDeleteDialog] = useState<DeleteDialogState | null>(null);
|
||||
@@ -294,7 +299,9 @@ function WorkbenchPage({
|
||||
const [promptSelectionRange, setPromptSelectionRange] = useState({ start: 0, end: 0 });
|
||||
const [mentionActiveIndex, setMentionActiveIndex] = useState(0);
|
||||
const [composerHidden, setComposerHidden] = useState(false);
|
||||
const [scrollActionHint, setScrollActionHint] = useState<"top" | "bottom" | null>(null);
|
||||
const [workspaceStarted, setWorkspaceStarted] = useState(false);
|
||||
const lastResetTokenRef = useRef(resetToken);
|
||||
|
||||
useEffect(() => {
|
||||
activeConversationIdRef.current = activeConversationId;
|
||||
@@ -420,7 +427,6 @@ function WorkbenchPage({
|
||||
const toolTheme = MODE_META[activeMode];
|
||||
const workbenchAccent = "#00ff88";
|
||||
const hasConversationRecords = activeConversationId !== null || messages.length > 0;
|
||||
const hasActivatedWorkspace = workspaceStarted || isGenerating || hasConversationRecords;
|
||||
const referenceCount = referenceItems.length;
|
||||
const activeVideoModelValue = toHappyHorseDisplayModel(videoModel);
|
||||
const activeModelValue =
|
||||
@@ -448,6 +454,7 @@ function WorkbenchPage({
|
||||
[conversations],
|
||||
);
|
||||
const hasSidebarRecords = conversationRecords.length > 0;
|
||||
const hasActivatedWorkspace = workspaceStarted || isGenerating || hasConversationRecords;
|
||||
|
||||
const activeConversationTitle = useMemo(() => {
|
||||
if (!activeConversationId) return "";
|
||||
@@ -464,7 +471,26 @@ function WorkbenchPage({
|
||||
setSidebarCollapsed(!hasSidebarRecords);
|
||||
}, [hasSidebarRecords]);
|
||||
|
||||
const imageQualityOptions = useMemo(() => getImageQualityOptions(imageModel), [imageModel]);
|
||||
const hasImageReferences = activeMode === "image" && referenceItems.some((item) => item.kind === "image");
|
||||
const isImageGridMode = activeMode === "image" && imageGridMode !== "single";
|
||||
const imageQualityContext = useMemo(
|
||||
() => ({
|
||||
hasReferenceImages: hasImageReferences,
|
||||
isGridMode: isImageGridMode,
|
||||
}),
|
||||
[hasImageReferences, isImageGridMode],
|
||||
);
|
||||
const imageQualityOptions = useMemo(
|
||||
() => getImageQualityOptionsForContext(imageModel, imageQualityContext),
|
||||
[imageModel, imageQualityContext],
|
||||
);
|
||||
const imageGridModeOptions = useMemo(
|
||||
() =>
|
||||
String(imageModel || "").toLowerCase().startsWith("wan2.7-")
|
||||
? GRID_MODE_OPTIONS.filter((option) => option.value !== "grid-25")
|
||||
: GRID_MODE_OPTIONS,
|
||||
[imageModel],
|
||||
);
|
||||
const videoQualityOptions = getVideoQualityOptions(videoModel);
|
||||
const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality);
|
||||
|
||||
@@ -543,6 +569,31 @@ function WorkbenchPage({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const hideScrollActionHint = useCallback(() => {
|
||||
if (scrollActionHintTimerRef.current !== null) {
|
||||
window.clearTimeout(scrollActionHintTimerRef.current);
|
||||
scrollActionHintTimerRef.current = null;
|
||||
}
|
||||
setScrollActionHint(null);
|
||||
}, []);
|
||||
|
||||
const showScrollActionHint = useCallback((direction: "top" | "bottom") => {
|
||||
if (scrollActionHintTimerRef.current !== null) {
|
||||
window.clearTimeout(scrollActionHintTimerRef.current);
|
||||
}
|
||||
setScrollActionHint(direction);
|
||||
scrollActionHintTimerRef.current = window.setTimeout(() => {
|
||||
setScrollActionHint(null);
|
||||
scrollActionHintTimerRef.current = null;
|
||||
}, 1400);
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (scrollActionHintTimerRef.current !== null) {
|
||||
window.clearTimeout(scrollActionHintTimerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const imageSettingGroups = useMemo<WorkbenchFieldGroup[]>(
|
||||
() => [
|
||||
{
|
||||
@@ -1175,9 +1226,15 @@ function WorkbenchPage({
|
||||
|
||||
useEffect(() => {
|
||||
if (!imageQualityOptions.some((option) => option.value === imageQuality)) {
|
||||
setImageQuality(getDefaultImageQuality(imageModel));
|
||||
setImageQuality(getDefaultImageQualityForContext(imageModel, imageQualityContext));
|
||||
}
|
||||
}, [imageModel, imageQuality, imageQualityOptions]);
|
||||
}, [imageModel, imageQuality, imageQualityContext, imageQualityOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!imageGridModeOptions.some((option) => option.value === imageGridMode)) {
|
||||
setImageGridMode("single");
|
||||
}
|
||||
}, [imageGridMode, imageGridModeOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeMode !== "video" || videoFrameMode !== "start-end" || referenceItems.length <= 2) return;
|
||||
@@ -1313,6 +1370,12 @@ function WorkbenchPage({
|
||||
activeConversationIdRef.current = null;
|
||||
}, [syncActiveGenerationUi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetToken === undefined || lastResetTokenRef.current === resetToken) return;
|
||||
lastResetTokenRef.current = resetToken;
|
||||
handleNewConversation();
|
||||
}, [handleNewConversation, resetToken]);
|
||||
|
||||
const handleSelectProject = useCallback((id: string) => {
|
||||
if (!id) {
|
||||
handleNewConversation();
|
||||
@@ -1467,6 +1530,7 @@ function WorkbenchPage({
|
||||
const atBottom = top + surface.clientHeight >= surface.scrollHeight - edgeThreshold;
|
||||
shouldFollowNewMessagesRef.current = atBottom;
|
||||
setComposerHidden(!(atTop || atBottom));
|
||||
hideScrollActionHint();
|
||||
lastScrollTopRef.current = top;
|
||||
};
|
||||
|
||||
@@ -1481,24 +1545,27 @@ function WorkbenchPage({
|
||||
shouldFollowNewMessagesRef.current = atBottom;
|
||||
if (atTop || atBottom) {
|
||||
setComposerHidden(false);
|
||||
hideScrollActionHint();
|
||||
} else if (Math.abs(delta) > scrollDeltaThreshold) {
|
||||
setComposerHidden(true);
|
||||
showScrollActionHint(delta < 0 ? "top" : "bottom");
|
||||
}
|
||||
lastScrollTopRef.current = top;
|
||||
};
|
||||
|
||||
surface.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => surface.removeEventListener("scroll", handleScroll);
|
||||
}, [hasActivatedWorkspace]);
|
||||
}, [hasActivatedWorkspace, hideScrollActionHint, showScrollActionHint]);
|
||||
|
||||
const scrollMessagesSurface = useCallback((direction: "top" | "bottom") => {
|
||||
const surface = messagesSurfaceRef.current;
|
||||
if (!surface) return;
|
||||
|
||||
const top = direction === "top" ? 0 : surface.scrollHeight;
|
||||
hideScrollActionHint();
|
||||
setComposerHidden(false);
|
||||
surface.scrollTo({ top, behavior: "smooth" });
|
||||
}, []);
|
||||
}, [hideScrollActionHint]);
|
||||
|
||||
const closeToolbarMenus = () => setToolbarMenuId(null);
|
||||
const toggleToolbarMenu = (menuId: Exclude<ToolbarMenuId, null>) => {
|
||||
@@ -2865,7 +2932,7 @@ function WorkbenchPage({
|
||||
<SelectChip
|
||||
chipId="image-grid-mode"
|
||||
value={imageGridMode}
|
||||
options={GRID_MODE_OPTIONS}
|
||||
options={imageGridModeOptions}
|
||||
disabled={disabled}
|
||||
isOpen={toolbarMenuId === "image-grid-mode"}
|
||||
onToggle={() => toggleToolbarMenu("image-grid-mode")}
|
||||
@@ -2993,9 +3060,29 @@ function WorkbenchPage({
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const renderPromptCaseOverlay = () =>
|
||||
selectedPromptCase ? (
|
||||
<div className="wb-prompt-case-modal" role="dialog" aria-modal="true" aria-labelledby="wb-prompt-case-title">
|
||||
const renderPromptCaseOverlay = () => {
|
||||
if (!selectedPromptCase) return null;
|
||||
|
||||
const measuredRatio = promptCaseMeasuredRatios[selectedPromptCase.id];
|
||||
const ratioParts = selectedPromptCase.ratio.replace(/\s+/g, "").split(":").map(Number);
|
||||
const declaredRatio =
|
||||
ratioParts.length === 2 && ratioParts[0] > 0 && ratioParts[1] > 0
|
||||
? ratioParts[0] / ratioParts[1]
|
||||
: null;
|
||||
const caseRatio =
|
||||
typeof measuredRatio === "number" && Number.isFinite(measuredRatio) && measuredRatio > 0
|
||||
? measuredRatio
|
||||
: declaredRatio;
|
||||
const copyLength = `${selectedPromptCase.summary} ${selectedPromptCase.prompt}`.length;
|
||||
const modalClassName = [
|
||||
"wb-prompt-case-modal",
|
||||
caseRatio && caseRatio < 0.72 ? "is-tall-media" : "",
|
||||
caseRatio && caseRatio >= 0.72 && caseRatio < 1 ? "is-portrait-media" : "",
|
||||
copyLength > 260 ? "is-long-copy" : "",
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
return (
|
||||
<div className={modalClassName} role="dialog" aria-modal="true" aria-labelledby="wb-prompt-case-title">
|
||||
<button
|
||||
type="button"
|
||||
className="wb-prompt-case-modal__backdrop"
|
||||
@@ -3004,7 +3091,11 @@ function WorkbenchPage({
|
||||
/>
|
||||
<section className="wb-prompt-case-modal__panel">
|
||||
<div className="wb-prompt-case-modal__media">
|
||||
<img src={selectedPromptCase.imageUrl} alt={selectedPromptCase.title} />
|
||||
<img
|
||||
src={selectedPromptCase.imageUrl}
|
||||
alt={selectedPromptCase.title}
|
||||
onLoad={(event) => handlePromptCaseImageLoad(selectedPromptCase.id, event)}
|
||||
/>
|
||||
</div>
|
||||
<aside className="wb-prompt-case-modal__sidebar">
|
||||
<button
|
||||
@@ -3044,7 +3135,8 @@ function WorkbenchPage({
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
if (!hasActivatedWorkspace) {
|
||||
return (
|
||||
@@ -3139,8 +3231,8 @@ function WorkbenchPage({
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{renderConversationSidebar()}
|
||||
</div>
|
||||
{renderConversationSidebar()}
|
||||
{renderMessagePreviewOverlay()}
|
||||
{renderPromptCaseOverlay()}
|
||||
{renderDeleteDialog()}
|
||||
@@ -3282,10 +3374,10 @@ function WorkbenchPage({
|
||||
{renderComposerToolbar(false, isGenerating)}
|
||||
</div>
|
||||
</section>
|
||||
<div className="wb-chat-scroll-actions" aria-label="聊天滚动">
|
||||
<div className={`wb-chat-scroll-actions${scrollActionHint ? ` is-showing-${scrollActionHint}` : ""}`} aria-label="聊天滚动">
|
||||
<button
|
||||
type="button"
|
||||
className="wb-chat-scroll-actions__button"
|
||||
className="wb-chat-scroll-actions__button wb-chat-scroll-actions__button--top"
|
||||
title="返回聊天顶部"
|
||||
aria-label="返回聊天顶部"
|
||||
onClick={() => scrollMessagesSurface("top")}
|
||||
@@ -3294,7 +3386,7 @@ function WorkbenchPage({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="wb-chat-scroll-actions__button"
|
||||
className="wb-chat-scroll-actions__button wb-chat-scroll-actions__button--bottom"
|
||||
title="到达聊天底部"
|
||||
aria-label="到达聊天底部"
|
||||
onClick={() => scrollMessagesSurface("bottom")}
|
||||
|
||||
@@ -231,7 +231,7 @@ export const MODE_OPTIONS: WorkbenchOption[] = (Object.keys(MODE_META) as Workbe
|
||||
}));
|
||||
|
||||
export const IMAGE_MODEL_OPTIONS: WorkbenchOption[] = [
|
||||
{ value: "wan2.7-image-pro", label: "wan 2.7 Pro 4K" },
|
||||
{ value: "wan2.7-image-pro", label: "wan 2.7 Pro" },
|
||||
{ value: "wan2.7-image", label: "wan 2.7" },
|
||||
{ value: "gpt-image-2", label: "omni-GPT" },
|
||||
{ value: "gpt-image-2-vip", label: "omni-GPT VIP" },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10845,8 +10845,44 @@
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.web-shell[data-ui-theme="dark-green"] {
|
||||
--dg-mobile-nav-height: 58px;
|
||||
--dg-mobile-nav-gap: 12px;
|
||||
--dg-mobile-nav-space: calc(var(--dg-mobile-nav-height) + var(--dg-mobile-nav-gap));
|
||||
}
|
||||
|
||||
.web-shell[data-ui-theme="dark-green"] .web-topbar {
|
||||
z-index: 72;
|
||||
}
|
||||
|
||||
.web-shell[data-ui-theme="dark-green"] .web-shell__content,
|
||||
.web-shell[data-ui-theme="dark-green"] .web-shell__page {
|
||||
min-height: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.web-shell[data-ui-theme="dark-green"] .web-shell__page {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.web-shell[data-ui-theme="dark-green"]:not([data-view="home"]):not([data-view="login"]):not([data-view="workbench"]):not([data-view="agent"]):not([data-view="avatarConsole"]) .web-shell__page {
|
||||
padding-top: var(--dg-mobile-nav-space);
|
||||
}
|
||||
|
||||
.web-shell[data-ui-theme="dark-green"] .profile-popover {
|
||||
position: fixed;
|
||||
top: calc(56px + var(--dg-mobile-nav-space) + env(safe-area-inset-top, 0px));
|
||||
right: 12px;
|
||||
z-index: 120;
|
||||
width: min(288px, calc(100vw - 24px));
|
||||
max-height: calc(100svh - 56px - var(--dg-mobile-nav-space) - 24px);
|
||||
overflow-y: auto;
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.web-shell[data-ui-theme="dark-green"] .floating-nav {
|
||||
top: calc(56px + env(safe-area-inset-top, 0px));
|
||||
z-index: 50;
|
||||
right: 12px;
|
||||
bottom: auto;
|
||||
left: 12px;
|
||||
|
||||
@@ -25,11 +25,30 @@ export function getImageQualityOptions(model: string): CanvasOption[] {
|
||||
: imageQualityOptions.filter((option) => option.value !== "4K");
|
||||
}
|
||||
|
||||
export function getImageQualityOptionsForContext(
|
||||
model: string,
|
||||
context?: { hasReferenceImages?: boolean; isGridMode?: boolean },
|
||||
): CanvasOption[] {
|
||||
const options = getImageQualityOptions(model);
|
||||
const shouldLimitTo2K =
|
||||
String(model || "").toLowerCase() === "wan2.7-image-pro" &&
|
||||
(context?.hasReferenceImages || context?.isGridMode);
|
||||
return shouldLimitTo2K ? options.filter((option) => option.value !== "4K") : options;
|
||||
}
|
||||
|
||||
export function getDefaultImageQuality(model: string): string {
|
||||
const options = getImageQualityOptions(model);
|
||||
return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K";
|
||||
}
|
||||
|
||||
export function getDefaultImageQualityForContext(
|
||||
model: string,
|
||||
context?: { hasReferenceImages?: boolean; isGridMode?: boolean },
|
||||
): string {
|
||||
const options = getImageQualityOptionsForContext(model, context);
|
||||
return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K";
|
||||
}
|
||||
|
||||
// ─── Video quality ────────────────────────────────────────────────────────────
|
||||
|
||||
function normalizeVideoModel(model: string): string {
|
||||
|
||||
Reference in New Issue
Block a user