diff --git a/ui/src/app/settings/page.tsx b/ui/src/app/settings/page.tsx index 84345d7..461286b 100644 --- a/ui/src/app/settings/page.tsx +++ b/ui/src/app/settings/page.tsx @@ -3,6 +3,7 @@ import { ExternalLink } from "lucide-react"; import { MCPSection } from "@/components/MCPSection"; +import { ModelConfigurationPreferencesSection } from "@/components/ModelConfigurationPreferencesSection"; import { TelemetrySection } from "@/components/TelemetrySection"; import { Card, @@ -23,6 +24,19 @@ export default function SettingsPage() {

+ + + Preferences + + Set organization-wide defaults such as the test phone number and + timezone. + + + + + + + MCP Server diff --git a/ui/src/components/ModelConfigurationPreferencesSection.tsx b/ui/src/components/ModelConfigurationPreferencesSection.tsx new file mode 100644 index 0000000..6fb03ec --- /dev/null +++ b/ui/src/components/ModelConfigurationPreferencesSection.tsx @@ -0,0 +1,221 @@ +"use client"; + +import { Save } from "lucide-react"; +import { useEffect, useId, useRef, useState } from "react"; +import TimezoneSelect, { type ITimezoneOption } from "react-timezone-select"; +import { toast } from "sonner"; + +import { + getModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGet, + saveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPut, +} from "@/client/sdk.gen"; +import type { OrganizationAiModelConfigurationPreferences } from "@/client/types.gen"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useUserConfig } from "@/context/UserConfigContext"; +import { detailFromError } from "@/lib/apiError"; +import { useAuth } from "@/lib/auth"; + +const emptyPreferences: OrganizationAiModelConfigurationPreferences = { + test_phone_number: "", + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC", +}; + +const timezoneSelectStyles = { + control: (base: Record, state: { isFocused: boolean }) => ({ + ...base, + minHeight: "36px", + fontSize: "14px", + backgroundColor: "var(--background)", + borderColor: state.isFocused ? "var(--ring)" : "var(--border)", + boxShadow: state.isFocused + ? "0 0 0 2px color-mix(in srgb, var(--ring) 20%, transparent)" + : "none", + "&:hover": { borderColor: "var(--border)" }, + }), + menu: (base: Record) => ({ + ...base, + zIndex: 9999, + backgroundColor: "var(--popover)", + border: "1px solid var(--border)", + boxShadow: + "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", + }), + menuList: (base: Record) => ({ + ...base, + backgroundColor: "var(--popover)", + padding: 0, + }), + option: ( + base: Record, + state: { isFocused: boolean; isSelected: boolean }, + ) => ({ + ...base, + backgroundColor: state.isSelected + ? "var(--accent)" + : state.isFocused + ? "var(--accent)" + : "var(--popover)", + color: "var(--foreground)", + cursor: "pointer", + "&:active": { backgroundColor: "var(--accent)" }, + }), + singleValue: (base: Record) => ({ + ...base, + color: "var(--foreground)", + }), + input: (base: Record) => ({ + ...base, + color: "var(--foreground)", + }), + placeholder: (base: Record) => ({ + ...base, + color: "var(--muted-foreground)", + }), + indicatorSeparator: (base: Record) => ({ + ...base, + backgroundColor: "var(--border)", + }), + dropdownIndicator: (base: Record) => ({ + ...base, + color: "var(--muted-foreground)", + "&:hover": { color: "var(--foreground)" }, + }), +}; + +function getTimezoneValue(tz: ITimezoneOption | string): string { + return typeof tz === "string" ? tz : tz.value; +} + +export function ModelConfigurationPreferencesSection() { + const { user, loading: authLoading } = useAuth(); + const { refreshConfig } = useUserConfig(); + const timezoneSelectId = useId(); + const hasFetched = useRef(false); + + const [preferences, setPreferences] = + useState(emptyPreferences); + const [timezone, setTimezone] = useState( + emptyPreferences.timezone || "UTC", + ); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (authLoading || !user || hasFetched.current) { + return; + } + hasFetched.current = true; + void fetchPreferences(); + }, [authLoading, user]); + + async function fetchPreferences() { + setLoading(true); + try { + const result = + await getModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGet(); + + if (result.error) { + toast.error( + detailFromError( + result.error, + "Failed to load model configuration preferences", + ), + ); + return; + } + + const nextPreferences = result.data || emptyPreferences; + setPreferences({ + test_phone_number: nextPreferences.test_phone_number || "", + timezone: nextPreferences.timezone || emptyPreferences.timezone, + }); + setTimezone( + nextPreferences.timezone || emptyPreferences.timezone || "UTC", + ); + } catch { + toast.error("Failed to load model configuration preferences"); + } finally { + setLoading(false); + } + } + + async function handleSave(e: React.FormEvent) { + e.preventDefault(); + setSaving(true); + try { + const result = + await saveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPut( + { + body: { + test_phone_number: preferences.test_phone_number || null, + timezone: getTimezoneValue(timezone), + }, + }, + ); + + if (result.error) { + toast.error(detailFromError(result.error, "Failed to save preferences")); + return; + } + if (!result.data) { + toast.error("Failed to save preferences"); + return; + } + + setPreferences({ + test_phone_number: result.data.test_phone_number || "", + timezone: result.data.timezone || emptyPreferences.timezone, + }); + setTimezone(result.data.timezone || emptyPreferences.timezone || "UTC"); + await refreshConfig(); + toast.success("Preferences saved"); + } catch { + toast.error("Failed to save preferences"); + } finally { + setSaving(false); + } + } + + if (loading) { + return

Loading...

; + } + + return ( +
+

+ Set organization-wide defaults used by testing and scheduling flows. +

+
+
+ + + setPreferences({ + ...preferences, + test_phone_number: event.target.value, + }) + } + placeholder="+15551234567" + /> +
+
+ + +
+
+ +
+ ); +} diff --git a/ui/src/components/ModelConfigurationV2.tsx b/ui/src/components/ModelConfigurationV2.tsx index 6cb4e6e..8f0a06d 100644 --- a/ui/src/components/ModelConfigurationV2.tsx +++ b/ui/src/components/ModelConfigurationV2.tsx @@ -1,19 +1,15 @@ "use client"; -import { ExternalLink, RefreshCw, Save } from "lucide-react"; -import { useEffect, useId, useRef, useState } from "react"; -import TimezoneSelect, { type ITimezoneOption } from "react-timezone-select"; +import { ExternalLink, RefreshCw } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; import { - getModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGet, getModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2Get, getModelConfigurationV2DefaultsApiV1OrganizationsModelConfigurationsV2DefaultsGet, migrateModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2MigratePost, - saveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPut, saveModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2Put, } from "@/client/sdk.gen"; import type { - OrganizationAiModelConfigurationPreferences, OrganizationAiModelConfigurationResponse, OrganizationAiModelConfigurationV2, } from "@/client/types.gen"; @@ -30,62 +26,11 @@ import { } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; import { useUserConfig } from "@/context/UserConfigContext"; import { detailFromError } from "@/lib/apiError"; import { useAuth } from "@/lib/auth"; -const emptyPreferences: OrganizationAiModelConfigurationPreferences = { - test_phone_number: "", - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC", -}; - -const timezoneSelectStyles = { - control: (base: Record, state: { isFocused: boolean }) => ({ - ...base, - minHeight: "36px", - fontSize: "14px", - backgroundColor: "var(--background)", - borderColor: state.isFocused ? "var(--ring)" : "var(--border)", - boxShadow: state.isFocused ? "0 0 0 2px color-mix(in srgb, var(--ring) 20%, transparent)" : "none", - "&:hover": { borderColor: "var(--border)" }, - }), - menu: (base: Record) => ({ - ...base, - zIndex: 9999, - backgroundColor: "var(--popover)", - border: "1px solid var(--border)", - boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", - }), - menuList: (base: Record) => ({ - ...base, - backgroundColor: "var(--popover)", - padding: 0, - }), - option: (base: Record, state: { isSelected: boolean; isFocused: boolean }) => ({ - ...base, - backgroundColor: state.isSelected ? "var(--accent)" : state.isFocused ? "var(--accent)" : "var(--popover)", - color: "var(--foreground)", - cursor: "pointer", - "&:active": { backgroundColor: "var(--accent)" }, - }), - singleValue: (base: Record) => ({ ...base, color: "var(--foreground)" }), - input: (base: Record) => ({ ...base, color: "var(--foreground)" }), - placeholder: (base: Record) => ({ ...base, color: "var(--muted-foreground)" }), - indicatorSeparator: (base: Record) => ({ ...base, backgroundColor: "var(--border)" }), - dropdownIndicator: (base: Record) => ({ - ...base, - color: "var(--muted-foreground)", - "&:hover": { color: "var(--foreground)" }, - }), -}; - -function getTimezoneValue(tz: ITimezoneOption | string): string { - return typeof tz === "string" ? tz : tz.value; -} - export default function ModelConfigurationV2({ docsUrl, initialAction, @@ -95,16 +40,12 @@ export default function ModelConfigurationV2({ }) { const auth = useAuth(); const { refreshConfig, saveUserConfig } = useUserConfig(); - const timezoneSelectId = useId(); const hasFetched = useRef(false); const hasAppliedInitialMigrationAction = useRef(false); const [defaults, setDefaults] = useState(null); const [response, setResponse] = useState(null); - const [preferences, setPreferences] = useState(emptyPreferences); - const [timezone, setTimezone] = useState(emptyPreferences.timezone || "UTC"); const [loading, setLoading] = useState(true); - const [savingPreferences, setSavingPreferences] = useState(false); const [migrating, setMigrating] = useState(false); const [migrationDialogOpen, setMigrationDialogOpen] = useState(false); const [error, setError] = useState(null); @@ -121,10 +62,9 @@ export default function ModelConfigurationV2({ const load = async () => { setLoading(true); setError(null); - const [defaultsResult, configResult, preferencesResult] = await Promise.all([ + const [defaultsResult, configResult] = await Promise.all([ getModelConfigurationV2DefaultsApiV1OrganizationsModelConfigurationsV2DefaultsGet(), getModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2Get(), - getModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGet(), ]); if (defaultsResult.error) { @@ -137,11 +77,6 @@ export default function ModelConfigurationV2({ setLoading(false); return; } - if (preferencesResult.error) { - setError(detailFromError(preferencesResult.error, "Failed to load model configuration preferences")); - setLoading(false); - return; - } const nextDefaults = defaultsResult.data as ModelConfigurationDefaultsV2; if (!nextDefaults || !configResult.data) { @@ -151,13 +86,6 @@ export default function ModelConfigurationV2({ } setDefaults(nextDefaults); applyResponse(configResult.data); - - const nextPreferences = preferencesResult.data || emptyPreferences; - setPreferences({ - test_phone_number: nextPreferences.test_phone_number || "", - timezone: nextPreferences.timezone || emptyPreferences.timezone, - }); - setTimezone(nextPreferences.timezone || emptyPreferences.timezone || "UTC"); setLoading(false); }; @@ -194,30 +122,6 @@ export default function ModelConfigurationV2({ setNotice("Model configuration saved"); }; - const savePreferences = async () => { - setSavingPreferences(true); - setError(null); - setNotice(null); - - const result = await saveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPut({ - body: { - test_phone_number: preferences.test_phone_number || null, - timezone: getTimezoneValue(timezone), - }, - }); - - if (result.error) { - setError(detailFromError(result.error, "Failed to save preferences")); - } else if (!result.data) { - setError("Failed to save preferences"); - } else { - setPreferences(result.data); - await refreshConfig(); - setNotice("Preferences saved"); - } - setSavingPreferences(false); - }; - const migrateConfiguration = async () => { if (!defaults) return; setMigrating(true); @@ -364,36 +268,6 @@ export default function ModelConfigurationV2({ onSave={saveConfiguration} /> )} - -
-
-

Preferences

-
-
-
- - setPreferences({ ...preferences, test_phone_number: event.target.value })} - placeholder="+15551234567" - /> -
-
- - -
-
- -
{migrationWarningDialog} );