mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
fix(coach): show the operator's central Codex when it answers
The operator's shared Codex is appended to the resolved chain but is never a chain entry, so the Coach's provider select had no option for it and showed its first option as the one answering. The answering provider now appears as a disabled option, with a note that the operator manages the connection.
This commit is contained in:
1 parent
2a556b47c3
commit
0c1dd1e967
9 files changed
+70
-9
No files matched your search
+3
-1
@@ -3138,7 +3138,9 @@
|
||||
"saving": "Wird gespeichert…",
|
||||
"saveFailed": "Die Änderung ließ sich nicht speichern.",
|
||||
"allSettings": "Alle KI-Einstellungen",
|
||||
"providerNotActive": "{provider} konnte nicht übernehmen: Es fehlt eine funktionierende Anmeldung oder ein Schlüssel. Richte das in den KI-Einstellungen ein."
|
||||
"providerNotActive": "{provider} konnte nicht übernehmen: Es fehlt eine funktionierende Anmeldung oder ein Schlüssel. Richte das in den KI-Einstellungen ein.",
|
||||
"operatorCodex": "ChatGPT (vom Betreiber bereitgestellt)",
|
||||
"operatorManaged": "Diese Verbindung verwaltet der Betreiber deines Servers. Sobald einer deiner eigenen Anbieter eingerichtet ist, antwortet er zuerst."
|
||||
},
|
||||
"answer": {
|
||||
"scrollToBottom": "Zur neuesten Nachricht",
|
||||
|
||||
+3
-1
@@ -3138,7 +3138,9 @@
|
||||
"saving": "Saving…",
|
||||
"saveFailed": "That change could not be saved.",
|
||||
"allSettings": "All AI settings",
|
||||
"providerNotActive": "{provider} could not take over: it has no working sign-in or key. Set it up in AI settings."
|
||||
"providerNotActive": "{provider} could not take over: it has no working sign-in or key. Set it up in AI settings.",
|
||||
"operatorCodex": "ChatGPT (shared by the operator)",
|
||||
"operatorManaged": "Your server's operator manages this connection. Once one of your own providers is set up, it answers first."
|
||||
},
|
||||
"answer": {
|
||||
"scrollToBottom": "Jump to the latest message",
|
||||
|
||||
+3
-1
@@ -3138,7 +3138,9 @@
|
||||
"saving": "Guardando…",
|
||||
"saveFailed": "No se pudo guardar el cambio.",
|
||||
"allSettings": "Todos los ajustes de IA",
|
||||
"providerNotActive": "{provider} no pudo encargarse: no tiene un inicio de sesión ni una clave que funcionen. Configúralo en los ajustes de IA."
|
||||
"providerNotActive": "{provider} no pudo encargarse: no tiene un inicio de sesión ni una clave que funcionen. Configúralo en los ajustes de IA.",
|
||||
"operatorCodex": "ChatGPT (compartido por el operador)",
|
||||
"operatorManaged": "El operador de tu servidor gestiona esta conexión. Cuando configures uno de tus propios proveedores, responderá primero."
|
||||
},
|
||||
"answer": {
|
||||
"scrollToBottom": "Ir al último mensaje",
|
||||
|
||||
+3
-1
@@ -3138,7 +3138,9 @@
|
||||
"saving": "Enregistrement…",
|
||||
"saveFailed": "Impossible d’enregistrer cette modification.",
|
||||
"allSettings": "Tous les paramètres IA",
|
||||
"providerNotActive": "{provider} n'a pas pu prendre le relais : aucune connexion ni clé valide. Configurez-le dans les réglages IA."
|
||||
"providerNotActive": "{provider} n'a pas pu prendre le relais : aucune connexion ni clé valide. Configurez-le dans les réglages IA.",
|
||||
"operatorCodex": "ChatGPT (partagé par l'opérateur)",
|
||||
"operatorManaged": "L'opérateur de votre serveur gère cette connexion. Dès qu'un de vos propres fournisseurs est configuré, c'est lui qui répond en premier."
|
||||
},
|
||||
"answer": {
|
||||
"scrollToBottom": "Aller au dernier message",
|
||||
|
||||
+3
-1
@@ -3138,7 +3138,9 @@
|
||||
"saving": "Salvataggio…",
|
||||
"saveFailed": "Impossibile salvare la modifica.",
|
||||
"allSettings": "Tutte le impostazioni IA",
|
||||
"providerNotActive": "{provider} non è subentrato: manca un accesso o una chiave funzionante. Configuralo nelle impostazioni IA."
|
||||
"providerNotActive": "{provider} non è subentrato: manca un accesso o una chiave funzionante. Configuralo nelle impostazioni IA.",
|
||||
"operatorCodex": "ChatGPT (condiviso dall'operatore)",
|
||||
"operatorManaged": "Questa connessione è gestita dall'operatore del tuo server. Quando configuri uno dei tuoi fornitori, risponde per primo."
|
||||
},
|
||||
"answer": {
|
||||
"scrollToBottom": "Vai all'ultimo messaggio",
|
||||
|
||||
+3
-1
@@ -3138,7 +3138,9 @@
|
||||
"saving": "저장 중…",
|
||||
"saveFailed": "변경 사항을 저장하지 못했어요.",
|
||||
"allSettings": "모든 AI 설정",
|
||||
"providerNotActive": "{provider}(으)로 전환하지 못했습니다. 작동하는 로그인이나 키가 없습니다. AI 설정에서 설정하세요."
|
||||
"providerNotActive": "{provider}(으)로 전환하지 못했습니다. 작동하는 로그인이나 키가 없습니다. AI 설정에서 설정하세요.",
|
||||
"operatorCodex": "ChatGPT (운영자 공유)",
|
||||
"operatorManaged": "이 연결은 서버 운영자가 관리합니다. 내 공급자를 하나라도 설정하면 그 공급자가 먼저 답합니다."
|
||||
},
|
||||
"answer": {
|
||||
"scrollToBottom": "최신 메시지로 이동",
|
||||
|
||||
+3
-1
@@ -3138,7 +3138,9 @@
|
||||
"saving": "Zapisywanie…",
|
||||
"saveFailed": "Nie udało się zapisać zmiany.",
|
||||
"allSettings": "Wszystkie ustawienia AI",
|
||||
"providerNotActive": "{provider} nie mógł przejąć odpowiedzi: brak działającego logowania lub klucza. Skonfiguruj go w ustawieniach AI."
|
||||
"providerNotActive": "{provider} nie mógł przejąć odpowiedzi: brak działającego logowania lub klucza. Skonfiguruj go w ustawieniach AI.",
|
||||
"operatorCodex": "ChatGPT (udostępniony przez operatora)",
|
||||
"operatorManaged": "Tym połączeniem zarządza operator Twojego serwera. Gdy skonfigurujesz własnego dostawcę, to on odpowie jako pierwszy."
|
||||
},
|
||||
"answer": {
|
||||
"scrollToBottom": "Przejdź do najnowszej wiadomości",
|
||||
|
||||
@@ -43,11 +43,11 @@ const PROVIDER: UserAIProvider = {
|
||||
compatReasoningEffort: null,
|
||||
};
|
||||
|
||||
function render(node: React.ReactNode) {
|
||||
function render(node: React.ReactNode, chain: ProviderChainData = CHAIN) {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
client.setQueryData(queryKeys.insightsProviderChain(), CHAIN);
|
||||
client.setQueryData(queryKeys.insightsProviderChain(), chain);
|
||||
client.setQueryData(queryKeys.userAiProvider(), PROVIDER);
|
||||
client.setQueryData(queryKeys.coachPrefs(), DEFAULT_COACH_PREFS);
|
||||
return renderToStaticMarkup(
|
||||
@@ -81,6 +81,23 @@ describe("<CoachSettingsBody>", () => {
|
||||
expect(select).not.toContain('value="codex"');
|
||||
});
|
||||
|
||||
it("shows the operator's central Codex as the active provider when it answers", () => {
|
||||
// `admin-codex` is never a chain entry, so no option matched it and the
|
||||
// select showed its first option as the one answering.
|
||||
const html = render(<CoachSettingsBody />, {
|
||||
...CHAIN,
|
||||
activeProvider: "admin-codex" as ProviderChainData["activeProvider"],
|
||||
});
|
||||
const select = html.match(
|
||||
/<select[^>]*data-slot="coach-quick-provider"[\s\S]*?<\/select>/,
|
||||
)?.[0];
|
||||
expect(select).toMatch(
|
||||
/<option value="admin-codex" disabled="" selected="">ChatGPT \(shared by the operator\)<\/option>/,
|
||||
);
|
||||
expect(select).not.toMatch(/value="local"[^>]*selected/);
|
||||
expect(html).toContain('data-slot="coach-quick-provider-operator"');
|
||||
});
|
||||
|
||||
it("shows the model and reasoning controls the active provider owns", () => {
|
||||
const html = render(<CoachSettingsBody />);
|
||||
expect(html).toContain('data-slot="coach-quick-model"');
|
||||
|
||||
@@ -82,6 +82,19 @@ export function switchDidNotTake(
|
||||
return fresh.activeProvider !== chosen;
|
||||
}
|
||||
|
||||
/**
|
||||
* The provider the server answers with when the select has no option for it:
|
||||
* the operator's central Codex (never a chain entry), or an entry that answers
|
||||
* without being offered. Shown as a disabled option so the select tells the
|
||||
* truth instead of showing its first option as active.
|
||||
*/
|
||||
export function activeOutsideOptions(
|
||||
active: string | null,
|
||||
options: readonly string[],
|
||||
): string | null {
|
||||
return active !== null && !options.includes(active) ? active : null;
|
||||
}
|
||||
|
||||
export type ModelControl =
|
||||
| { kind: "preset"; presets: readonly string[]; value: string | null }
|
||||
| { kind: "gateway"; value: string | null }
|
||||
@@ -201,6 +214,8 @@ export function CoachModelPicker() {
|
||||
const model = modelControlFor(active, userProvider);
|
||||
const reasoning = reasoningFieldFor(active, userProvider);
|
||||
const busy = switchProvider.isPending || patchProvider.isPending;
|
||||
// The server may answer with `admin-codex`, which the UI's type omits.
|
||||
const unlisted = activeOutsideOptions(active as string | null, options);
|
||||
|
||||
if (chainQuery.isError || providerQuery.isError) {
|
||||
return (
|
||||
@@ -259,12 +274,27 @@ export function CoachModelPicker() {
|
||||
{t("insights.coach.frame.noProvider")}
|
||||
</option>
|
||||
) : null}
|
||||
{unlisted ? (
|
||||
<option value={unlisted} disabled>
|
||||
{unlisted === "admin-codex"
|
||||
? t("insights.coach.frame.operatorCodex")
|
||||
: t(`settings.ai.providerChain.types.${unlisted}`)}
|
||||
</option>
|
||||
) : null}
|
||||
{options.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{t(`settings.ai.providerChain.types.${p}`)}
|
||||
</option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
{unlisted === "admin-codex" ? (
|
||||
<p
|
||||
data-slot="coach-quick-provider-operator"
|
||||
className="text-muted-foreground mt-1 text-xs"
|
||||
>
|
||||
{t("insights.coach.frame.operatorManaged")}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{model.kind === "preset" ? (
|
||||
|
||||
Reference in new issue
Block a user