src / config / ui / PrioritySlotEditor.ts
src / config / ui / PrioritySlotEditor.ts
export interface PrioritySlotOption {
id: number;
value: string;
displayName: string;
}
/**
* Reusable PrioritySlotEditor component responsible for managing provider priority orders.
* Supports mapping numeric provider IDs (e.g. 1, 2, 3) or canonical names (e.g. exa, tavily).
* Operates purely on `string[]` input and output.
* Guarantees zero duplicates, zero missing providers, and implements auto-swap de-duplication.
*/
export class PrioritySlotEditor {
private availableProviders: PrioritySlotOption[];
constructor(availableProviders: PrioritySlotOption[]) {
this.availableProviders = availableProviders;
}
/**
* Return array of available provider options.
*/
public getAvailableProviders(): PrioritySlotOption[] {
return [...this.availableProviders];
}
/**
* Normalize any raw array or string (numeric IDs like "1,2,3" or names like "exa,tavily")
* into a valid ordered string[] containing every available provider name exactly once.
*
* @param rawOrder String array or comma-separated string representation.
* @returns Normalized string[] array with zero duplicates.
*/
public normalizeOrder(rawOrder?: string[] | string): string[] {
const allValues = this.availableProviders.map((p) => p.value);
// If completely unconfigured (undefined, null, or empty string/array), return all registered providers
if (rawOrder === undefined || rawOrder === null) {
return [...allValues];
}
if (typeof rawOrder === "string" && !rawOrder.trim()) {
return [...allValues];
}
if (Array.isArray(rawOrder) && rawOrder.length === 0) {
return [...allValues];
}
let parsed: string[] = [];
if (Array.isArray(rawOrder)) {
parsed = rawOrder.map((s) => String(s).trim().toLowerCase()).filter(Boolean);
} else if (typeof rawOrder === "string") {
parsed = rawOrder
.split(",")
.map((s) => s.trim().toLowerCase())
.filter(Boolean);
}
// If parsed has no non-empty tokens after splitting (e.g., ",,"), treat as empty string (all providers)
if (parsed.length === 0) {
return [...allValues];
}
const result: string[] = [];
for (let token of parsed) {
token = token.trim().toLowerCase();
// 1. Try matching by numeric provider ID (e.g., "1" -> exa)
const numId = Number.parseInt(token, 10);
let matchedSpec: PrioritySlotOption | undefined;
if (!Number.isNaN(numId)) {
matchedSpec = this.availableProviders.find((p) => p.id === numId);
}
// 2. Try matching by provider string name (e.g., "exa" -> exa)
if (!matchedSpec) {
matchedSpec = this.availableProviders.find((p) => p.value.toLowerCase() === token);
}
if (matchedSpec && !result.includes(matchedSpec.value)) {
result.push(matchedSpec.value);
}
}
// Return ONLY the valid user-requested providers (deduplicated).
// Note: Do NOT automatically append missing providers.
// If user provided non-empty input but zero tokens were valid (e.g. "99,a"), result is [].
return result;
}
/**
* Handle user changing slot at targetIndex to newValue (numeric ID or provider name).
* If newValue is already selected in another slot (existingIndex), automatically swap the two positions.
*
* @param currentOrder Current ordered string[] array.
* @param targetIndex Slot position index (0-based) being modified.
* @param newValue Selected provider ID or name for targetIndex.
* @returns New updated and de-duplicated string[] array.
*/
public handleSlotChange(
currentOrder: string[],
targetIndex: number,
newValue: string
): string[] {
const normalized = this.normalizeOrder(currentOrder);
// Resolve newValue to canonical provider name
let targetName = newValue.trim().toLowerCase();
const numId = Number.parseInt(targetName, 10);
if (!Number.isNaN(numId)) {
const spec = this.availableProviders.find((p) => p.id === numId);
if (spec) targetName = spec.value;
}
if (targetIndex < 0 || targetIndex >= normalized.length) {
return normalized;
}
const existingIndex = normalized.indexOf(targetName);
if (existingIndex !== -1 && existingIndex !== targetIndex) {
// Auto-swap: swap values between targetIndex and existingIndex
const oldValue = normalized[targetIndex];
normalized[targetIndex] = targetName;
normalized[existingIndex] = oldValue;
} else {
normalized[targetIndex] = targetName;
}
return this.normalizeOrder(normalized);
}
}
export interface PrioritySlotOption {
id: number;
value: string;
displayName: string;
}
/**
* Reusable PrioritySlotEditor component responsible for managing provider priority orders.
* Supports mapping numeric provider IDs (e.g. 1, 2, 3) or canonical names (e.g. exa, tavily).
* Operates purely on `string[]` input and output.
* Guarantees zero duplicates, zero missing providers, and implements auto-swap de-duplication.
*/
export class PrioritySlotEditor {
private availableProviders: PrioritySlotOption[];
constructor(availableProviders: PrioritySlotOption[]) {
this.availableProviders = availableProviders;
}
/**
* Return array of available provider options.
*/
public getAvailableProviders(): PrioritySlotOption[] {
return [...this.availableProviders];
}
/**
* Normalize any raw array or string (numeric IDs like "1,2,3" or names like "exa,tavily")
* into a valid ordered string[] containing every available provider name exactly once.
*
* @param rawOrder String array or comma-separated string representation.
* @returns Normalized string[] array with zero duplicates.
*/
public normalizeOrder(rawOrder?: string[] | string): string[] {
const allValues = this.availableProviders.map((p) => p.value);
// If completely unconfigured (undefined, null, or empty string/array), return all registered providers
if (rawOrder === undefined || rawOrder === null) {
return [...allValues];
}
if (typeof rawOrder === "string" && !rawOrder.trim()) {
return [...allValues];
}
if (Array.isArray(rawOrder) && rawOrder.length === 0) {
return [...allValues];
}
let parsed: string[] = [];
if (Array.isArray(rawOrder)) {
parsed = rawOrder.map((s) => String(s).trim().toLowerCase()).filter(Boolean);
} else if (typeof rawOrder === "string") {
parsed = rawOrder
.split(",")
.map((s) => s.trim().toLowerCase())
.filter(Boolean);
}
// If parsed has no non-empty tokens after splitting (e.g., ",,"), treat as empty string (all providers)
if (parsed.length === 0) {
return [...allValues];
}
const result: string[] = [];
for (let token of parsed) {
token = token.trim().toLowerCase();
// 1. Try matching by numeric provider ID (e.g., "1" -> exa)
const numId = Number.parseInt(token, 10);
let matchedSpec: PrioritySlotOption | undefined;
if (!Number.isNaN(numId)) {
matchedSpec = this.availableProviders.find((p) => p.id === numId);
}
// 2. Try matching by provider string name (e.g., "exa" -> exa)
if (!matchedSpec) {
matchedSpec = this.availableProviders.find((p) => p.value.toLowerCase() === token);
}
if (matchedSpec && !result.includes(matchedSpec.value)) {
result.push(matchedSpec.value);
}
}
// Return ONLY the valid user-requested providers (deduplicated).
// Note: Do NOT automatically append missing providers.
// If user provided non-empty input but zero tokens were valid (e.g. "99,a"), result is [].
return result;
}
/**
* Handle user changing slot at targetIndex to newValue (numeric ID or provider name).
* If newValue is already selected in another slot (existingIndex), automatically swap the two positions.
*
* @param currentOrder Current ordered string[] array.
* @param targetIndex Slot position index (0-based) being modified.
* @param newValue Selected provider ID or name for targetIndex.
* @returns New updated and de-duplicated string[] array.
*/
public handleSlotChange(
currentOrder: string[],
targetIndex: number,
newValue: string
): string[] {
const normalized = this.normalizeOrder(currentOrder);
// Resolve newValue to canonical provider name
let targetName = newValue.trim().toLowerCase();
const numId = Number.parseInt(targetName, 10);
if (!Number.isNaN(numId)) {
const spec = this.availableProviders.find((p) => p.id === numId);
if (spec) targetName = spec.value;
}
if (targetIndex < 0 || targetIndex >= normalized.length) {
return normalized;
}
const existingIndex = normalized.indexOf(targetName);
if (existingIndex !== -1 && existingIndex !== targetIndex) {
// Auto-swap: swap values between targetIndex and existingIndex
const oldValue = normalized[targetIndex];
normalized[targetIndex] = targetName;
normalized[existingIndex] = oldValue;
} else {
normalized[targetIndex] = targetName;
}
return this.normalizeOrder(normalized);
}
}