fix(local-asr): preserve child state across refreshes

This commit is contained in:
Chris233 committed 2026-08-21 20:07:30 +08:00
1 parent 86012fce42
commit cb3d3f8a3a
2 files changed
+103 -36

No files matched your search

@@ -1,4 +1,5 @@
import { readFile } from 'node:fs/promises';
import ts from 'typescript';
const source = await readFile(
new URL('../src/pages/LocalAsr/index.tsx', import.meta.url),
@@ -9,6 +10,58 @@ const refreshPolling = source.match(
/window\.setInterval\(\(\) => \{\s*void refresh\(\)\s*\}, 3000\)/g,
) ?? [];
const sourceFile = ts.createSourceFile(
'LocalAsr/index.tsx',
source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TSX,
);
const localAsr = sourceFile.statements.find(
(statement) =>
ts.isFunctionDeclaration(statement) && statement.name?.text === 'LocalAsr',
);
if (!localAsr?.body) {
throw new Error('LocalAsr component declaration is missing');
}
const jsxComponentNames = new Set();
const collectJsxComponentNames = (node) => {
if (
(ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) &&
ts.isIdentifier(node.tagName)
) {
jsxComponentNames.add(node.tagName.text);
}
ts.forEachChild(node, collectJsxComponentNames);
};
collectJsxComponentNames(localAsr.body);
const createsComponentType = (node) =>
ts.isArrowFunction(node) ||
ts.isFunctionExpression(node) ||
(ts.isConditionalExpression(node) &&
(createsComponentType(node.whenTrue) || createsComponentType(node.whenFalse)));
const nestedJsxComponents = localAsr.body.statements.flatMap((statement) => {
if (!ts.isVariableStatement(statement)) return [];
return statement.declarationList.declarations
.filter(
(declaration) =>
ts.isIdentifier(declaration.name) &&
jsxComponentNames.has(declaration.name.text) &&
declaration.initializer &&
createsComponentType(declaration.initializer),
)
.map((declaration) => declaration.name.text);
});
if (nestedJsxComponents.length > 0) {
throw new Error(
`LocalAsr must not create JSX component types during render; found ${nestedJsxComponents.join(', ')}`,
);
}
if (refreshPolling.length !== 1) {
throw new Error(`LocalAsr should have one refresh poller, found ${refreshPolling.length}`);
}
@@ -29,4 +82,6 @@ for (const contract of [
}
}
console.log('LocalAsr keeps one refresh poller and pauses it for the download dialog');
console.log(
'LocalAsr keeps one refresh poller, pauses it for the download dialog, and preserves stable JSX component types',
);
+47 -35
View File
@@ -155,6 +155,48 @@ interface LocalAsrProps {
embedded?: boolean
}
interface LocalAsrContentWrapperProps {
embedded: boolean
children: ReactNode
}
// 必须保持为模块级组件:如果在 LocalAsr 渲染函数内定义,3 秒刷新引发的任意
// setState 都会创建新的组件类型,React 会重挂整棵子树并清空 Collapsible /
// SelectLite 等子组件的交互状态。
function LocalAsrContentWrapper({
embedded,
children,
}: LocalAsrContentWrapperProps) {
if (embedded) return <>{children}</>
return (
<div
style={{
padding: "20px 28px 32px",
overflowY: "auto",
height: "100%",
}}
>
{children}
</div>
)
}
function LocalAsrGroupTitle({ children }: { children: ReactNode }) {
return (
<div
style={{
fontSize: 12.5,
fontWeight: 600,
color: "var(--ol-ink-3)",
letterSpacing: "0.02em",
margin: "18px 0 8px",
}}
>
{children}
</div>
)
}
type RefreshGuard = () => boolean
export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
@@ -1805,38 +1847,6 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
? t("localAsr.foundryRetryPrepare")
: t("localAsr.sherpaPrepare")
// embedded=true 嵌入「高级」设置:跳过外层 page padding/height、PageHeader,
// 与独立警告 Card——AdvancedSection 自己负责标题与短警告 + 启用时的浮层 popup,
// LocalAsr 只输出实际功能 Cards(Foundry / Qwen3 模型状态 / 模型列表)。
const Wrapper = embedded
? (props: { children: ReactNode }) => <>{props.children}</>
: (props: { children: ReactNode }) => (
<div
style={{
padding: "20px 28px 32px",
overflowY: "auto",
height: "100%",
}}
>
{props.children}
</div>
)
// 本地模型页分组标题:模型选择 / 下载与管理 / 其他。
const GroupTitle = ({ children }: { children: ReactNode }) => (
<div
style={{
fontSize: 12.5,
fontWeight: 600,
color: "var(--ol-ink-3)",
letterSpacing: "0.02em",
margin: "18px 0 8px",
}}
>
{children}
</div>
)
// ─── 两栏看板的统一模型条目(Qwen3 / sherpa-onnx / foundry 归一化) ───
// allSidebarEntries = 全目录(下载弹窗用,未下载/下载中/已下载全列出,
// 让「下载新模型」弹窗能选到所有可获取的模型);
@@ -2057,7 +2067,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
: null
return (
<Wrapper>
<LocalAsrContentWrapper embedded={embedded}>
{!embedded && (
<PageHeader
kicker={t("localAsr.kicker")}
@@ -2550,7 +2560,9 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
)}
{/* ─── 分组:下载与管理(各引擎的模型获取/准备/下载) ─── */}
<GroupTitle>{t("localAsr.groupDownload")}</GroupTitle>
<LocalAsrGroupTitle>
{t("localAsr.groupDownload")}
</LocalAsrGroupTitle>
{IS_WINDOWS && (
@@ -3400,7 +3412,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
</div>
</Card>
)}
</Wrapper>
</LocalAsrContentWrapper>
)
}