diff --git a/VERSION b/VERSION index 38f77a65..276cbf9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.1 +2.3.0 diff --git a/apps/web/src/app/(admin)/admin/settings/page.tsx b/apps/web/src/app/(admin)/admin/settings/page.tsx index f258847f..37698b6c 100644 --- a/apps/web/src/app/(admin)/admin/settings/page.tsx +++ b/apps/web/src/app/(admin)/admin/settings/page.tsx @@ -1,2132 +1,24 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { toast } from "sonner"; -import type { ConnectivityResult, ConnectivityTarget } from "@rbrasier/domain"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { AiProviderCard } from "@/components/settings/ai-provider-card"; +import { AuthMethodsCard } from "@/components/settings/auth-methods-card"; import { - Dialog, - DialogBody, - DialogCloseButton, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { trpc } from "@/trpc/client"; - -type Provider = "anthropic" | "openai" | "mistral" | "bedrock"; - -const PROVIDER_LABEL: Record = { - anthropic: "Anthropic (Claude)", - openai: "OpenAI", - mistral: "Mistral", - bedrock: "Amazon Bedrock", -}; - -// Targets exercised by the header "Test all" button, in card order. -const ALL_CONNECTIVITY_TARGETS: ConnectivityTarget[] = [ - "ai", - "n8n", - "embeddings", - "storage", - "email", - "entra", -]; - -type BadgeState = - | { status: "idle" } - | { status: "testing" } - | { status: "ok"; latencyMs?: number; message?: string } - | { status: "skipped"; message?: string } - | { status: "failed"; message?: string }; - -interface ConnectivityController { - states: Partial>; - runTest: (target: ConnectivityTarget) => Promise; - runAll: (targets: ConnectivityTarget[]) => Promise; - isBusy: boolean; -} - -const toBadge = (result: ConnectivityResult): BadgeState => { - if (result.skipped) return { status: "skipped", message: result.message }; - if (result.ok) return { status: "ok", latencyMs: result.latencyMs, message: result.message }; - return { status: "failed", message: result.message }; -}; - -function useConnectivity(): ConnectivityController { - const [states, setStates] = useState>>({}); - const mutation = trpc.settings.testConnectivity.useMutation(); - - const runTest = useCallback( - async (target: ConnectivityTarget) => { - setStates((prev) => ({ ...prev, [target]: { status: "testing" } })); - try { - const result = await mutation.mutateAsync({ target }); - setStates((prev) => ({ ...prev, [target]: toBadge(result) })); - } catch (error) { - const message = error instanceof Error ? error.message : "Probe failed"; - setStates((prev) => ({ ...prev, [target]: { status: "failed", message } })); - } - }, - [mutation], - ); - - // Fan out to per-card probes in parallel so each badge resolves independently. - const runAll = useCallback( - async (targets: ConnectivityTarget[]) => { - await Promise.all(targets.map((target) => runTest(target))); - }, - [runTest], - ); - - const isBusy = Object.values(states).some((state) => state?.status === "testing"); - - return { states, runTest, runAll, isBusy }; -} - -function ConnectivityBadge({ target, state }: { target: ConnectivityTarget; state?: BadgeState }) { - if (!state || state.status === "idle") return null; - - const testId = `connectivity-badge-${target}`; - if (state.status === "testing") { - return ( - - Testing… - - ); - } - if (state.status === "ok") { - return ( - - Reachable{typeof state.latencyMs === "number" ? ` · ${state.latencyMs} ms` : ""} - - ); - } - if (state.status === "skipped") { - return ( - - {state.message ?? "Not configured"} - - ); - } - return ( - - Failed{state.message ? `: ${state.message}` : ""} - - ); -} - -function ConnectivityTest({ - target, - controller, -}: { - target: ConnectivityTarget; - controller: ConnectivityController; -}) { - const state = controller.states[target]; - return ( -
- - -
- ); -} - -function OrganisationNameCard() { - const orgNameQuery = trpc.settings.get.useQuery({ key: "organisation_name" }); - const setMutation = trpc.settings.set.useMutation({ - onSuccess: () => toast.success("Organisation name saved"), - onError: () => toast.error("Failed to save organisation name"), - }); - - const [value, setValue] = useState(""); - - useEffect(() => { - if (orgNameQuery.data?.value !== undefined) { - setValue(orgNameQuery.data.value); - } - }, [orgNameQuery.data?.value]); - - const handleSave = () => { - setMutation.mutate({ key: "organisation_name", value: value.trim() }); - }; - - return ( - - - General - - -
- -

- Used in AI system prompts to give the assistant context about your organisation. -

- setValue(e.target.value)} - placeholder="e.g. Acme Legal" - disabled={orgNameQuery.isLoading} - // Password managers / autofill inject attributes (e.g. caret-color, - // fdprocessedid) onto inputs after SSR, producing a benign dev-mode - // hydration warning. Suppress it for this field only. - suppressHydrationWarning - /> -
- -
-
- ); -} - -function GlobalInstructionsCard() { - const query = trpc.settings.get.useQuery({ key: "global_prompt" }); - const setMutation = trpc.settings.set.useMutation({ - onSuccess: () => toast.success("Global AI instructions saved"), - onError: () => toast.error("Failed to save global AI instructions"), - }); - - const [value, setValue] = useState(""); - - useEffect(() => { - if (query.data?.value !== undefined) { - setValue(query.data.value); - } - }, [query.data?.value]); - - const handleSave = () => { - setMutation.mutate({ key: "global_prompt", value: value.trim() }); - }; - - return ( - - - Global AI Instructions - - -
- -

- Added to every session's system prompt across all flows — use it for house - style, tone, or spelling (e.g. “Be matter-of-fact and professional. Use - Australian English spelling.”). Leave blank for none. -

-