feat: add AI settings controls
This commit is contained in:
15
src/lib/ai-create-flow.ts
Normal file
15
src/lib/ai-create-flow.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type AiCreateOutcome =
|
||||
| "review-single"
|
||||
| "create-single"
|
||||
| "create-multiple";
|
||||
|
||||
export const getAiCreateOutcome = (
|
||||
eventCount: number,
|
||||
skipAiReview: boolean,
|
||||
): AiCreateOutcome => {
|
||||
if (eventCount > 1) {
|
||||
return "create-multiple";
|
||||
}
|
||||
|
||||
return skipAiReview ? "create-single" : "review-single";
|
||||
};
|
||||
32
src/lib/ai-feature-flags.ts
Normal file
32
src/lib/ai-feature-flags.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
const ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
||||
const DISABLED_VALUES = new Set(["0", "false", "no", "off"]);
|
||||
|
||||
const normalizeFlagValue = (value?: string) => value?.trim().toLowerCase();
|
||||
|
||||
export const isAiFlagEnabled = (value?: string): boolean => {
|
||||
const normalizedValue = normalizeFlagValue(value);
|
||||
if (!normalizedValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ENABLED_VALUES.has(normalizedValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DISABLED_VALUES.has(normalizedValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isAdminAiEnabled = (
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
) => isAiFlagEnabled(env.AI_ENABLED);
|
||||
|
||||
export const isClientAiEnabled = (
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
) => isAiFlagEnabled(env.NEXT_PUBLIC_AI_ENABLED ?? env.AI_ENABLED);
|
||||
|
||||
export const getAiDisabledMessage = () =>
|
||||
"AI integrations are currently disabled by the administrator.";
|
||||
22
src/lib/ui-shell-contract.ts
Normal file
22
src/lib/ui-shell-contract.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const APP_HEADER_SURFACE_CLASSES =
|
||||
"glass-surface mb-4 flex items-center justify-between gap-3 px-4 py-3";
|
||||
|
||||
export const APP_SECTION_SURFACE_CLASSES = "glass-panel p-4 sm:p-5";
|
||||
|
||||
export const APP_ACTION_BAR_CLASSES = "glass-subtle mb-4 p-3";
|
||||
|
||||
export const APP_NAV_SURFACE_CLASSES =
|
||||
"glass-surface fixed inset-x-4 bottom-4 mx-auto flex max-w-3xl items-center justify-between px-3 py-2 sm:inset-x-6 lg:inset-x-8";
|
||||
|
||||
const CONNECTION_BADGE_BASE_CLASSES =
|
||||
"gap-1.5 border px-2.5 py-1 text-xs font-medium shadow-none";
|
||||
|
||||
export const getConnectionBadgeClasses = (isOnline: boolean) =>
|
||||
cn(
|
||||
CONNECTION_BADGE_BASE_CLASSES,
|
||||
isOnline
|
||||
? "border-emerald-500/35 bg-emerald-500/15 text-emerald-700 dark:border-emerald-400/25 dark:bg-emerald-500/15 dark:text-emerald-300 [&>svg]:text-emerald-500"
|
||||
: "border-border/70 bg-muted/55 text-muted-foreground dark:bg-muted/35 [&>svg]:text-muted-foreground",
|
||||
);
|
||||
118
src/lib/user-settings.ts
Normal file
118
src/lib/user-settings.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface UserSettings {
|
||||
aiEnabled: boolean;
|
||||
skipAiReview: boolean;
|
||||
}
|
||||
|
||||
export const USER_SETTINGS_STORAGE_KEY = "localcal:user-settings";
|
||||
|
||||
const DEFAULT_USER_SETTINGS: UserSettings = {
|
||||
aiEnabled: true,
|
||||
skipAiReview: false,
|
||||
};
|
||||
|
||||
export const getDefaultUserSettings = (): UserSettings => ({
|
||||
...DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
|
||||
type UserSettingsStorage = Pick<Storage, "getItem" | "setItem">;
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null;
|
||||
|
||||
const parseUserSettings = (value: unknown): UserSettings => {
|
||||
if (!isRecord(value)) {
|
||||
return getDefaultUserSettings();
|
||||
}
|
||||
|
||||
return {
|
||||
aiEnabled:
|
||||
typeof value.aiEnabled === "boolean"
|
||||
? value.aiEnabled
|
||||
: DEFAULT_USER_SETTINGS.aiEnabled,
|
||||
skipAiReview:
|
||||
typeof value.skipAiReview === "boolean"
|
||||
? value.skipAiReview
|
||||
: DEFAULT_USER_SETTINGS.skipAiReview,
|
||||
};
|
||||
};
|
||||
|
||||
export const loadUserSettings = (
|
||||
storage?: UserSettingsStorage,
|
||||
): UserSettings => {
|
||||
if (!storage) {
|
||||
return getDefaultUserSettings();
|
||||
}
|
||||
|
||||
try {
|
||||
const storedValue = storage.getItem(USER_SETTINGS_STORAGE_KEY);
|
||||
if (!storedValue) {
|
||||
return getDefaultUserSettings();
|
||||
}
|
||||
|
||||
return parseUserSettings(JSON.parse(storedValue));
|
||||
} catch {
|
||||
return getDefaultUserSettings();
|
||||
}
|
||||
};
|
||||
|
||||
export const saveUserSettings = (
|
||||
settings: UserSettings,
|
||||
storage?: UserSettingsStorage,
|
||||
): UserSettings => {
|
||||
if (!storage) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
try {
|
||||
storage.setItem(USER_SETTINGS_STORAGE_KEY, JSON.stringify(settings));
|
||||
} catch {
|
||||
return settings;
|
||||
}
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
const getBrowserStorage = (): UserSettingsStorage | undefined => {
|
||||
if (typeof window === "undefined") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return window.localStorage;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const useUserSettings = () => {
|
||||
const [settings, setSettings] = useState<UserSettings>(
|
||||
getDefaultUserSettings,
|
||||
);
|
||||
const [hasLoadedSettings, setHasLoadedSettings] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSettings(loadUserSettings(getBrowserStorage()));
|
||||
setHasLoadedSettings(true);
|
||||
}, []);
|
||||
|
||||
const updateSettings = (changes: Partial<UserSettings>) => {
|
||||
setSettings((currentSettings) => {
|
||||
const nextSettings = {
|
||||
...currentSettings,
|
||||
...changes,
|
||||
};
|
||||
|
||||
saveUserSettings(nextSettings, getBrowserStorage());
|
||||
|
||||
return nextSettings;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
hasLoadedSettings,
|
||||
settings,
|
||||
updateSettings,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user