From a6238a22dd1b432e0e7633dcde72bca05e38d25b Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 21 Jan 2025 22:06:36 +0700 Subject: [PATCH 01/75] fix: Correct index for Windows download button (#4500) --- docs/src/components/DropdownDownload/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/components/DropdownDownload/index.tsx b/docs/src/components/DropdownDownload/index.tsx index 26e0f49d6..c0cdfb73b 100644 --- a/docs/src/components/DropdownDownload/index.tsx +++ b/docs/src/components/DropdownDownload/index.tsx @@ -65,7 +65,7 @@ const DropdownDownload = ({ lastRelease }: Props) => { const userAgent = navigator.userAgent if (userAgent.includes('Windows')) { // windows user - setDefaultSystem(systems[2]) + setDefaultSystem(systems[1]) } else if (userAgent.includes('Linux')) { // linux user setDefaultSystem(systems[3]) From a59ba000c02da1989a073834d1487916b5f7c67f Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Wed, 22 Jan 2025 21:17:38 +0700 Subject: [PATCH 02/75] chore: ignore external contributors --- .github/workflows/auto-assign-author.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/auto-assign-author.yml b/.github/workflows/auto-assign-author.yml index 0e861df00..13c93c717 100644 --- a/.github/workflows/auto-assign-author.yml +++ b/.github/workflows/auto-assign-author.yml @@ -6,6 +6,7 @@ on: jobs: assign-author: runs-on: ubuntu-latest + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} permissions: pull-requests: write steps: From 53f2943b2c0fdf5eadc84dffffca76d0b1cb7c2f Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 22 Jan 2025 21:39:55 +0700 Subject: [PATCH 03/75] fix: typo meta description (#4507) * fix: typo meta description * chore: fix meta desc --- docs/src/pages/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/pages/index.mdx b/docs/src/pages/index.mdx index 4a22a7174..ef3484297 100644 --- a/docs/src/pages/index.mdx +++ b/docs/src/pages/index.mdx @@ -1,6 +1,6 @@ --- title: "Jan: Open source ChatGPT-alternative that runs 100% offline" -description: "Chat with AI without privact concerns. Jan is an open-source alternative to ChatGPT, running AI models locally on your device." +description: "Chat with AI without privacy concerns. Jan is an open-source alternative to ChatGPT, running AI models locally on your device." keywords: [ Jan, From 959c09dbbd38614c7a06a3eacc84ea52380a39cb Mon Sep 17 00:00:00 2001 From: Ashley Date: Fri, 24 Jan 2025 00:22:47 +0700 Subject: [PATCH 04/75] Removed outdated Raycast instruction --- .../workflow-automation/raycast.mdx | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 docs/src/pages/integrations/workflow-automation/raycast.mdx diff --git a/docs/src/pages/integrations/workflow-automation/raycast.mdx b/docs/src/pages/integrations/workflow-automation/raycast.mdx deleted file mode 100644 index 01c5a4866..000000000 --- a/docs/src/pages/integrations/workflow-automation/raycast.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Raycast -keywords: - [ - Jan, - Customizable Intelligence, LLM, - local AI, - privacy focus, - free and open source, - private and offline, - conversational AI, - no-subscription fee, - large language models, - raycast integration, - Raycast, - ] -description: A step-by-step guide on integrating Jan with Raycast. ---- - -import { Steps } from 'nextra/components' - -# Raycast - -## Integrate Raycast with Jan - -[Raycast](https://www.raycast.com/) is a productivity tool designed for macOS that enhances workflow efficiency by providing quick access to various tasks and functionalities through a keyboard-driven interface. To integrate Raycast with Jan, follow the steps below: - - -### Step 1: Download the TinyLlama Model - -1. Open Jan app. -2. Go to the **Hub** and download the TinyLlama model. -3. The model will be available at `~jan/models/tinyllama-1.1b`. - -### Step 2: Clone and Run the Program - -1. Clone this [GitHub repository](https://github.com/InNoobWeTrust/nitro-raycast). -2. Execute the project using the following command: - -```bash title="Node.js" -npm i && npm run dev -``` - -### Step 3: Search for Nitro and Run the Model - -Search for `Nitro` using the program, and you can use the models from Jan in RayCast. - \ No newline at end of file From 83550cd0d1bf3d982bd5161c127638f4ad5fed9b Mon Sep 17 00:00:00 2001 From: Doan Bui Date: Fri, 24 Jan 2025 09:31:26 +0700 Subject: [PATCH 05/75] Feat: Allow HTTP proxy authentication inputs (#4479) * sub dir * setting proxy * test useConfigurations * fix lint * test * test 2 * update check --- web/helpers/atoms/AppConfig.atom.ts | 54 +++ web/hooks/useConfigurations.test.ts | 137 ++++++ web/hooks/useConfigurations.ts | 45 +- .../Advanced/ProxySettings/index.test.tsx | 147 +++++++ .../Settings/Advanced/ProxySettings/index.tsx | 394 ++++++++++++++++++ web/screens/Settings/Advanced/index.test.tsx | 23 - web/screens/Settings/Advanced/index.tsx | 95 +---- web/screens/Settings/SettingDetail/index.tsx | 11 +- 8 files changed, 806 insertions(+), 100 deletions(-) create mode 100644 web/hooks/useConfigurations.test.ts create mode 100644 web/screens/Settings/Advanced/ProxySettings/index.test.tsx create mode 100644 web/screens/Settings/Advanced/ProxySettings/index.tsx diff --git a/web/helpers/atoms/AppConfig.atom.ts b/web/helpers/atoms/AppConfig.atom.ts index 68a375f3b..db4157422 100644 --- a/web/helpers/atoms/AppConfig.atom.ts +++ b/web/helpers/atoms/AppConfig.atom.ts @@ -5,8 +5,15 @@ const EXPERIMENTAL_FEATURE = 'experimentalFeature' const PROXY_FEATURE_ENABLED = 'proxyFeatureEnabled' const VULKAN_ENABLED = 'vulkanEnabled' const IGNORE_SSL = 'ignoreSSLFeature' +const VERIFY_PROXY_SSL = 'verifyProxySSL' +const VERIFY_PROXY_HOST_SSL = 'verifyProxyHostSSL' +const VERIFY_PEER_SSL = 'verifyPeerSSL' +const VERIFY_HOST_SSL = 'verifyHostSSL' const HTTPS_PROXY_FEATURE = 'httpsProxyFeature' +const PROXY_USERNAME = 'proxyUsername' +const PROXY_PASSWORD = 'proxyPassword' const QUICK_ASK_ENABLED = 'quickAskEnabled' +const NO_PROXY = 'noProxy' export const janDataFolderPathAtom = atom('') @@ -27,9 +34,56 @@ export const proxyAtom = atomWithStorage(HTTPS_PROXY_FEATURE, '', undefined, { getOnInit: true, }) +export const proxyUsernameAtom = atomWithStorage( + PROXY_USERNAME, + '', + undefined, + { getOnInit: true } +) + +export const proxyPasswordAtom = atomWithStorage( + PROXY_PASSWORD, + '', + undefined, + { getOnInit: true } +) + export const ignoreSslAtom = atomWithStorage(IGNORE_SSL, false, undefined, { getOnInit: true, }) + +export const noProxyAtom = atomWithStorage(NO_PROXY, '', undefined, { + getOnInit: false, +}) + +export const verifyProxySslAtom = atomWithStorage( + VERIFY_PROXY_SSL, + false, + undefined, + { getOnInit: true } +) + +export const verifyProxyHostSslAtom = atomWithStorage( + VERIFY_PROXY_HOST_SSL, + false, + undefined, + { getOnInit: true } +) + +export const verifyPeerSslAtom = atomWithStorage( + VERIFY_PEER_SSL, + false, + undefined, + { getOnInit: true } +) + +export const verifyHostSslAtom = atomWithStorage( + VERIFY_HOST_SSL, + false, + undefined, + { getOnInit: true } +) + export const vulkanEnabledAtom = atomWithStorage( VULKAN_ENABLED, false, diff --git a/web/hooks/useConfigurations.test.ts b/web/hooks/useConfigurations.test.ts new file mode 100644 index 000000000..1fab89f59 --- /dev/null +++ b/web/hooks/useConfigurations.test.ts @@ -0,0 +1,137 @@ +import { renderHook, act } from '@testing-library/react' +import { useConfigurations } from './useConfigurations' +import { useAtomValue } from 'jotai' +import { extensionManager } from '@/extension' + +// Mock dependencies +jest.mock('jotai', () => { + const originalModule = jest.requireActual('jotai') + return { + ...originalModule, + useAtomValue: jest.fn(), + } +}) + +jest.mock('@/extension', () => ({ + extensionManager: { + get: jest.fn(), + }, +})) + +describe('useConfigurations', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should call configurePullOptions with correct proxy settings when proxy is enabled', () => { + // Explicitly set mock return values for each call + (useAtomValue as jest.Mock) + .mockReturnValueOnce(true) // proxyEnabled + .mockReturnValueOnce('http://proxy.example.com') // proxyUrl + .mockReturnValueOnce('') // proxyIgnoreSSL + .mockReturnValueOnce(true) // verifyProxySSL + .mockReturnValueOnce(true) // verifyProxyHostSSL + .mockReturnValueOnce(true) // verifyPeerSSL + .mockReturnValueOnce(true) // verifyHostSSL + .mockReturnValueOnce('') // noProxy + .mockReturnValueOnce('username') // proxyUsername + .mockReturnValueOnce('password') // proxyPassword + + + const mockConfigurePullOptions = jest.fn() + ;(extensionManager.get as jest.Mock).mockReturnValue({ + configurePullOptions: mockConfigurePullOptions, + }) + + const { result } = renderHook(() => useConfigurations()) + + act(() => { + result.current.configurePullOptions() + }) + + expect(mockConfigurePullOptions).toHaveBeenCalledWith({ + proxy_username: 'username', + proxy_password: 'password', + proxy_url: 'http://proxy.example.com', + verify_proxy_ssl: true, + verify_proxy_host_ssl: true, + verify_peer_ssl: true, + verify_host_ssl: true, + no_proxy: '', + }) + }) + + it('should call configurePullOptions with empty proxy settings when proxy is disabled', () => { + // Mock atom values + ;(useAtomValue as jest.Mock) + .mockReturnValueOnce(false) // proxyEnabled + .mockReturnValueOnce('') // proxyUrl + .mockReturnValueOnce(false) // proxyIgnoreSSL + .mockReturnValueOnce('') // noProxy + .mockReturnValueOnce('') // proxyUsername + .mockReturnValueOnce('') // proxyPassword + .mockReturnValueOnce(false) // verifyProxySSL + .mockReturnValueOnce(false) // verifyProxyHostSSL + .mockReturnValueOnce(false) // verifyPeerSSL + .mockReturnValueOnce(false) // verifyHostSSL + + const mockConfigurePullOptions = jest.fn() + ;(extensionManager.get as jest.Mock).mockReturnValue({ + configurePullOptions: mockConfigurePullOptions, + }) + + const { result } = renderHook(() => useConfigurations()) + + act(() => { + result.current.configurePullOptions() + }) + + expect(mockConfigurePullOptions).toHaveBeenCalledWith({ + proxy_username: '', + proxy_password: '', + proxy_url: '', + verify_proxy_ssl: false, + verify_proxy_host_ssl: false, + verify_peer_ssl: false, + verify_host_ssl: false, + no_proxy: '', + }) + }) + + it('should set all verify SSL to false when proxyIgnoreSSL is true', () => { + // Mock atom values + ;(useAtomValue as jest.Mock) + .mockReturnValueOnce(true) // proxyEnabled + .mockReturnValueOnce('http://proxy.example.com') // proxyUrl + .mockReturnValueOnce(true) // proxyIgnoreSSL + .mockReturnValueOnce(true) // verifyProxySSL + .mockReturnValueOnce(true) // verifyProxyHostSSL + .mockReturnValueOnce(true) // verifyPeerSSL + .mockReturnValueOnce(true) // verifyHostSSL + .mockReturnValueOnce('') // noProxy + .mockReturnValueOnce('username') // proxyUsername + .mockReturnValueOnce('password') // proxyPassword + + const mockConfigurePullOptions = jest.fn() + ;(extensionManager.get as jest.Mock).mockReturnValue({ + configurePullOptions: mockConfigurePullOptions, + }) + + const { result } = renderHook(() => useConfigurations()) + + act(() => { + result.current.configurePullOptions() + }) + + expect(mockConfigurePullOptions).toHaveBeenCalledWith({ + proxy_username: 'username', + proxy_password: 'password', + proxy_url: 'http://proxy.example.com', + verify_proxy_ssl: false, + verify_proxy_host_ssl: false, + verify_peer_ssl: false, + verify_host_ssl: false, + no_proxy: '', + }) + }) +}) \ No newline at end of file diff --git a/web/hooks/useConfigurations.ts b/web/hooks/useConfigurations.ts index 681239033..0bd96e760 100644 --- a/web/hooks/useConfigurations.ts +++ b/web/hooks/useConfigurations.ts @@ -6,14 +6,28 @@ import { useAtomValue } from 'jotai' import { extensionManager } from '@/extension' import { ignoreSslAtom, + noProxyAtom, proxyAtom, proxyEnabledAtom, + proxyPasswordAtom, + proxyUsernameAtom, + verifyHostSslAtom, + verifyPeerSslAtom, + verifyProxyHostSslAtom, + verifyProxySslAtom, } from '@/helpers/atoms/AppConfig.atom' export const useConfigurations = () => { const proxyEnabled = useAtomValue(proxyEnabledAtom) const proxyUrl = useAtomValue(proxyAtom) const proxyIgnoreSSL = useAtomValue(ignoreSslAtom) + const verifyProxySSL = useAtomValue(verifyProxySslAtom) + const verifyProxyHostSSL = useAtomValue(verifyProxyHostSslAtom) + const verifyPeerSSL = useAtomValue(verifyPeerSslAtom) + const verifyHostSSL = useAtomValue(verifyHostSslAtom) + const noProxy = useAtomValue(noProxyAtom) + const proxyUsername = useAtomValue(proxyUsernameAtom) + const proxyPassword = useAtomValue(proxyPasswordAtom) const configurePullOptions = useCallback(() => { extensionManager @@ -21,20 +35,45 @@ export const useConfigurations = () => { ?.configurePullOptions( proxyEnabled ? { + proxy_username: proxyUsername, + proxy_password: proxyPassword, proxy_url: proxyUrl, - verify_peer_ssl: !proxyIgnoreSSL, + verify_proxy_ssl: proxyIgnoreSSL ? false : verifyProxySSL, + verify_proxy_host_ssl: proxyIgnoreSSL + ? false + : verifyProxyHostSSL, + verify_peer_ssl: proxyIgnoreSSL ? false : verifyPeerSSL, + verify_host_ssl: proxyIgnoreSSL ? false : verifyHostSSL, + no_proxy: noProxy, } : { + proxy_username: '', + proxy_password: '', proxy_url: '', + verify_proxy_ssl: false, + verify_proxy_host_ssl: false, verify_peer_ssl: false, + verify_host_ssl: false, + no_proxy: '', } ) - }, [proxyEnabled, proxyUrl, proxyIgnoreSSL]) + }, [ + proxyEnabled, + proxyUrl, + proxyIgnoreSSL, + noProxy, + proxyUsername, + proxyPassword, + verifyProxySSL, + verifyProxyHostSSL, + verifyPeerSSL, + verifyHostSSL, + ]) useEffect(() => { configurePullOptions() // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [configurePullOptions]) return { configurePullOptions, diff --git a/web/screens/Settings/Advanced/ProxySettings/index.test.tsx b/web/screens/Settings/Advanced/ProxySettings/index.test.tsx new file mode 100644 index 000000000..1cd4d645f --- /dev/null +++ b/web/screens/Settings/Advanced/ProxySettings/index.test.tsx @@ -0,0 +1,147 @@ +/** + * @jest-environment jsdom + */ + +import React from 'react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import '@testing-library/jest-dom' +import ProxySettings from '.' + +// Mock ResizeObserver +class ResizeObserverMock { + observe() {} + unobserve() {} + disconnect() {} +} + +global.ResizeObserver = ResizeObserverMock as any + +// Mock global window.core +global.window.core = { + api: { + getAppConfigurations: () => jest.fn(), + updateAppConfiguration: () => jest.fn(), + relaunch: () => jest.fn(), + }, +} + +// Mock dependencies +jest.mock('@/hooks/useConfigurations', () => ({ + useConfigurations: () => ({ + configurePullOptions: jest.fn(), + }), +})) + +jest.mock('jotai', () => { + const originalModule = jest.requireActual('jotai') + return { + ...originalModule, + useAtom: jest.fn().mockImplementation((atom) => { + switch (atom) { + case 'proxyEnabledAtom': + return [true, jest.fn()] + case 'proxyAtom': + return ['', jest.fn()] + case 'proxyUsernameAtom': + return ['', jest.fn()] + case 'proxyPasswordAtom': + return ['', jest.fn()] + case 'ignoreSslAtom': + return [false, jest.fn()] + case 'verifyProxySslAtom': + return [true, jest.fn()] + case 'verifyProxyHostSslAtom': + return [true, jest.fn()] + case 'verifyPeerSslAtom': + return [true, jest.fn()] + case 'verifyHostSslAtom': + return [true, jest.fn()] + case 'noProxyAtom': + return ['localhost', jest.fn()] + default: + return [null, jest.fn()] + } + }), + } +}) + +describe('ProxySettings', () => { + const mockOnBack = jest.fn() + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('renders the component', async () => { + render() + + await waitFor(() => { + expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument() + expect(screen.getByText('Proxy URL')).toBeInTheDocument() + expect(screen.getByText('Authentication')).toBeInTheDocument() + expect(screen.getByText('No Proxy')).toBeInTheDocument() + expect(screen.getByText('SSL Verification')).toBeInTheDocument() + }) + }) + + it('handles back navigation', async () => { + render() + + const backButton = screen.getByText('Advanced Settings') + fireEvent.click(backButton) + + expect(mockOnBack).toHaveBeenCalled() + }) + + it('toggles password visibility', () => { + render() + + const passwordVisibilityToggle = screen.getByTestId('password-visibility-toggle') + const passwordInput = screen.getByTestId('proxy-password') + + expect(passwordInput).toHaveAttribute('type', 'password') + + fireEvent.click(passwordVisibilityToggle) + expect(passwordInput).toHaveAttribute('type', 'text') + + fireEvent.click(passwordVisibilityToggle) + expect(passwordInput).toHaveAttribute('type', 'password') + }) + + it('allows clearing input fields', async () => { + render() + + // Test clearing proxy URL + const proxyInput = screen.getByTestId('proxy-input') + fireEvent.change(proxyInput, { target: { value: 'http://test.proxy' } }) + + const clearProxyButton = screen.getByTestId('clear-proxy-button') + fireEvent.click(clearProxyButton) + expect(proxyInput).toHaveValue('') + + // Test clearing username + const usernameInput = screen.getByTestId('proxy-username') + fireEvent.change(usernameInput, { target: { value: 'testuser' } }) + + // Test clearing password + const passwordInput = screen.getByTestId('proxy-password') + fireEvent.change(passwordInput, { target: { value: 'testpassword' } }) + }) + + it('renders SSL verification switches', async () => { + render() + + const sslSwitches = [ + 'Ignore SSL certificates', + 'Verify Proxy SSL', + 'Verify Proxy Host SSL', + 'Verify Peer SSL', + 'Verify Host SSL' + ] + + sslSwitches.forEach(switchText => { + const switchElement = screen.getByText(switchText) + expect(switchElement).toBeInTheDocument() + }) + }) +}) \ No newline at end of file diff --git a/web/screens/Settings/Advanced/ProxySettings/index.tsx b/web/screens/Settings/Advanced/ProxySettings/index.tsx new file mode 100644 index 000000000..879aef7f4 --- /dev/null +++ b/web/screens/Settings/Advanced/ProxySettings/index.tsx @@ -0,0 +1,394 @@ +import { useCallback, useState } from 'react' + +import { Input, ScrollArea, Switch } from '@janhq/joi' +import { useAtom } from 'jotai' +import { EyeIcon, EyeOffIcon, XIcon, ArrowLeftIcon } from 'lucide-react' +import { useDebouncedCallback } from 'use-debounce' + +import { useConfigurations } from '@/hooks/useConfigurations' + +import { + ignoreSslAtom, + proxyAtom, + proxyEnabledAtom, + verifyProxySslAtom, + verifyProxyHostSslAtom, + verifyPeerSslAtom, + verifyHostSslAtom, + noProxyAtom, + proxyUsernameAtom, + proxyPasswordAtom, +} from '@/helpers/atoms/AppConfig.atom' + +const ProxySettings = ({ onBack }: { onBack: () => void }) => { + const [proxyEnabled] = useAtom(proxyEnabledAtom) + const [proxy, setProxy] = useAtom(proxyAtom) + const [noProxy, setNoProxy] = useAtom(noProxyAtom) + const [partialProxy, setPartialProxy] = useState(proxy) + const [proxyUsername, setProxyUsername] = useAtom(proxyUsernameAtom) + const [proxyPassword, setProxyPassword] = useAtom(proxyPasswordAtom) + const [proxyPartialPassword, setProxyPartialPassword] = + useState(proxyPassword) + const [proxyPartialUsername, setProxyPartialUsername] = + useState(proxyUsername) + const { configurePullOptions } = useConfigurations() + const [ignoreSSL, setIgnoreSSL] = useAtom(ignoreSslAtom) + const [verifyProxySSL, setVerifyProxySSL] = useAtom(verifyProxySslAtom) + const [verifyProxyHostSSL, setVerifyProxyHostSSL] = useAtom( + verifyProxyHostSslAtom + ) + const [verifyPeerSSL, setVerifyPeerSSL] = useAtom(verifyPeerSslAtom) + const [verifyHostSSL, setVerifyHostSSL] = useAtom(verifyHostSslAtom) + const [showPassword, setShowPassword] = useState(false) + + const updatePullOptions = useDebouncedCallback( + () => configurePullOptions(), + 1000 + ) + + const onProxyChange = useDebouncedCallback((value: string) => { + if (value.trim().startsWith('http')) { + setProxy(value.trim()) + updatePullOptions() + } else { + setProxy('') + updatePullOptions() + } + }, 1000) + + const onProxyUsernameChange = useDebouncedCallback((value: string) => { + setProxyUsername(value) + updatePullOptions() + }, 1000) + + const onProxyPasswordChange = useDebouncedCallback((value: string) => { + setProxyPassword(value) + updatePullOptions() + }, 1000) + + const handleProxyInputChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value || '' + setPartialProxy(value) + onProxyChange(value) + }, + [setPartialProxy, onProxyChange] + ) + + const handleProxyUsernameInputChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value || '' + setProxyPartialUsername(value) + onProxyUsernameChange(value) + }, + [setProxyPartialUsername, onProxyUsernameChange] + ) + + const handleProxyPasswordInputChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value || '' + setProxyPartialPassword(value) + onProxyPasswordChange(value) + }, + [setProxyPartialPassword, onProxyPasswordChange] + ) + + const onNoProxyChange = useCallback( + (e: React.ChangeEvent) => { + const listNoProxy = e.target.value || '' + const listNoProxyTrim = listNoProxy.split(',').map((item) => item.trim()) + setNoProxy(listNoProxyTrim.join(',')) + updatePullOptions() + }, + [setNoProxy, updatePullOptions] + ) + + return ( + + {/* Header */} +
+
+ + / + + HTTPS Proxy + +
+
+ + {/* Content */} +
+
+
+

Proxy Configuration

+
+ +
+
+
+
+ +

+ URL and port of your proxy server. +

+
+ +
+
+ + {partialProxy && ( + + )} +
+ } + /> +
+
+
+
+
+ +
+
+
+
+ +

