feat(ai-settings): add per-card "Save and test" button
The single global "Save endpoints" button sat far below the fold and the per-card "Test endpoint" button probed the server-stored settings, so it ignored unsaved form edits. Replace each endpoint card's "Test endpoint" button with a combined "Save and test" button that persists the whole form first and only runs the card's connection probe on a successful save; the global "Save endpoints" button is kept for save-only. - Add handleSaveAndTest: save (rethrows on failure) then probe; skip the test if the save fails (the mutation already surfaces the error). - Add savingTestCapability state so only the clicked card spins during the shared save while all save controls stay disabled (no concurrent saves). - Reset the previous probe result when a new save+test starts. - Add the "Save and test" en-US translation key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1205,6 +1205,7 @@
|
|||||||
"Transcribe as you speak, cutting on pauses": "Transcribe as you speak, cutting on pauses",
|
"Transcribe as you speak, cutting on pauses": "Transcribe as you speak, cutting on pauses",
|
||||||
"Voice dictation is not available yet.": "Voice dictation is not available yet.",
|
"Voice dictation is not available yet.": "Voice dictation is not available yet.",
|
||||||
"Test endpoint": "Test endpoint",
|
"Test endpoint": "Test endpoint",
|
||||||
|
"Save and test": "Save and test",
|
||||||
"Save endpoints": "Save endpoints",
|
"Save endpoints": "Save endpoints",
|
||||||
"Configured and enabled": "Configured and enabled",
|
"Configured and enabled": "Configured and enabled",
|
||||||
"Configured but disabled": "Configured but disabled",
|
"Configured but disabled": "Configured but disabled",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
useUpdateAiSettingsMutation,
|
useUpdateAiSettingsMutation,
|
||||||
} from "@/features/workspace/queries/ai-settings-query.ts";
|
} from "@/features/workspace/queries/ai-settings-query.ts";
|
||||||
import {
|
import {
|
||||||
|
AiTestCapability,
|
||||||
IAiSettingsUpdate,
|
IAiSettingsUpdate,
|
||||||
SttApiStyle,
|
SttApiStyle,
|
||||||
} from "@/features/workspace/services/ai-settings-service.ts";
|
} from "@/features/workspace/services/ai-settings-service.ts";
|
||||||
@@ -252,6 +253,12 @@ export default function AiProviderSettings() {
|
|||||||
const embedTest = useTestAiConnectionMutation();
|
const embedTest = useTestAiConnectionMutation();
|
||||||
const sttTest = useTestAiConnectionMutation();
|
const sttTest = useTestAiConnectionMutation();
|
||||||
|
|
||||||
|
// Which card's "Save and test" is currently mid-save. The save mutation is
|
||||||
|
// shared, so without this every save-and-test button would spin at once;
|
||||||
|
// this lets only the clicked card's button show the spinner during the save.
|
||||||
|
const [savingTestCapability, setSavingTestCapability] =
|
||||||
|
useState<AiTestCapability | null>(null);
|
||||||
|
|
||||||
// Agent roles drive the public-share assistant identity picker. Admin-gated
|
// Agent roles drive the public-share assistant identity picker. Admin-gated
|
||||||
// (the component returns early for non-admins), same as the AI settings query.
|
// (the component returns early for non-admins), same as the AI settings query.
|
||||||
const { data: roles } = useAiRolesQuery(isAdmin);
|
const { data: roles } = useAiRolesQuery(isAdmin);
|
||||||
@@ -408,6 +415,28 @@ export default function AiProviderSettings() {
|
|||||||
form.resetDirty();
|
form.resetDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Save and test" for a single card: the connection test probes the
|
||||||
|
// SERVER-STORED settings, so the whole form must be persisted before testing.
|
||||||
|
// Save first (handleSubmit rethrows on failure and the mutation already shows
|
||||||
|
// its own error notification); only run the probe on a successful save.
|
||||||
|
async function handleSaveAndTest(
|
||||||
|
capability: AiTestCapability,
|
||||||
|
test: ReturnType<typeof useTestAiConnectionMutation>,
|
||||||
|
) {
|
||||||
|
setSavingTestCapability(capability);
|
||||||
|
// Clear any previous probe result so the stale "successful/failed" text does
|
||||||
|
// not linger next to the spinner while the (now preceding) save runs.
|
||||||
|
test.reset();
|
||||||
|
try {
|
||||||
|
await handleSubmit(form.values);
|
||||||
|
} catch {
|
||||||
|
return; // save failed — error already surfaced; do not test stale settings
|
||||||
|
} finally {
|
||||||
|
setSavingTestCapability(null);
|
||||||
|
}
|
||||||
|
test.mutate(capability);
|
||||||
|
}
|
||||||
|
|
||||||
function handleClearKey() {
|
function handleClearKey() {
|
||||||
setKeyCleared(true);
|
setKeyCleared(true);
|
||||||
setHasApiKey(false);
|
setHasApiKey(false);
|
||||||
@@ -780,10 +809,13 @@ export default function AiProviderSettings() {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
loading={chatTest.isPending}
|
loading={savingTestCapability === "chat" || chatTest.isPending}
|
||||||
onClick={() => chatTest.mutate("chat")}
|
disabled={
|
||||||
|
updateMutation.isPending || chatTest.isPending || !form.isValid()
|
||||||
|
}
|
||||||
|
onClick={() => void handleSaveAndTest("chat", chatTest)}
|
||||||
>
|
>
|
||||||
{t("Test endpoint")}
|
{t("Save and test")}
|
||||||
</Button>
|
</Button>
|
||||||
{chatTest.data &&
|
{chatTest.data &&
|
||||||
(chatTest.data.ok ? (
|
(chatTest.data.ok ? (
|
||||||
@@ -905,10 +937,13 @@ export default function AiProviderSettings() {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
loading={embedTest.isPending}
|
loading={savingTestCapability === "embeddings" || embedTest.isPending}
|
||||||
onClick={() => embedTest.mutate("embeddings")}
|
disabled={
|
||||||
|
updateMutation.isPending || embedTest.isPending || !form.isValid()
|
||||||
|
}
|
||||||
|
onClick={() => void handleSaveAndTest("embeddings", embedTest)}
|
||||||
>
|
>
|
||||||
{t("Test endpoint")}
|
{t("Save and test")}
|
||||||
</Button>
|
</Button>
|
||||||
{embedTest.data &&
|
{embedTest.data &&
|
||||||
(embedTest.data.ok ? (
|
(embedTest.data.ok ? (
|
||||||
@@ -1099,10 +1134,13 @@ export default function AiProviderSettings() {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
loading={sttTest.isPending}
|
loading={savingTestCapability === "stt" || sttTest.isPending}
|
||||||
onClick={() => sttTest.mutate("stt")}
|
disabled={
|
||||||
|
updateMutation.isPending || sttTest.isPending || !form.isValid()
|
||||||
|
}
|
||||||
|
onClick={() => void handleSaveAndTest("stt", sttTest)}
|
||||||
>
|
>
|
||||||
{t("Test endpoint")}
|
{t("Save and test")}
|
||||||
</Button>
|
</Button>
|
||||||
{sttTest.data &&
|
{sttTest.data &&
|
||||||
(sttTest.data.ok ? (
|
(sttTest.data.ok ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user