diff --git a/web-app/src/containers/RenderMarkdown.tsx b/web-app/src/containers/RenderMarkdown.tsx index 444f1ae57..2b6807eba 100644 --- a/web-app/src/containers/RenderMarkdown.tsx +++ b/web-app/src/containers/RenderMarkdown.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import ReactMarkdown, { Components } from 'react-markdown' import remarkGfm from 'remark-gfm' import remarkEmoji from 'remark-emoji' @@ -154,7 +153,7 @@ const CodeComponent = memo( )} - {React.createElement(SyntaxHighlighter as React.ComponentType, { + {React.createElement(SyntaxHighlighter as React.ComponentType>, { style: prismStyles[ codeBlockStyle diff --git a/web-app/src/containers/__tests__/SettingsMenu.test.tsx b/web-app/src/containers/__tests__/SettingsMenu.test.tsx index c7f0eff06..a8532da89 100644 --- a/web-app/src/containers/__tests__/SettingsMenu.test.tsx +++ b/web-app/src/containers/__tests__/SettingsMenu.test.tsx @@ -6,6 +6,37 @@ import { useNavigate, useMatches } from '@tanstack/react-router' import { useGeneralSetting } from '@/hooks/useGeneralSetting' import { useModelProvider } from '@/hooks/useModelProvider' +// Mock global platform constants - simulate desktop (Tauri) environment +Object.defineProperty(global, 'IS_IOS', { value: false, writable: true }) +Object.defineProperty(global, 'IS_ANDROID', { value: false, writable: true }) +Object.defineProperty(global, 'IS_WEB_APP', { value: false, writable: true }) + +// Mock platform features +vi.mock('@/lib/platform/const', () => ({ + PlatformFeatures: { + hardwareMonitoring: true, + shortcut: true, // Desktop has shortcuts enabled + localInference: true, + localApiServer: true, + modelHub: true, + systemIntegrations: true, + httpsProxy: true, + defaultProviders: true, + analytics: true, + webAutoModelSelection: false, + modelProviderSettings: true, + mcpAutoApproveTools: false, + mcpServersSettings: true, + extensionsSettings: true, + assistants: true, + authentication: false, + googleAnalytics: false, + alternateShortcutBindings: false, + firstMessagePersistedThread: false, + temporaryChat: false, + }, +})) + // Mock dependencies vi.mock('@tanstack/react-router', () => ({ Link: ({ children, to, className }: any) => ( @@ -81,6 +112,12 @@ describe('SettingsMenu', () => { expect(screen.getByText('common:appearance')).toBeInTheDocument() expect(screen.getByText('common:privacy')).toBeInTheDocument() expect(screen.getByText('common:modelProviders')).toBeInTheDocument() + // Platform-specific features tested separately + }) + + it('renders keyboard shortcuts on desktop platforms', () => { + // This test assumes desktop platform (mocked in setup with shortcut: true) + render() expect(screen.getByText('common:keyboardShortcuts')).toBeInTheDocument() expect(screen.getByText('common:hardware')).toBeInTheDocument() expect(screen.getByText('common:local_api_server')).toBeInTheDocument() diff --git a/web-app/src/hooks/useAuth.ts b/web-app/src/hooks/useAuth.ts index eec4c3b97..47cc4a69e 100644 --- a/web-app/src/hooks/useAuth.ts +++ b/web-app/src/hooks/useAuth.ts @@ -7,6 +7,18 @@ import { import { PlatformFeature } from '@/lib/platform/types' import { PlatformFeatures } from '@/lib/platform/const' +// Type definition for the auth service from extensions-web +interface AuthServiceInterface { + getAllProviders: () => string[] + loginWithProvider: (providerId: ProviderType) => Promise + handleProviderCallback: (providerId: ProviderType, code: string, state?: string) => Promise + isAuthenticatedWithProvider: (providerId: ProviderType) => boolean + logout: () => Promise + getCurrentUser: (forceRefresh?: boolean) => Promise + isAuthenticated: () => boolean + onAuthEvent: (callback: (event: MessageEvent) => void) => () => void +} + interface AuthState { // Auth service authService: JanAuthService | null @@ -69,7 +81,7 @@ const useAuthStore = create()((set, get) => ({ if (!authService) { return [] } - return (authService as any).getAllProviders() + return (authService as AuthServiceInterface).getAllProviders() }, loginWithProvider: async (providerId: ProviderType) => { @@ -78,7 +90,7 @@ const useAuthStore = create()((set, get) => ({ throw new Error('Authentication not available on this platform') } - await (authService as any).loginWithProvider(providerId) + await (authService as AuthServiceInterface).loginWithProvider(providerId) }, handleProviderCallback: async ( @@ -91,7 +103,7 @@ const useAuthStore = create()((set, get) => ({ throw new Error('Authentication not available on this platform') } - await (authService as any).handleProviderCallback(providerId, code, state) + await (authService as AuthServiceInterface).handleProviderCallback(providerId, code, state) // Reload auth state after successful callback await loadAuthState() }, @@ -102,7 +114,7 @@ const useAuthStore = create()((set, get) => ({ return false } - return (authService as any).isAuthenticatedWithProvider(providerId) + return (authService as AuthServiceInterface).isAuthenticatedWithProvider(providerId) }, logout: async () => { @@ -133,7 +145,7 @@ const useAuthStore = create()((set, get) => ({ } try { - const profile = await (authService as any).getCurrentUser(forceRefresh) + const profile = await (authService as AuthServiceInterface).getCurrentUser(forceRefresh) set({ user: profile, isAuthenticated: profile !== null, @@ -156,11 +168,11 @@ const useAuthStore = create()((set, get) => ({ set({ isLoading: true }) // Check if user is authenticated with any provider - const isAuth = (authService as any).isAuthenticated() + const isAuth = (authService as AuthServiceInterface).isAuthenticated() // Load user profile if authenticated if (isAuth) { - const profile = await (authService as any).getCurrentUser(forceRefresh) + const profile = await (authService as AuthServiceInterface).getCurrentUser(forceRefresh) set({ user: profile, isAuthenticated: profile !== null, @@ -184,12 +196,12 @@ const useAuthStore = create()((set, get) => ({ subscribeToAuthEvents: (callback: (event: MessageEvent) => void) => { const { authService } = get() - if (!authService || typeof (authService as any).onAuthEvent !== 'function') { + if (!authService || typeof (authService as AuthServiceInterface).onAuthEvent !== 'function') { return () => {} // Return no-op cleanup } try { - return (authService as any).onAuthEvent(callback) + return (authService as AuthServiceInterface).onAuthEvent(callback) } catch (error) { console.warn('Failed to subscribe to auth events:', error) return () => {} diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index 357fc3a8d..acb4ce804 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -131,6 +131,7 @@ export const useChat = () => { }) } return currentThread + // eslint-disable-next-line react-hooks/exhaustive-deps }, [createThread, retrieveThread, router]) const restartModel = useCallback( diff --git a/web-app/src/hooks/useLocalApiServer.ts b/web-app/src/hooks/useLocalApiServer.ts index 44389272d..862964f23 100644 --- a/web-app/src/hooks/useLocalApiServer.ts +++ b/web-app/src/hooks/useLocalApiServer.ts @@ -41,7 +41,7 @@ export const useLocalApiServer = create()( serverHost: '127.0.0.1', setServerHost: (value) => set({ serverHost: value }), // Use port 0 (auto-assign) for mobile to avoid conflicts, 1337 for desktop - serverPort: (typeof window !== 'undefined' && (window as any).IS_ANDROID) || (typeof window !== 'undefined' && (window as any).IS_IOS) ? 0 : 1337, + serverPort: (typeof window !== 'undefined' && (window as { IS_ANDROID?: boolean }).IS_ANDROID) || (typeof window !== 'undefined' && (window as { IS_IOS?: boolean }).IS_IOS) ? 0 : 1337, setServerPort: (value) => set({ serverPort: value }), apiPrefix: '/v1', setApiPrefix: (value) => set({ apiPrefix: value }), diff --git a/web-app/src/main.tsx b/web-app/src/main.tsx index 7cd390f56..bcca4f384 100644 --- a/web-app/src/main.tsx +++ b/web-app/src/main.tsx @@ -16,7 +16,7 @@ const setupMobileViewport = () => { if (isMobile) { // Update viewport meta tag to disable zoom - let viewportMeta = document.querySelector('meta[name="viewport"]') + const viewportMeta = document.querySelector('meta[name="viewport"]') if (viewportMeta) { viewportMeta.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover' diff --git a/web-app/vite.config.ts b/web-app/vite.config.ts index 317f219e2..70c75a008 100644 --- a/web-app/vite.config.ts +++ b/web-app/vite.config.ts @@ -91,6 +91,7 @@ export default defineConfig(({ mode }) => { watch: { // 3. tell vite to ignore watching `src-tauri` ignored: ['**/src-tauri/**'], + usePolling: true }, }, }