diff --git a/src/App.tsx b/src/App.tsx index ba85629..9c6c0fb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -368,6 +368,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); @@ -453,6 +454,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") { @@ -461,7 +465,7 @@ function App() { if (isWorkspaceView(view)) { setWorkspaceExpanded(true); } - }, [setView, setWorkspaceExpanded]); + }, [session, setView, setWorkspaceExpanded]); const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => { clearAllUserStorage(); @@ -1301,11 +1305,13 @@ function App() { case "workbench": return ( ); case "home": diff --git a/src/features/workbench/WorkbenchPage.tsx b/src/features/workbench/WorkbenchPage.tsx index a91d243..af93be9 100644 --- a/src/features/workbench/WorkbenchPage.tsx +++ b/src/features/workbench/WorkbenchPage.tsx @@ -201,6 +201,7 @@ interface WorkbenchPageProps { onRequireLogin: (input: CreatePreviewTaskInput) => void; onOpenResultInCanvas?: (payload: import("./workbenchConstants").WorkbenchResultActionPayload) => void; onRefreshUsage?: () => void; + resetToken?: number; } // ─── Component ─────────────────────────────────────────────────────────── @@ -226,6 +227,7 @@ function WorkbenchPage({ onRequireLogin, onOpenResultInCanvas, onRefreshUsage, + resetToken, }: WorkbenchPageProps) { const textareaRef = useRef(null); const referenceInputRef = useRef(null); @@ -244,10 +246,11 @@ function WorkbenchPage({ const activeConversationIdRef = useRef(null); const messagesRef = useRef([]); const conversationMessagesCacheRef = useRef>(new Map()); - const skipConversationAutoSelectRef = useRef(false); + const skipConversationAutoSelectRef = useRef(Boolean(resetToken)); const keepaliveTasksRef = useRef>(readStoredKeepaliveTasks()); const taskAbortControllersRef = useRef>(new Map()); const lastScrollTopRef = useRef(0); + const scrollActionHintTimerRef = useRef(null); const shouldFollowNewMessagesRef = useRef(true); const pendingScrollToLatestRef = useRef(true); const genTracker = useGenerationTasks({ sourceView: "workbench" }); @@ -256,7 +259,7 @@ function WorkbenchPage({ const [activeMode, setActiveMode] = useState("video"); const [inputValue, setInputValue] = useState(""); - const [messages, setMessages] = useState(() => readStoredMessages()); + const [messages, setMessages] = useState(() => (resetToken ? [] : readStoredMessages())); const [promptHistory, setPromptHistory] = useState(() => readStoredPromptHistory()); const [toolbarMenuId, setToolbarMenuId] = useState(null); const [referenceItems, setReferenceItems] = useState([]); @@ -279,7 +282,7 @@ function WorkbenchPage({ const [projectError, setProjectError] = useState(null); const [conversations, setConversations] = useState([]); const [activeConversationId, setActiveConversationId] = useState(() => - readStoredActiveConversationId(readStoredMessages()), + resetToken ? null : readStoredActiveConversationId(readStoredMessages()), ); const [sidebarCollapsed, setSidebarCollapsed] = useState(true); const [deleteDialog, setDeleteDialog] = useState(null); @@ -289,7 +292,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; @@ -415,7 +420,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 = @@ -443,6 +447,7 @@ function WorkbenchPage({ [conversations], ); const hasSidebarRecords = conversationRecords.length > 0; + const hasActivatedWorkspace = workspaceStarted || isGenerating || hasConversationRecords; const activeConversationTitle = useMemo(() => { if (!activeConversationId) return ""; @@ -496,6 +501,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( () => [ { @@ -1266,6 +1296,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(); @@ -1420,6 +1456,7 @@ function WorkbenchPage({ const atBottom = top + surface.clientHeight >= surface.scrollHeight - edgeThreshold; shouldFollowNewMessagesRef.current = atBottom; setComposerHidden(!(atTop || atBottom)); + hideScrollActionHint(); lastScrollTopRef.current = top; }; @@ -1434,24 +1471,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) => { @@ -2933,9 +2973,29 @@ function WorkbenchPage({ ) : null; - const renderPromptCaseOverlay = () => - selectedPromptCase ? ( -
+ 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 ( +
- ) : null; + ); + }; if (!hasActivatedWorkspace) { return ( @@ -3079,8 +3144,8 @@ function WorkbenchPage({
+ {renderConversationSidebar()} - {renderConversationSidebar()} {renderMessagePreviewOverlay()} {renderPromptCaseOverlay()} {renderDeleteDialog()} @@ -3227,10 +3292,10 @@ function WorkbenchPage({ {renderComposerToolbar(false, isGenerating)} -
+