fix: decouple org preference and ai model preferences

This commit is contained in:
Abhishek Kumar 2026-06-09 15:40:34 +05:30
parent e26d902425
commit 01d898fc72
21 changed files with 460 additions and 238 deletions

View file

@ -7,6 +7,7 @@ import { useEffect,useState } from 'react';
import {
getDailyReportApiV1OrganizationsReportsDailyGet,
getDailyRunsDetailApiV1OrganizationsReportsDailyRunsGet,
getPreferencesApiV1OrganizationsPreferencesGet,
getWorkflowOptionsApiV1OrganizationsReportsWorkflowsGet
} from '@/client/sdk.gen';
import type { WorkflowRunDetail } from '@/client/types.gen';
@ -16,7 +17,6 @@ import { Card } from '@/components/ui/card';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import { useUserConfig } from '@/context/UserConfigContext';
import { useAuth } from '@/lib/auth';
import { DispositionChart } from './components/DispositionChart';
@ -57,11 +57,9 @@ export default function ReportsPage() {
const [report, setReport] = useState<DailyReport | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { userConfig } = useUserConfig();
const [timezone, setTimezone] = useState('America/New_York');
const auth = useAuth();
const timezone = userConfig?.timezone || 'America/New_York';
// Fetch workflows on mount
useEffect(() => {
const fetchWorkflows = async () => {
@ -80,6 +78,22 @@ export default function ReportsPage() {
fetchWorkflows();
}, [auth.isAuthenticated]);
useEffect(() => {
const fetchPreferences = async () => {
if (!auth.isAuthenticated) return;
try {
const response = await getPreferencesApiV1OrganizationsPreferencesGet();
if (response.data?.timezone) {
setTimezone(response.data.timezone);
}
} catch (err) {
console.error('Failed to fetch organization preferences:', err);
}
};
fetchPreferences();
}, [auth.isAuthenticated]);
// Fetch report data when date or workflow changes
useEffect(() => {
const fetchReport = async () => {

View file

@ -3,7 +3,7 @@
import { ExternalLink } from "lucide-react";
import { MCPSection } from "@/components/MCPSection";
import { ModelConfigurationPreferencesSection } from "@/components/ModelConfigurationPreferencesSection";
import { OrganizationPreferencesSection } from "@/components/OrganizationPreferencesSection";
import { TelemetrySection } from "@/components/TelemetrySection";
import {
Card,
@ -33,7 +33,7 @@ export default function SettingsPage() {
</CardDescription>
</CardHeader>
<CardContent>
<ModelConfigurationPreferencesSection />
<OrganizationPreferencesSection />
</CardContent>
</Card>

View file

@ -6,8 +6,8 @@ import { useCallback, useEffect, useId, useState } from 'react';
import TimezoneSelect, { type ITimezoneOption } from 'react-timezone-select';
import { toast } from 'sonner';
import { downloadUsageRunsReportApiV1OrganizationsUsageRunsReportGet, getDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGet, getMpsCreditsApiV1OrganizationsUsageMpsCreditsGet, getUsageHistoryApiV1OrganizationsUsageRunsGet } from '@/client/sdk.gen';
import type { DailyUsageBreakdownResponse, MpsCreditsResponse, UsageHistoryResponse, WorkflowRunUsageResponse } from '@/client/types.gen';
import { downloadUsageRunsReportApiV1OrganizationsUsageRunsReportGet, getDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGet, getMpsCreditsApiV1OrganizationsUsageMpsCreditsGet, getPreferencesApiV1OrganizationsPreferencesGet, getUsageHistoryApiV1OrganizationsUsageRunsGet, savePreferencesApiV1OrganizationsPreferencesPut } from '@/client/sdk.gen';
import type { DailyUsageBreakdownResponse, MpsCreditsResponse, OrganizationPreferences, UsageHistoryResponse, WorkflowRunUsageResponse } from '@/client/types.gen';
import { CallTypeCell } from '@/components/CallTypeCell';
import { DailyUsageTable } from '@/components/DailyUsageTable';
import { FilterBuilder } from '@/components/filters/FilterBuilder';
@ -36,7 +36,7 @@ const getLocalTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
export default function UsagePage() {
const router = useRouter();
const searchParams = useSearchParams();
const { userConfig, saveUserConfig, loading: userConfigLoading, organizationPricing } = useUserConfig();
const { organizationPricing } = useUserConfig();
const auth = useAuth();
// MPS credits state
@ -74,6 +74,8 @@ export default function UsagePage() {
const localTimezone = getLocalTimezone();
const [selectedTimezone, setSelectedTimezone] = useState<ITimezoneOption | string>('');
const [savingTimezone, setSavingTimezone] = useState(false);
const [preferences, setPreferences] = useState<OrganizationPreferences>({});
const [preferencesLoading, setPreferencesLoading] = useState(true);
const timezoneSelectId = useId(); // Stable ID for react-select to prevent hydration mismatch
// Fetch MPS credits
@ -168,6 +170,23 @@ export default function UsagePage() {
}
}, [auth.isAuthenticated, organizationPricing]);
const fetchPreferences = useCallback(async () => {
if (!auth.isAuthenticated) return;
setPreferencesLoading(true);
try {
const response = await getPreferencesApiV1OrganizationsPreferencesGet();
const nextPreferences = response.data || {};
setPreferences(nextPreferences);
setSelectedTimezone(nextPreferences.timezone || localTimezone);
} catch (error) {
console.error('Failed to fetch organization preferences:', error);
setSelectedTimezone(localTimezone);
} finally {
setPreferencesLoading(false);
}
}, [auth.isAuthenticated, localTimezone]);
// Download a CSV of all runs matching the current filters.
const handleDownloadReport = async () => {
if (!auth.isAuthenticated) return;
@ -203,31 +222,31 @@ export default function UsagePage() {
const handleTimezoneChange = async (timezone: ITimezoneOption | string) => {
setSelectedTimezone(timezone);
setSavingTimezone(true);
const previousTimezone = preferences.timezone || localTimezone;
try {
const tzValue = typeof timezone === 'string' ? timezone : timezone.value;
await saveUserConfig({ timezone: tzValue });
const response = await savePreferencesApiV1OrganizationsPreferencesPut({
body: {
...preferences,
timezone: tzValue,
},
});
if (response.error) {
throw new Error('Failed to save timezone');
}
setPreferences(response.data || { ...preferences, timezone: tzValue });
} catch (error) {
console.error('Failed to save timezone:', error);
// Revert to previous timezone on error
const prevTz = userConfig?.timezone || localTimezone;
setSelectedTimezone(prevTz);
setSelectedTimezone(previousTimezone);
} finally {
setSavingTimezone(false);
}
};
// Update timezone when userConfig loads
// Update timezone when organization preferences load.
useEffect(() => {
if (!userConfigLoading) {
// Config has loaded - set the timezone
if (userConfig?.timezone) {
setSelectedTimezone(userConfig.timezone);
} else {
// No saved timezone, use local
setSelectedTimezone(localTimezone);
}
}
}, [userConfig, userConfigLoading, localTimezone]);
fetchPreferences();
}, [fetchPreferences]);
// Initial load - fetch when auth becomes available
useEffect(() => {
@ -340,8 +359,8 @@ export default function UsagePage() {
instanceId={timezoneSelectId}
value={selectedTimezone}
onChange={handleTimezoneChange}
isDisabled={savingTimezone || userConfigLoading}
placeholder={userConfigLoading ? "Loading..." : "Select timezone"}
isDisabled={savingTimezone || preferencesLoading}
placeholder={preferencesLoading ? "Loading..." : "Select timezone"}
styles={{
control: (base, state) => ({
...base,

View file

@ -4,15 +4,21 @@ import 'react-international-phone/style.css';
import { Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { PhoneInput } from 'react-international-phone';
import {
getPreferencesApiV1OrganizationsPreferencesGet,
initiateCallApiV1TelephonyInitiateCallPost,
listPhoneNumbersApiV1OrganizationsTelephonyConfigsConfigIdPhoneNumbersGet,
listTelephonyConfigurationsApiV1OrganizationsTelephonyConfigsGet
listTelephonyConfigurationsApiV1OrganizationsTelephonyConfigsGet,
savePreferencesApiV1OrganizationsPreferencesPut,
} from '@/client/sdk.gen';
import type { PhoneNumberResponse, TelephonyConfigurationListItem } from '@/client/types.gen';
import type {
OrganizationPreferences,
PhoneNumberResponse,
TelephonyConfigurationListItem,
} from '@/client/types.gen';
import { Button } from "@/components/ui/button";
import {
Dialog,
@ -33,6 +39,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useUserConfig } from "@/context/UserConfigContext";
import { detailFromError } from "@/lib/apiError";
interface PhoneCallDialogProps {
open: boolean;
@ -48,21 +55,40 @@ export const PhoneCallDialog = ({
user,
}: PhoneCallDialogProps) => {
const router = useRouter();
const { userConfig, saveUserConfig } = useUserConfig();
const [phoneNumber, setPhoneNumber] = useState(userConfig?.test_phone_number || "");
const { refreshConfig } = useUserConfig();
const [preferences, setPreferences] = useState<OrganizationPreferences>({});
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
const [phoneNumber, setPhoneNumber] = useState("");
const [callLoading, setCallLoading] = useState(false);
const [callError, setCallError] = useState<string | null>(null);
const [callSuccessMsg, setCallSuccessMsg] = useState<string | null>(null);
const [phoneChanged, setPhoneChanged] = useState(false);
const [checkingConfig, setCheckingConfig] = useState(false);
const [needsConfiguration, setNeedsConfiguration] = useState<boolean | null>(null);
const [sipMode, setSipMode] = useState(() => /^(PJSIP|SIP)\//i.test(userConfig?.test_phone_number || ""));
const [sipMode, setSipMode] = useState(false);
const [telephonyConfigs, setTelephonyConfigs] = useState<TelephonyConfigurationListItem[]>([]);
const [selectedConfigId, setSelectedConfigId] = useState<string>("");
const [fromPhoneNumbers, setFromPhoneNumbers] = useState<PhoneNumberResponse[]>([]);
const [selectedFromPhoneNumberId, setSelectedFromPhoneNumberId] = useState<string>("");
const [loadingPhoneNumbers, setLoadingPhoneNumbers] = useState(false);
const fetchPreferences = useCallback(async () => {
const result =
await getPreferencesApiV1OrganizationsPreferencesGet();
if (result.error) {
throw new Error(detailFromError(result.error, "Failed to load phone preferences"));
}
return result.data || {};
}, []);
const applyPreferences = useCallback((nextPreferences: OrganizationPreferences) => {
const saved = nextPreferences.test_phone_number || "";
setPreferences(nextPreferences);
setPhoneNumber(saved);
setSipMode(/^(PJSIP|SIP)\//i.test(saved));
setPhoneChanged(false);
}, []);
// Check telephony configuration when dialog opens
useEffect(() => {
const checkConfig = async () => {
@ -97,6 +123,33 @@ export const PhoneCallDialog = ({
checkConfig();
}, [open]);
// Load organization-scoped call preferences when dialog opens.
useEffect(() => {
if (!open) return;
let cancelled = false;
setPreferencesLoaded(false);
const loadPreferences = async () => {
try {
const nextPreferences = await fetchPreferences();
if (cancelled) return;
applyPreferences(nextPreferences);
setPreferencesLoaded(true);
} catch (err) {
if (cancelled) return;
applyPreferences({});
setPreferencesLoaded(false);
setCallError(err instanceof Error ? err.message : "Failed to load phone preferences");
}
};
loadPreferences();
return () => {
cancelled = true;
};
}, [applyPreferences, fetchPreferences, open]);
// Reset state when dialog closes
useEffect(() => {
if (!open) {
@ -149,22 +202,9 @@ export const PhoneCallDialog = ({
};
}, [open, selectedConfigId]);
// Keep phoneNumber in sync with userConfig when dialog opens
useEffect(() => {
if (open) {
const saved = userConfig?.test_phone_number || "";
setPhoneNumber(saved);
setSipMode(/^(PJSIP|SIP)\//i.test(saved));
setPhoneChanged(false);
setCallError(null);
setCallSuccessMsg(null);
setCallLoading(false);
}
}, [open, userConfig?.test_phone_number]);
const handlePhoneInputChange = (formattedValue: string) => {
setPhoneNumber(formattedValue);
setPhoneChanged(formattedValue !== userConfig?.test_phone_number);
setPhoneChanged(formattedValue !== (preferences.test_phone_number || ""));
setCallError(null);
setCallSuccessMsg(null);
};
@ -174,17 +214,39 @@ export const PhoneCallDialog = ({
router.push('/telephony-configurations');
};
const savePhoneNumberPreference = async () => {
const currentPreferences = preferencesLoaded ? preferences : await fetchPreferences();
const result =
await savePreferencesApiV1OrganizationsPreferencesPut({
body: {
...currentPreferences,
test_phone_number: phoneNumber || null,
},
});
if (result.error) {
throw new Error(detailFromError(result.error, "Failed to save phone preferences"));
}
if (!result.data) {
throw new Error("Failed to save phone preferences");
}
setPreferences(result.data);
setPreferencesLoaded(true);
setPhoneChanged(false);
await refreshConfig();
};
const handleStartCall = async () => {
setCallLoading(true);
setCallError(null);
setCallSuccessMsg(null);
try {
if (!user || !userConfig) return;
if (!user) return;
// Save phone number if it has changed
if (phoneChanged) {
await saveUserConfig({ ...userConfig, test_phone_number: phoneNumber });
setPhoneChanged(false);
await savePhoneNumberPreference();
}
const response = await initiateCallApiV1TelephonyInitiateCallPost({

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -3580,20 +3580,6 @@ export type OpenRouterLlmConfiguration = {
base_url?: string;
};
/**
* OrganizationAIModelConfigurationPreferences
*/
export type OrganizationAiModelConfigurationPreferences = {
/**
* Test Phone Number
*/
test_phone_number?: string | null;
/**
* Timezone
*/
timezone?: string | null;
};
/**
* OrganizationAIModelConfigurationResponse
*/
@ -3610,7 +3596,6 @@ export type OrganizationAiModelConfigurationResponse = {
effective_configuration: {
[key: string]: unknown;
};
preferences: OrganizationAiModelConfigurationPreferences;
/**
* Source
*/
@ -3633,6 +3618,20 @@ export type OrganizationAiModelConfigurationV2 = {
byok?: ByokaiModelConfiguration | null;
};
/**
* OrganizationPreferences
*/
export type OrganizationPreferences = {
/**
* Test Phone Number
*/
test_phone_number?: string | null;
/**
* Timezone
*/
timezone?: string | null;
};
/**
* PhoneNumberCreateRequest
*
@ -10025,7 +10024,7 @@ export type MigrateModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2Mi
export type MigrateModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2MigratePostResponse = MigrateModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2MigratePostResponses[keyof MigrateModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2MigratePostResponses];
export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetData = {
export type GetPreferencesApiV1OrganizationsPreferencesGetData = {
body?: never;
headers?: {
/**
@ -10039,10 +10038,10 @@ export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfiguration
};
path?: never;
query?: never;
url: '/api/v1/organizations/model-configurations/preferences';
url: '/api/v1/organizations/preferences';
};
export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetErrors = {
export type GetPreferencesApiV1OrganizationsPreferencesGetErrors = {
/**
* Not found
*/
@ -10053,19 +10052,19 @@ export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfiguration
422: HttpValidationError;
};
export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetError = GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetErrors[keyof GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetErrors];
export type GetPreferencesApiV1OrganizationsPreferencesGetError = GetPreferencesApiV1OrganizationsPreferencesGetErrors[keyof GetPreferencesApiV1OrganizationsPreferencesGetErrors];
export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetResponses = {
export type GetPreferencesApiV1OrganizationsPreferencesGetResponses = {
/**
* Successful Response
*/
200: OrganizationAiModelConfigurationPreferences;
200: OrganizationPreferences;
};
export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetResponse = GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetResponses[keyof GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetResponses];
export type GetPreferencesApiV1OrganizationsPreferencesGetResponse = GetPreferencesApiV1OrganizationsPreferencesGetResponses[keyof GetPreferencesApiV1OrganizationsPreferencesGetResponses];
export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutData = {
body: OrganizationAiModelConfigurationPreferences;
export type SavePreferencesApiV1OrganizationsPreferencesPutData = {
body: OrganizationPreferences;
headers?: {
/**
* Authorization
@ -10078,10 +10077,10 @@ export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfiguratio
};
path?: never;
query?: never;
url: '/api/v1/organizations/model-configurations/preferences';
url: '/api/v1/organizations/preferences';
};
export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutErrors = {
export type SavePreferencesApiV1OrganizationsPreferencesPutErrors = {
/**
* Not found
*/
@ -10092,16 +10091,16 @@ export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfiguratio
422: HttpValidationError;
};
export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutError = SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutErrors[keyof SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutErrors];
export type SavePreferencesApiV1OrganizationsPreferencesPutError = SavePreferencesApiV1OrganizationsPreferencesPutErrors[keyof SavePreferencesApiV1OrganizationsPreferencesPutErrors];
export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutResponses = {
export type SavePreferencesApiV1OrganizationsPreferencesPutResponses = {
/**
* Successful Response
*/
200: OrganizationAiModelConfigurationPreferences;
200: OrganizationPreferences;
};
export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutResponse = SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutResponses[keyof SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutResponses];
export type SavePreferencesApiV1OrganizationsPreferencesPutResponse = SavePreferencesApiV1OrganizationsPreferencesPutResponses[keyof SavePreferencesApiV1OrganizationsPreferencesPutResponses];
export type ListTelephonyConfigurationsApiV1OrganizationsTelephonyConfigsGetData = {
body?: never;

View file

@ -6,10 +6,10 @@ import TimezoneSelect, { type ITimezoneOption } from "react-timezone-select";
import { toast } from "sonner";
import {
getModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGet,
saveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPut,
getPreferencesApiV1OrganizationsPreferencesGet,
savePreferencesApiV1OrganizationsPreferencesPut,
} from "@/client/sdk.gen";
import type { OrganizationAiModelConfigurationPreferences } from "@/client/types.gen";
import type { OrganizationPreferences } from "@/client/types.gen";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -17,7 +17,7 @@ import { useUserConfig } from "@/context/UserConfigContext";
import { detailFromError } from "@/lib/apiError";
import { useAuth } from "@/lib/auth";
const emptyPreferences: OrganizationAiModelConfigurationPreferences = {
const emptyPreferences: OrganizationPreferences = {
test_phone_number: "",
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
};
@ -88,14 +88,14 @@ function getTimezoneValue(tz: ITimezoneOption | string): string {
return typeof tz === "string" ? tz : tz.value;
}
export function ModelConfigurationPreferencesSection() {
export function OrganizationPreferencesSection() {
const { user, loading: authLoading } = useAuth();
const { refreshConfig } = useUserConfig();
const timezoneSelectId = useId();
const hasFetched = useRef(false);
const [preferences, setPreferences] =
useState<OrganizationAiModelConfigurationPreferences>(emptyPreferences);
useState<OrganizationPreferences>(emptyPreferences);
const [timezone, setTimezone] = useState<ITimezoneOption | string>(
emptyPreferences.timezone || "UTC",
);
@ -114,13 +114,13 @@ export function ModelConfigurationPreferencesSection() {
setLoading(true);
try {
const result =
await getModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGet();
await getPreferencesApiV1OrganizationsPreferencesGet();
if (result.error) {
toast.error(
detailFromError(
result.error,
"Failed to load model configuration preferences",
"Failed to load organization preferences",
),
);
return;
@ -135,7 +135,7 @@ export function ModelConfigurationPreferencesSection() {
nextPreferences.timezone || emptyPreferences.timezone || "UTC",
);
} catch {
toast.error("Failed to load model configuration preferences");
toast.error("Failed to load organization preferences");
} finally {
setLoading(false);
}
@ -146,7 +146,7 @@ export function ModelConfigurationPreferencesSection() {
setSaving(true);
try {
const result =
await saveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPut(
await savePreferencesApiV1OrganizationsPreferencesPut(
{
body: {
test_phone_number: preferences.test_phone_number || null,