diff --git a/extensions-web/src/jan-provider-web/api.ts b/extensions-web/src/jan-provider-web/api.ts index 97a9608f2..1d15c31b9 100644 --- a/extensions-web/src/jan-provider-web/api.ts +++ b/extensions-web/src/jan-provider-web/api.ts @@ -4,8 +4,9 @@ */ import { getSharedAuthService, JanAuthService } from '../shared' -import { JanModel, janProviderStore } from './store' import { ApiError } from '../shared/types/errors' +import { JAN_API_ROUTES } from './const' +import { JanModel, janProviderStore } from './store' // JAN_API_BASE is defined in vite.config.ts @@ -19,12 +20,7 @@ const TEMPORARY_CHAT_ID = 'temporary-chat' */ function getChatCompletionConfig(request: JanChatCompletionRequest, stream: boolean = false) { const isTemporaryChat = request.conversation_id === TEMPORARY_CHAT_ID - - // For temporary chats, use the stateless /chat/completions endpoint - // For regular conversations, use the stateful /conv/chat/completions endpoint - const endpoint = isTemporaryChat - ? `${JAN_API_BASE}/chat/completions` - : `${JAN_API_BASE}/conv/chat/completions` + const endpoint = `${JAN_API_BASE}${JAN_API_ROUTES.CHAT_COMPLETIONS}` const payload = { ...request, @@ -44,9 +40,30 @@ function getChatCompletionConfig(request: JanChatCompletionRequest, stream: bool return { endpoint, payload, isTemporaryChat } } -export interface JanModelsResponse { +interface JanModelSummary { + id: string object: string - data: JanModel[] + owned_by: string + created?: number +} + +interface JanModelsResponse { + object: string + data: JanModelSummary[] +} + +interface JanModelCatalogResponse { + id: string + supported_parameters?: { + names?: string[] + default?: Record + } + extras?: { + supported_parameters?: string[] + default_parameters?: Record + [key: string]: unknown + } + [key: string]: unknown } export interface JanChatMessage { @@ -112,6 +129,8 @@ export interface JanChatCompletionChunk { export class JanApiClient { private static instance: JanApiClient private authService: JanAuthService + private modelsCache: JanModel[] | null = null + private modelsFetchPromise: Promise | null = null private constructor() { this.authService = getSharedAuthService() @@ -124,25 +143,64 @@ export class JanApiClient { return JanApiClient.instance } - async getModels(): Promise { + async getModels(options?: { forceRefresh?: boolean }): Promise { try { + const forceRefresh = options?.forceRefresh ?? false + + if (forceRefresh) { + this.modelsCache = null + } else if (this.modelsCache) { + return this.modelsCache + } + + if (this.modelsFetchPromise) { + return this.modelsFetchPromise + } + janProviderStore.setLoadingModels(true) janProviderStore.clearError() - const response = await this.authService.makeAuthenticatedRequest( - `${JAN_API_BASE}/conv/models` - ) + this.modelsFetchPromise = (async () => { + const response = await this.authService.makeAuthenticatedRequest( + `${JAN_API_BASE}${JAN_API_ROUTES.MODELS}` + ) - const models = response.data || [] - janProviderStore.setModels(models) - - return models + const summaries = response.data || [] + + const models: JanModel[] = await Promise.all( + summaries.map(async (summary) => { + const supportedParameters = await this.fetchSupportedParameters(summary.id) + const capabilities = this.deriveCapabilitiesFromParameters(supportedParameters) + + return { + id: summary.id, + object: summary.object, + owned_by: summary.owned_by, + created: summary.created, + capabilities, + supportedParameters, + } + }) + ) + + this.modelsCache = models + janProviderStore.setModels(models) + + return models + })() + + return await this.modelsFetchPromise } catch (error) { + this.modelsCache = null + this.modelsFetchPromise = null + const errorMessage = error instanceof ApiError ? error.message : error instanceof Error ? error.message : 'Failed to fetch models' janProviderStore.setError(errorMessage) janProviderStore.setLoadingModels(false) throw error + } finally { + this.modelsFetchPromise = null } } @@ -254,7 +312,7 @@ export class JanApiClient { async initialize(): Promise { try { janProviderStore.setAuthenticated(true) - // Fetch initial models + // Fetch initial models (cached for subsequent calls) await this.getModels() console.log('Jan API client initialized successfully') } catch (error) { @@ -266,6 +324,52 @@ export class JanApiClient { janProviderStore.setInitializing(false) } } + + private async fetchSupportedParameters(modelId: string): Promise { + try { + const endpoint = `${JAN_API_BASE}${JAN_API_ROUTES.MODEL_CATALOGS}/${this.encodeModelIdForCatalog(modelId)}` + const catalog = await this.authService.makeAuthenticatedRequest(endpoint) + return this.extractSupportedParameters(catalog) + } catch (error) { + console.warn(`Failed to fetch catalog metadata for model "${modelId}":`, error) + return [] + } + } + + private encodeModelIdForCatalog(modelId: string): string { + return modelId + .split('/') + .map((segment) => encodeURIComponent(segment)) + .join('/') + } + + private extractSupportedParameters(catalog: JanModelCatalogResponse | null | undefined): string[] { + if (!catalog) { + return [] + } + + const primaryNames = catalog.supported_parameters?.names + if (Array.isArray(primaryNames) && primaryNames.length > 0) { + return [...new Set(primaryNames)] + } + + const extraNames = catalog.extras?.supported_parameters + if (Array.isArray(extraNames) && extraNames.length > 0) { + return [...new Set(extraNames)] + } + + return [] + } + + private deriveCapabilitiesFromParameters(parameters: string[]): string[] { + const capabilities = new Set() + + if (parameters.includes('tools')) { + capabilities.add('tools') + } + + return Array.from(capabilities) + } } export const janApiClient = JanApiClient.getInstance() diff --git a/extensions-web/src/jan-provider-web/const.ts b/extensions-web/src/jan-provider-web/const.ts new file mode 100644 index 000000000..8f691551d --- /dev/null +++ b/extensions-web/src/jan-provider-web/const.ts @@ -0,0 +1,7 @@ +export const JAN_API_ROUTES = { + MODELS: '/models', + CHAT_COMPLETIONS: '/chat/completions', + MODEL_CATALOGS: '/models/catalogs', +} as const + +export const MODEL_PROVIDER_STORAGE_KEY = 'model-provider' diff --git a/extensions-web/src/jan-provider-web/helpers.ts b/extensions-web/src/jan-provider-web/helpers.ts new file mode 100644 index 000000000..09edb9867 --- /dev/null +++ b/extensions-web/src/jan-provider-web/helpers.ts @@ -0,0 +1,122 @@ +import type { JanModel } from './store' +import { MODEL_PROVIDER_STORAGE_KEY } from './const' + +type StoredModel = { + id?: string + capabilities?: unknown + [key: string]: unknown +} + +type StoredProvider = { + provider?: string + models?: StoredModel[] + [key: string]: unknown +} + +type StoredState = { + state?: { + providers?: StoredProvider[] + [key: string]: unknown + } + version?: number + [key: string]: unknown +} + +const normalizeCapabilities = (capabilities: unknown): string[] => { + if (!Array.isArray(capabilities)) { + return [] + } + + return [...new Set(capabilities.filter((item): item is string => typeof item === 'string'))].sort( + (a, b) => a.localeCompare(b) + ) +} + +/** + * Synchronize Jan models stored in localStorage with the latest server state. + * Returns true if the stored data was modified (including being cleared). + */ +export function syncJanModelsLocalStorage( + remoteModels: JanModel[], + storageKey: string = MODEL_PROVIDER_STORAGE_KEY +): boolean { + const rawStorage = localStorage.getItem(storageKey) + if (!rawStorage) { + return false + } + + let storedState: StoredState + try { + storedState = JSON.parse(rawStorage) as StoredState + } catch (error) { + console.warn('Failed to parse Jan model storage; clearing entry.', error) + localStorage.removeItem(storageKey) + return true + } + + const providers = storedState?.state?.providers + if (!Array.isArray(providers)) { + return false + } + + const remoteModelMap = new Map(remoteModels.map((model) => [model.id, model])) + let storageUpdated = false + + for (const provider of providers) { + if (provider.provider !== 'jan' || !Array.isArray(provider.models)) { + continue + } + + const updatedModels: StoredModel[] = [] + + for (const model of provider.models) { + const modelId = typeof model.id === 'string' ? model.id : null + if (!modelId) { + storageUpdated = true + continue + } + + const remoteModel = remoteModelMap.get(modelId) + if (!remoteModel) { + console.log(`Removing unknown Jan model from localStorage: ${modelId}`) + storageUpdated = true + continue + } + + const storedCapabilities = normalizeCapabilities(model.capabilities) + const remoteCapabilities = normalizeCapabilities(remoteModel.capabilities) + + const capabilitiesMatch = + storedCapabilities.length === remoteCapabilities.length && + storedCapabilities.every((cap, index) => cap === remoteCapabilities[index]) + + if (!capabilitiesMatch) { + console.log( + `Updating capabilities for Jan model ${modelId}:`, + storedCapabilities, + '=>', + remoteCapabilities + ) + updatedModels.push({ + ...model, + capabilities: remoteModel.capabilities, + }) + storageUpdated = true + } else { + updatedModels.push(model) + } + } + + if (updatedModels.length !== provider.models.length) { + storageUpdated = true + } + + provider.models = updatedModels + } + + if (storageUpdated) { + localStorage.setItem(storageKey, JSON.stringify(storedState)) + } + + return storageUpdated +} diff --git a/extensions-web/src/jan-provider-web/provider.ts b/extensions-web/src/jan-provider-web/provider.ts index 67e513c3f..a535b0fa0 100644 --- a/extensions-web/src/jan-provider-web/provider.ts +++ b/extensions-web/src/jan-provider-web/provider.ts @@ -14,12 +14,10 @@ import { ImportOptions, } from '@janhq/core' // cspell: disable-line import { janApiClient, JanChatMessage } from './api' +import { syncJanModelsLocalStorage } from './helpers' import { janProviderStore } from './store' import { ApiError } from '../shared/types/errors' -// Jan models support tools via MCP -const JAN_MODEL_CAPABILITIES = ['tools'] as const - export default class JanProviderWeb extends AIEngine { readonly provider = 'jan' private activeSessions: Map = new Map() @@ -28,11 +26,11 @@ export default class JanProviderWeb extends AIEngine { console.log('Loading Jan Provider Extension...') try { - // Check and clear invalid Jan models (capabilities mismatch) - this.validateJanModelsLocalStorage() - - // Initialize authentication and fetch models + // Initialize authentication await janApiClient.initialize() + // Check and sync stored Jan models against latest catalog data + await this.validateJanModelsLocalStorage() + console.log('Jan Provider Extension loaded successfully') } catch (error) { console.error('Failed to load Jan Provider Extension:', error) @@ -43,59 +41,17 @@ export default class JanProviderWeb extends AIEngine { } // Verify Jan models capabilities in localStorage - private validateJanModelsLocalStorage() { + private async validateJanModelsLocalStorage(): Promise { try { console.log('Validating Jan models in localStorage...') - const storageKey = 'model-provider' - const data = localStorage.getItem(storageKey) - if (!data) return - const parsed = JSON.parse(data) - if (!parsed?.state?.providers) return + const remoteModels = await janApiClient.getModels() + const storageUpdated = syncJanModelsLocalStorage(remoteModels) - // Check if any Jan model has incorrect capabilities - let hasInvalidModel = false - - for (const provider of parsed.state.providers) { - if (provider.provider === 'jan' && provider.models) { - for (const model of provider.models) { - console.log(`Checking Jan model: ${model.id}`, model.capabilities) - if ( - JSON.stringify(model.capabilities) !== - JSON.stringify(JAN_MODEL_CAPABILITIES) - ) { - hasInvalidModel = true - console.log( - `Found invalid Jan model: ${model.id}, clearing localStorage` - ) - break - } - } - } - if (hasInvalidModel) break - } - - // If any invalid model found, just clear the storage - if (hasInvalidModel) { - // Force clear the storage - localStorage.removeItem(storageKey) - // Verify it's actually removed - const afterRemoval = localStorage.getItem(storageKey) - // If still present, try setting to empty state - if (afterRemoval) { - // Try alternative clearing method - localStorage.setItem( - storageKey, - JSON.stringify({ - state: { providers: [] }, - version: parsed.version || 3, - }) - ) - } + if (storageUpdated) { console.log( - 'Cleared model-provider from localStorage due to invalid Jan capabilities' + 'Synchronized Jan models in localStorage with server capabilities; reloading...' ) - // Force a page reload to ensure clean state window.location.reload() } } catch (error) { @@ -132,7 +88,7 @@ export default class JanProviderWeb extends AIEngine { path: undefined, // Remote model, no local path owned_by: model.owned_by, object: model.object, - capabilities: [...JAN_MODEL_CAPABILITIES], + capabilities: [...model.capabilities], } : undefined ) @@ -153,7 +109,7 @@ export default class JanProviderWeb extends AIEngine { path: undefined, // Remote model, no local path owned_by: model.owned_by, object: model.object, - capabilities: [...JAN_MODEL_CAPABILITIES], + capabilities: [...model.capabilities], })) } catch (error) { console.error('Failed to list Jan models:', error) diff --git a/extensions-web/src/jan-provider-web/store.ts b/extensions-web/src/jan-provider-web/store.ts index 2ff341147..887a72246 100644 --- a/extensions-web/src/jan-provider-web/store.ts +++ b/extensions-web/src/jan-provider-web/store.ts @@ -9,6 +9,9 @@ export interface JanModel { id: string object: string owned_by: string + created?: number + capabilities: string[] + supportedParameters?: string[] } export interface JanProviderState { diff --git a/extensions-web/src/shared/auth/service.ts b/extensions-web/src/shared/auth/service.ts index eb15c4893..55371a940 100644 --- a/extensions-web/src/shared/auth/service.ts +++ b/extensions-web/src/shared/auth/service.ts @@ -5,7 +5,7 @@ declare const JAN_API_BASE: string -import { User, AuthState, AuthBroadcastMessage } from './types' +import { User, AuthState, AuthBroadcastMessage, AuthTokens } from './types' import { AUTH_STORAGE_KEYS, AUTH_ENDPOINTS, @@ -115,7 +115,7 @@ export class JanAuthService { // Store tokens and set authenticated state this.accessToken = tokens.access_token - this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000 + this.tokenExpiryTime = this.computeTokenExpiry(tokens) this.setAuthProvider(providerId) this.authBroadcast.broadcastLogin() @@ -158,7 +158,7 @@ export class JanAuthService { const tokens = await refreshToken() this.accessToken = tokens.access_token - this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000 + this.tokenExpiryTime = this.computeTokenExpiry(tokens) } catch (error) { console.error('Failed to refresh access token:', error) if (error instanceof ApiError && error.isStatus(401)) { @@ -343,6 +343,23 @@ export class JanAuthService { localStorage.removeItem(AUTH_STORAGE_KEYS.AUTH_PROVIDER) } + private computeTokenExpiry(tokens: AuthTokens): number { + if (tokens.expires_at) { + const expiresAt = new Date(tokens.expires_at).getTime() + if (!Number.isNaN(expiresAt)) { + return expiresAt + } + console.warn('Invalid expires_at format in auth tokens:', tokens.expires_at) + } + + if (typeof tokens.expires_in === 'number') { + return Date.now() + tokens.expires_in * 1000 + } + + console.warn('Auth tokens missing expiry information; defaulting to immediate expiry') + return Date.now() + } + /** * Ensure guest access is available */ @@ -352,7 +369,7 @@ export class JanAuthService { if (!this.accessToken || Date.now() > this.tokenExpiryTime) { const tokens = await guestLogin() this.accessToken = tokens.access_token - this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000 + this.tokenExpiryTime = this.computeTokenExpiry(tokens) } } catch (error) { console.error('Failed to ensure guest access:', error) @@ -387,7 +404,6 @@ export class JanAuthService { case AUTH_EVENTS.LOGOUT: // Another tab logged out, clear our state this.clearAuthState() - this.ensureGuestAccess().catch(console.error) break } }) diff --git a/extensions-web/src/shared/auth/types.ts b/extensions-web/src/shared/auth/types.ts index 65f2dd06a..3e5df6e3c 100644 --- a/extensions-web/src/shared/auth/types.ts +++ b/extensions-web/src/shared/auth/types.ts @@ -16,7 +16,8 @@ export type AuthType = ProviderType | 'guest' export interface AuthTokens { access_token: string - expires_in: number + expires_in?: number + expires_at?: string object: string } diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index 744ff5bab..564e295c4 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -44,7 +44,6 @@ import { McpExtensionToolLoader } from './McpExtensionToolLoader' import { ExtensionTypeEnum, MCPExtension, fs, RAGExtension } from '@janhq/core' import { ExtensionManager } from '@/lib/extension' import { useAttachments } from '@/hooks/useAttachments' -import { open } from '@tauri-apps/plugin-dialog' import { toast } from 'sonner' import { PlatformFeatures } from '@/lib/platform/const' import { PlatformFeature } from '@/lib/platform/types' @@ -333,7 +332,7 @@ const ChatInput = ({ toast.info('Attachments are disabled in Settings') return } - const selection = await open({ + const selection = await serviceHub.dialog().open({ multiple: true, filters: [ { diff --git a/web-app/src/containers/LeftPanel.tsx b/web-app/src/containers/LeftPanel.tsx index 8fe4b3c24..56973cd1d 100644 --- a/web-app/src/containers/LeftPanel.tsx +++ b/web-app/src/containers/LeftPanel.tsx @@ -56,7 +56,8 @@ const mainMenus = [ title: 'common:projects.title', icon: IconFolderPlus, route: route.project, - isEnabled: !(IS_IOS || IS_ANDROID), + isEnabled: + PlatformFeatures[PlatformFeature.PROJECTS] && !(IS_IOS || IS_ANDROID), }, ] @@ -88,6 +89,7 @@ const LeftPanel = () => { const navigate = useNavigate() const [searchTerm, setSearchTerm] = useState('') const { isAuthenticated } = useAuth() + const projectsEnabled = PlatformFeatures[PlatformFeature.PROJECTS] const isSmallScreen = useSmallScreen() const prevScreenSizeRef = useRef(null) @@ -402,7 +404,9 @@ const LeftPanel = () => { })} - {filteredProjects.length > 0 && !(IS_IOS || IS_ANDROID) && ( + {projectsEnabled && + filteredProjects.length > 0 && + !(IS_IOS || IS_ANDROID) && (
@@ -670,23 +674,29 @@ const LeftPanel = () => { {/* Project Dialogs */} - - + {projectsEnabled && ( + <> + + + + )} ) } diff --git a/web-app/src/containers/ThreadList.tsx b/web-app/src/containers/ThreadList.tsx index e48a2373d..97ddda856 100644 --- a/web-app/src/containers/ThreadList.tsx +++ b/web-app/src/containers/ThreadList.tsx @@ -25,6 +25,8 @@ import { useLeftPanel } from '@/hooks/useLeftPanel' import { useMessages } from '@/hooks/useMessages' import { cn, extractThinkingContent } from '@/lib/utils' import { useSmallScreen } from '@/hooks/useMediaQuery' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform/types' import { DropdownMenu, @@ -88,6 +90,7 @@ const SortableItem = memo( 'threadId' in match.params && match.params.threadId === thread.id ) + const projectsEnabled = PlatformFeatures[PlatformFeature.PROJECTS] const handleClick = (e: MouseEvent) => { if (openDropdown) { @@ -111,6 +114,9 @@ const SortableItem = memo( }, [thread.title]) const availableProjects = useMemo(() => { + if (!projectsEnabled) { + return [] + } return folders .filter((f) => { // Exclude the current project page we're on @@ -120,9 +126,18 @@ const SortableItem = memo( return true }) .sort((a, b) => b.updated_at - a.updated_at) - }, [folders, currentProjectId, thread.metadata?.project?.id]) + }, [ + projectsEnabled, + folders, + currentProjectId, + thread.metadata?.project?.id, + ]) const assignThreadToProject = (threadId: string, projectId: string) => { + if (!projectsEnabled) { + return + } + const project = getFolderById(projectId) if (project && updateThread) { const projectMetadata = { @@ -234,37 +249,39 @@ const SortableItem = memo( onDropdownClose={() => setOpenDropdown(false)} /> - - - - {t('common:projects.addToProject')} - - - {availableProjects.length === 0 ? ( - - - {t('common:projects.noProjectsAvailable')} - - - ) : ( - availableProjects.map((folder) => ( - { - e.stopPropagation() - assignThreadToProject(thread.id, folder.id) - }} - > - - - {folder.name} + {projectsEnabled && ( + + + + {t('common:projects.addToProject')} + + + {availableProjects.length === 0 ? ( + + + {t('common:projects.noProjectsAvailable')} - )) - )} - - - {thread.metadata?.project && ( + ) : ( + availableProjects.map((folder) => ( + { + e.stopPropagation() + assignThreadToProject(thread.id, folder.id) + }} + > + + + {folder.name} + + + )) + )} + + + )} + {projectsEnabled && thread.metadata?.project && ( <> ({ IconAtom: () => Atom, IconTool: () => Tool, IconCodeCircle2: () => Code, + IconPaperclip: () => Paperclip, + IconLoader2: () => Loader, + IconCheck: () => Check, IconPlayerStopFilled: () => Stop, IconX: () => X, })) diff --git a/web-app/src/containers/__tests__/DropdownModelProvider.displayName.test.tsx b/web-app/src/containers/__tests__/DropdownModelProvider.displayName.test.tsx index 5f5fba96a..d1ca8a189 100644 --- a/web-app/src/containers/__tests__/DropdownModelProvider.displayName.test.tsx +++ b/web-app/src/containers/__tests__/DropdownModelProvider.displayName.test.tsx @@ -73,6 +73,7 @@ vi.mock('@/lib/platform/const', () => ({ PlatformFeatures: { WEB_AUTO_MODEL_SELECTION: false, MODEL_PROVIDER_SETTINGS: true, + projects: true, }, })) diff --git a/web-app/src/containers/__tests__/LeftPanel.test.tsx b/web-app/src/containers/__tests__/LeftPanel.test.tsx index d8fcccc33..4bb62409a 100644 --- a/web-app/src/containers/__tests__/LeftPanel.test.tsx +++ b/web-app/src/containers/__tests__/LeftPanel.test.tsx @@ -122,6 +122,7 @@ vi.mock('@/lib/platform/const', () => ({ ASSISTANTS: true, MODEL_HUB: true, AUTHENTICATION: false, + projects: true, }, })) diff --git a/web-app/src/containers/__tests__/SettingsMenu.test.tsx b/web-app/src/containers/__tests__/SettingsMenu.test.tsx index a8532da89..d5d88af5b 100644 --- a/web-app/src/containers/__tests__/SettingsMenu.test.tsx +++ b/web-app/src/containers/__tests__/SettingsMenu.test.tsx @@ -34,6 +34,7 @@ vi.mock('@/lib/platform/const', () => ({ alternateShortcutBindings: false, firstMessagePersistedThread: false, temporaryChat: false, + projects: true, }, })) diff --git a/web-app/src/lib/platform/const.ts b/web-app/src/lib/platform/const.ts index ead0d6751..881448f3d 100644 --- a/web-app/src/lib/platform/const.ts +++ b/web-app/src/lib/platform/const.ts @@ -38,6 +38,10 @@ export const PlatformFeatures: Record = { // Default model providers (OpenAI, Anthropic, etc.) - disabled for web-only Jan builds [PlatformFeature.DEFAULT_PROVIDERS]: isPlatformTauri(), + // Projects management + [PlatformFeature.PROJECTS]: + isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(), + // Analytics and telemetry - disabled for web [PlatformFeature.ANALYTICS]: isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(), diff --git a/web-app/src/lib/platform/types.ts b/web-app/src/lib/platform/types.ts index 823105e13..01cb305d1 100644 --- a/web-app/src/lib/platform/types.ts +++ b/web-app/src/lib/platform/types.ts @@ -35,6 +35,8 @@ export enum PlatformFeature { // Default model providers (OpenAI, Anthropic, etc.) DEFAULT_PROVIDERS = 'defaultProviders', + // Projects management + PROJECTS = 'projects', // Analytics and telemetry ANALYTICS = 'analytics', diff --git a/web-app/src/routes/project/$projectId.tsx b/web-app/src/routes/project/$projectId.tsx index a87a87e09..25fd4552b 100644 --- a/web-app/src/routes/project/$projectId.tsx +++ b/web-app/src/routes/project/$projectId.tsx @@ -11,6 +11,7 @@ import ThreadList from '@/containers/ThreadList' import DropdownAssistant from '@/containers/DropdownAssistant' import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformGuard } from '@/lib/platform/PlatformGuard' import { PlatformFeature } from '@/lib/platform/types' import { IconMessage } from '@tabler/icons-react' import { cn } from '@/lib/utils' @@ -22,6 +23,14 @@ export const Route = createFileRoute('/project/$projectId')({ }) function ProjectPage() { + return ( + + + + ) +} + +function ProjectPageContent() { const { t } = useTranslation() const { projectId } = useParams({ from: '/project/$projectId' }) const { getFolderById } = useThreadManagement() diff --git a/web-app/src/routes/project/index.tsx b/web-app/src/routes/project/index.tsx index be3e20cf6..f6ac50a5d 100644 --- a/web-app/src/routes/project/index.tsx +++ b/web-app/src/routes/project/index.tsx @@ -4,6 +4,8 @@ import { useState, useMemo } from 'react' import { useThreadManagement } from '@/hooks/useThreadManagement' import { useThreads } from '@/hooks/useThreads' import { useTranslation } from '@/i18n/react-i18next-compat' +import { PlatformGuard } from '@/lib/platform/PlatformGuard' +import { PlatformFeature } from '@/lib/platform/types' import HeaderPage from '@/containers/HeaderPage' import ThreadList from '@/containers/ThreadList' @@ -28,7 +30,11 @@ export const Route = createFileRoute('/project/')({ }) function Project() { - return + return ( + + + + ) } function ProjectContent() { diff --git a/web-app/src/test/setup.ts b/web-app/src/test/setup.ts index b2286c2f3..79045cec0 100644 --- a/web-app/src/test/setup.ts +++ b/web-app/src/test/setup.ts @@ -17,6 +17,7 @@ vi.mock('@/lib/platform/const', () => ({ systemIntegrations: true, httpsProxy: true, defaultProviders: true, + projects: true, analytics: true, webAutoModelSelection: false, modelProviderSettings: true, @@ -25,6 +26,7 @@ vi.mock('@/lib/platform/const', () => ({ extensionsSettings: true, assistants: true, authentication: false, + attachments: true, } }))