Commit 763794d2 authored by edy's avatar edy

fix(ui): smooth startup curtain progress

parent 2d5b1d70
Pipeline #18479 failed
...@@ -65,6 +65,7 @@ import { useHomeNavigation } from "./features/shell/useHomeNavigation"; ...@@ -65,6 +65,7 @@ import { useHomeNavigation } from "./features/shell/useHomeNavigation";
import { TopBar } from "./features/shell/TopBar"; import { TopBar } from "./features/shell/TopBar";
import { useSidebarModel } from "./features/shell/useSidebarModel"; import { useSidebarModel } from "./features/shell/useSidebarModel";
import { import {
getNextStartupVisualProgress,
getStartupCurtainFootnote, getStartupCurtainFootnote,
getStartupCurtainStatus, getStartupCurtainStatus,
getStartupProgress, getStartupProgress,
...@@ -641,7 +642,15 @@ export default function App() { ...@@ -641,7 +642,15 @@ export default function App() {
setVectcutFileBaseUrlDraft(drafts.vectcutFileBaseUrl); setVectcutFileBaseUrlDraft(drafts.vectcutFileBaseUrl);
setVectcutApiKeyDraft(drafts.vectcutApiKey); setVectcutApiKeyDraft(drafts.vectcutApiKey);
}, [config]); }, [config]);
const startupProgress = getStartupProgress(startupPhase); const startupProgressTarget = chatLaunchState === "ready" ? 1 : getStartupProgress(startupPhase);
const [visualStartupProgress, setVisualStartupProgress] = useState(startupProgressTarget);
useEffect(() => {
if (chatLaunchState === "error") {
return;
}
setVisualStartupProgress((currentProgress) => getNextStartupVisualProgress(currentProgress, startupProgressTarget));
}, [chatLaunchState, startupProgressTarget]);
const startupCurtainStatus = getStartupCurtainStatus(startupPhase, chatLaunchState); const startupCurtainStatus = getStartupCurtainStatus(startupPhase, chatLaunchState);
const startupCurtainFootnote = getStartupCurtainFootnote(chatLaunchState); const startupCurtainFootnote = getStartupCurtainFootnote(chatLaunchState);
const { const {
...@@ -1639,12 +1648,12 @@ export default function App() { ...@@ -1639,12 +1648,12 @@ export default function App() {
<div className="startup-overlay-body"> <div className="startup-overlay-body">
{chatLaunchState !== "error" ? ( {chatLaunchState !== "error" ? (
<div className="startup-overlay-progress" aria-hidden="true"> <div className="startup-overlay-progress" aria-hidden="true">
<span style={{ width: String(Math.round(startupProgress * 100)) + "%" }} /> <span style={{ width: String(Math.round(visualStartupProgress * 100)) + "%" }} />
</div> </div>
) : null} ) : null}
<div className="startup-overlay-status"> <div className="startup-overlay-status">
<strong>{startupCurtainStatus}</strong> <strong>{startupCurtainStatus}</strong>
<span>{startupCurtainFootnote}</span> {startupCurtainFootnote ? <span>{startupCurtainFootnote}</span> : null}
{chatLaunchState === "error" && startupMessage ? ( {chatLaunchState === "error" && startupMessage ? (
<p className="startup-overlay-detail">{startupMessage}</p> <p className="startup-overlay-detail">{startupMessage}</p>
) : null} ) : null}
......
...@@ -5,12 +5,12 @@ export type WorkspaceStatusTone = "positive" | "warning" | "info" ...@@ -5,12 +5,12 @@ export type WorkspaceStatusTone = "positive" | "warning" | "info"
export const startupCurtainCopy = { export const startupCurtainCopy = {
brandTitle: "\u5343\u5320\u00b7\u95ee\u5929", brandTitle: "\u5343\u5320\u00b7\u95ee\u5929",
brandTagline: "START YOUR IDEAS", brandTagline: "START YOUR IDEAS",
loadingLabel: "\u6b63\u5728\u4e3a\u60a8\u51c6\u5907\u5bf9\u8bdd\u73af\u5883", loadingLabel: "准备中",
syncingConfig: "\u6b63\u5728\u540c\u6b65\u5de5\u4f5c\u914d\u7f6e", syncingConfig: "准备配置",
syncingProjects: "\u6b63\u5728\u540c\u6b65\u9879\u76ee\u914d\u7f6e", syncingProjects: "同步项目",
startingRuntime: "\u6b63\u5728\u5524\u8d77\u672c\u5730\u52a9\u624b", startingRuntime: "启动助手",
connectingGateway: "\u6b63\u5728\u5efa\u7acb\u5bf9\u8bdd\u8fde\u63a5", connectingGateway: "连接服务",
ready: "\u51c6\u5907\u5b8c\u6210\uff0c\u6b63\u5728\u8fdb\u5165\u5bf9\u8bdd", ready: "即将进入",
errorTitle: "\u542f\u52a8\u73af\u5883\u6682\u672a\u5c31\u7eea", errorTitle: "\u542f\u52a8\u73af\u5883\u6682\u672a\u5c31\u7eea",
errorFootnote: "\u8bf7\u91cd\u65b0\u51c6\u5907\uff1b\u5982\u4ecd\u5931\u8d25\uff0c\u53ef\u524d\u5f80\u8bbe\u7f6e\u6216\u5bfc\u51fa\u8bca\u65ad\u7ee7\u7eed\u6392\u67e5\u3002", errorFootnote: "\u8bf7\u91cd\u65b0\u51c6\u5907\uff1b\u5982\u4ecd\u5931\u8d25\uff0c\u53ef\u524d\u5f80\u8bbe\u7f6e\u6216\u5bfc\u51fa\u8bca\u65ad\u7ee7\u7eed\u6392\u67e5\u3002",
retryHint: "\u51c6\u5907\u5931\u8d25\u540e\u53ef\u91cd\u65b0\u5c1d\u8bd5\uff0c\u6216\u524d\u5f80\u8bbe\u7f6e\u68c0\u67e5\u5bc6\u94a5\u4e0e\u7f51\u7edc\u3002" retryHint: "\u51c6\u5907\u5931\u8d25\u540e\u53ef\u91cd\u65b0\u5c1d\u8bd5\uff0c\u6216\u524d\u5f80\u8bbe\u7f6e\u68c0\u67e5\u5bc6\u94a5\u4e0e\u7f51\u7edc\u3002"
...@@ -61,6 +61,10 @@ export function getStartupProgress(phase: WorkspaceSummary["startupPhase"] | und ...@@ -61,6 +61,10 @@ export function getStartupProgress(phase: WorkspaceSummary["startupPhase"] | und
} }
} }
export function getNextStartupVisualProgress(currentProgress: number, targetProgress: number): number {
return Math.max(currentProgress, targetProgress)
}
export function getStartupCurtainStatus( export function getStartupCurtainStatus(
phase: WorkspaceSummary["startupPhase"] | undefined, phase: WorkspaceSummary["startupPhase"] | undefined,
launchState: ChatLaunchState launchState: ChatLaunchState
...@@ -86,7 +90,5 @@ export function getStartupCurtainStatus( ...@@ -86,7 +90,5 @@ export function getStartupCurtainStatus(
} }
export function getStartupCurtainFootnote(launchState: ChatLaunchState): string { export function getStartupCurtainFootnote(launchState: ChatLaunchState): string {
return launchState === "error" return launchState === "error" ? startupCurtainCopy.errorFootnote : ""
? startupCurtainCopy.errorFootnote
: startupCurtainCopy.loadingLabel
} }
...@@ -532,7 +532,7 @@ ...@@ -532,7 +532,7 @@
width: min(500px, 100%); width: min(500px, 100%);
display: grid; display: grid;
justify-items: center; justify-items: center;
gap: 16px; gap: 14px;
margin-top: 34px; margin-top: 34px;
} }
...@@ -550,15 +550,20 @@ ...@@ -550,15 +550,20 @@
border-radius: inherit; border-radius: inherit;
background: linear-gradient(90deg, #69bcff 0%, #3f8cff 100%); background: linear-gradient(90deg, #69bcff 0%, #3f8cff 100%);
box-shadow: 0 0 18px rgba(63, 140, 255, 0.28); box-shadow: 0 0 18px rgba(63, 140, 255, 0.28);
transition: width 240ms ease; transition: width 1600ms cubic-bezier(0.22, 1, 0.36, 1);
} }
.startup-overlay-status { .startup-overlay-status {
min-height: 24px;
display: grid; display: grid;
gap: 8px; gap: 6px;
justify-items: center; justify-items: center;
} }
.startup-overlay.error .startup-overlay-status {
min-height: auto;
}
.startup-overlay-status strong { .startup-overlay-status strong {
color: #1f426d; color: #1f426d;
font-size: 16px; font-size: 16px;
......
import test from "node:test"
import assert from "node:assert/strict"
import {
getNextStartupVisualProgress,
getStartupCurtainFootnote,
getStartupCurtainStatus
} from "../src/features/shell/startupStatus.ts"
test("startup curtain uses concise one-line loading statuses", () => {
assert.equal(getStartupCurtainStatus("syncing-config", "starting"), "准备配置")
assert.equal(getStartupCurtainStatus("syncing-projects", "starting"), "同步项目")
assert.equal(getStartupCurtainStatus("starting-runtime", "starting"), "启动助手")
assert.equal(getStartupCurtainStatus("connecting-gateway", "starting"), "连接服务")
assert.equal(getStartupCurtainStatus("ready", "ready"), "即将进入")
assert.equal(getStartupCurtainStatus(undefined, "starting"), "准备中")
})
test("startup curtain only keeps footnote text for errors", () => {
assert.equal(getStartupCurtainFootnote("starting"), "")
assert.equal(getStartupCurtainFootnote("error").length > 0, true)
})
test("visual startup progress advances without retreating", () => {
assert.equal(getNextStartupVisualProgress(0.56, 0.82), 0.82)
assert.equal(getNextStartupVisualProgress(0.82, 0.24), 0.82)
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment