TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { useApi, api, revalidate } from "@/lib/client/api";4import type { ModelsResponse, PolyModel, ProviderId, UserPreferences, PublicConnection, Folder } from "@/lib/client/types";56export interface AppUser {7 id: string;8 email: string;9 name: string;10 role?: string | null;11 emailVerified: boolean;12 isAdmin: boolean;13 onboardingCompletedAt: string | null;14 createdAt: string;15}1617/** Smart-router selection sentinel — stored in `selectedModelKey` when the user picks AUTO. */18export const AUTO_MODEL_KEY = "auto";1920interface AppState {21 user: AppUser;22 preferences: UserPreferences;23 updatePreferences: (patch: Partial<UserPreferences> & { onboardingCompleted?: boolean }) => Promise<void>;24 models: PolyModel[];25 modelsByKey: Map<string, PolyModel>;26 favorites: Set<string>;27 recents: string[];28 /** User-defined labels per model key ("My research model"). */29 labels: Record<string, string>;30 connectedProviders: Set<ProviderId>;31 connections: PublicConnection[];32 folders: Folder[];33 loadingModels: boolean;34 toggleFavorite: (key: string) => Promise<void>;35 setLabel: (key: string, label: string | null) => Promise<void>;36 refreshModels: () => Promise<void>;37 refreshConnections: () => Promise<void>;38 refreshFolders: () => Promise<void>;39 /** Currently selected model key for new chats (persisted in localStorage). `AUTO_MODEL_KEY` = smart router. */40 selectedModelKey: string | null;41 setSelectedModelKey: (key: string | null) => void;42 /** Active project (workspace) for new chats; null = no project. Persisted. */43 activeProjectId: string | null;44 setActiveProjectId: (id: string | null) => void;45 sidebarOpen: boolean;46 setSidebarOpen: (v: boolean) => void;47 paletteOpen: boolean;48 setPaletteOpen: (v: boolean) => void;49 /** Open the global conversation search (⌘K search mode / mobile sheet). */50 searchOpen: boolean;51 setSearchOpen: (v: boolean) => void;52}5354const Ctx = React.createContext<AppState | null>(null);5556export function useApp(): AppState {57 const v = React.useContext(Ctx);58 if (!v) throw new Error("useApp outside AppProvider");59 return v;60}6162const LS_MODEL = "polyllm:selected-model";63const LS_PROJECT = "polyllm:active-project";6465export function AppProvider({ user, preferences: initialPrefs, children }: { user: AppUser; preferences: UserPreferences; children: React.ReactNode }) {66 const [preferences, setPreferences] = React.useState(initialPrefs);67 const models = useApi<ModelsResponse>("/api/models");68 const connections = useApi<{ connections: PublicConnection[] }>("/api/providers");69 const foldersQ = useApi<{ folders: Folder[] }>("/api/folders");70 const [selectedModelKey, setSelected] = React.useState<string | null>(null);71 const [activeProjectId, setProject] = React.useState<string | null>(null);72 const [sidebarOpen, setSidebarOpen] = React.useState(false);73 const [paletteOpen, setPaletteOpen] = React.useState(false);74 const [searchOpen, setSearchOpen] = React.useState(false);7576 React.useEffect(() => {77 const stored = typeof window !== "undefined" ? window.localStorage.getItem(LS_MODEL) : null;78 // eslint-disable-next-line react-hooks/set-state-in-effect79 setSelected(stored ?? initialPrefs.defaultModelKey ?? null);80 setProject(window.localStorage.getItem(LS_PROJECT) || null);81 }, [initialPrefs.defaultModelKey]);8283 const modelList = React.useMemo(() => models.data?.models ?? [], [models.data]);84 const modelsByKey = React.useMemo(() => new Map(modelList.map((m) => [m.key, m])), [modelList]);85 const favorites = React.useMemo(() => new Set(models.data?.favorites ?? []), [models.data]);86 const labels = React.useMemo(() => models.data?.labels ?? {}, [models.data]);87 const connectedProviders = React.useMemo(() => new Set(models.data?.connectedProviders ?? []), [models.data]);8889 // Fall back to the first connected model when the stored selection is unusable (AUTO is always valid).90 React.useEffect(() => {91 if (!models.data) return;92 if (selectedModelKey === AUTO_MODEL_KEY) return;93 if (selectedModelKey && modelsByKey.has(selectedModelKey) && connectedProviders.has(modelsByKey.get(selectedModelKey)!.provider)) return;94 const first = modelList.find((m) => connectedProviders.has(m.provider) && m.status !== "deprecated");95 if (first && first.key !== selectedModelKey) {96 // eslint-disable-next-line react-hooks/set-state-in-effect97 setSelected(first.key);98 }99 }, [models.data, modelList, modelsByKey, connectedProviders, selectedModelKey]);100101 const setSelectedModelKey = React.useCallback((key: string | null) => {102 setSelected(key);103 if (key) window.localStorage.setItem(LS_MODEL, key);104 else window.localStorage.removeItem(LS_MODEL);105 }, []);106107 const setActiveProjectId = React.useCallback((id: string | null) => {108 setProject(id);109 if (id) window.localStorage.setItem(LS_PROJECT, id);110 else window.localStorage.removeItem(LS_PROJECT);111 }, []);112113 const value: AppState = {114 user,115 preferences,116 updatePreferences: async (patch) => {117 const res = await api<{ preferences: UserPreferences }>("/api/preferences", { method: "PATCH", json: patch });118 setPreferences(res.preferences);119 },120 models: modelList,121 modelsByKey,122 favorites,123 recents: models.data?.recents ?? [],124 labels,125 connectedProviders,126 connections: connections.data?.connections ?? [],127 folders: foldersQ.data?.folders ?? [],128 loadingModels: models.isLoading,129 toggleFavorite: async (key) => {130 await api("/api/models", { method: "POST", json: { action: "toggle-favorite", modelKey: key } });131 await models.mutate();132 },133 setLabel: async (key, label) => {134 await api("/api/models", { method: "POST", json: { action: "set-label", modelKey: key, label } });135 await models.mutate();136 },137 refreshModels: async () => {138 await models.mutate();139 },140 refreshConnections: async () => {141 await Promise.all([connections.mutate(), models.mutate()]);142 },143 refreshFolders: async () => {144 await foldersQ.mutate();145 },146 selectedModelKey,147 setSelectedModelKey,148 activeProjectId,149 setActiveProjectId,150 sidebarOpen,151 setSidebarOpen,152 paletteOpen,153 setPaletteOpen,154 searchOpen,155 setSearchOpen,156 };157 return <Ctx.Provider value={value}>{children}</Ctx.Provider>;158}159160export function invalidateConversations() {161 return revalidate("/api/conversations");162}163export function invalidateProjects() {164 return revalidate("/api/projects");165}166