Avatar Customization + Update Agent Brain (#23)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
export type AgentAvatarHairStyle = "short" | "parted" | "spiky" | "bun";
|
||||
export type AgentAvatarTopStyle = "tee" | "hoodie" | "jacket";
|
||||
export type AgentAvatarBottomStyle = "pants" | "shorts" | "cuffed";
|
||||
export type AgentAvatarHatStyle = "none" | "cap" | "beanie";
|
||||
|
||||
export type AgentAvatarProfile = {
|
||||
version: 1;
|
||||
seed: string;
|
||||
body: {
|
||||
skinTone: string;
|
||||
};
|
||||
hair: {
|
||||
style: AgentAvatarHairStyle;
|
||||
color: string;
|
||||
};
|
||||
clothing: {
|
||||
topStyle: AgentAvatarTopStyle;
|
||||
topColor: string;
|
||||
bottomStyle: AgentAvatarBottomStyle;
|
||||
bottomColor: string;
|
||||
shoesColor: string;
|
||||
};
|
||||
accessories: {
|
||||
glasses: boolean;
|
||||
headset: boolean;
|
||||
hatStyle: AgentAvatarHatStyle;
|
||||
backpack: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type ColorOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
type EnumOption<T extends string> = {
|
||||
id: T;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const AGENT_AVATAR_SKIN_TONE_OPTIONS: ColorOption[] = [
|
||||
{ id: "fair", label: "Fair", color: "#f7d7c2" },
|
||||
{ id: "light", label: "Light", color: "#f4c58a" },
|
||||
{ id: "warm", label: "Warm", color: "#d8a06e" },
|
||||
{ id: "tan", label: "Tan", color: "#b7794e" },
|
||||
{ id: "deep", label: "Deep", color: "#8a5a3b" },
|
||||
{ id: "rich", label: "Rich", color: "#5d3a24" },
|
||||
];
|
||||
|
||||
export const AGENT_AVATAR_HAIR_STYLE_OPTIONS: EnumOption<AgentAvatarHairStyle>[] = [
|
||||
{ id: "short", label: "Short" },
|
||||
{ id: "parted", label: "Parted" },
|
||||
{ id: "spiky", label: "Spiky" },
|
||||
{ id: "bun", label: "Bun" },
|
||||
];
|
||||
|
||||
export const AGENT_AVATAR_HAIR_COLOR_OPTIONS: ColorOption[] = [
|
||||
{ id: "ink", label: "Ink", color: "#151515" },
|
||||
{ id: "espresso", label: "Espresso", color: "#3e2723" },
|
||||
{ id: "walnut", label: "Walnut", color: "#6b4f3a" },
|
||||
{ id: "auburn", label: "Auburn", color: "#7b341e" },
|
||||
{ id: "blonde", label: "Blonde", color: "#d6b56c" },
|
||||
{ id: "violet", label: "Violet", color: "#7c3aed" },
|
||||
{ id: "cyan", label: "Cyan", color: "#0891b2" },
|
||||
{ id: "pink", label: "Pink", color: "#db2777" },
|
||||
];
|
||||
|
||||
export const AGENT_AVATAR_TOP_STYLE_OPTIONS: EnumOption<AgentAvatarTopStyle>[] = [
|
||||
{ id: "tee", label: "Tee" },
|
||||
{ id: "hoodie", label: "Hoodie" },
|
||||
{ id: "jacket", label: "Jacket" },
|
||||
];
|
||||
|
||||
export const AGENT_AVATAR_BOTTOM_STYLE_OPTIONS: EnumOption<AgentAvatarBottomStyle>[] = [
|
||||
{ id: "pants", label: "Pants" },
|
||||
{ id: "shorts", label: "Shorts" },
|
||||
{ id: "cuffed", label: "Cuffed" },
|
||||
];
|
||||
|
||||
export const AGENT_AVATAR_HAT_STYLE_OPTIONS: EnumOption<AgentAvatarHatStyle>[] = [
|
||||
{ id: "none", label: "None" },
|
||||
{ id: "cap", label: "Cap" },
|
||||
{ id: "beanie", label: "Beanie" },
|
||||
];
|
||||
|
||||
export const AGENT_AVATAR_CLOTHING_COLOR_OPTIONS: ColorOption[] = [
|
||||
{ id: "graphite", label: "Graphite", color: "#2d3748" },
|
||||
{ id: "sky", label: "Sky", color: "#7090ff" },
|
||||
{ id: "mint", label: "Mint", color: "#34d399" },
|
||||
{ id: "amber", label: "Amber", color: "#f59e0b" },
|
||||
{ id: "rose", label: "Rose", color: "#f43f5e" },
|
||||
{ id: "violet", label: "Violet", color: "#8b5cf6" },
|
||||
{ id: "cream", label: "Cream", color: "#f5f5f4" },
|
||||
{ id: "slate", label: "Slate", color: "#64748b" },
|
||||
];
|
||||
|
||||
export const AGENT_AVATAR_SHOE_COLOR_OPTIONS: ColorOption[] = [
|
||||
{ id: "black", label: "Black", color: "#1a1a1a" },
|
||||
{ id: "navy", label: "Navy", color: "#1e3a8a" },
|
||||
{ id: "brown", label: "Brown", color: "#7c4a2d" },
|
||||
{ id: "white", label: "White", color: "#e5e7eb" },
|
||||
];
|
||||
|
||||
const AGENT_AVATAR_VERSION = 1 as const;
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
|
||||
const coerceString = (value: unknown) => (typeof value === "string" ? value.trim() : "");
|
||||
|
||||
const hashSeed = (seed: string) => {
|
||||
let hash = 2166136261;
|
||||
for (let index = 0; index < seed.length; index += 1) {
|
||||
hash ^= seed.charCodeAt(index);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return hash >>> 0;
|
||||
};
|
||||
|
||||
const pick = <T,>(values: readonly T[], index: number) => values[index % values.length];
|
||||
|
||||
const resolveColor = (value: unknown, options: ColorOption[], fallback: string) => {
|
||||
const color = coerceString(value).toLowerCase();
|
||||
if (!color) return fallback;
|
||||
const option =
|
||||
options.find((entry) => entry.id === color) ??
|
||||
options.find((entry) => entry.color.toLowerCase() === color);
|
||||
return option?.color ?? fallback;
|
||||
};
|
||||
|
||||
const resolveEnumOption = <T extends string>(
|
||||
value: unknown,
|
||||
options: EnumOption<T>[],
|
||||
fallback: T,
|
||||
): T => {
|
||||
const normalized = coerceString(value).toLowerCase();
|
||||
const match = options.find((entry) => entry.id === normalized);
|
||||
return match?.id ?? fallback;
|
||||
};
|
||||
|
||||
export const createAgentAvatarProfileFromSeed = (seed: string): AgentAvatarProfile => {
|
||||
const normalizedSeed = seed.trim() || "agent";
|
||||
const hash = hashSeed(normalizedSeed);
|
||||
const skinTone = pick(AGENT_AVATAR_SKIN_TONE_OPTIONS, hash).color;
|
||||
const hairStyle = pick(AGENT_AVATAR_HAIR_STYLE_OPTIONS, hash >>> 3).id;
|
||||
const hairColor = pick(AGENT_AVATAR_HAIR_COLOR_OPTIONS, hash >>> 5).color;
|
||||
const topStyle = pick(AGENT_AVATAR_TOP_STYLE_OPTIONS, hash >>> 7).id;
|
||||
const topColor = pick(AGENT_AVATAR_CLOTHING_COLOR_OPTIONS, hash >>> 9).color;
|
||||
const bottomStyle = pick(AGENT_AVATAR_BOTTOM_STYLE_OPTIONS, hash >>> 11).id;
|
||||
const bottomColor = pick(AGENT_AVATAR_CLOTHING_COLOR_OPTIONS, hash >>> 13).color;
|
||||
const shoesColor = pick(AGENT_AVATAR_SHOE_COLOR_OPTIONS, hash >>> 15).color;
|
||||
const hatStyle = pick(AGENT_AVATAR_HAT_STYLE_OPTIONS, hash >>> 17).id;
|
||||
|
||||
return {
|
||||
version: AGENT_AVATAR_VERSION,
|
||||
seed: normalizedSeed,
|
||||
body: {
|
||||
skinTone,
|
||||
},
|
||||
hair: {
|
||||
style: hairStyle,
|
||||
color: hairColor,
|
||||
},
|
||||
clothing: {
|
||||
topStyle,
|
||||
topColor,
|
||||
bottomStyle,
|
||||
bottomColor,
|
||||
shoesColor,
|
||||
},
|
||||
accessories: {
|
||||
glasses: Boolean((hash >>> 19) % 2),
|
||||
headset: Boolean((hash >>> 20) % 2),
|
||||
hatStyle,
|
||||
backpack: Boolean((hash >>> 21) % 2),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createDefaultAgentAvatarProfile = (seed: string): AgentAvatarProfile =>
|
||||
createAgentAvatarProfileFromSeed(seed);
|
||||
|
||||
export const normalizeAgentAvatarProfile = (
|
||||
value: unknown,
|
||||
fallbackSeed: string,
|
||||
): AgentAvatarProfile => {
|
||||
if (typeof value === "string") {
|
||||
return createAgentAvatarProfileFromSeed(value);
|
||||
}
|
||||
|
||||
const baseProfile = createAgentAvatarProfileFromSeed(fallbackSeed);
|
||||
if (!isRecord(value)) {
|
||||
return baseProfile;
|
||||
}
|
||||
|
||||
const body = isRecord(value.body) ? value.body : {};
|
||||
const hair = isRecord(value.hair) ? value.hair : {};
|
||||
const clothing = isRecord(value.clothing) ? value.clothing : {};
|
||||
const accessories = isRecord(value.accessories) ? value.accessories : {};
|
||||
const normalizedSeed = coerceString(value.seed) || baseProfile.seed;
|
||||
|
||||
return {
|
||||
version: AGENT_AVATAR_VERSION,
|
||||
seed: normalizedSeed,
|
||||
body: {
|
||||
skinTone: resolveColor(
|
||||
body.skinTone,
|
||||
AGENT_AVATAR_SKIN_TONE_OPTIONS,
|
||||
baseProfile.body.skinTone,
|
||||
),
|
||||
},
|
||||
hair: {
|
||||
style: resolveEnumOption(
|
||||
hair.style,
|
||||
AGENT_AVATAR_HAIR_STYLE_OPTIONS,
|
||||
baseProfile.hair.style,
|
||||
),
|
||||
color: resolveColor(
|
||||
hair.color,
|
||||
AGENT_AVATAR_HAIR_COLOR_OPTIONS,
|
||||
baseProfile.hair.color,
|
||||
),
|
||||
},
|
||||
clothing: {
|
||||
topStyle: resolveEnumOption(
|
||||
clothing.topStyle,
|
||||
AGENT_AVATAR_TOP_STYLE_OPTIONS,
|
||||
baseProfile.clothing.topStyle,
|
||||
),
|
||||
topColor: resolveColor(
|
||||
clothing.topColor,
|
||||
AGENT_AVATAR_CLOTHING_COLOR_OPTIONS,
|
||||
baseProfile.clothing.topColor,
|
||||
),
|
||||
bottomStyle: resolveEnumOption(
|
||||
clothing.bottomStyle,
|
||||
AGENT_AVATAR_BOTTOM_STYLE_OPTIONS,
|
||||
baseProfile.clothing.bottomStyle,
|
||||
),
|
||||
bottomColor: resolveColor(
|
||||
clothing.bottomColor,
|
||||
AGENT_AVATAR_CLOTHING_COLOR_OPTIONS,
|
||||
baseProfile.clothing.bottomColor,
|
||||
),
|
||||
shoesColor: resolveColor(
|
||||
clothing.shoesColor,
|
||||
AGENT_AVATAR_SHOE_COLOR_OPTIONS,
|
||||
baseProfile.clothing.shoesColor,
|
||||
),
|
||||
},
|
||||
accessories: {
|
||||
glasses:
|
||||
typeof accessories.glasses === "boolean"
|
||||
? accessories.glasses
|
||||
: baseProfile.accessories.glasses,
|
||||
headset:
|
||||
typeof accessories.headset === "boolean"
|
||||
? accessories.headset
|
||||
: baseProfile.accessories.headset,
|
||||
hatStyle: resolveEnumOption(
|
||||
accessories.hatStyle,
|
||||
AGENT_AVATAR_HAT_STYLE_OPTIONS,
|
||||
baseProfile.accessories.hatStyle,
|
||||
),
|
||||
backpack:
|
||||
typeof accessories.backpack === "boolean"
|
||||
? accessories.backpack
|
||||
: baseProfile.accessories.backpack,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,8 +1,10 @@
|
||||
import { fetchJson } from "@/lib/http";
|
||||
import type { AgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
import type {
|
||||
StudioAnalyticsPreferencePatch,
|
||||
StudioFocusedPreference,
|
||||
StudioGatewaySettingsPublic,
|
||||
StudioOfficePreferencePatch,
|
||||
StudioSettingsPublic,
|
||||
StudioSettingsPatch,
|
||||
StudioStandupPreferencePatch,
|
||||
@@ -20,10 +22,11 @@ export type StudioSettingsLoadOptions = {
|
||||
};
|
||||
|
||||
type FocusedPatch = Record<string, Partial<StudioFocusedPreference> | null>;
|
||||
type AvatarsPatch = Record<string, Record<string, string | null> | null>;
|
||||
type AvatarsPatch = Record<string, Record<string, AgentAvatarProfile | null> | null>;
|
||||
type DeskAssignmentsPatch = Record<string, Record<string, string | null> | null>;
|
||||
type AnalyticsPatch = Record<string, StudioAnalyticsPreferencePatch | null>;
|
||||
type VoiceRepliesPatch = Record<string, StudioVoiceRepliesPreferencePatch | null>;
|
||||
type OfficePatch = Record<string, StudioOfficePreferencePatch | null>;
|
||||
type StandupPatch = Record<string, StudioStandupPreferencePatch | null>;
|
||||
|
||||
export type StudioSettingsCoordinatorTransport = {
|
||||
@@ -143,6 +146,30 @@ const mergeVoiceRepliesPatch = (
|
||||
return merged;
|
||||
};
|
||||
|
||||
const mergeOfficePatch = (
|
||||
current: OfficePatch | undefined,
|
||||
next: OfficePatch | undefined
|
||||
): OfficePatch | undefined => {
|
||||
if (!current && !next) return undefined;
|
||||
const merged: OfficePatch = { ...(current ?? {}) };
|
||||
for (const [gatewayKey, value] of Object.entries(next ?? {})) {
|
||||
if (value === null) {
|
||||
merged[gatewayKey] = null;
|
||||
continue;
|
||||
}
|
||||
const existing = merged[gatewayKey];
|
||||
if (existing && existing !== null) {
|
||||
merged[gatewayKey] = {
|
||||
...existing,
|
||||
...value,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
merged[gatewayKey] = { ...value };
|
||||
}
|
||||
return merged;
|
||||
};
|
||||
|
||||
const mergeStandupPatch = (
|
||||
current: StandupPatch | undefined,
|
||||
next: StandupPatch | undefined
|
||||
@@ -210,6 +237,7 @@ const mergeStudioPatch = (
|
||||
...(next.deskAssignments ? { deskAssignments: { ...next.deskAssignments } } : {}),
|
||||
...(next.analytics ? { analytics: { ...next.analytics } } : {}),
|
||||
...(next.voiceReplies ? { voiceReplies: { ...next.voiceReplies } } : {}),
|
||||
...(next.office ? { office: { ...next.office } } : {}),
|
||||
...(next.standup ? { standup: { ...next.standup } } : {}),
|
||||
};
|
||||
}
|
||||
@@ -221,6 +249,7 @@ const mergeStudioPatch = (
|
||||
);
|
||||
const analytics = mergeAnalyticsPatch(current.analytics, next.analytics);
|
||||
const voiceReplies = mergeVoiceRepliesPatch(current.voiceReplies, next.voiceReplies);
|
||||
const office = mergeOfficePatch(current.office, next.office);
|
||||
const standup = mergeStandupPatch(current.standup, next.standup);
|
||||
return {
|
||||
...(next.gateway !== undefined
|
||||
@@ -233,6 +262,7 @@ const mergeStudioPatch = (
|
||||
...(deskAssignments ? { deskAssignments } : {}),
|
||||
...(analytics ? { analytics } : {}),
|
||||
...(voiceReplies ? { voiceReplies } : {}),
|
||||
...(office ? { office } : {}),
|
||||
...(standup ? { standup } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
+99
-17
@@ -4,6 +4,8 @@ import type {
|
||||
StandupManualEntry,
|
||||
StandupScheduleConfig,
|
||||
} from "@/lib/office/standup/types";
|
||||
import type { AgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
import { normalizeAgentAvatarProfile } from "@/lib/avatars/profile";
|
||||
|
||||
export type StudioGatewaySettings = {
|
||||
url: string;
|
||||
@@ -60,7 +62,16 @@ export type StudioVoiceRepliesPreferencePatch = {
|
||||
speed?: number;
|
||||
};
|
||||
|
||||
export type StudioOfficePreference = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type StudioOfficePreferencePatch = {
|
||||
title?: string | null;
|
||||
};
|
||||
|
||||
export type StudioDeskAssignments = Record<string, string>;
|
||||
export type StudioAgentAvatars = Record<string, AgentAvatarProfile>;
|
||||
|
||||
export type StudioStandupPreference = StandupConfig;
|
||||
|
||||
@@ -83,10 +94,11 @@ export type StudioSettings = {
|
||||
version: 1;
|
||||
gateway: StudioGatewaySettings | null;
|
||||
focused: Record<string, StudioFocusedPreference>;
|
||||
avatars: Record<string, Record<string, string>>;
|
||||
avatars: Record<string, StudioAgentAvatars>;
|
||||
deskAssignments: Record<string, StudioDeskAssignments>;
|
||||
analytics: Record<string, StudioAnalyticsPreference>;
|
||||
voiceReplies: Record<string, StudioVoiceRepliesPreference>;
|
||||
office: Record<string, StudioOfficePreference>;
|
||||
standup?: Record<string, StudioStandupPreference>;
|
||||
};
|
||||
|
||||
@@ -98,10 +110,11 @@ export type StudioSettingsPublic = Omit<StudioSettings, "gateway" | "standup"> &
|
||||
export type StudioSettingsPatch = {
|
||||
gateway?: StudioGatewaySettingsPatch | null;
|
||||
focused?: Record<string, Partial<StudioFocusedPreference> | null>;
|
||||
avatars?: Record<string, Record<string, string | null> | null>;
|
||||
avatars?: Record<string, Record<string, AgentAvatarProfile | null> | null>;
|
||||
deskAssignments?: Record<string, Record<string, string | null> | null>;
|
||||
analytics?: Record<string, StudioAnalyticsPreferencePatch | null>;
|
||||
voiceReplies?: Record<string, StudioVoiceRepliesPreferencePatch | null>;
|
||||
office?: Record<string, StudioOfficePreferencePatch | null>;
|
||||
standup?: Record<string, StudioStandupPreferencePatch | null>;
|
||||
};
|
||||
|
||||
@@ -257,6 +270,20 @@ const normalizeOptionalIsoString = (
|
||||
return trimmed ? trimmed : null;
|
||||
};
|
||||
|
||||
const DEFAULT_OFFICE_TITLE = "Luke Headquarters";
|
||||
|
||||
const normalizeOfficeTitle = (
|
||||
value: unknown,
|
||||
fallback: string = DEFAULT_OFFICE_TITLE
|
||||
) => {
|
||||
const title = coerceString(value);
|
||||
return (title || fallback).slice(0, 48);
|
||||
};
|
||||
|
||||
export const defaultStudioOfficePreference = (): StudioOfficePreference => ({
|
||||
title: DEFAULT_OFFICE_TITLE,
|
||||
});
|
||||
|
||||
const normalizeStandupScheduleConfig = (
|
||||
value: unknown,
|
||||
fallback: StandupScheduleConfig = defaultStudioStandupScheduleConfig()
|
||||
@@ -400,20 +427,18 @@ const normalizeFocused = (value: unknown): Record<string, StudioFocusedPreferenc
|
||||
return focused;
|
||||
};
|
||||
|
||||
const normalizeAvatars = (value: unknown): Record<string, Record<string, string>> => {
|
||||
const normalizeAvatars = (value: unknown): Record<string, StudioAgentAvatars> => {
|
||||
if (!isRecord(value)) return {};
|
||||
const avatars: Record<string, Record<string, string>> = {};
|
||||
const avatars: Record<string, StudioAgentAvatars> = {};
|
||||
for (const [gatewayKeyRaw, gatewayRaw] of Object.entries(value)) {
|
||||
const gatewayKey = normalizeGatewayKey(gatewayKeyRaw);
|
||||
if (!gatewayKey) continue;
|
||||
if (!isRecord(gatewayRaw)) continue;
|
||||
const entries: Record<string, string> = {};
|
||||
for (const [agentIdRaw, seedRaw] of Object.entries(gatewayRaw)) {
|
||||
const entries: StudioAgentAvatars = {};
|
||||
for (const [agentIdRaw, avatarRaw] of Object.entries(gatewayRaw)) {
|
||||
const agentId = coerceString(agentIdRaw);
|
||||
if (!agentId) continue;
|
||||
const seed = coerceString(seedRaw);
|
||||
if (!seed) continue;
|
||||
entries[agentId] = seed;
|
||||
entries[agentId] = normalizeAgentAvatarProfile(avatarRaw, agentId);
|
||||
}
|
||||
avatars[gatewayKey] = entries;
|
||||
}
|
||||
@@ -522,6 +547,27 @@ const normalizeVoiceReplies = (
|
||||
return voiceReplies;
|
||||
};
|
||||
|
||||
const normalizeOfficePreference = (
|
||||
value: unknown,
|
||||
fallback: StudioOfficePreference = defaultStudioOfficePreference()
|
||||
): StudioOfficePreference => {
|
||||
if (!isRecord(value)) return fallback;
|
||||
return {
|
||||
title: normalizeOfficeTitle(value.title, fallback.title),
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeOffice = (value: unknown): Record<string, StudioOfficePreference> => {
|
||||
if (!isRecord(value)) return {};
|
||||
const office: Record<string, StudioOfficePreference> = {};
|
||||
for (const [gatewayKeyRaw, officeRaw] of Object.entries(value)) {
|
||||
const gatewayKey = normalizeGatewayKey(gatewayKeyRaw);
|
||||
if (!gatewayKey) continue;
|
||||
office[gatewayKey] = normalizeOfficePreference(officeRaw);
|
||||
}
|
||||
return office;
|
||||
};
|
||||
|
||||
export const defaultStudioSettings = (): StudioSettings => ({
|
||||
version: SETTINGS_VERSION,
|
||||
gateway: null,
|
||||
@@ -530,6 +576,7 @@ export const defaultStudioSettings = (): StudioSettings => ({
|
||||
deskAssignments: {},
|
||||
analytics: {},
|
||||
voiceReplies: {},
|
||||
office: {},
|
||||
standup: {},
|
||||
});
|
||||
|
||||
@@ -579,6 +626,7 @@ export const normalizeStudioSettings = (raw: unknown): StudioSettings => {
|
||||
const deskAssignments = normalizeDeskAssignments(raw.deskAssignments);
|
||||
const analytics = normalizeAnalytics(raw.analytics);
|
||||
const voiceReplies = normalizeVoiceReplies(raw.voiceReplies);
|
||||
const office = normalizeOffice(raw.office);
|
||||
const standup = normalizeStandup(raw.standup);
|
||||
return {
|
||||
version: SETTINGS_VERSION,
|
||||
@@ -588,6 +636,7 @@ export const normalizeStudioSettings = (raw: unknown): StudioSettings => {
|
||||
deskAssignments,
|
||||
analytics,
|
||||
voiceReplies,
|
||||
office,
|
||||
standup,
|
||||
};
|
||||
};
|
||||
@@ -603,6 +652,7 @@ export const mergeStudioSettings = (
|
||||
const nextDeskAssignments = { ...current.deskAssignments };
|
||||
const nextAnalytics = { ...current.analytics };
|
||||
const nextVoiceReplies = { ...current.voiceReplies };
|
||||
const nextOffice = { ...current.office };
|
||||
const nextStandup = { ...(current.standup ?? {}) };
|
||||
if (patch.focused) {
|
||||
for (const [keyRaw, value] of Object.entries(patch.focused)) {
|
||||
@@ -626,19 +676,14 @@ export const mergeStudioSettings = (
|
||||
}
|
||||
if (!isRecord(gatewayPatch)) continue;
|
||||
const existing = nextAvatars[gatewayKey] ? { ...nextAvatars[gatewayKey] } : {};
|
||||
for (const [agentIdRaw, seedPatchRaw] of Object.entries(gatewayPatch)) {
|
||||
for (const [agentIdRaw, avatarPatchRaw] of Object.entries(gatewayPatch)) {
|
||||
const agentId = coerceString(agentIdRaw);
|
||||
if (!agentId) continue;
|
||||
if (seedPatchRaw === null) {
|
||||
if (avatarPatchRaw === null) {
|
||||
delete existing[agentId];
|
||||
continue;
|
||||
}
|
||||
const seed = coerceString(seedPatchRaw);
|
||||
if (!seed) {
|
||||
delete existing[agentId];
|
||||
continue;
|
||||
}
|
||||
existing[agentId] = seed;
|
||||
existing[agentId] = normalizeAgentAvatarProfile(avatarPatchRaw, agentId);
|
||||
}
|
||||
nextAvatars[gatewayKey] = existing;
|
||||
}
|
||||
@@ -713,6 +758,24 @@ export const mergeStudioSettings = (
|
||||
);
|
||||
}
|
||||
}
|
||||
if (patch.office) {
|
||||
for (const [gatewayKeyRaw, officePatch] of Object.entries(patch.office)) {
|
||||
const gatewayKey = normalizeGatewayKey(gatewayKeyRaw);
|
||||
if (!gatewayKey) continue;
|
||||
if (officePatch === null) {
|
||||
delete nextOffice[gatewayKey];
|
||||
continue;
|
||||
}
|
||||
const fallback = nextOffice[gatewayKey] ?? defaultStudioOfficePreference();
|
||||
nextOffice[gatewayKey] = normalizeOfficePreference(
|
||||
{
|
||||
...fallback,
|
||||
...officePatch,
|
||||
},
|
||||
fallback
|
||||
);
|
||||
}
|
||||
}
|
||||
if (patch.standup) {
|
||||
for (const [gatewayKeyRaw, standupPatch] of Object.entries(patch.standup)) {
|
||||
const gatewayKey = normalizeGatewayKey(gatewayKeyRaw);
|
||||
@@ -769,6 +832,7 @@ export const mergeStudioSettings = (
|
||||
deskAssignments: nextDeskAssignments,
|
||||
analytics: nextAnalytics,
|
||||
voiceReplies: nextVoiceReplies,
|
||||
office: nextOffice,
|
||||
standup: nextStandup,
|
||||
};
|
||||
};
|
||||
@@ -787,6 +851,15 @@ export const resolveAgentAvatarSeed = (
|
||||
gatewayUrl: string,
|
||||
agentId: string
|
||||
): string | null => {
|
||||
const profile = resolveAgentAvatarProfile(settings, gatewayUrl, agentId);
|
||||
return profile?.seed ?? null;
|
||||
};
|
||||
|
||||
export const resolveAgentAvatarProfile = (
|
||||
settings: StudioSettings | StudioSettingsPublic,
|
||||
gatewayUrl: string,
|
||||
agentId: string
|
||||
): AgentAvatarProfile | null => {
|
||||
const gatewayKey = normalizeGatewayKey(gatewayUrl);
|
||||
if (!gatewayKey) return null;
|
||||
const agentKey = coerceString(agentId);
|
||||
@@ -821,6 +894,15 @@ export const resolveVoiceRepliesPreference = (
|
||||
return settings.voiceReplies[gatewayKey] ?? defaultStudioVoiceRepliesPreference();
|
||||
};
|
||||
|
||||
export const resolveOfficePreference = (
|
||||
settings: StudioSettings | StudioSettingsPublic,
|
||||
gatewayUrl: string
|
||||
): StudioOfficePreference => {
|
||||
const gatewayKey = normalizeGatewayKey(gatewayUrl);
|
||||
if (!gatewayKey) return defaultStudioOfficePreference();
|
||||
return settings.office[gatewayKey] ?? defaultStudioOfficePreference();
|
||||
};
|
||||
|
||||
export const resolveStandupPreference = (
|
||||
settings: StudioSettings | StudioSettingsPublic,
|
||||
gatewayUrl: string
|
||||
|
||||
Reference in New Issue
Block a user