Compare commits

..

7 Commits

13 changed files with 147 additions and 1772 deletions
+1 -7
View File
@@ -374,7 +374,6 @@ function App() {
}))); })));
const [ecommerceEverMounted, setEcommerceEverMounted] = useState(false); const [ecommerceEverMounted, setEcommerceEverMounted] = useState(false);
const [workbenchResetToken, setWorkbenchResetToken] = useState(0);
const isEcommerceActive = activeView === "ecommerce" || activeView === "ecommerceHub"; const isEcommerceActive = activeView === "ecommerce" || activeView === "ecommerceHub";
useEffect(() => { useEffect(() => {
if (isEcommerceActive && !ecommerceEverMounted) setEcommerceEverMounted(true); if (isEcommerceActive && !ecommerceEverMounted) setEcommerceEverMounted(true);
@@ -460,9 +459,6 @@ function App() {
); );
const handleSetView = useCallback((view: WebViewKey) => { const handleSetView = useCallback((view: WebViewKey) => {
if (view === "workbench" && Boolean(session)) {
setWorkbenchResetToken((token) => token + 1);
}
window.location.hash = `/${view}`; window.location.hash = `/${view}`;
setView(view); setView(view);
if (view !== "login") { if (view !== "login") {
@@ -471,7 +467,7 @@ function App() {
if (isWorkspaceView(view)) { if (isWorkspaceView(view)) {
setWorkspaceExpanded(true); setWorkspaceExpanded(true);
} }
}, [session, setView, setWorkspaceExpanded]); }, [setView, setWorkspaceExpanded]);
const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => { const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => {
clearAllUserStorage(); clearAllUserStorage();
@@ -1317,13 +1313,11 @@ function App() {
case "workbench": case "workbench":
return ( return (
<WorkbenchPage <WorkbenchPage
key={`workbench-${workbenchResetToken}`}
isAuthenticated={Boolean(session)} isAuthenticated={Boolean(session)}
session={session} session={session}
onRequireLogin={handleRequireTaskLogin} onRequireLogin={handleRequireTaskLogin}
onOpenResultInCanvas={handleOpenResultInCanvas} onOpenResultInCanvas={handleOpenResultInCanvas}
onRefreshUsage={refreshUsage} onRefreshUsage={refreshUsage}
resetToken={workbenchResetToken}
/> />
); );
case "home": case "home":
+12
View File
@@ -248,6 +248,17 @@ function isNonAuthErrorCode(code: string | undefined): boolean {
].includes(code); ].includes(code);
} }
function isAuthFailureResponse(status: number, payload: unknown): boolean {
if (status === 401) return true;
if (status !== 403) return false;
const code = getPayloadCode(payload);
if (code === "SESSION_REPLACED" || code === "TOKEN_EXPIRED" || code === "ACCOUNT_DISABLED") return true;
const message = getPayloadMessage(payload) || "";
return /账号已禁用|登录已过期|登录状态|session|token|企业信息不存在/i.test(message);
}
function notifySessionExpired(status: number, response: Response, payload: unknown): void { function notifySessionExpired(status: number, response: Response, payload: unknown): void {
if (status !== 401 && status !== 403) return; if (status !== 401 && status !== 403) return;
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
@@ -263,6 +274,7 @@ function notifySessionExpired(status: number, response: Response, payload: unkno
// Non-auth 403 errors (enterprise model access, insufficient balance) must // Non-auth 403 errors (enterprise model access, insufficient balance) must
// not trigger session expiry. // not trigger session expiry.
if (status === 403 && isNonAuthErrorCode(getPayloadCode(payload))) return; if (status === 403 && isNonAuthErrorCode(getPayloadCode(payload))) return;
if (!isAuthFailureResponse(status, payload)) return;
const now = Date.now(); const now = Date.now();
if (now - lastSessionExpiredEventAt < 1500) return; if (now - lastSessionExpiredEventAt < 1500) return;
@@ -10,6 +10,7 @@ import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react";
import { aiGenerationClient } from "../../api/aiGenerationClient"; import { aiGenerationClient } from "../../api/aiGenerationClient";
import { communityClient } from "../../api/communityClient"; import { communityClient } from "../../api/communityClient";
import WorkspacePageShell from "../../components/WorkspacePageShell"; import WorkspacePageShell from "../../components/WorkspacePageShell";
import "../../styles/pages/compliance.css";
import type { WebCanvasWorkflow, WebUserSession } from "../../types"; import type { WebCanvasWorkflow, WebUserSession } from "../../types";
import { getWorkflowCoverUrl, isCanvasWorkflow } from "../community/communityCaseUtils"; import { getWorkflowCoverUrl, isCanvasWorkflow } from "../community/communityCaseUtils";
import { canManageCommunityCases } from "./communityPermissions"; import { canManageCommunityCases } from "./communityPermissions";
@@ -11,6 +11,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { communityClient, type ServerCommunityCase } from "../../api/communityClient"; import { communityClient, type ServerCommunityCase } from "../../api/communityClient";
import { reportClient, type AdminReportItem } from "../../api/reportClient"; import { reportClient, type AdminReportItem } from "../../api/reportClient";
import WorkspacePageShell from "../../components/WorkspacePageShell"; import WorkspacePageShell from "../../components/WorkspacePageShell";
import "../../styles/pages/compliance.css";
import type { WebUserSession } from "../../types"; import type { WebUserSession } from "../../types";
import { canManageCommunityCases, canReviewCommunity } from "./communityPermissions"; import { canManageCommunityCases, canReviewCommunity } from "./communityPermissions";
+1
View File
@@ -2,6 +2,7 @@ import { CheckCircleOutlined, FlagOutlined, MailOutlined, PhoneOutlined } from "
import { useEffect, useState, type FormEvent } from "react"; import { useEffect, useState, type FormEvent } from "react";
import { publicConfigClient, type WebPublicConfig } from "../../api/publicConfigClient"; import { publicConfigClient, type WebPublicConfig } from "../../api/publicConfigClient";
import { reportClient, type ReportInput } from "../../api/reportClient"; import { reportClient, type ReportInput } from "../../api/reportClient";
import "../../styles/pages/compliance.css";
type SubmitState = "idle" | "loading" | "success" | "error"; type SubmitState = "idle" | "loading" | "success" | "error";
+78 -88
View File
@@ -67,7 +67,6 @@ import { downloadResultAsset } from "./workbenchDownload";
import { translateTaskError } from "../../utils/translateTaskError"; import { translateTaskError } from "../../utils/translateTaskError";
import { import {
buildLocalTimeoutMessage, buildLocalTimeoutMessage,
formatTextTokenUsage,
getTaskTimeoutPolicy, getTaskTimeoutPolicy,
isTaskLocallyTimedOut, isTaskLocallyTimedOut,
} from "../../utils/taskLifecycle"; } from "../../utils/taskLifecycle";
@@ -79,7 +78,7 @@ import {
import { isViduModel } from "../../utils/viduRouting"; import { isViduModel } from "../../utils/viduRouting";
import { isPixverseModel } from "../../utils/pixverseRouting"; import { isPixverseModel } from "../../utils/pixverseRouting";
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel"; import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
import { ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy"; import { calculateEnterpriseVideoCredits, ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
import { import {
getImageQualityOptions, getImageQualityOptions,
getDefaultImageQuality, getDefaultImageQuality,
@@ -201,7 +200,6 @@ interface WorkbenchPageProps {
onRequireLogin: (input: CreatePreviewTaskInput) => void; onRequireLogin: (input: CreatePreviewTaskInput) => void;
onOpenResultInCanvas?: (payload: import("./workbenchConstants").WorkbenchResultActionPayload) => void; onOpenResultInCanvas?: (payload: import("./workbenchConstants").WorkbenchResultActionPayload) => void;
onRefreshUsage?: () => void; onRefreshUsage?: () => void;
resetToken?: number;
} }
// ─── Component ─────────────────────────────────────────────────────────── // ─── Component ───────────────────────────────────────────────────────────
@@ -221,13 +219,18 @@ const MODE_ICONS: Record<WorkbenchMode, ReactNode> = {
video: <VideoCameraOutlined />, video: <VideoCameraOutlined />,
}; };
function formatCreditValue(value: number): string {
if (!Number.isFinite(value)) return "-";
if (value >= 100) return Math.round(value).toLocaleString("zh-CN");
return Number(value.toFixed(2)).toString();
}
function WorkbenchPage({ function WorkbenchPage({
isAuthenticated, isAuthenticated,
session, session,
onRequireLogin, onRequireLogin,
onOpenResultInCanvas, onOpenResultInCanvas,
onRefreshUsage, onRefreshUsage,
resetToken,
}: WorkbenchPageProps) { }: WorkbenchPageProps) {
const textareaRef = useRef<HTMLTextAreaElement | null>(null); const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const referenceInputRef = useRef<HTMLInputElement | null>(null); const referenceInputRef = useRef<HTMLInputElement | null>(null);
@@ -246,11 +249,10 @@ function WorkbenchPage({
const activeConversationIdRef = useRef<number | null>(null); const activeConversationIdRef = useRef<number | null>(null);
const messagesRef = useRef<ChatMessage[]>([]); const messagesRef = useRef<ChatMessage[]>([]);
const conversationMessagesCacheRef = useRef<Map<number, ChatMessage[]>>(new Map()); const conversationMessagesCacheRef = useRef<Map<number, ChatMessage[]>>(new Map());
const skipConversationAutoSelectRef = useRef(Boolean(resetToken)); const skipConversationAutoSelectRef = useRef(false);
const keepaliveTasksRef = useRef<Record<string, WorkbenchKeepaliveTask>>(readStoredKeepaliveTasks()); const keepaliveTasksRef = useRef<Record<string, WorkbenchKeepaliveTask>>(readStoredKeepaliveTasks());
const taskAbortControllersRef = useRef<Map<string, AbortController>>(new Map()); const taskAbortControllersRef = useRef<Map<string, AbortController>>(new Map());
const lastScrollTopRef = useRef(0); const lastScrollTopRef = useRef(0);
const scrollActionHintTimerRef = useRef<number | null>(null);
const shouldFollowNewMessagesRef = useRef(true); const shouldFollowNewMessagesRef = useRef(true);
const pendingScrollToLatestRef = useRef(true); const pendingScrollToLatestRef = useRef(true);
const genTracker = useGenerationTasks({ sourceView: "workbench" }); const genTracker = useGenerationTasks({ sourceView: "workbench" });
@@ -259,7 +261,7 @@ function WorkbenchPage({
const [activeMode, setActiveMode] = useState<WorkbenchMode>("video"); const [activeMode, setActiveMode] = useState<WorkbenchMode>("video");
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const [messages, setMessages] = useState<ChatMessage[]>(() => (resetToken ? [] : readStoredMessages())); const [messages, setMessages] = useState<ChatMessage[]>(() => readStoredMessages());
const [promptHistory, setPromptHistory] = useState<string[]>(() => readStoredPromptHistory()); const [promptHistory, setPromptHistory] = useState<string[]>(() => readStoredPromptHistory());
const [toolbarMenuId, setToolbarMenuId] = useState<ToolbarMenuId>(null); const [toolbarMenuId, setToolbarMenuId] = useState<ToolbarMenuId>(null);
const [referenceItems, setReferenceItems] = useState<ReferenceItem[]>([]); const [referenceItems, setReferenceItems] = useState<ReferenceItem[]>([]);
@@ -282,7 +284,7 @@ function WorkbenchPage({
const [projectError, setProjectError] = useState<string | null>(null); const [projectError, setProjectError] = useState<string | null>(null);
const [conversations, setConversations] = useState<ConversationSummary[]>([]); const [conversations, setConversations] = useState<ConversationSummary[]>([]);
const [activeConversationId, setActiveConversationId] = useState<number | null>(() => const [activeConversationId, setActiveConversationId] = useState<number | null>(() =>
resetToken ? null : readStoredActiveConversationId(readStoredMessages()), readStoredActiveConversationId(readStoredMessages()),
); );
const [sidebarCollapsed, setSidebarCollapsed] = useState(true); const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
const [deleteDialog, setDeleteDialog] = useState<DeleteDialogState | null>(null); const [deleteDialog, setDeleteDialog] = useState<DeleteDialogState | null>(null);
@@ -292,9 +294,7 @@ function WorkbenchPage({
const [promptSelectionRange, setPromptSelectionRange] = useState({ start: 0, end: 0 }); const [promptSelectionRange, setPromptSelectionRange] = useState({ start: 0, end: 0 });
const [mentionActiveIndex, setMentionActiveIndex] = useState(0); const [mentionActiveIndex, setMentionActiveIndex] = useState(0);
const [composerHidden, setComposerHidden] = useState(false); const [composerHidden, setComposerHidden] = useState(false);
const [scrollActionHint, setScrollActionHint] = useState<"top" | "bottom" | null>(null);
const [workspaceStarted, setWorkspaceStarted] = useState(false); const [workspaceStarted, setWorkspaceStarted] = useState(false);
const lastResetTokenRef = useRef(resetToken);
useEffect(() => { useEffect(() => {
activeConversationIdRef.current = activeConversationId; activeConversationIdRef.current = activeConversationId;
@@ -420,6 +420,7 @@ function WorkbenchPage({
const toolTheme = MODE_META[activeMode]; const toolTheme = MODE_META[activeMode];
const workbenchAccent = "#00ff88"; const workbenchAccent = "#00ff88";
const hasConversationRecords = activeConversationId !== null || messages.length > 0; const hasConversationRecords = activeConversationId !== null || messages.length > 0;
const hasActivatedWorkspace = workspaceStarted || isGenerating || hasConversationRecords;
const referenceCount = referenceItems.length; const referenceCount = referenceItems.length;
const activeVideoModelValue = toHappyHorseDisplayModel(videoModel); const activeVideoModelValue = toHappyHorseDisplayModel(videoModel);
const activeModelValue = const activeModelValue =
@@ -447,7 +448,6 @@ function WorkbenchPage({
[conversations], [conversations],
); );
const hasSidebarRecords = conversationRecords.length > 0; const hasSidebarRecords = conversationRecords.length > 0;
const hasActivatedWorkspace = workspaceStarted || isGenerating || hasConversationRecords;
const activeConversationTitle = useMemo(() => { const activeConversationTitle = useMemo(() => {
if (!activeConversationId) return ""; if (!activeConversationId) return "";
@@ -469,6 +469,48 @@ function WorkbenchPage({
const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality); const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality);
const imageSettingsSummary = `${imageRatio} / ${imageQuality}`; const imageSettingsSummary = `${imageRatio} / ${imageQuality}`;
const billingEstimate = useMemo(() => {
if (activeMode === "image") {
return {
label: "预计 20 积分",
title: `图片生成按任务计费:${activeModel}${imageSettingsSummary},预计 20 积分`,
};
}
if (activeMode === "video") {
try {
const durationSeconds = Math.max(1, Math.ceil(Number(videoDuration) || 1));
const credits = calculateEnterpriseVideoCredits({
model: activeModelValue,
resolution: videoQuality,
durationSeconds,
muted: false,
hasReferenceVideo: referenceItems.some((item) => item.kind === "video"),
});
return {
label: `预计 ${formatCreditValue(credits)} 积分`,
title: `${activeModel}${videoQualityLabel}${durationSeconds} 秒,预计 ${formatCreditValue(credits)} 积分`,
};
} catch {
return {
label: "计费以提交后为准",
title: "当前模型的预估计费暂不可用,实际扣费以服务端结算为准",
};
}
}
return {
label: "按 Token 结算",
title: "文本对话按输入、输出 Token 实际用量结算,完成后显示本次积分",
};
}, [
activeMode,
activeModel,
activeModelValue,
imageSettingsSummary,
referenceItems,
videoDuration,
videoQuality,
videoQualityLabel,
]);
const composerPlaceholder = const composerPlaceholder =
referenceItems.length > 0 ? `${toolTheme.placeholder},可输入 @ 引用参考内容` : toolTheme.placeholder; referenceItems.length > 0 ? `${toolTheme.placeholder},可输入 @ 引用参考内容` : toolTheme.placeholder;
const dropdownDirection = hasActivatedWorkspace ? "up" : "down"; const dropdownDirection = hasActivatedWorkspace ? "up" : "down";
@@ -501,31 +543,6 @@ 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[]>( const imageSettingGroups = useMemo<WorkbenchFieldGroup[]>(
() => [ () => [
{ {
@@ -1296,12 +1313,6 @@ function WorkbenchPage({
activeConversationIdRef.current = null; activeConversationIdRef.current = null;
}, [syncActiveGenerationUi]); }, [syncActiveGenerationUi]);
useEffect(() => {
if (resetToken === undefined || lastResetTokenRef.current === resetToken) return;
lastResetTokenRef.current = resetToken;
handleNewConversation();
}, [handleNewConversation, resetToken]);
const handleSelectProject = useCallback((id: string) => { const handleSelectProject = useCallback((id: string) => {
if (!id) { if (!id) {
handleNewConversation(); handleNewConversation();
@@ -1456,7 +1467,6 @@ function WorkbenchPage({
const atBottom = top + surface.clientHeight >= surface.scrollHeight - edgeThreshold; const atBottom = top + surface.clientHeight >= surface.scrollHeight - edgeThreshold;
shouldFollowNewMessagesRef.current = atBottom; shouldFollowNewMessagesRef.current = atBottom;
setComposerHidden(!(atTop || atBottom)); setComposerHidden(!(atTop || atBottom));
hideScrollActionHint();
lastScrollTopRef.current = top; lastScrollTopRef.current = top;
}; };
@@ -1471,27 +1481,24 @@ function WorkbenchPage({
shouldFollowNewMessagesRef.current = atBottom; shouldFollowNewMessagesRef.current = atBottom;
if (atTop || atBottom) { if (atTop || atBottom) {
setComposerHidden(false); setComposerHidden(false);
hideScrollActionHint();
} else if (Math.abs(delta) > scrollDeltaThreshold) { } else if (Math.abs(delta) > scrollDeltaThreshold) {
setComposerHidden(true); setComposerHidden(true);
showScrollActionHint(delta < 0 ? "top" : "bottom");
} }
lastScrollTopRef.current = top; lastScrollTopRef.current = top;
}; };
surface.addEventListener("scroll", handleScroll, { passive: true }); surface.addEventListener("scroll", handleScroll, { passive: true });
return () => surface.removeEventListener("scroll", handleScroll); return () => surface.removeEventListener("scroll", handleScroll);
}, [hasActivatedWorkspace, hideScrollActionHint, showScrollActionHint]); }, [hasActivatedWorkspace]);
const scrollMessagesSurface = useCallback((direction: "top" | "bottom") => { const scrollMessagesSurface = useCallback((direction: "top" | "bottom") => {
const surface = messagesSurfaceRef.current; const surface = messagesSurfaceRef.current;
if (!surface) return; if (!surface) return;
const top = direction === "top" ? 0 : surface.scrollHeight; const top = direction === "top" ? 0 : surface.scrollHeight;
hideScrollActionHint();
setComposerHidden(false); setComposerHidden(false);
surface.scrollTo({ top, behavior: "smooth" }); surface.scrollTo({ top, behavior: "smooth" });
}, [hideScrollActionHint]); }, []);
const closeToolbarMenus = () => setToolbarMenuId(null); const closeToolbarMenus = () => setToolbarMenuId(null);
const toggleToolbarMenu = (menuId: Exclude<ToolbarMenuId, null>) => { const toggleToolbarMenu = (menuId: Exclude<ToolbarMenuId, null>) => {
@@ -2585,7 +2592,15 @@ function WorkbenchPage({
} }
}; };
const sendDisabled = !inputValue.trim() || (activeMode !== "chat" && getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3); const activeGenerationCount = getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id));
const generationLimitReached = activeMode !== "chat" && activeGenerationCount >= 3;
const promptIsEmpty = !inputValue.trim();
const sendDisabled = promptIsEmpty || generationLimitReached;
const sendButtonTitle = promptIsEmpty
? "输入内容后可发送"
: generationLimitReached
? `当前已有 ${activeGenerationCount} 个任务进行中,请等待任一任务完成`
: billingEstimate.title;
const suggestedPrompts = [ const suggestedPrompts = [
{ text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode }, { text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode },
@@ -2922,10 +2937,15 @@ function WorkbenchPage({
)} )}
</div> </div>
<div className="wb-composer__toolbar-right"> <div className="wb-composer__toolbar-right">
<span className="wb-composer__billing-estimate" title={billingEstimate.title}>
{billingEstimate.label}
</span>
<button <button
type="button" type="button"
className={`wb-composer__send-primary${isGenerating ? " is-loading" : ""}`} className={`wb-composer__send-primary${isGenerating ? " is-loading" : ""}`}
disabled={sendDisabled || isGenerating} disabled={sendDisabled || isGenerating}
title={isGenerating ? "任务处理中" : sendButtonTitle}
aria-label={isGenerating ? "任务处理中" : sendButtonTitle}
onClick={() => { onClick={() => {
if (getCachedRole() === "admin") console.log("[ai/workbench-send-click]", { if (getCachedRole() === "admin") console.log("[ai/workbench-send-click]", {
mode: activeMode, mode: activeMode,
@@ -2973,29 +2993,9 @@ function WorkbenchPage({
</div> </div>
) : null; ) : null;
const renderPromptCaseOverlay = () => { const renderPromptCaseOverlay = () =>
if (!selectedPromptCase) return null; selectedPromptCase ? (
<div className="wb-prompt-case-modal" role="dialog" aria-modal="true" aria-labelledby="wb-prompt-case-title">
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 <button
type="button" type="button"
className="wb-prompt-case-modal__backdrop" className="wb-prompt-case-modal__backdrop"
@@ -3004,11 +3004,7 @@ function WorkbenchPage({
/> />
<section className="wb-prompt-case-modal__panel"> <section className="wb-prompt-case-modal__panel">
<div className="wb-prompt-case-modal__media"> <div className="wb-prompt-case-modal__media">
<img <img src={selectedPromptCase.imageUrl} alt={selectedPromptCase.title} />
src={selectedPromptCase.imageUrl}
alt={selectedPromptCase.title}
onLoad={(event) => handlePromptCaseImageLoad(selectedPromptCase.id, event)}
/>
</div> </div>
<aside className="wb-prompt-case-modal__sidebar"> <aside className="wb-prompt-case-modal__sidebar">
<button <button
@@ -3048,8 +3044,7 @@ function WorkbenchPage({
</aside> </aside>
</section> </section>
</div> </div>
); ) : null;
};
if (!hasActivatedWorkspace) { if (!hasActivatedWorkspace) {
return ( return (
@@ -3144,8 +3139,8 @@ function WorkbenchPage({
</div> </div>
</div> </div>
{renderConversationSidebar()}
</div> </div>
{renderConversationSidebar()}
{renderMessagePreviewOverlay()} {renderMessagePreviewOverlay()}
{renderPromptCaseOverlay()} {renderPromptCaseOverlay()}
{renderDeleteDialog()} {renderDeleteDialog()}
@@ -3231,11 +3226,6 @@ function WorkbenchPage({
<span>{message.taskStatusLabel || generationStatus}</span> <span>{message.taskStatusLabel || generationStatus}</span>
</div> </div>
)} )}
{message.role === "assistant" && message.mode === "chat" && message.status === "completed" && (
<div className="ai-chat-task-billing-note">
{formatTextTokenUsage(message.taskUsage)}
</div>
)}
{(message.resultUrl || (message.result && message.status !== "thinking")) && ( {(message.resultUrl || (message.result && message.status !== "thinking")) && (
<ResultCard <ResultCard
message={message} message={message}
@@ -3292,10 +3282,10 @@ function WorkbenchPage({
{renderComposerToolbar(false, isGenerating)} {renderComposerToolbar(false, isGenerating)}
</div> </div>
</section> </section>
<div className={`wb-chat-scroll-actions${scrollActionHint ? ` is-showing-${scrollActionHint}` : ""}`} aria-label="聊天滚动"> <div className="wb-chat-scroll-actions" aria-label="聊天滚动">
<button <button
type="button" type="button"
className="wb-chat-scroll-actions__button wb-chat-scroll-actions__button--top" className="wb-chat-scroll-actions__button"
title="返回聊天顶部" title="返回聊天顶部"
aria-label="返回聊天顶部" aria-label="返回聊天顶部"
onClick={() => scrollMessagesSurface("top")} onClick={() => scrollMessagesSurface("top")}
@@ -3304,7 +3294,7 @@ function WorkbenchPage({
</button> </button>
<button <button
type="button" type="button"
className="wb-chat-scroll-actions__button wb-chat-scroll-actions__button--bottom" className="wb-chat-scroll-actions__button"
title="到达聊天底部" title="到达聊天底部"
aria-label="到达聊天底部" aria-label="到达聊天底部"
onClick={() => scrollMessagesSurface("bottom")} onClick={() => scrollMessagesSurface("bottom")}
+2 -2
View File
@@ -33,7 +33,7 @@ const initialState: SessionState = {
loginPromptOpen: false, loginPromptOpen: false,
pendingAction: null, pendingAction: null,
sessionReplacedOpen: false, sessionReplacedOpen: false,
sessionReplacedMessage: '您的账号已在其他设备登录,此设备的登录状态已失效。', sessionReplacedMessage: '当前账号已在其他设备登录,此设备的登录状态已失效。',
}; };
export const useSessionStore = create<SessionState & SessionActions>((set) => ({ export const useSessionStore = create<SessionState & SessionActions>((set) => ({
@@ -55,7 +55,7 @@ export const useSessionStore = create<SessionState & SessionActions>((set) => ({
showSessionReplaced: (message) => set({ showSessionReplaced: (message) => set({
sessionReplacedOpen: true, sessionReplacedOpen: true,
sessionReplacedMessage: message || '您的账号已在其他设备登录(最多同时 2 台设备),此设备的登录状态已失效。', sessionReplacedMessage: message || '当前账号已在其他设备登录,此设备的登录状态已失效。',
}), }),
hideSessionReplaced: () => set({ sessionReplacedOpen: false }), hideSessionReplaced: () => set({ sessionReplacedOpen: false }),
+19 -1
View File
@@ -3,7 +3,11 @@
flex-direction: column; flex-direction: column;
gap: 18px; gap: 18px;
width: min(1180px, calc(100vw - 48px)); width: min(1180px, calc(100vw - 48px));
margin: 0 auto;
height: 100%;
min-height: 0;
padding: 24px; padding: 24px;
overflow: hidden;
} }
.beta-admin-toolbar { .beta-admin-toolbar {
@@ -90,6 +94,8 @@
display: grid; display: grid;
grid-template-columns: 300px minmax(0, 1fr); grid-template-columns: 300px minmax(0, 1fr);
gap: 16px; gap: 16px;
flex: 1;
min-height: 0;
align-items: start; align-items: start;
} }
@@ -97,7 +103,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
max-height: calc(100vh - 220px); max-height: 100%;
overflow: auto; overflow: auto;
padding-right: 4px; padding-right: 4px;
} }
@@ -174,6 +180,10 @@
flex-direction: column; flex-direction: column;
gap: 14px; gap: 14px;
min-width: 0; min-width: 0;
max-height: 100%;
min-height: 0;
overflow: auto;
padding-right: 4px;
} }
.beta-admin-detail__header, .beta-admin-detail__header,
@@ -376,6 +386,7 @@
.beta-admin-page__inner { .beta-admin-page__inner {
width: min(100%, calc(100vw - 24px)); width: min(100%, calc(100vw - 24px));
padding: 16px 12px; padding: 16px 12px;
overflow: auto;
} }
.beta-admin-toolbar, .beta-admin-toolbar,
@@ -385,11 +396,18 @@
.beta-admin-layout { .beta-admin-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
overflow: visible;
} }
.beta-admin-list { .beta-admin-list {
max-height: none; max-height: none;
} }
.beta-admin-detail {
max-height: none;
overflow: visible;
padding-right: 0;
}
} }
@media (max-width: 640px) { @media (max-width: 640px) {
+15
View File
@@ -11915,6 +11915,21 @@
gap: 6px; gap: 6px;
} }
.wb-composer__billing-estimate {
max-width: 138px;
padding: 6px 9px;
border: 2px solid #111;
background: #fffbe8;
color: #111;
font-size: 12px;
font-weight: 800;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-shadow: 2px 2px 0 #111;
}
.wb-composer__send-primary { .wb-composer__send-primary {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
File diff suppressed because it is too large Load Diff
+8 -36
View File
@@ -1794,6 +1794,14 @@
box-shadow: none; box-shadow: none;
} }
.web-shell[data-ui-theme="dark-green"] .wb-composer__billing-estimate {
border: 1px solid var(--border-default);
border-radius: 999px;
background: var(--bg-elevated);
color: var(--fg-body);
box-shadow: none;
}
.web-shell[data-ui-theme="dark-green"] .wb-composer__send-primary:hover:not(:disabled) { .web-shell[data-ui-theme="dark-green"] .wb-composer__send-primary:hover:not(:disabled) {
background: var(--accent-hover); background: var(--accent-hover);
color: var(--dg-button-text); color: var(--dg-button-text);
@@ -10837,44 +10845,8 @@
} }
@media (max-width: 900px) { @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 { .web-shell[data-ui-theme="dark-green"] .floating-nav {
top: calc(56px + env(safe-area-inset-top, 0px)); top: calc(56px + env(safe-area-inset-top, 0px));
z-index: 50;
right: 12px; right: 12px;
bottom: auto; bottom: auto;
left: 12px; left: 12px;
+4 -3
View File
@@ -40,6 +40,7 @@ export const ENTERPRISE_VIDEO_RESOLUTION_OPTIONS = [
export const ENTERPRISE_DEFAULT_VIDEO_MODEL = HAPPY_HORSE_UI_MODEL; export const ENTERPRISE_DEFAULT_VIDEO_MODEL = HAPPY_HORSE_UI_MODEL;
export const ENTERPRISE_DEFAULT_VIDEO_RESOLUTION = "1080P"; export const ENTERPRISE_DEFAULT_VIDEO_RESOLUTION = "1080P";
const CREDITS_PER_CNY = 100;
export interface EnterpriseVideoPricingInput { export interface EnterpriseVideoPricingInput {
model: string; model: string;
@@ -74,11 +75,11 @@ export function getEnterpriseVideoCreditRate(input: EnterpriseVideoPricingInput)
} }
if (model.includes("vidu")) { if (model.includes("vidu")) {
return resolution === "720P" ? 0.4 : 0.8; return resolution === "720P" ? 0.6 : 1.0;
} }
if (model.includes("pixverse")) { if (model.includes("pixverse")) {
return resolution === "720P" ? 0.4 : 0.8; return resolution === "720P" ? 0.6 : 1.0;
} }
if (model.includes("kling")) { if (model.includes("kling")) {
@@ -94,5 +95,5 @@ export function getEnterpriseVideoCreditRate(input: EnterpriseVideoPricingInput)
export function calculateEnterpriseVideoCredits(input: EnterpriseVideoPricingInput): number { export function calculateEnterpriseVideoCredits(input: EnterpriseVideoPricingInput): number {
const duration = Math.max(1, Math.ceil(Number(input.durationSeconds) || 1)); const duration = Math.max(1, Math.ceil(Number(input.durationSeconds) || 1));
return Number((getEnterpriseVideoCreditRate(input) * duration).toFixed(2)); return Number((getEnterpriseVideoCreditRate(input) * duration * CREDITS_PER_CNY).toFixed(2));
} }
+5 -3
View File
@@ -32,8 +32,10 @@ export interface TextTokenUsage {
totalTokens?: number; totalTokens?: number;
} }
export const TEXT_INPUT_CREDITS_PER_MILLION = 2; const CREDITS_PER_CNY = 100;
export const TEXT_OUTPUT_CREDITS_PER_MILLION = 5;
export const TEXT_INPUT_CREDITS_PER_MILLION = 2 * CREDITS_PER_CNY;
export const TEXT_OUTPUT_CREDITS_PER_MILLION = 5 * CREDITS_PER_CNY;
const IMAGE_TIMEOUT_POLICY: TaskTimeoutPolicy = { const IMAGE_TIMEOUT_POLICY: TaskTimeoutPolicy = {
submitTimeoutMs: 90_000, submitTimeoutMs: 90_000,
@@ -151,7 +153,7 @@ export function estimateTextTokenCredits(usage: TextTokenUsage): number {
} }
export function formatTextTokenUsage(usage?: TextTokenUsage | null): string { export function formatTextTokenUsage(usage?: TextTokenUsage | null): string {
const rule = "文本计费规则:输入 Token 每百万 2 积分,输出 Token 每百万 5 积分,实际以服务端结算为准。"; const rule = "文本计费规则:输入 Token 每百万 200 积分,输出 Token 每百万 500 积分,实际以服务端结算为准。";
if (!usage) return rule; if (!usage) return rule;
const promptTokens = Math.max(0, Number(usage.promptTokens || 0)); const promptTokens = Math.max(0, Number(usage.promptTokens || 0));
const completionTokens = Math.max(0, Number(usage.completionTokens || 0)); const completionTokens = Math.max(0, Number(usage.completionTokens || 0));