mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-19 05:10:01 -05:00
fix(ai): disable categorization when last model is deleted
When the last AI model is deleted, categorization is now forced off server-side, preventing it from silently restarting when a new model is added. The frontend distinguishes between loading and no connections, hiding the feature toggle instead of disabling it, and removes the unused "Add a model first" message.
This commit is contained in:
1 parent
e542cc4473
commit
bfc3fa0d72
6 files changed
+36
-27
No files matched your search
@@ -744,8 +744,11 @@ router.delete(
|
||||
requireAdmin,
|
||||
asyncHandler(async (req, res) => {
|
||||
await deleteAiConnection(req.params.id);
|
||||
// A feature set to it goes back to the default.
|
||||
if ((await getAiCategorizationPrefs()).aiConnectionId === req.params.id) {
|
||||
// With no model left every feature switches off, so adding one later doesn't quietly start them again.
|
||||
if (!(await listAiConnections()).length) {
|
||||
await setAiCategorizationPrefs({ enabled: false, aiConnectionId: null });
|
||||
} else if ((await getAiCategorizationPrefs()).aiConnectionId === req.params.id) {
|
||||
// A feature set to it goes back to the default.
|
||||
await setAiCategorizationPrefs({ aiConnectionId: null });
|
||||
}
|
||||
res.status(204).end();
|
||||
|
||||
@@ -406,6 +406,10 @@ describe("AI categorization", () => {
|
||||
const remaining = await request(app).get("/api/settings/ai-connections").set(auth());
|
||||
expect(remaining.body).toMatchObject([{ id: third.id, is_default: true }]);
|
||||
|
||||
// Deleting the last model switches the features off.
|
||||
await request(app).delete(`/api/settings/ai-connections/${third.id}`).set(auth());
|
||||
expect((await request(app).get("/api/settings/ai-categorization").set(auth())).body.enabled).toBe(false);
|
||||
|
||||
const unknown = await request(app)
|
||||
.patch("/api/settings/ai-categorization")
|
||||
.set(auth())
|
||||
|
||||
@@ -659,8 +659,7 @@
|
||||
"categorization": "AI Categorization",
|
||||
"openSettings": "Settings",
|
||||
"settingsAria": "{{feature}} settings",
|
||||
"toggleAria": "Turn {{feature}} on or off",
|
||||
"needsModel": "Add a model first"
|
||||
"toggleAria": "Turn {{feature}} on or off"
|
||||
},
|
||||
"aiCategorization": {
|
||||
"heading": "AI Categorization",
|
||||
|
||||
@@ -663,8 +663,7 @@
|
||||
"categorization": "DI kategorizavimas",
|
||||
"openSettings": "Nustatymai",
|
||||
"settingsAria": "{{feature}} nustatymai",
|
||||
"toggleAria": "Įjungti arba išjungti: {{feature}}",
|
||||
"needsModel": "Pirmiausia pridėkite modelį"
|
||||
"toggleAria": "Įjungti arba išjungti: {{feature}}"
|
||||
},
|
||||
"aiCategorization": {
|
||||
"heading": "DI kategorizavimas",
|
||||
|
||||
@@ -16,7 +16,8 @@ import AiCategorizationDialog from "./AiCategorizationDialog";
|
||||
import CategorizationAnimation from "./CategorizationAnimation";
|
||||
import { AiLogo } from "./aiLogos";
|
||||
|
||||
type Props = { connections: AiConnection[]; onUnauthorized?: () => void };
|
||||
/** `connections` is null while the models are loading. */
|
||||
type Props = { connections: AiConnection[] | null; onUnauthorized?: () => void };
|
||||
|
||||
const TILE = 112;
|
||||
const tileShadow = "inset 0 0 0 1px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04)";
|
||||
@@ -29,7 +30,10 @@ export default function AiFeaturesSection({ connections, onUnauthorized }: Props
|
||||
const [switching, setSwitching] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Read again when models come or go: deleting the last one switches the features off on the server.
|
||||
const modelCount = connections?.length;
|
||||
useEffect(() => {
|
||||
if (modelCount === undefined) return;
|
||||
let active = true;
|
||||
void settingsApi
|
||||
.getAiCategorization()
|
||||
@@ -43,7 +47,7 @@ export default function AiFeaturesSection({ connections, onUnauthorized }: Props
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [onUnauthorized, t]);
|
||||
}, [modelCount, onUnauthorized, t]);
|
||||
|
||||
const toggle = async (enabled: boolean) => {
|
||||
setSwitching(true);
|
||||
@@ -60,15 +64,17 @@ export default function AiFeaturesSection({ connections, onUnauthorized }: Props
|
||||
|
||||
// The AI it would use: the one it's set to while that exists, otherwise the default, as the backend picks.
|
||||
const model =
|
||||
connections.find((connection) => connection.id === settings?.ai_connection_id) ??
|
||||
connections.find((connection) => connection.is_default);
|
||||
const enabled = Boolean(settings?.enabled);
|
||||
connections?.find((connection) => connection.id === settings?.ai_connection_id) ??
|
||||
connections?.find((connection) => connection.is_default);
|
||||
const hasModel = Boolean(connections?.length);
|
||||
// Without a model nothing can run, whatever was saved, and there's nothing to switch on until one is added.
|
||||
const enabled = Boolean(settings?.enabled) && hasModel;
|
||||
const label = t("adminSettings.aiFeatures.categorization");
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<SectionHeader title={t("adminSettings.aiFeatures.heading")} subtitle={t("adminSettings.aiFeatures.subtitle")} />
|
||||
{settings === null && !error ? (
|
||||
{(settings === null || connections === null) && !error ? (
|
||||
<CircularProgress size={20} />
|
||||
) : (
|
||||
<Box>
|
||||
@@ -122,19 +128,17 @@ export default function AiFeaturesSection({ connections, onUnauthorized }: Props
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Tooltip title={connections.length ? "" : t("adminSettings.aiFeatures.needsModel")}>
|
||||
<span>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enabled}
|
||||
onChange={(event) => void toggle(event.target.checked)}
|
||||
disabled={!settings || switching || (!connections.length && !enabled)}
|
||||
slotProps={{
|
||||
input: { "aria-label": t("adminSettings.aiFeatures.toggleAria", { feature: label }) },
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{hasModel && (
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enabled}
|
||||
onChange={(event) => void toggle(event.target.checked)}
|
||||
disabled={!settings || switching}
|
||||
slotProps={{
|
||||
input: { "aria-label": t("adminSettings.aiFeatures.toggleAria", { feature: label }) },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -151,7 +155,7 @@ export default function AiFeaturesSection({ connections, onUnauthorized }: Props
|
||||
{open && settings && (
|
||||
<AiCategorizationDialog
|
||||
settings={settings}
|
||||
connections={connections}
|
||||
connections={connections ?? []}
|
||||
onClose={() => setOpen(false)}
|
||||
onSaved={setSettings}
|
||||
onUnauthorized={onUnauthorized}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function AiPage({ onUnauthorized }: Props) {
|
||||
return (
|
||||
<Stack spacing={4} divider={<Divider />}>
|
||||
<AiConnectionsSection connections={connections} onChanged={load} onUnauthorized={onUnauthorized} />
|
||||
<AiFeaturesSection connections={connections ?? []} onUnauthorized={onUnauthorized} />
|
||||
<AiFeaturesSection connections={connections} onUnauthorized={onUnauthorized} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user