easyai-ai-gateway/apps/web/src/routing.ts

223 lines
7.9 KiB
TypeScript

import type { AdminSection, ApiDocSection, PageKey, PlaygroundMode, WorkspaceSection, WorkspaceTaskQuery } from './types';
export interface AppRouteState {
activePage: PageKey;
adminSection: AdminSection;
apiDocSection: ApiDocSection;
playgroundMode: PlaygroundMode;
workspaceSection: WorkspaceSection;
workspaceTaskQuery: WorkspaceTaskQuery;
}
export const defaultRouteState: AppRouteState = {
activePage: 'home',
adminSection: 'overview',
apiDocSection: 'chat',
playgroundMode: 'chat',
workspaceSection: 'overview',
workspaceTaskQuery: defaultWorkspaceTaskQuery(),
};
const workspacePaths: Record<WorkspaceSection, string> = {
overview: '/workspace/overview',
billing: '/workspace/billing',
apiKeys: '/workspace/api-keys',
tasks: '/workspace/tasks',
transactions: '/workspace/transactions',
};
const adminPaths: Record<AdminSection, string> = {
overview: '/admin/overview',
globalModels: '/admin/providers',
baseModels: '/admin/base-models',
pricing: '/admin/pricing',
platforms: '/admin/platforms',
realtimeLoad: '/admin/realtime-load',
tenants: '/admin/tenants',
users: '/admin/users',
userGroups: '/admin/user-groups',
auditLogs: '/admin/audit-logs',
runtime: '/admin/runtime',
accessRules: '/admin/access-rules',
systemSettings: '/admin/system-settings',
};
const docsPaths: Record<ApiDocSection, string> = {
guideBaseUrl: '/docs/auth',
guideWebhook: '/docs/webhook',
guideErrors: '/docs/errors',
guideTesting: '/docs/testing',
chat: '/docs/chat',
responses: '/docs/responses',
embeddings: '/docs/embeddings',
reranks: '/docs/reranks',
imageGeneration: '/docs/images/generations',
imageEdit: '/docs/images/edits',
videoGeneration: '/docs/videos/generations',
asyncMode: '/docs/async',
taskRetrieve: '/docs/tasks/retrieve',
pricing: '/docs/pricing',
files: '/docs/files',
};
const playgroundPaths: Record<PlaygroundMode, string> = {
chat: '/playground/chat',
image: '/playground/image',
video: '/playground/video',
};
const workspaceSections = reverseMap(workspacePaths);
const adminSections = reverseMap(adminPaths);
const docsSections = reverseMap(docsPaths);
const playgroundSections = reverseMap(playgroundPaths);
export function parseAppRoute(input = currentLocationPath()): AppRouteState {
const origin = typeof window === 'undefined' ? 'http://localhost' : window.location.origin;
const url = new URL(input, origin);
const path = normalizePath(url.pathname);
const workspaceTaskQuery = parseWorkspaceTaskQuery(url.searchParams);
if (path === '/') return { ...defaultRouteState };
if (path.startsWith('/playground')) {
return { ...defaultRouteState, activePage: 'playground', playgroundMode: parsePlaygroundMode(path), workspaceTaskQuery };
}
if (path === '/models') return { ...defaultRouteState, activePage: 'models', workspaceTaskQuery };
if (path.startsWith('/workspace')) {
return { ...defaultRouteState, activePage: 'workspace', workspaceSection: parseWorkspaceSection(path), workspaceTaskQuery };
}
if (path.startsWith('/admin')) {
return { ...defaultRouteState, activePage: 'admin', adminSection: parseAdminSection(path), workspaceTaskQuery };
}
if (path.startsWith('/docs')) {
return { ...defaultRouteState, activePage: 'docs', apiDocSection: parseDocSection(path), workspaceTaskQuery };
}
return { ...defaultRouteState };
}
function currentLocationPath() {
if (typeof window === 'undefined') return '/';
return `${window.location.pathname}${window.location.search}`;
}
export function pathForPage(page: PageKey, route: AppRouteState): string {
if (page === 'playground') return pathForPlaygroundMode(route.playgroundMode);
if (page === 'models') return '/models';
if (page === 'workspace') return pathForWorkspaceSection(route.workspaceSection);
if (page === 'admin') return pathForAdminSection(route.adminSection);
if (page === 'docs') return pathForApiDocSection(route.apiDocSection);
return '/';
}
export function pathForWorkspaceSection(section: WorkspaceSection) {
return workspacePaths[section] ?? workspacePaths.overview;
}
export function pathForWorkspaceTaskQuery(query: WorkspaceTaskQuery) {
const normalized = normalizeWorkspaceTaskQuery(query);
const search = new URLSearchParams();
if (normalized.query) search.set('q', normalized.query);
if (normalized.modelType) search.set('type', normalized.modelType);
if (normalized.createdFrom) search.set('from', normalized.createdFrom);
if (normalized.createdTo) search.set('to', normalized.createdTo);
if (normalized.page !== 1) search.set('page', String(normalized.page));
if (normalized.pageSize !== 10) search.set('pageSize', String(normalized.pageSize));
const suffix = search.toString();
return `${workspacePaths.tasks}${suffix ? `?${suffix}` : ''}`;
}
export function pathForAdminSection(section: AdminSection) {
return adminPaths[section] ?? adminPaths.overview;
}
export function pathForApiDocSection(section: ApiDocSection) {
return docsPaths[section] ?? docsPaths.chat;
}
export function pathForPlaygroundMode(mode: PlaygroundMode) {
return playgroundPaths[mode] ?? playgroundPaths.chat;
}
function parseWorkspaceSection(path: string): WorkspaceSection {
return workspaceSections[path] ?? (path === '/workspace' ? 'overview' : 'overview');
}
function parseAdminSection(path: string): AdminSection {
if (path === '/admin') return 'overview';
if (path === '/admin/models/global') return 'baseModels';
if (path === '/admin/usergroups') return 'userGroups';
return adminSections[path] ?? 'overview';
}
function parseDocSection(path: string): ApiDocSection {
if (path === '/docs') return 'chat';
if (path === '/docs/playground') return 'guideTesting';
if (path === '/docs/api/chat') return 'chat';
if (path === '/docs/api/responses') return 'responses';
if (path === '/docs/api/embeddings') return 'embeddings';
if (path === '/docs/api/reranks') return 'reranks';
if (path === '/docs/api/media') return 'imageGeneration';
if (path === '/docs/api/videos') return 'videoGeneration';
if (path === '/docs/api/tasks') return 'taskRetrieve';
return docsSections[path] ?? 'chat';
}
function parsePlaygroundMode(path: string): PlaygroundMode {
if (path === '/playground') return 'chat';
return playgroundSections[path] ?? 'chat';
}
function normalizePath(pathname: string) {
const path = pathname.replace(/\/+$/, '');
return path || '/';
}
function reverseMap<T extends string>(value: Record<T, string>) {
return Object.fromEntries(Object.entries(value).map(([key, path]) => [path, key])) as Record<string, T>;
}
export function defaultWorkspaceTaskQuery(): WorkspaceTaskQuery {
return {
query: '',
modelType: '',
createdFrom: '',
createdTo: '',
page: 1,
pageSize: 10,
};
}
export function normalizeWorkspaceTaskQuery(query: WorkspaceTaskQuery): WorkspaceTaskQuery {
return {
query: query.query.trim(),
modelType: query.modelType.trim(),
createdFrom: query.createdFrom.trim(),
createdTo: query.createdTo.trim(),
page: positiveInt(query.page, 1),
pageSize: clampPageSize(query.pageSize),
};
}
export function workspaceTaskQueryKey(query: WorkspaceTaskQuery) {
const normalized = normalizeWorkspaceTaskQuery(query);
return JSON.stringify(normalized);
}
function parseWorkspaceTaskQuery(search: URLSearchParams): WorkspaceTaskQuery {
return normalizeWorkspaceTaskQuery({
query: search.get('q') ?? search.get('query') ?? '',
modelType: search.get('type') ?? search.get('modelType') ?? '',
createdFrom: search.get('from') ?? search.get('createdFrom') ?? '',
createdTo: search.get('to') ?? search.get('createdTo') ?? '',
page: Number(search.get('page') ?? 1),
pageSize: Number(search.get('pageSize') ?? search.get('limit') ?? 10),
});
}
function positiveInt(value: number, fallback: number) {
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
}
function clampPageSize(value: number) {
const normalized = positiveInt(value, 10);
return Math.min(100, Math.max(1, normalized));
}