+ Credentials for your proxy server (if required). +

+
+
+ + {proxyUsername && ( + + )} +
+ } + /> + + {proxyPassword && ( + + )} + +
+ } + /> +
+
+
+ + + {/* No Proxy */} +
+
+
+
+ +

+ List of hosts that should bypass the proxy. +

+
+
+ + {noProxy && ( + + )} +
+ } + /> +
+
+
+ + +
+

SSL Verification

+
+ + {/* Ignore SSL certificates */} +
+
+
+
+ Ignore SSL certificates +
+
+

+ Allow self-signed or unverified certificates (may be required + for certain proxies). Enable this reduces security. Only use + this if you trust your proxy server. +

+
+ { + setIgnoreSSL(e.target.checked) + updatePullOptions() + }} + /> +
+ + {/* Verify Proxy SSL */} +
+
+
+
Verify Proxy SSL
+
+

+ Validate SSL certificate when connecting to the proxy server. +

+
+ { + setVerifyProxySSL(e.target.checked) + updatePullOptions() + }} + /> +
+ + {/* Verify Proxy Host SSL */} +
+
+
+
+ Verify Proxy Host SSL +
+
+

+ Validate SSL certificate of the proxy server host. +

+
+ { + setVerifyProxyHostSSL(e.target.checked) + updatePullOptions() + }} + /> +
+ + {/* Verify Peer SSL */} +
+
+
+
Verify Peer SSL
+
+

