fix(local-asr): preserve child state across refreshes
This commit is contained in:
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',
|
||||
);
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user