Move out app init (#4185)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import { ChatType } from '../types/chat';
|
||||
import { Recipe } from '../recipe';
|
||||
import { initializeSystem } from './providerUtils';
|
||||
import { initializeCostDatabase } from './costDatabase';
|
||||
import {
|
||||
type ExtensionConfig,
|
||||
type FixedExtensionEntry,
|
||||
MalformedConfigError,
|
||||
} from '../components/ConfigContext';
|
||||
import { backupConfig, initConfig, readAllConfig, recoverConfig, validateConfig } from '../api';
|
||||
import { COST_TRACKING_ENABLED } from '../updates';
|
||||
|
||||
interface InitializationDependencies {
|
||||
getExtensions?: (b: boolean) => Promise<FixedExtensionEntry[]>;
|
||||
addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||
setPairChat: (chat: ChatType | ((prev: ChatType) => ChatType)) => void;
|
||||
provider: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export const initializeApp = async ({
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setPairChat,
|
||||
provider,
|
||||
model,
|
||||
}: InitializationDependencies) => {
|
||||
console.log(`Initializing app`);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const viewType = urlParams.get('view');
|
||||
const resumeSessionId = urlParams.get('resumeSessionId');
|
||||
const recipeConfig = window.appConfig.get('recipe');
|
||||
|
||||
if (resumeSessionId) {
|
||||
console.log('Session resume detected, letting useChat hook handle navigation');
|
||||
await initializeForSessionResume({ getExtensions, addExtension, provider, model });
|
||||
return;
|
||||
}
|
||||
|
||||
if (recipeConfig && typeof recipeConfig === 'object') {
|
||||
console.log('Recipe deeplink detected, initializing system for recipe');
|
||||
await initializeForRecipe({
|
||||
recipeConfig: recipeConfig as Recipe,
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setPairChat,
|
||||
provider,
|
||||
model,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewType) {
|
||||
handleViewTypeDeepLink(viewType, recipeConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
const costDbPromise = COST_TRACKING_ENABLED
|
||||
? initializeCostDatabase().catch((error) => {
|
||||
console.error('Failed to initialize cost database:', error);
|
||||
})
|
||||
: (() => {
|
||||
console.log('Cost tracking disabled, skipping cost database initialization');
|
||||
return Promise.resolve();
|
||||
})();
|
||||
|
||||
await initConfig();
|
||||
|
||||
try {
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch (error) {
|
||||
console.warn('Initial config read failed, attempting recovery:', error);
|
||||
await handleConfigRecovery();
|
||||
}
|
||||
|
||||
if (provider && model) {
|
||||
try {
|
||||
const initPromises = [
|
||||
initializeSystem(provider, model, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
}),
|
||||
];
|
||||
|
||||
if (COST_TRACKING_ENABLED) {
|
||||
initPromises.push(costDbPromise);
|
||||
}
|
||||
|
||||
await Promise.all(initPromises);
|
||||
} catch (error) {
|
||||
console.error('Error in system initialization:', error);
|
||||
if (error instanceof MalformedConfigError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
window.location.hash = '#/';
|
||||
window.history.replaceState({}, '', '#/');
|
||||
};
|
||||
|
||||
const initializeForSessionResume = async ({
|
||||
getExtensions,
|
||||
addExtension,
|
||||
provider,
|
||||
model,
|
||||
}: Pick<InitializationDependencies, 'getExtensions' | 'addExtension' | 'provider' | 'model'>) => {
|
||||
await initConfig();
|
||||
await readAllConfig({ throwOnError: true });
|
||||
|
||||
await initializeSystem(provider, model, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
});
|
||||
};
|
||||
|
||||
const initializeForRecipe = async ({
|
||||
recipeConfig,
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setPairChat,
|
||||
provider,
|
||||
model,
|
||||
}: Pick<
|
||||
InitializationDependencies,
|
||||
'getExtensions' | 'addExtension' | 'setPairChat' | 'provider' | 'model'
|
||||
> & {
|
||||
recipeConfig: Recipe;
|
||||
}) => {
|
||||
await initConfig();
|
||||
await readAllConfig({ throwOnError: true });
|
||||
|
||||
await initializeSystem(provider, model, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
});
|
||||
|
||||
setPairChat((prevChat) => ({
|
||||
...prevChat,
|
||||
recipeConfig: recipeConfig,
|
||||
title: recipeConfig?.title || 'Recipe Chat',
|
||||
messages: [],
|
||||
messageHistoryIndex: 0,
|
||||
}));
|
||||
|
||||
window.location.hash = '#/pair';
|
||||
window.history.replaceState(
|
||||
{
|
||||
recipeConfig: recipeConfig,
|
||||
resetChat: true,
|
||||
},
|
||||
'',
|
||||
'#/pair'
|
||||
);
|
||||
};
|
||||
|
||||
const handleViewTypeDeepLink = (viewType: string, recipeConfig: unknown) => {
|
||||
if (viewType === 'recipeEditor' && recipeConfig) {
|
||||
window.location.hash = '#/recipe-editor';
|
||||
window.history.replaceState({ config: recipeConfig }, '', '#/recipe-editor');
|
||||
} else {
|
||||
const routeMap: Record<string, string> = {
|
||||
chat: '#/',
|
||||
pair: '#/pair',
|
||||
settings: '#/settings',
|
||||
sessions: '#/sessions',
|
||||
schedules: '#/schedules',
|
||||
recipes: '#/recipes',
|
||||
permission: '#/permission',
|
||||
ConfigureProviders: '#/configure-providers',
|
||||
sharedSession: '#/shared-session',
|
||||
recipeEditor: '#/recipe-editor',
|
||||
welcome: '#/welcome',
|
||||
};
|
||||
|
||||
const route = routeMap[viewType];
|
||||
if (route) {
|
||||
window.location.hash = route;
|
||||
window.history.replaceState({}, '', route);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfigRecovery = async () => {
|
||||
const configVersion = localStorage.getItem('configVersion');
|
||||
const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3;
|
||||
|
||||
if (shouldMigrateExtensions) {
|
||||
console.log('Performing extension migration...');
|
||||
try {
|
||||
await backupConfig({ throwOnError: true });
|
||||
await initConfig();
|
||||
} catch (migrationError) {
|
||||
console.error('Migration failed:', migrationError);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Attempting config recovery...');
|
||||
try {
|
||||
await validateConfig({ throwOnError: true });
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch {
|
||||
console.log('Config validation failed, attempting recovery...');
|
||||
try {
|
||||
await recoverConfig({ throwOnError: true });
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch {
|
||||
console.warn('Config recovery failed, reinitializing...');
|
||||
await initConfig();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { NavigateFunction } from 'react-router-dom';
|
||||
|
||||
export type View =
|
||||
| 'welcome'
|
||||
| 'chat'
|
||||
| 'pair'
|
||||
| 'settings'
|
||||
| 'extensions'
|
||||
| 'moreModels'
|
||||
| 'configureProviders'
|
||||
| 'configPage'
|
||||
| 'ConfigureProviders'
|
||||
| 'settingsV2'
|
||||
| 'sessions'
|
||||
| 'schedules'
|
||||
| 'sharedSession'
|
||||
| 'loading'
|
||||
| 'recipeEditor'
|
||||
| 'recipes'
|
||||
| 'permission';
|
||||
|
||||
export type ViewOptions = {
|
||||
extensionId?: string;
|
||||
showEnvVars?: boolean;
|
||||
deepLinkConfig?: unknown;
|
||||
resumedSession?: unknown;
|
||||
sessionDetails?: unknown;
|
||||
error?: string;
|
||||
shareToken?: string;
|
||||
baseUrl?: string;
|
||||
config?: unknown;
|
||||
parentView?: View;
|
||||
parentViewOptions?: ViewOptions;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export const createNavigationHandler = (navigate: NavigateFunction) => {
|
||||
return (view: View, options?: ViewOptions) => {
|
||||
switch (view) {
|
||||
case 'chat':
|
||||
navigate('/', { state: options });
|
||||
break;
|
||||
case 'pair':
|
||||
navigate('/pair', { state: options });
|
||||
break;
|
||||
case 'settings':
|
||||
navigate('/settings', { state: options });
|
||||
break;
|
||||
case 'sessions':
|
||||
navigate('/sessions', { state: options });
|
||||
break;
|
||||
case 'schedules':
|
||||
navigate('/schedules', { state: options });
|
||||
break;
|
||||
case 'recipes':
|
||||
navigate('/recipes', { state: options });
|
||||
break;
|
||||
case 'permission':
|
||||
navigate('/permission', { state: options });
|
||||
break;
|
||||
case 'ConfigureProviders':
|
||||
navigate('/configure-providers', { state: options });
|
||||
break;
|
||||
case 'sharedSession':
|
||||
navigate('/shared-session', { state: options });
|
||||
break;
|
||||
case 'recipeEditor':
|
||||
navigate('/recipe-editor', { state: options });
|
||||
break;
|
||||
case 'welcome':
|
||||
navigate('/welcome', { state: options });
|
||||
break;
|
||||
case 'extensions':
|
||||
navigate('/extensions', { state: options });
|
||||
break;
|
||||
default:
|
||||
navigate('/', { state: options });
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user