+ Validate SSL certificate of the peer connections. +

+
+ { + setVerifyPeerSSL(e.target.checked) + updatePullOptions() + }} + /> +
+ + {/* Verify Host SSL */} +
+
+
+
Verify Host SSL
+
+

+ Validate SSL certificate of destination hosts. +

+
+ { + setVerifyHostSSL(e.target.checked) + updatePullOptions() + }} + /> +
+ + +
+ ) +} + +export default ProxySettings diff --git a/web/screens/Settings/Advanced/index.test.tsx b/web/screens/Settings/Advanced/index.test.tsx index 7a0f3ade5..b43b57beb 100644 --- a/web/screens/Settings/Advanced/index.test.tsx +++ b/web/screens/Settings/Advanced/index.test.tsx @@ -64,7 +64,6 @@ describe('Advanced', () => { await waitFor(() => { expect(screen.getByText('Experimental Mode')).toBeInTheDocument() expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument() - expect(screen.getByText('Ignore SSL certificates')).toBeInTheDocument() expect(screen.getByText('Jan Data Folder')).toBeInTheDocument() expect(screen.getByText('Reset to Factory Settings')).toBeInTheDocument() }) @@ -102,28 +101,6 @@ describe('Advanced', () => { expect(proxyToggle).toBeChecked() }) - it('updates proxy settings', async () => { - render() - let proxyInput - await waitFor(() => { - const proxyToggle = screen.getByTestId(/proxy-switch/i) - fireEvent.click(proxyToggle) - proxyInput = screen.getByTestId(/proxy-input/i) - fireEvent.change(proxyInput, { target: { value: 'http://proxy.com' } }) - }) - expect(proxyInput).toHaveValue('http://proxy.com') - }) - - it('toggles ignore SSL certificates', async () => { - render() - let ignoreSslToggle - await waitFor(() => { - expect(screen.getByText('Ignore SSL certificates')).toBeInTheDocument() - ignoreSslToggle = screen.getByTestId(/ignore-ssl-switch/i) - fireEvent.click(ignoreSslToggle) - }) - expect(ignoreSslToggle).toBeChecked() - }) it('renders DataFolder component', async () => { render() diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 16f1fd0b6..df6db2d2b 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState, useCallback, ChangeEvent } from 'react' +import { useEffect, useState, ChangeEvent } from 'react' import { openExternalUrl, AppConfiguration } from '@janhq/core' @@ -15,7 +15,7 @@ import { } from '@janhq/joi' import { useAtom, useAtomValue, useSetAtom } from 'jotai' -import { ChevronDownIcon } from 'lucide-react' +import { ChevronDownIcon, ArrowRightIcon } from 'lucide-react' import { AlertTriangleIcon, AlertCircleIcon } from 'lucide-react' import { twMerge } from 'tailwind-merge' @@ -35,8 +35,6 @@ import FactoryReset from './FactoryReset' import { experimentalFeatureEnabledAtom, - ignoreSslAtom, - proxyAtom, proxyEnabledAtom, vulkanEnabledAtom, quickAskEnabledAtom, @@ -56,7 +54,7 @@ type GPU = { * Advanced Settings Screen * @returns */ -const Advanced = () => { +const Advanced = ({ setSubdir }: { setSubdir: (subdir: string) => void }) => { const [experimentalEnabled, setExperimentalEnabled] = useAtom( experimentalFeatureEnabledAtom ) @@ -64,10 +62,6 @@ const Advanced = () => { const [proxyEnabled, setProxyEnabled] = useAtom(proxyEnabledAtom) const quickAskEnabled = useAtomValue(quickAskEnabledAtom) - const [proxy, setProxy] = useAtom(proxyAtom) - const [ignoreSSL, setIgnoreSSL] = useAtom(ignoreSslAtom) - - const [partialProxy, setPartialProxy] = useState(proxy) const [gpuEnabled, setGpuEnabled] = useState(false) const [gpuList, setGpuList] = useState([]) const [gpusInUse, setGpusInUse] = useState([]) @@ -98,22 +92,6 @@ const Advanced = () => { () => configurePullOptions(), 300 ) - /** - * Handle proxy change - */ - const onProxyChange = useCallback( - (event: ChangeEvent) => { - const value = event.target.value || '' - setPartialProxy(value) - if (value.trim().startsWith('http')) { - setProxy(value.trim()) - } else { - setProxy('') - } - updatePullOptions() - }, - [setPartialProxy, setProxy, updatePullOptions] - ) /** * Update Quick Ask Enabled @@ -448,61 +426,32 @@ const Advanced = () => { - {/* Proxy */} + {/* Proxy Settings Link */}
-
-
-
HTTPS Proxy
+
+
+
+
HTTPS Proxy
+
+

+ Optional proxy server for internet connections +

-

- Optional proxy server for internet connections. Only HTTPS proxies - supported. -

-
- -
- { - setProxyEnabled(!proxyEnabled) - updatePullOptions() - }} - /> -
- :@:'} - value={partialProxy} - onChange={onProxyChange} +
+ { + e.stopPropagation() + setProxyEnabled(!proxyEnabled) + updatePullOptions() + }} /> + setSubdir('proxy')} />
- {/* Ignore SSL certificates */} -
-
-
-
- Ignore SSL certificates -
-
-

- Allow self-signed or unverified certificates - may be required for - certain proxies. -

-
- { - setIgnoreSSL(e.target.checked) - updatePullOptions() - }} - /> -
- {experimentalEnabled && (
diff --git a/web/screens/Settings/SettingDetail/index.tsx b/web/screens/Settings/SettingDetail/index.tsx index d4a2c4d82..0d85ccbf4 100644 --- a/web/screens/Settings/SettingDetail/index.tsx +++ b/web/screens/Settings/SettingDetail/index.tsx @@ -1,9 +1,12 @@ +import React, { useState } from 'react' + import { InferenceEngine } from '@janhq/core' import { useAtomValue } from 'jotai' import { useGetEngines } from '@/hooks/useEngineManagement' import Advanced from '@/screens/Settings/Advanced' +import ProxySettings from '@/screens/Settings/Advanced/ProxySettings' import AppearanceOptions from '@/screens/Settings/Appearance' import ExtensionCatalog from '@/screens/Settings/CoreExtensions' import Engines from '@/screens/Settings/Engines' @@ -21,6 +24,7 @@ import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom' const SettingDetail = () => { const selectedSetting = useAtomValue(selectedSettingAtom) const { engines } = useGetEngines() + const [subdir, setSubdir] = useState(null) switch (selectedSetting) { case 'Engines': @@ -39,7 +43,12 @@ const SettingDetail = () => { return case 'Advanced Settings': - return + switch (subdir) { + case 'proxy': + return setSubdir(null)} /> + default: + return + } case 'My Models': return From 5dc184eed999e37533b715454a2bfa73f3136750 Mon Sep 17 00:00:00 2001 From: Emre Can Kartal <159995642+eckartal@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:47:45 +0700 Subject: [PATCH 06/75] docs: add privacy policy page and update navigation (#4472) - Add comprehensive privacy policy page - Hide privacy policy from navigation - Update privacy page content - Configure _meta.json for proper page visibility --- docs/src/pages/docs/_meta.json | 5 + docs/src/pages/docs/privacy-policy.mdx | 125 +++++++++++++++++++++++++ docs/src/pages/docs/privacy.mdx | 58 +++++++----- 3 files changed, 165 insertions(+), 23 deletions(-) create mode 100644 docs/src/pages/docs/privacy-policy.mdx diff --git a/docs/src/pages/docs/_meta.json b/docs/src/pages/docs/_meta.json index b395ff7af..4f7852626 100644 --- a/docs/src/pages/docs/_meta.json +++ b/docs/src/pages/docs/_meta.json @@ -14,6 +14,11 @@ "desktop": "Installation", "data-folder": "Jan Data Folder", "privacy": "Privacy", + "privacy-policy": { + "type": "page", + "display": "hidden", + "title": "Privacy Policy" + }, "user-guides": { "title": "BASIC USAGE", "type": "separator" diff --git a/docs/src/pages/docs/privacy-policy.mdx b/docs/src/pages/docs/privacy-policy.mdx new file mode 100644 index 000000000..525e10091 --- /dev/null +++ b/docs/src/pages/docs/privacy-policy.mdx @@ -0,0 +1,125 @@ +--- +title: Jan Privacy Policy +description: Jan's data collection practices, privacy measures, and your rights. Learn how we protect your data and maintain transparency. +--- + +# Privacy Policy + +
+ Last Updated: January 16, 2025 +
+ +## Introduction + +We are committed to protecting your privacy and ensuring you have control over your data. This Privacy Policy outlines what information Menlo Research Pte Ltd (the "Company") collects from users of the Jan desktop app and website (the "Services"), how the Company uses that information, and the measures the Company takes to safeguard that information. + +## 1. Data Collection and Consent + +### Explicit Consent + +The Company does not collect any data until you explicitly allow tracking. + +### Tracking Preferences + +Upon first launching the Jan desktop app or visiting the website, you will be prompted to set your tracking preferences. These preferences can be modified at any time via the app's Settings menu or the website's Privacy Settings. + +### Legal Basis + +Pursuant to the European Union's General Data Protection Regulation (EU) 2016/679 (the "GDPR"), the Company processes data based on your explicit consent (GDPR Article 6(1)(a)). This means: + +- The Company only processes your data after receiving clear, affirmative consent from you. +- You may withdraw your consent at any time through the app's Settings menu or the website's Privacy Settings. +- If you withdraw your consent, the Company will stop optional data collection from the effective date of withdrawal. +- Your withdrawal of consent will not affect the lawfulness of processing before its withdrawal. + +## 2. Data We Do Not Collect + +Regardless of your analytics permissions, the Company does not collect the following: + +- Chat History: Your conversations with the Jan app are private and inaccessible to the Company. +- Chat Settings: Your personalized settings remain solely with you. +- Language Models: The specific language models you use are not tracked. + +## 3. Uses of Information + +To build a reliable and user-friendly product offering, understanding how the Jan app is used is essential. If you permit tracking, the Company collects product analytics data to: + +- Improve User Experience: Enhance app functionality based on usage patterns; and +- Measure Engagement: Assess active users and retention rates to ensure ongoing value. + +## 4. Product Analytics + +### Data Collected + +When you opt-in to tracking, we collect the following anonymous data: + +- Active Users: Number of daily active users to gauge engagement. +- Retention Rates: Track if users continue to find value in the Jan app over time. + +### Data Anonymity + +- User ID: Analytics data is tied to a randomly generated user ID, ensuring no link to your personal identity. +- Privacy Assurance: Your chat history and personal data are not tracked or linked to your usage data. + +## 5. What We Do Not Track + +Even with analytics permissions granted, the Company does not track the following: + +- Conversations: Your interactions with the Jan app remain private. +- Files: The Company does not scan, upload, or view your files. +- Personal Identity: The Company does not collect personally identifiable information about users. +- Prompts: Your prompts and prompt templates are not monitored. +- Conversation Metrics: The Company does not track context length or conversation length. +- Model Usage: The specific models you use or their types are not tracked. +- Storage: You retain full control over storing your files and logs, and your privacy is prioritized. + +## 6. Using Cloud Models + +The Jan app allows you to connect to cloud-based model APIs (e.g. GPT, Claude models). + +- Data Handling: The API provider processes your messages directly; the Jan app does not access or store these messages. +- Local Models: Choosing local models ensures all data remains on your device, with no external access. + +## 7. Data Storage and Processing + +### Analytics Provider + +The Company uses PostHog EU for analytics, which ensures all data is processed within the European Union. + +### Data Security + +- Encryption: All data transfers are encrypted using Transport Layer Security (TLS) to ensure secure transmission. +- Storage: PostHog securely manages the data the Company collects. For more information, please refer to PostHog's GDPR documentation. + +## 8. Data Retention + +- Retention Period: The Company retains analytics data for up to 12 months unless otherwise required to comply with any applicable legal requirements. +- Deletion Requests: If you wish to request the deletion of your analytics data, you may do so by sending a written request to hello@jan.ai. + +## 9. Your Rights and Choices + +- Access and Control: You may access, modify, or delete your tracking preferences at any time through the Jan app or website settings. +- Data Requests: If you have any requests related to your data, please address them to hello@jan.ai. + +## 10. Children's Privacy + +Our Services are not targeted at children under the age of 13. The Company does not knowingly collect data from children under the age of 13. If the Company becomes aware that data of persons under the age of 13 has been collected without verifiable parental consent, the Company will take appropriate actions to delete this information. + +## 11. Changes to the Privacy Policy + +The Company reserves the right, at its sole discretion, to update this Privacy Policy at any time to reflect changes in the practices or legal requirements of the Company. The Company will use reasonable efforts to notify you of any significant changes via app notifications, the website, or email. Your continued use of the Services following such updates means you accept those changes. + +## 12. Cookies and Tracking Technologies + +Our website utilizes cookies to: + +- Enhance user experience; and +- Measure website traffic and usage patterns. + +Most browsers allow you to remove or manage cookie functions and adjust your privacy and security preferences. + +For more details, please refer to our Cookie Policy. + +## 13. Contact Us + +For any questions or concerns about this Privacy Policy or our data practices, please contact hello@jan.ai. \ No newline at end of file diff --git a/docs/src/pages/docs/privacy.mdx b/docs/src/pages/docs/privacy.mdx index d3be5b6de..84d9ba61c 100644 --- a/docs/src/pages/docs/privacy.mdx +++ b/docs/src/pages/docs/privacy.mdx @@ -1,5 +1,5 @@ --- -title: Jan Privacy +title: Jan's Privacy Approach description: Jan is an app that allows you to own your AI. We prioritize your control over your data and explain what data we collect and why. keywords: [ @@ -19,45 +19,57 @@ keywords: ] --- +--- + +import { Callout } from 'nextra/components' + # Privacy -Jan is an app that allows you to own your AI. We prioritize your control over your data and explain what data we collect and why. +Jan is an app that allows you to own your AI. We prioritize local AI models and your control over your data. This page explains what data we collect and why. No tricks. -- Jan can't see your chats with AI -- You're free to opt out + +For a comprehensive overview of our privacy practices, you can read our full [Privacy Policy](/docs/privacy-policy). + + + +We don't collect any data until you explicitly allow tracking. + + +You'll be asked about your tracking preferences when you first launch the app, and you can change them at any time in Settings. + +Regardless of your analytics permissions, Jan will **never** access your chat history, chat settings, or the language models you have used. ## Why and what we track -To build a reliable, user-friendly AI that you own, we need to understand how Jan is used. We collect two types of data: performance data and usage data. +To build a reliable, user-friendly AI that you own, we need to understand how Jan is used. If users allowed us to track, we collect product analytics data. -### Performance data -We track app crashes and collect technical details about what went wrong, along with basic information about the hardware you’re using. - -When Jan crashes, we collect technical details about what went wrong. - -- Specific AI model in use during the crash -- Hardware: `CPU`, `GPU`, `RAM` -- Logs: `Date/Time`, `OS & version`, `app version`, `error codes & messages`. - -### Usage data +### Product Analytics We track data like how often the app is opened to check: - **Active Users**: How many people use Jan daily to measure engagement - **Retention Rates**: To understand if users are finding value in Jan over time -Usage data is tied to a randomly generated telemetry ID. None of our usage data can be linked to your personal identity. +Product analytics data is tied to a randomly generated user ID. None of our usage data can be linked to your personal identity. Your chat history and personal data are never tracked. -## What we **don’t** track: -- Your conversations with Jan. Those stay on your device. -- Your files. We don’t scan, upload, or even look at them. -- Anything tied to your identity. +## What we **don't** track + + +Even if you grant analytics permissions, Jan doesn't track many of your private activities. + + +- We don't track your conversations with Jan. +- We don't scan, upload, or look at your files. +- We don't collect anything tied to your identity. +- We don't track your prompts and prompt templates. +- We don't monitor context length or conversation length. +- We don't track the models you have used or their types. + +You store the files and logs that are a priority for your privacy yourself. ## Using Cloud Models Jan allows you to connect cloud model APIs. If you choose to use cloud-based models (e.g. GPT, Claude models), the API provider handling the model will have access to your messages as part of processing the request. Again, Jan doesn't see or store these messages - they go directly to the provider. Remember: With local models, everything stays on your device, so no one - not even us- can see your messages. ## Where we store & process data -We use [PostHog](https://posthog.com/eu) EU for analytics, ensuring all data is processed within the European Union. This setup complies with GDPR and other strict privacy regulations. PostHog lets us self-host and securely manage the data we collect. Read more [on PostHog's GDPR doc](https://posthog.com/docs/privacy/gdpr-compliance). - -For a detailed breakdown of the analytics data we collect, you can check out our analytics repo. If you have any questions or concerns, feel free to reach out to us at hi@jan.ai. \ No newline at end of file +We use [PostHog](https://posthog.com/eu) EU for analytics, ensuring all data is processed within the European Union. This setup complies with GDPR and other strict privacy regulations. PostHog lets us securely manage the data we collect. Read more [on PostHog's GDPR doc](https://posthog.com/docs/privacy/gdpr-compliance). \ No newline at end of file From cdc7d2ba47d0d90c4d909c9b32f838abdb0c2ff2 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Sun, 26 Jan 2025 20:41:52 +0700 Subject: [PATCH 07/75] enhancement: minor ui refinements (#4521) * enhancement: minor ui refinements * chore: update test case --- extensions/yarn.lock | 24 +++++++------- web/containers/CenterPanelContainer/index.tsx | 8 +++-- web/containers/Layout/TopPanel/index.tsx | 16 ++++++--- web/containers/LeftPanelContainer/index.tsx | 15 +++++---- web/containers/Loader/ModelReload.tsx | 5 --- web/containers/Providers/index.tsx | 2 -- .../RightPanelContainer/index.test.tsx | 33 +++++++++++-------- web/containers/RightPanelContainer/index.tsx | 16 +++++---- .../Settings/Advanced/ProxySettings/index.tsx | 2 -- web/screens/Settings/Engines/index.tsx | 1 - 10 files changed, 64 insertions(+), 58 deletions(-) diff --git a/extensions/yarn.lock b/extensions/yarn.lock index acf2965ca..f2c3b342b 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -509,61 +509,61 @@ __metadata: "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=7dd866&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f3025c&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/da0eed6e552ce2ff6f52a087e6e221101c3d0c03d92820840ee80c3ca1a17317a66525cb5bf59b6c1e8bd2e36e54763008f97e13000ae339dac49f5682fcfa65 + checksum: 10c0/cda3dff029cc6ce8a9ddcd8ac3ff039b783eed9252c1c3f0b3f34a2cf68c00dc2755997b56c3c5796502aa7316b69b57758b15f338e64b4a8ef14b34d23b6c99 languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=7dd866&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f3025c&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/da0eed6e552ce2ff6f52a087e6e221101c3d0c03d92820840ee80c3ca1a17317a66525cb5bf59b6c1e8bd2e36e54763008f97e13000ae339dac49f5682fcfa65 + checksum: 10c0/cda3dff029cc6ce8a9ddcd8ac3ff039b783eed9252c1c3f0b3f34a2cf68c00dc2755997b56c3c5796502aa7316b69b57758b15f338e64b4a8ef14b34d23b6c99 languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=7dd866&locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f3025c&locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/da0eed6e552ce2ff6f52a087e6e221101c3d0c03d92820840ee80c3ca1a17317a66525cb5bf59b6c1e8bd2e36e54763008f97e13000ae339dac49f5682fcfa65 + checksum: 10c0/cda3dff029cc6ce8a9ddcd8ac3ff039b783eed9252c1c3f0b3f34a2cf68c00dc2755997b56c3c5796502aa7316b69b57758b15f338e64b4a8ef14b34d23b6c99 languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=7dd866&locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f3025c&locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/da0eed6e552ce2ff6f52a087e6e221101c3d0c03d92820840ee80c3ca1a17317a66525cb5bf59b6c1e8bd2e36e54763008f97e13000ae339dac49f5682fcfa65 + checksum: 10c0/cda3dff029cc6ce8a9ddcd8ac3ff039b783eed9252c1c3f0b3f34a2cf68c00dc2755997b56c3c5796502aa7316b69b57758b15f338e64b4a8ef14b34d23b6c99 languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=7dd866&locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f3025c&locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/da0eed6e552ce2ff6f52a087e6e221101c3d0c03d92820840ee80c3ca1a17317a66525cb5bf59b6c1e8bd2e36e54763008f97e13000ae339dac49f5682fcfa65 + checksum: 10c0/cda3dff029cc6ce8a9ddcd8ac3ff039b783eed9252c1c3f0b3f34a2cf68c00dc2755997b56c3c5796502aa7316b69b57758b15f338e64b4a8ef14b34d23b6c99 languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fmonitoring-extension%40workspace%3Amonitoring-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=7dd866&locator=%40janhq%2Fmonitoring-extension%40workspace%3Amonitoring-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f3025c&locator=%40janhq%2Fmonitoring-extension%40workspace%3Amonitoring-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/da0eed6e552ce2ff6f52a087e6e221101c3d0c03d92820840ee80c3ca1a17317a66525cb5bf59b6c1e8bd2e36e54763008f97e13000ae339dac49f5682fcfa65 + checksum: 10c0/cda3dff029cc6ce8a9ddcd8ac3ff039b783eed9252c1c3f0b3f34a2cf68c00dc2755997b56c3c5796502aa7316b69b57758b15f338e64b4a8ef14b34d23b6c99 languageName: node linkType: hard diff --git a/web/containers/CenterPanelContainer/index.tsx b/web/containers/CenterPanelContainer/index.tsx index b3df8face..2be54ac09 100644 --- a/web/containers/CenterPanelContainer/index.tsx +++ b/web/containers/CenterPanelContainer/index.tsx @@ -7,9 +7,9 @@ import { twMerge } from 'tailwind-merge' import { MainViewState } from '@/constants/screens' -import { LEFT_PANEL_WIDTH } from '../LeftPanelContainer' +import { leftPanelWidthAtom } from '../LeftPanelContainer' -import { RIGHT_PANEL_WIDTH } from '../RightPanelContainer' +import { rightPanelWidthAtom } from '../RightPanelContainer' import { mainViewStateAtom, @@ -28,6 +28,8 @@ const CenterPanelContainer = ({ children, isShowStarterScreen }: Props) => { const showLeftPanel = useAtomValue(showLeftPanelAtom) const showRightPanel = useAtomValue(showRightPanelAtom) const mainViewState = useAtomValue(mainViewStateAtom) + const rightPanelWidth = useAtomValue(rightPanelWidthAtom) + const leftPanelWidth = useAtomValue(leftPanelWidthAtom) return (
{ maxWidth: matches ? '100%' : mainViewState === MainViewState.Thread && !isShowStarterScreen - ? `calc(100% - (${showRightPanel ? Number(localStorage.getItem(RIGHT_PANEL_WIDTH)) : 0}px + ${showLeftPanel ? Number(localStorage.getItem(LEFT_PANEL_WIDTH)) : 0}px))` + ? `calc(100% - (${showRightPanel ? rightPanelWidth : 0}px + ${showLeftPanel ? leftPanelWidth : 0}px))` : '100%', }} > diff --git a/web/containers/Layout/TopPanel/index.tsx b/web/containers/Layout/TopPanel/index.tsx index 9ba393e09..e60b279b1 100644 --- a/web/containers/Layout/TopPanel/index.tsx +++ b/web/containers/Layout/TopPanel/index.tsx @@ -1,11 +1,9 @@ import { Fragment } from 'react' -import { Button } from '@janhq/joi' +import { Button, Tooltip } from '@janhq/joi' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { PanelLeftCloseIcon, - PanelLeftOpenIcon, - PanelRightOpenIcon, PanelRightCloseIcon, MinusIcon, MenuIcon, @@ -13,6 +11,8 @@ import { PaletteIcon, XIcon, PenSquareIcon, + Settings2, + History, } from 'lucide-react' import { twMerge } from 'tailwind-merge' @@ -91,7 +91,10 @@ const TopPanel = () => { ) : ( )} @@ -135,7 +138,10 @@ const TopPanel = () => { } }} > - + } + content="Thread Settings" + /> )} diff --git a/web/containers/LeftPanelContainer/index.tsx b/web/containers/LeftPanelContainer/index.tsx index c6665a037..523af5ddb 100644 --- a/web/containers/LeftPanelContainer/index.tsx +++ b/web/containers/LeftPanelContainer/index.tsx @@ -7,7 +7,7 @@ import { } from 'react' import { ScrollArea, useClickOutside, useMediaQuery } from '@janhq/joi' -import { useAtom, useAtomValue } from 'jotai' +import { atom, useAtom, useAtomValue } from 'jotai' import { twMerge } from 'tailwind-merge' @@ -18,13 +18,12 @@ type Props = PropsWithChildren const DEFAULT_LEFT_PANEL_WIDTH = 200 export const LEFT_PANEL_WIDTH = 'leftPanelWidth' +export const leftPanelWidthAtom = atom(DEFAULT_LEFT_PANEL_WIDTH) const LeftPanelContainer = ({ children }: Props) => { const [leftPanelRef, setLeftPanelRef] = useState(null) const [isResizing, setIsResizing] = useState(false) - const [threadLeftPanelWidth, setLeftPanelWidth] = useState( - Number(localStorage.getItem(LEFT_PANEL_WIDTH)) || DEFAULT_LEFT_PANEL_WIDTH - ) + const [leftPanelWidth, setLeftPanelWidth] = useAtom(leftPanelWidthAtom) const [showLeftPanel, setShowLeftPanel] = useAtom(showLeftPanelAtom) const matches = useMediaQuery('(max-width: 880px)') const reduceTransparent = useAtomValue(reduceTransparentAtom) @@ -37,10 +36,12 @@ const LeftPanelContainer = ({ children }: Props) => { const startResizing = useCallback(() => { setIsResizing(true) + document.body.classList.add('select-none') }, []) const stopResizing = useCallback(() => { setIsResizing(false) + document.body.classList.remove('select-none') }, []) const resize = useCallback( @@ -69,7 +70,7 @@ const LeftPanelContainer = ({ children }: Props) => { } } }, - [isResizing, leftPanelRef, setShowLeftPanel] + [isResizing, leftPanelRef, setLeftPanelWidth, setShowLeftPanel] ) useEffect(() => { @@ -83,7 +84,7 @@ const LeftPanelContainer = ({ children }: Props) => { window.removeEventListener('mousemove', resize) window.removeEventListener('mouseup', stopResizing) } - }, [resize, stopResizing]) + }, [resize, setLeftPanelWidth, stopResizing]) return (
{ reduceTransparent && 'left-0 border-r border-[hsla(var(--app-border))] bg-[hsla(var(--left-panel-bg))]' )} - style={{ width: showLeftPanel ? threadLeftPanelWidth : 0 }} + style={{ width: showLeftPanel ? leftPanelWidth : 0 }} onMouseDown={(e) => isResizing && e.stopPropagation()} > diff --git a/web/containers/Loader/ModelReload.tsx b/web/containers/Loader/ModelReload.tsx index 29709c0da..fbe673788 100644 --- a/web/containers/Loader/ModelReload.tsx +++ b/web/containers/Loader/ModelReload.tsx @@ -44,11 +44,6 @@ export default function ModelReload() { Reloading model {stateModel.model?.id}
-
- - Model is reloading to apply new changes. - -
) } diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index bed0f07ec..84e0860ea 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -4,8 +4,6 @@ import { PropsWithChildren } from 'react' import { Toaster } from 'react-hot-toast' -import { SWRConfig } from 'swr' - import EventListener from '@/containers/Providers/EventListener' import JotaiWrapper from '@/containers/Providers/Jotai' diff --git a/web/containers/RightPanelContainer/index.test.tsx b/web/containers/RightPanelContainer/index.test.tsx index 4bb08913f..b8549254b 100644 --- a/web/containers/RightPanelContainer/index.test.tsx +++ b/web/containers/RightPanelContainer/index.test.tsx @@ -1,9 +1,11 @@ import '@testing-library/jest-dom' +import { waitFor } from '@testing-library/react' import React from 'react' import { render, fireEvent } from '@testing-library/react' -import RightPanelContainer from './index' -import { useAtom } from 'jotai' +import RightPanelContainer, { rightPanelWidthAtom } from './index' +import { showRightPanelAtom } from '@/helpers/atoms/App.atom' +import { reduceTransparentAtom } from '@/helpers/atoms/Setting.atom' // Mocking ResizeObserver class ResizeObserver { @@ -34,24 +36,24 @@ jest.mock('jotai', () => { const originalModule = jest.requireActual('jotai') return { ...originalModule, - useAtom: jest.fn(), - useAtomValue: jest.fn(), + useAtomValue: jest.fn((atom) => { + if (atom === reduceTransparentAtom) return false + if (atom === showRightPanelAtom) return true + }), + useAtom: jest.fn((atom) => { + if (atom === rightPanelWidthAtom) return [280, jest.fn()] + if (atom === showRightPanelAtom) return [true, mockSetShowRightPanel] + return [null, jest.fn()] + }), } }) const mockSetShowRightPanel = jest.fn() -const mockShowRightPanel = true // Change this to test the panel visibility beforeEach(() => { // Setting up the localStorage mock localStorage.clear() localStorage.setItem('rightPanelWidth', '280') // Setting a default width - - // Mocking the atom behavior - ;(useAtom as jest.Mock).mockImplementation(() => [ - mockShowRightPanel, - mockSetShowRightPanel, - ]) }) describe('RightPanelContainer', () => { @@ -66,12 +68,15 @@ describe('RightPanelContainer', () => { expect(getByText('Child Content')).toBeInTheDocument() }) - it('initializes width from localStorage', () => { + it('initializes width from localStorage', async () => { const { container } = render() - // Check the width from localStorage is applied const rightPanel = container.firstChild as HTMLDivElement - expect(rightPanel.style.width).toBe('280px') // Width from localStorage + + // Wait for the width to be applied + await waitFor(() => { + expect(rightPanel.style.width).toBe('280px') // Correct width from localStorage + }) }) it('changes width on resizing', () => { diff --git a/web/containers/RightPanelContainer/index.tsx b/web/containers/RightPanelContainer/index.tsx index 27d339bb7..7443ab61a 100644 --- a/web/containers/RightPanelContainer/index.tsx +++ b/web/containers/RightPanelContainer/index.tsx @@ -7,7 +7,7 @@ import { } from 'react' import { ScrollArea, useClickOutside, useMediaQuery } from '@janhq/joi' -import { useAtom, useAtomValue } from 'jotai' +import { atom, useAtom, useAtomValue } from 'jotai' import { twMerge } from 'tailwind-merge' @@ -19,11 +19,11 @@ type Props = PropsWithChildren const DEFAULT_RIGHT_PANEL_WIDTH = 280 export const RIGHT_PANEL_WIDTH = 'rightPanelWidth' +export const rightPanelWidthAtom = atom(DEFAULT_RIGHT_PANEL_WIDTH) + const RightPanelContainer = ({ children }: Props) => { const [isResizing, setIsResizing] = useState(false) - const [threadRightPanelWidth, setRightPanelWidth] = useState( - Number(localStorage.getItem(RIGHT_PANEL_WIDTH)) || DEFAULT_RIGHT_PANEL_WIDTH - ) + const [rightPanelWidth, setRightPanelWidth] = useAtom(rightPanelWidthAtom) const [rightPanelRef, setRightPanelRef] = useState( null ) @@ -40,10 +40,12 @@ const RightPanelContainer = ({ children }: Props) => { const startResizing = useCallback(() => { setIsResizing(true) + document.body.classList.add('select-none') }, []) const stopResizing = useCallback(() => { setIsResizing(false) + document.body.classList.remove('select-none') }, []) const resize = useCallback( @@ -72,7 +74,7 @@ const RightPanelContainer = ({ children }: Props) => { } } }, - [isResizing, rightPanelRef, setShowRightPanel] + [isResizing, rightPanelRef, setRightPanelWidth, setShowRightPanel] ) useEffect(() => { @@ -86,7 +88,7 @@ const RightPanelContainer = ({ children }: Props) => { window.removeEventListener('mousemove', resize) window.removeEventListener('mouseup', stopResizing) } - }, [resize, stopResizing]) + }, [resize, setRightPanelWidth, stopResizing]) return (
{ reduceTransparent && 'border-l border-[hsla(var(--app-border))] bg-[hsla(var(--right-panel-bg))]' )} - style={{ width: showRightPanel ? threadRightPanelWidth : 0 }} + style={{ width: showRightPanel ? rightPanelWidth : 0 }} onMouseDown={(e) => isResizing && e.preventDefault()} > diff --git a/web/screens/Settings/Advanced/ProxySettings/index.tsx b/web/screens/Settings/Advanced/ProxySettings/index.tsx index 879aef7f4..446738ea2 100644 --- a/web/screens/Settings/Advanced/ProxySettings/index.tsx +++ b/web/screens/Settings/Advanced/ProxySettings/index.tsx @@ -10,7 +10,6 @@ import { useConfigurations } from '@/hooks/useConfigurations' import { ignoreSslAtom, proxyAtom, - proxyEnabledAtom, verifyProxySslAtom, verifyProxyHostSslAtom, verifyPeerSslAtom, @@ -21,7 +20,6 @@ import { } from '@/helpers/atoms/AppConfig.atom' const ProxySettings = ({ onBack }: { onBack: () => void }) => { - const [proxyEnabled] = useAtom(proxyEnabledAtom) const [proxy, setProxy] = useAtom(proxyAtom) const [noProxy, setNoProxy] = useAtom(noProxyAtom) const [partialProxy, setPartialProxy] = useState(proxy) diff --git a/web/screens/Settings/Engines/index.tsx b/web/screens/Settings/Engines/index.tsx index 4ad155939..ded8b5a90 100644 --- a/web/screens/Settings/Engines/index.tsx +++ b/web/screens/Settings/Engines/index.tsx @@ -2,7 +2,6 @@ import React from 'react' import { InferenceEngine } from '@janhq/core' import { ScrollArea } from '@janhq/joi' -import { useAtomValue } from 'jotai' import { useGetEngines } from '@/hooks/useEngineManagement' From 32436121c7868e7598f06b541e5013536fb5d0b9 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Sun, 26 Jan 2025 20:42:03 +0700 Subject: [PATCH 08/75] feat: remove umami (#4520) --- .../workflows/template-build-linux-x64.yml | 4 +- .github/workflows/template-build-macos.yml | 4 +- .../workflows/template-build-windows-x64.yml | 4 +- docs/src/pages/about/handbook/analytics.mdx | 4 +- docs/src/pages/privacy.mdx | 15 +- web/containers/Providers/index.tsx | 3 - web/public/umami_script.js | 210 ------------------ web/utils/umami.tsx | 67 ------ 8 files changed, 8 insertions(+), 303 deletions(-) delete mode 100644 web/public/umami_script.js delete mode 100644 web/utils/umami.tsx diff --git a/.github/workflows/template-build-linux-x64.yml b/.github/workflows/template-build-linux-x64.yml index 9d12c4394..85b050e62 100644 --- a/.github/workflows/template-build-linux-x64.yml +++ b/.github/workflows/template-build-linux-x64.yml @@ -103,7 +103,7 @@ jobs: # check public_provider is true or not echo "public_provider is ${{ inputs.public_provider }}" if [ "${{ inputs.public_provider }}" == "none" ]; then - make build + make build else make build-and-publish fi @@ -122,8 +122,6 @@ jobs: make build-and-publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }} - ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }} POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} diff --git a/.github/workflows/template-build-macos.yml b/.github/workflows/template-build-macos.yml index b415d665d..2eabd9ce2 100644 --- a/.github/workflows/template-build-macos.yml +++ b/.github/workflows/template-build-macos.yml @@ -134,7 +134,7 @@ jobs: # check public_provider is true or not echo "public_provider is ${{ inputs.public_provider }}" if [ "${{ inputs.public_provider }}" == "none" ]; then - make build + make build else make build-and-publish fi @@ -168,8 +168,6 @@ jobs: APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APP_PATH: '.' DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }} - ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }} - ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }} POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} diff --git a/.github/workflows/template-build-windows-x64.yml b/.github/workflows/template-build-windows-x64.yml index 52ff22ce3..a317b4960 100644 --- a/.github/workflows/template-build-windows-x64.yml +++ b/.github/workflows/template-build-windows-x64.yml @@ -136,7 +136,7 @@ jobs: # check public_provider is true or not echo "public_provider is ${{ inputs.public_provider }}" if [ "${{ inputs.public_provider }}" == "none" ]; then - make build + make build else make build-and-publish fi @@ -160,8 +160,6 @@ jobs: make build-and-publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }} - ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }} AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} diff --git a/docs/src/pages/about/handbook/analytics.mdx b/docs/src/pages/about/handbook/analytics.mdx index 9e7833e32..5cc34209d 100644 --- a/docs/src/pages/about/handbook/analytics.mdx +++ b/docs/src/pages/about/handbook/analytics.mdx @@ -23,6 +23,4 @@ Adhering to Jan's privacy preserving philosophy, our analytics philosophy is to ## What is tracked 1. By default, Github tracks downloads and device metadata for all public GitHub repositories. This helps us troubleshoot & ensure cross-platform support. -2. We use [Umami](https://umami.is/) to collect, analyze, and understand application data while maintaining visitor privacy and data ownership. We are using the Umami Cloud in Europe to ensure GDPR compliance. Please see [Umami Privacy Policy](https://umami.is/privacy) for more details. -3. We use Umami to track a single `app.opened` event without additional user metadata, in order to understand retention. In addition, we track `app.version` to understand app version usage. -4. Additionally, we plan to enable a `Settings` feature for users to turn off all tracking. +2. Additionally, we plan to enable a `Settings` feature for users to turn off all tracking. diff --git a/docs/src/pages/privacy.mdx b/docs/src/pages/privacy.mdx index bb77327e6..44127a37a 100644 --- a/docs/src/pages/privacy.mdx +++ b/docs/src/pages/privacy.mdx @@ -10,27 +10,20 @@ Homebrew Computer Company is committed to protecting your privacy and ensuring t ## Data Collection -Jan, Cortex, and all Homebrew Computer Company products do not collect personally identifying information. You can read about [our philosophy](/about#philosophy) here and audit our open-source codebases. +Jan, Cortex, and all Homebrew Computer Company products do not collect personally identifying information. You can read about [our philosophy](/about#philosophy) here and audit our open-source codebases. ### When you voluntarily provide data -We -do- collect personal information you voluntarily provide us, e.g., when you sign up for our newsletter, join our Discord, or contact us via email. +We -do- collect personal information you voluntarily provide us, e.g., when you sign up for our newsletter, join our Discord, or contact us via email. -### Jan +### Jan -Jan runs with privacy by default and is used 100% offline on your own computer. Your data (e.g., conversation history, usage logs) are stored locally and never leave your computer. +Jan runs with privacy by default and is used 100% offline on your own computer. Your data (e.g., conversation history, usage logs) are stored locally and never leave your computer. If you use a Remote AI API (e.g., OpenAI API, Groq API), your data will naturally travel to their servers. They will be subject to the privacy policy of the respective API provider. -Jan uses [Umami](https://umami.is/) for analytics, which is a privacy-focused, GDPR-compliant analytics tool that does not track personal information. We use this to get aggregate reports on OS and hardware types and prioritize our engineering roadmap. As per [Umami's Privacy Policy](https://umami.is/privacy), Umami uses the following data points to generate its reports: - -- OS and device characteristics -- IP address - -Jan does not get any of this data, and we do not track IP addresses or other identifying information. We are actively looking into more privacy-respecting ways to handle analytics, crash reports, and telemetry and would love to work with the community on this. - ### Cortex Cortex is a library that runs large language models (LLMs) locally on your computer. Cortex does not collect any personal information. diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index 84e0860ea..5d14ea95a 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -9,8 +9,6 @@ import JotaiWrapper from '@/containers/Providers/Jotai' import ThemeWrapper from '@/containers/Providers/Theme' -import Umami from '@/utils/umami' - import { CoreConfigurator } from './CoreConfigurator' import DataLoader from './DataLoader' @@ -26,7 +24,6 @@ const Providers = ({ children }: PropsWithChildren) => { - <> diff --git a/web/public/umami_script.js b/web/public/umami_script.js deleted file mode 100644 index b9db0b024..000000000 --- a/web/public/umami_script.js +++ /dev/null @@ -1,210 +0,0 @@ -!(function () { - 'use strict' - !(function (t) { - var e = t.screen, - n = e.width, - r = e.height, - a = t.navigator.language, - i = t.location, - o = t.localStorage, - u = t.document, - c = t.history, - f = 'jan.ai', - s = 'mainpage', - l = i.search, - d = u.currentScript - if (d) { - var m = 'data-', - h = d.getAttribute.bind(d), - v = h(m + 'website-id'), - p = h(m + 'host-url'), - g = 'false' !== h(m + 'auto-track'), - y = h(m + 'do-not-track'), - b = h(m + 'domains') || '', - S = b.split(',').map(function (t) { - return t.trim() - }), - k = - (p ? p.replace(/\/$/, '') : d.src.split('/').slice(0, -1).join('/')) + - '/api/send', - w = n + 'x' + r, - N = /data-umami-event-([\w-_]+)/, - T = m + 'umami-event', - j = 300, - A = function (t, e, n) { - var r = t[e] - return function () { - for (var e = [], a = arguments.length; a--; ) e[a] = arguments[a] - return n.apply(null, e), r.apply(t, e) - } - }, - x = function () { - return { - website: v, - hostname: f, - screen: w, - language: a, - title: M, - url: I, - referrer: J, - } - }, - E = function () { - return ( - (o && o.getItem('umami.disabled')) || - (y && - (function () { - var e = t.doNotTrack, - n = t.navigator, - r = t.external, - a = 'msTrackingProtectionEnabled', - i = - e || - n.doNotTrack || - n.msDoNotTrack || - (r && a in r && r[a]()) - return '1' == i || 'yes' === i - })()) || - (b && !S.includes(f)) - ) - }, - O = function (t, e, n) { - n && - ((J = I), - (I = (function (t) { - try { - return new URL(t).pathname - } catch (e) { - return t - } - })(n.toString())) !== J && setTimeout(D, j)) - }, - L = function (t, e) { - if ((void 0 === e && (e = 'event'), !E())) { - var n = { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'Content-Type': 'application/json', - } - return ( - void 0 !== K && (n['x-umami-cache'] = K), - fetch(k, { - method: 'POST', - body: JSON.stringify({ - type: e, - payload: t, - }), - headers: n, - }) - .then(function (t) { - return t.text() - }) - .then(function (t) { - return (K = t) - }) - .catch(function () {}) - ) - } - }, - D = function (t, e) { - return L( - 'string' == typeof t - ? Object.assign({}, x(), { - name: t, - data: 'object' == typeof e ? e : void 0, - }) - : 'object' == typeof t - ? t - : 'function' == typeof t - ? t(x()) - : x() - ) - } - t.umami || - (t.umami = { - track: D, - identify: function (t) { - return L( - Object.assign({}, x(), { - data: t, - }), - 'identify' - ) - }, - }) - var K, - P, - _, - q, - C, - I = '' + s + l, - J = u.referrer, - M = u.title - if (g && !E()) { - ;(c.pushState = A(c, 'pushState', O)), - (c.replaceState = A(c, 'replaceState', O)), - (C = function (t) { - var e = t.getAttribute.bind(t), - n = e(T) - if (n) { - var r = {} - return ( - t.getAttributeNames().forEach(function (t) { - var n = t.match(N) - n && (r[n[1]] = e(t)) - }), - D(n, r) - ) - } - return Promise.resolve() - }), - u.addEventListener( - 'click', - function (t) { - var e = t.target, - n = - 'A' === e.tagName - ? e - : (function (t, e) { - for (var n = t, r = 0; r < e; r++) { - if ('A' === n.tagName) return n - if (!(n = n.parentElement)) return null - } - return null - })(e, 10) - if (n) { - var r = n.href, - a = - '_blank' === n.target || - t.ctrlKey || - t.shiftKey || - t.metaKey || - (t.button && 1 === t.button) - if (n.getAttribute(T) && r) - return ( - a || t.preventDefault(), - C(n).then(function () { - a || (i.href = r) - }) - ) - } else C(e) - }, - !0 - ), - (_ = new MutationObserver(function (t) { - var e = t[0] - M = e && e.target ? e.target.text : void 0 - })), - (q = u.querySelector('head > title')) && - _.observe(q, { - subtree: !0, - characterData: !0, - childList: !0, - }) - var R = function () { - 'complete' !== u.readyState || P || (D(), (P = !0)) - } - u.addEventListener('readystatechange', R, !0), R() - } - } - })(window) -})() diff --git a/web/utils/umami.tsx b/web/utils/umami.tsx deleted file mode 100644 index dc406a7d2..000000000 --- a/web/utils/umami.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useEffect } from 'react' - -import Script from 'next/script' - -// Define the type for the umami data object -interface UmamiData { - version: string -} - -declare global { - interface Window { - umami: - | { - track: (event: string, data?: UmamiData) => void - } - | undefined - } -} - -const Umami = () => { - const appVersion = VERSION - const analyticsScriptPath = './umami_script.js' - const analyticsId = ANALYTICS_ID - - useEffect(() => { - if (!appVersion || !analyticsScriptPath || !analyticsId) return - - const ping = () => { - // Check if umami is defined before ping - if (window.umami !== null && typeof window.umami !== 'undefined') { - window.umami.track(appVersion, { - version: appVersion, - }) - } - } - - // Wait for umami to be defined before ping - if (window.umami !== null && typeof window.umami !== 'undefined') { - ping() - } else { - // Listen for umami script load event - document.addEventListener('umami:loaded', ping) - } - - // Cleanup function to remove event listener if the component unmounts - return () => { - document.removeEventListener('umami:loaded', ping) - } - }, [appVersion, analyticsScriptPath, analyticsId]) - - return ( - <> - {appVersion && analyticsScriptPath && analyticsId && ( -