已开启
feat(web): 沙箱不可用时界面区分「计算就绪」状态并显著提示(Issue #91) #160
hugoxk创建于 9月20日
feat(web): 沙箱不可用时界面区分「计算就绪」状态并显著提示(Issue #91) #160
已开启
共 9 个文件变更+73-3
| @@ -1212,6 +1212,8 @@ export function App({ initialToken }: { initialToken?: string } = {}) { | |||
| 1212 | const providerSettingsRef = useRef<ProviderModelSettingsHandle>(null); | 1212 | const providerSettingsRef = useRef<ProviderModelSettingsHandle>(null); |
| 1213 | const thinkingNormalizationInFlight = useRef<string | undefined>(undefined); | 1213 | const thinkingNormalizationInFlight = useRef<string | undefined>(undefined); |
| 1214 | const [error, setErrorState] = useState<string>(); | 1214 | const [error, setErrorState] = useState<string>(); |
| 1215 | + const [computeUnavailable, setComputeUnavailable] = useState(false); | ||
| 1216 | + const [computeUnavailableDetail, setComputeUnavailableDetail] = useState<string>(""); | ||
| 1215 | const [systemSettingsErrors, setSystemSettingsErrors] = useState<string[]>([]); | 1217 | const [systemSettingsErrors, setSystemSettingsErrors] = useState<string[]>([]); |
| 1216 | const [scopedSettingsErrors, setScopedSettingsErrors] = useState<string[]>([]); | 1218 | const [scopedSettingsErrors, setScopedSettingsErrors] = useState<string[]>([]); |
| 1217 | const [dragActive, setDragActive] = useState(false); | 1219 | const [dragActive, setDragActive] = useState(false); |
| @@ -1425,6 +1427,33 @@ export function App({ initialToken }: { initialToken?: string } = {}) { | |||
| 1425 | })); | 1427 | })); |
| 1426 | }, [client]); | 1428 | }, [client]); |
| 1427 | 1429 | ||
| 1430 | + // Poll the aggregated health endpoint so the compute-readiness state (the | ||
| 1431 | + // sandbox the runner executes code under) follows the real runner, not just | ||
| 1432 | + // whether the local API is reachable. The green connection dot means only | ||
| 1433 | + // "API reachable"; a sandbox that cannot start must surface separately so | ||
| 1434 | + // the user does not invest in a run that will fail at first tool call. | ||
| 1435 | + useEffect(() => { | ||
| 1436 | + let active = true; | ||
| 1437 | + const poll = async (): Promise<void> => { | ||
| 1438 | + try { | ||
| 1439 | + const health = await client.getApiHealth(); | ||
| 1440 | + if (!active) return; | ||
| 1441 | + const runner = health.runner; | ||
| 1442 | + const sandboxUsable = runner && "sandboxUsable" in runner ? runner.sandboxUsable : undefined; | ||
| 1443 | + setComputeUnavailable(sandboxUsable === false); | ||
| 1444 | + setComputeUnavailableDetail(sandboxUsable === false && runner && "sandboxDetail" in runner ? runner.sandboxDetail ?? "" : ""); | ||
| 1445 | + } catch { | ||
| 1446 | + if (active) setComputeUnavailable(false); | ||
| 1447 | + } | ||
| 1448 | + }; | ||
| 1449 | + void poll(); | ||
| 1450 | + const timer = window.setInterval(() => void poll(), 30_000); | ||
| 1451 | + return () => { | ||
| 1452 | + active = false; | ||
| 1453 | + window.clearInterval(timer); | ||
| 1454 | + }; | ||
| 1455 | + }, [client]); | ||
| 1456 | + | ||
| 1428 | // Opening System settings starts a dialog-owned refresh. Failures from this | 1457 | // Opening System settings starts a dialog-owned refresh. Failures from this |
| 1429 | // operation stay in the dialog instead of being inferred from whichever | 1458 | // operation stay in the dialog instead of being inferred from whichever |
| 1430 | // overlay happens to be open when an unrelated background request fails. | 1459 | // overlay happens to be open when an unrelated background request fails. |
| @@ -4404,9 +4433,10 @@ export function App({ initialToken }: { initialToken?: string } = {}) { | |||
| 4404 | sessionTokens={sessionUsage?.totals.totalTokens} | 4433 | sessionTokens={sessionUsage?.totals.totalTokens} |
| 4405 | /> | 4434 | /> |
| 4406 | ) : null} | 4435 | ) : null} |
| 4407 | - <span className={error ? "connection bad" : "connection"} title={error ? t("app.connectionErrorTooltip", { error }) : t("app.connectionOkTooltip")}><i />{error ? t("app.connectionErrorChip") : null}</span> | 4436 | + <span className={error ? "connection bad" : computeUnavailable ? "connection degraded" : "connection"} title={error ? t("app.connectionErrorTooltip", { error }) : computeUnavailable ? t("app.connectionDegradedTooltip", { detail: computeUnavailableDetail || t("app.computeUnavailableBanner") }) : t("app.connectionOkTooltip")}><i />{error ? t("app.connectionErrorChip") : computeUnavailable ? t("app.connectionDegradedChip") : null}</span> |
| 4408 | </div> | 4437 | </div> |
| 4409 | </div> : null} | 4438 | </div> : null} |
| 4439 | + {computeUnavailable ? <div className="compute-unavailable-notice" role="alert">{t("app.computeUnavailableBanner")}{computeUnavailableDetail ? ` · ${computeUnavailableDetail}` : null}</div> : null} | ||
| 4410 | {sessionPending ? ( | 4440 | {sessionPending ? ( |
| 4411 | <div className="messages"><SkeletonRows className="message-skeleton" count={3} /></div> | 4441 | <div className="messages"><SkeletonRows className="message-skeleton" count={3} /></div> |
| 4412 | ) : !session ? ( | 4442 | ) : !session ? ( |
| @@ -2325,7 +2325,10 @@ export const en = { | |||
| 2325 | "app.runnerSelectionChip": "Runners · {count}", | 2325 | "app.runnerSelectionChip": "Runners · {count}", |
| 2326 | "app.connectionErrorTooltip": "Needs attention: {error}", | 2326 | "app.connectionErrorTooltip": "Needs attention: {error}", |
| 2327 | "app.connectionOkTooltip": "Local API connected", | 2327 | "app.connectionOkTooltip": "Local API connected", |
| 2328 | + "app.connectionDegradedTooltip": "Connected, but analysis cannot run: {detail}", | ||
| 2328 | "app.connectionErrorChip": "Needs attention", | 2329 | "app.connectionErrorChip": "Needs attention", |
| 2330 | + "app.connectionDegradedChip": "Compute unavailable", | ||
| 2331 | + "app.computeUnavailableBanner": "This environment cannot run analyses. The sandbox could not be prepared. You can still browse history and artifacts.", | ||
| 2329 | "app.sessionIntroPrompt": "Try “Research human TP53 and create a cited evidence brief.”", | 2332 | "app.sessionIntroPrompt": "Try “Research human TP53 and create a cited evidence brief.”", |
| 2330 | "app.roleYou": "You", | 2333 | "app.roleYou": "You", |
| 2331 | "app.roleResearcher": "Researcher", | 2334 | "app.roleResearcher": "Researcher", |
| @@ -4913,7 +4916,10 @@ export const zhCN: Partial<Record<MessageKey, string>> = { | |||
| 4913 | "app.runnerSelectionChip": "Runner · {count}", | 4916 | "app.runnerSelectionChip": "Runner · {count}", |
| 4914 | "app.connectionErrorTooltip": "需要处理:{error}", | 4917 | "app.connectionErrorTooltip": "需要处理:{error}", |
| 4915 | "app.connectionOkTooltip": "本地 API 已连接", | 4918 | "app.connectionOkTooltip": "本地 API 已连接", |
| 4919 | + "app.connectionDegradedTooltip": "已连接,但无法执行分析:{detail}", | ||
| 4916 | "app.connectionErrorChip": "需要处理", | 4920 | "app.connectionErrorChip": "需要处理", |
| 4921 | + "app.connectionDegradedChip": "计算不可用", | ||
| 4922 | + "app.computeUnavailableBanner": "当前环境无法执行分析任务。沙箱环境未就绪。你仍可浏览历史会话与产物。", | ||
| 4917 | "app.sessionIntroPrompt": "试试「研究人类 TP53,并生成一份带引用的证据简报。」", | 4923 | "app.sessionIntroPrompt": "试试「研究人类 TP53,并生成一份带引用的证据简报。」", |
| 4918 | "app.roleYou": "你", | 4924 | "app.roleYou": "你", |
| 4919 | "app.roleResearcher": "研究者", | 4925 | "app.roleResearcher": "研究者", |