Files
maskanx_cm_frontend/src/pages/Settings/Models/components/sections/ModelsSection.tsx
T
AFFAANhandClaude Opus 5 39231daeeb fix(lint): clear the 7 exhaustive-deps warnings carried all session
Every lint run this session reported the same 7 warnings across 4 files.
Fixed properly, not suppressed:

- Chat/index.tsx: effect depended on the whole liveChatStatus object;
  narrowed to liveChatStatus?.requestId, the one field it actually reads.
- Heartbeat/index.tsx: fetchConfig wrapped in useCallback([form, t]) and
  added to its effect's dependency array, instead of an empty array
  silencing the warning on a function that closes over both.
- Environments/index.tsx: `t` (i18n) added to 4 useCallback dependency
  arrays that read it but didn't declare it.
- ModelsSection.tsx: effect depended on the currentSlot object reference;
  extracted currentProviderId/currentModel primitives so it only reruns
  when the actual values change, not on every new object identity.

Build 0 errors, lint 0 errors / 0 warnings (was 7).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 19:03:15 +05:30

256 lines
7.8 KiB
TypeScript

import { useState, useEffect, useMemo } from "react";
import { SaveOutlined } from "@ant-design/icons";
import { Select, Button, message } from "@agentscope-ai/design";
import type { ModelSlotRequest, ProviderInfo } from "../../../../../api/types";
import api from "../../../../../api";
import { useTranslation } from "react-i18next";
import { MASKANX_AI_PROVIDER_ID } from "../../../../../shared/providerMeta";
import styles from "../../index.module.less";
import { OpenRouterRouting } from "./OpenRouterRouting";
interface ModelsSectionProps {
providers: ProviderInfo[];
activeModels: {
active_llm?: {
provider_id?: string;
model?: string;
};
} | null;
onSaved: () => void;
}
export function ModelsSection({
providers,
activeModels,
onSaved,
}: ModelsSectionProps) {
const { t } = useTranslation();
const [saving, setSaving] = useState(false);
const [selectedProviderId, setSelectedProviderId] = useState<
string | undefined
>(undefined);
const [selectedModel, setSelectedModel] = useState<string | undefined>(
undefined,
);
const [dirty, setDirty] = useState(false);
const [MaskanXAiUsage, setMaskanXAiUsage] = useState<{
messages_limit?: number | null;
messages_used?: number | null;
messages_remaining?: number | null;
} | null>(null);
const [MaskanXAiUsageLoading, setMaskanXAiUsageLoading] = useState(false);
const [MaskanXAiUsageError, setMaskanXAiUsageError] = useState<string | null>(
null,
);
const currentSlot = activeModels?.active_llm;
const currentProviderId = currentSlot?.provider_id;
const currentModel = currentSlot?.model;
const eligible = useMemo(
() =>
providers.filter((p) => {
if (p.supports_llm === false) {
return false;
}
// Ollama: need base_url AND models (to connect to daemon)
if (p.id === "ollama") {
return !!p.current_base_url && (p.models?.length ?? 0) > 0;
}
// Local providers (llama.cpp, mlx): need models only
if (p.is_local) {
return (p.models?.length ?? 0) > 0;
}
// Custom providers: need base_url AND models
if (p.is_custom) {
return !!p.current_base_url && (p.models?.length ?? 0) > 0;
}
// Built-in remote providers (modelscope, dashscope, etc.): need API key
return !!p.current_api_key;
}),
[providers],
);
useEffect(() => {
if (currentProviderId || currentModel) {
setSelectedProviderId(currentProviderId || undefined);
setSelectedModel(currentModel || undefined);
}
setDirty(false);
}, [currentProviderId, currentModel]);
const chosenProvider = providers.find((p) => p.id === selectedProviderId);
const modelOptions = chosenProvider?.models ?? [];
const hasModels = modelOptions.length > 0;
useEffect(() => {
if (selectedProviderId !== MASKANX_AI_PROVIDER_ID) {
setMaskanXAiUsage(null);
setMaskanXAiUsageError(null);
setMaskanXAiUsageLoading(false);
return;
}
let cancelled = false;
setMaskanXAiUsageLoading(true);
setMaskanXAiUsageError(null);
api
.getProviderUsage(MASKANX_AI_PROVIDER_ID)
.then((usage) => {
if (cancelled) return;
setMaskanXAiUsage(usage);
})
.catch(() => {
if (cancelled) return;
setMaskanXAiUsage(null);
setMaskanXAiUsageError(t("models.MaskanXAiUsageUnavailable"));
})
.finally(() => {
if (!cancelled) setMaskanXAiUsageLoading(false);
});
return () => {
cancelled = true;
};
}, [selectedProviderId, t]);
const handleProviderChange = (pid: string) => {
setSelectedProviderId(pid);
setSelectedModel(undefined);
setDirty(true);
};
const handleModelChange = (model: string) => {
setSelectedModel(model);
setDirty(true);
};
const handleSave = async () => {
if (!selectedProviderId || !selectedModel) return;
const body: ModelSlotRequest = {
provider_id: selectedProviderId,
model: selectedModel,
};
setSaving(true);
try {
await api.setActiveLlm(body);
message.success(t("models.llmModelUpdated"));
setDirty(false);
onSaved();
} catch (error) {
const errMsg =
error instanceof Error ? error.message : t("models.failedToSave");
message.error(errMsg);
} finally {
setSaving(false);
}
};
const isActive =
currentSlot &&
currentSlot.provider_id === selectedProviderId &&
currentSlot.model === selectedModel;
const canSave = dirty && !!selectedProviderId && !!selectedModel;
const MaskanXAiUsageForDisplay =
selectedProviderId === MASKANX_AI_PROVIDER_ID &&
MaskanXAiUsage != null &&
typeof MaskanXAiUsage?.messages_remaining === "number" &&
typeof MaskanXAiUsage?.messages_limit === "number"
? MaskanXAiUsage
: null;
return (
<div className={styles.slotSection}>
<div className={styles.slotHeader}>
<h3 className={styles.slotTitle}>{t("models.llmConfiguration")}</h3>
{currentSlot?.provider_id && currentSlot?.model && (
<span className={styles.slotCurrent}>
{t("models.active", {
provider: currentSlot.provider_id,
model: currentSlot.model,
})}
</span>
)}
</div>
<div className={styles.slotForm}>
<div className={styles.slotField}>
<label className={styles.slotLabel}>{t("models.provider")}</label>
<Select
style={{ width: "100%" }}
placeholder={t("models.selectProvider")}
value={selectedProviderId}
onChange={handleProviderChange}
listHeight={300}
options={eligible.map((p) => ({
value: p.id,
label: p.name,
}))}
/>
</div>
<div className={styles.slotField}>
<label className={styles.slotLabel}>{t("models.model")}</label>
{selectedProviderId === "openrouter" ? (
<OpenRouterRouting
models={modelOptions}
value={selectedModel}
onChange={handleModelChange}
/>
) : (
<Select
style={{ width: "100%" }}
placeholder={
hasModels ? t("models.selectModel") : t("models.addModelFirst")
}
disabled={!hasModels}
showSearch
optionFilterProp="label"
value={selectedModel}
onChange={handleModelChange}
options={modelOptions.map((m) => ({
value: m.id,
label: `${m.name} (${m.id})`,
}))}
/>
)}
{selectedProviderId === MASKANX_AI_PROVIDER_ID && (
<div className={styles.MaskanXAiUsageHint}>
{MaskanXAiUsageLoading
? t("models.MaskanXAiUsageLoading")
: MaskanXAiUsageForDisplay
? t("models.MaskanXAiMessagesRemaining", {
remaining: MaskanXAiUsageForDisplay.messages_remaining,
limit: MaskanXAiUsageForDisplay.messages_limit,
})
: MaskanXAiUsageError || t("models.MaskanXAiUsageUnavailable")}
</div>
)}
</div>
<div
className={styles.slotField}
style={{ flex: "0 0 auto", minWidth: "120px" }}
>
<label className={styles.slotLabel} style={{ visibility: "hidden" }}>
{t("models.actions")}
</label>
<Button
type="primary"
loading={saving}
disabled={!canSave}
onClick={handleSave}
block
icon={<SaveOutlined />}
>
{isActive ? t("models.saved") : t("models.save")}
</Button>
</div>
</div>
</div>
);
}