mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 02:19:43 +00:00
Replace the Supabase application data plane with the Node/PostgreSQL API, migrate mobile, admin, web, and MCP callers, add controlled cutover tooling, and define hardened Dokploy infrastructure.
188 lines
5.8 KiB
TypeScript
188 lines
5.8 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { adminData } from '@/lib/adminApi';
|
|
|
|
export interface CouncilGenerator {
|
|
model: string;
|
|
temperature?: number;
|
|
}
|
|
|
|
export type CouncilSelectionMode = 'whole_set' | 'cherry_pick';
|
|
|
|
export interface AiConfig {
|
|
id: string;
|
|
openrouter_api_key: string | null;
|
|
default_model: string | null;
|
|
default_temperature: number | null;
|
|
model_generate: string | null;
|
|
temperature_generate: number | null;
|
|
model_fix: string | null;
|
|
temperature_fix: number | null;
|
|
model_polish: string | null;
|
|
temperature_polish: number | null;
|
|
council_enabled: boolean;
|
|
council_generator_model: string | null; // Legacy - kept for backwards compatibility
|
|
council_generators: CouncilGenerator[] | null; // New array of generators
|
|
council_reviewer_model: string | null;
|
|
council_reviewer_temperature: number | null;
|
|
council_selection_mode: CouncilSelectionMode | null; // 'whole_set' or 'cherry_pick'
|
|
cherry_pick_ensure_intensity_distribution: boolean | null; // Balance intensity levels in cherry-pick mode
|
|
classifier_enabled: boolean | null;
|
|
classifier_model: string | null;
|
|
classifier_temperature: number | null;
|
|
classifier_prompt: string | null;
|
|
heuristics_enabled: boolean | null;
|
|
heuristic_min_text_length: number | null;
|
|
heuristic_whitelist_max_length: number | null;
|
|
heuristic_skip_if_no_alnum: boolean | null;
|
|
heuristic_skip_media_without_text: boolean | null;
|
|
heuristic_record_reason: boolean | null;
|
|
heuristic_use_default_whitelist: boolean | null;
|
|
heuristic_use_default_keywords: boolean | null;
|
|
heuristic_whitelist: string | null;
|
|
heuristic_keyword_triggers: string | null;
|
|
updated_at: string | null;
|
|
updated_by: string | null;
|
|
}
|
|
|
|
interface UseAiConfigReturn {
|
|
config: AiConfig | null;
|
|
loading: boolean;
|
|
error: string | null;
|
|
refetch: () => Promise<void>;
|
|
updateConfig: (updates: Partial<AiConfig>) => Promise<{ error: string | null }>;
|
|
}
|
|
|
|
// Cache for the config to avoid refetching on every use
|
|
let cachedConfig: AiConfig | null = null;
|
|
let cacheTimestamp: number = 0;
|
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
|
|
export function useAiConfig(): UseAiConfigReturn {
|
|
const [config, setConfig] = useState<AiConfig | null>(cachedConfig);
|
|
const [loading, setLoading] = useState(!cachedConfig);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchConfig = useCallback(async () => {
|
|
// Use cache if valid
|
|
if (cachedConfig && Date.now() - cacheTimestamp < CACHE_TTL) {
|
|
setConfig(cachedConfig);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const { data, error: fetchError } = await adminData
|
|
.from('ai_config')
|
|
.select('*')
|
|
.limit(1)
|
|
.maybeSingle();
|
|
|
|
if (fetchError) {
|
|
// If user doesn't have access (not super admin), that's expected
|
|
if (fetchError.message.includes('permission')) {
|
|
setError('Access denied. Super admin privileges required.');
|
|
} else {
|
|
setError(fetchError.message);
|
|
}
|
|
setConfig(null);
|
|
} else if (data) {
|
|
cachedConfig = data as AiConfig;
|
|
cacheTimestamp = Date.now();
|
|
setConfig(data as AiConfig);
|
|
} else {
|
|
// No config row exists yet
|
|
setConfig(null);
|
|
setError('AI config not initialized. Please contact support.');
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to fetch AI config');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const updateConfig = useCallback(async (updates: Partial<AiConfig>): Promise<{ error: string | null }> => {
|
|
if (!config?.id) {
|
|
return { error: 'No config to update' };
|
|
}
|
|
|
|
try {
|
|
const { error: updateError } = await adminData
|
|
.from('ai_config')
|
|
.update(updates)
|
|
.eq('id', config.id);
|
|
|
|
if (updateError) {
|
|
return { error: updateError.message };
|
|
}
|
|
|
|
// Invalidate cache and refetch
|
|
cachedConfig = null;
|
|
cacheTimestamp = 0;
|
|
await fetchConfig();
|
|
|
|
return { error: null };
|
|
} catch (err) {
|
|
return { error: err instanceof Error ? err.message : 'Failed to update AI config' };
|
|
}
|
|
}, [config?.id, fetchConfig]);
|
|
|
|
useEffect(() => {
|
|
fetchConfig();
|
|
}, [fetchConfig]);
|
|
|
|
return {
|
|
config,
|
|
loading,
|
|
error,
|
|
refetch: fetchConfig,
|
|
updateConfig,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the cached AI config synchronously (for use in openai.ts)
|
|
* Returns null if not yet loaded
|
|
*/
|
|
export function getCachedAiConfig(): AiConfig | null {
|
|
return cachedConfig;
|
|
}
|
|
|
|
/**
|
|
* Preload the AI config (call this on app init)
|
|
*/
|
|
export async function preloadAiConfig(): Promise<AiConfig | null> {
|
|
if (cachedConfig && Date.now() - cacheTimestamp < CACHE_TTL) {
|
|
return cachedConfig;
|
|
}
|
|
|
|
try {
|
|
const { data } = await adminData
|
|
.from('ai_config')
|
|
.select('*')
|
|
.limit(1)
|
|
.maybeSingle();
|
|
|
|
if (data) {
|
|
cachedConfig = data as AiConfig;
|
|
cacheTimestamp = Date.now();
|
|
return cachedConfig;
|
|
}
|
|
} catch {
|
|
// Silently fail - we'll fall back to env vars
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Clear the config cache (useful when logging out)
|
|
*/
|
|
export function clearAiConfigCache(): void {
|
|
cachedConfig = null;
|
|
cacheTimestamp = 0;
|
|
}
|