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>
256 lines
7.8 KiB
TypeScript
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>
|
|
);
|
|
}
|