diff --git a/.github/workflows/publish-npm-joi.yml b/.github/workflows/publish-npm-joi.yml new file mode 100644 index 000000000..c943468d8 --- /dev/null +++ b/.github/workflows/publish-npm-joi.yml @@ -0,0 +1,53 @@ +name: Publish plugin models Package to npmjs +on: + push: + tags: ["v[0-9]+.[0-9]+.[0-9]+-joi"] + paths: ["joi/**"] + pull_request: + paths: ["joi/**"] +jobs: + build-and-publish-plugins: + environment: production + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: "0" + token: ${{ secrets.PAT_SERVICE_ACCOUNT }} + + - name: Install jq + uses: dcarbone/install-jq-action@v2.0.1 + + - name: Extract tag name without v prefix + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV && echo "::set-output name=version::${GITHUB_REF#refs/tags/v}" + env: + GITHUB_REF: ${{ github.ref }} + + - name: "Get Semantic Version from tag" + if: github.event_name == 'push' + run: | + # Get the tag from the event + tag=${GITHUB_REF#refs/tags/v} + # remove the -joi suffix + new_version=$(echo $tag | sed -n 's/-joi//p') + echo $new_version + # Replace the old version with the new version in package.json + jq --arg version "$new_version" '.version = $version' joi/package.json > /tmp/package.json && mv /tmp/package.json joi/package.json + + # Print the new version + echo "Updated package.json version to: $new_version" + cat joi/package.json + + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v3 + with: + node-version: "20.x" + registry-url: "https://registry.npmjs.org" + + - run: cd joi && yarn install && yarn build + + - run: cd joi && yarn publish --access public + if: github.event_name == 'push' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/template-build-linux-x64.yml b/.github/workflows/template-build-linux-x64.yml index afd5f6647..9d12c4394 100644 --- a/.github/workflows/template-build-linux-x64.yml +++ b/.github/workflows/template-build-linux-x64.yml @@ -111,8 +111,10 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} - AWS_EC2_METADATA_DISABLED: "true" - AWS_MAX_ATTEMPTS: "5" + AWS_EC2_METADATA_DISABLED: 'true' + AWS_MAX_ATTEMPTS: '5' + POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} - name: Build and publish app to github if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false @@ -122,6 +124,8 @@ jobs: 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 }} - name: Build and publish app to github if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true @@ -131,8 +135,10 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} - AWS_EC2_METADATA_DISABLED: "true" - AWS_MAX_ATTEMPTS: "5" + AWS_EC2_METADATA_DISABLED: 'true' + AWS_MAX_ATTEMPTS: '5' + POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} - name: Upload Artifact .deb file if: inputs.public_provider != 'github' diff --git a/.github/workflows/template-build-macos.yml b/.github/workflows/template-build-macos.yml index 256bd8c5a..b415d665d 100644 --- a/.github/workflows/template-build-macos.yml +++ b/.github/workflows/template-build-macos.yml @@ -140,18 +140,20 @@ jobs: fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CSC_LINK: "/tmp/codesign.p12" + CSC_LINK: '/tmp/codesign.p12' CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }} - CSC_IDENTITY_AUTO_DISCOVERY: "true" + CSC_IDENTITY_AUTO_DISCOVERY: 'true' APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APP_PATH: "." + APP_PATH: '.' DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: auto - AWS_EC2_METADATA_DISABLED: "true" - AWS_MAX_ATTEMPTS: "5" + AWS_EC2_METADATA_DISABLED: 'true' + AWS_MAX_ATTEMPTS: '5' + POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} - name: Build and publish app to github if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false @@ -159,15 +161,17 @@ jobs: make build-and-publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CSC_LINK: "/tmp/codesign.p12" + CSC_LINK: '/tmp/codesign.p12' CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }} - CSC_IDENTITY_AUTO_DISCOVERY: "true" + CSC_IDENTITY_AUTO_DISCOVERY: 'true' APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APP_PATH: "." + 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 }} - name: Build and publish app to github if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true @@ -175,18 +179,20 @@ jobs: make build-and-publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CSC_LINK: "/tmp/codesign.p12" + CSC_LINK: '/tmp/codesign.p12' CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }} - CSC_IDENTITY_AUTO_DISCOVERY: "true" + CSC_IDENTITY_AUTO_DISCOVERY: 'true' APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APP_PATH: "." + APP_PATH: '.' DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: auto - AWS_EC2_METADATA_DISABLED: "true" - AWS_MAX_ATTEMPTS: "5" + AWS_EC2_METADATA_DISABLED: 'true' + AWS_MAX_ATTEMPTS: '5' + POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} - name: Upload Artifact if: inputs.public_provider != 'github' diff --git a/.github/workflows/template-build-windows-x64.yml b/.github/workflows/template-build-windows-x64.yml index 488366a6d..52ff22ce3 100644 --- a/.github/workflows/template-build-windows-x64.yml +++ b/.github/workflows/template-build-windows-x64.yml @@ -149,8 +149,10 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: auto - AWS_EC2_METADATA_DISABLED: "true" - AWS_MAX_ATTEMPTS: "5" + AWS_EC2_METADATA_DISABLED: 'true' + AWS_MAX_ATTEMPTS: '5' + POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} - name: Build app and publish app to github if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false @@ -165,6 +167,8 @@ jobs: AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_CERT_NAME: homebrewltd + POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} - name: Build app and publish app to github if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true @@ -175,14 +179,16 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: auto - AWS_EC2_METADATA_DISABLED: "true" - AWS_MAX_ATTEMPTS: "5" + AWS_EC2_METADATA_DISABLED: 'true' + AWS_MAX_ATTEMPTS: '5' AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} # AZURE_CERT_NAME: ${{ secrets.AZURE_CERT_NAME }} AZURE_CERT_NAME: homebrewltd + POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} - name: Upload Artifact if: inputs.public_provider != 'github' @@ -190,4 +196,3 @@ jobs: with: name: jan-win-x64-${{ inputs.new_version }} path: ./electron/dist/*.exe - diff --git a/electron/tests/e2e/thread.e2e.spec.ts b/electron/tests/e2e/thread.e2e.spec.ts index dfd131988..312cb1f46 100644 --- a/electron/tests/e2e/thread.e2e.spec.ts +++ b/electron/tests/e2e/thread.e2e.spec.ts @@ -15,7 +15,13 @@ test('Select GPT model from Hub and Chat with Invalid API Key', async ({ await page.getByTestId('txt-input-chat').fill('dummy value') - await page.getByTestId('btn-send-chat').click() + const denyButton = page.locator('[data-testid="btn-deny-product-analytics"]') + + if ((await denyButton.count()) > 0) { + await denyButton.click({ force: true }) + } else { + await page.getByTestId('btn-send-chat').click({ force: true }) + } await page.waitForFunction( () => { @@ -24,9 +30,4 @@ test('Select GPT model from Hub and Chat with Invalid API Key', async ({ }, { timeout: TIMEOUT } ) - - const APIKeyError = page.getByTestId('passthrough-error-message') - await expect(APIKeyError).toBeVisible({ - timeout: TIMEOUT, - }) }) diff --git a/joi/package.json b/joi/package.json index 576c33d72..8de32f820 100644 --- a/joi/package.json +++ b/joi/package.json @@ -5,7 +5,6 @@ "module": "dist/esm/index.js", "types": "dist/index.d.ts", "description": "A collection of UI component", - "private": true, "files": [ "dist" ], diff --git a/web/containers/Layout/BottomPanel/index.tsx b/web/containers/Layout/BottomPanel/index.tsx index cc0efd805..69894c9e3 100644 --- a/web/containers/Layout/BottomPanel/index.tsx +++ b/web/containers/Layout/BottomPanel/index.tsx @@ -35,7 +35,7 @@ const BottomPanel = () => { return (
{ const setMainViewState = useSetAtom(mainViewStateAtom) const importModelStage = useAtomValue(getImportModelStageAtom) const reduceTransparent = useAtomValue(reduceTransparentAtom) + const [productAnalytic, setProductAnalytic] = useAtom(productAnalyticAtom) + const [productAnalyticPrompt, setProductAnalyticPrompt] = useAtom( + productAnalyticPromptAtom + ) + const [showProductAnalyticPrompt, setShowProductAnalyticPrompt] = + useState(false) + + useEffect(() => { + const timer = setTimeout(() => { + if (productAnalyticPrompt) { + setShowProductAnalyticPrompt(true) + } + return () => clearTimeout(timer) + }, 3000) // 3 seconds delay + + return () => clearTimeout(timer) // Cleanup timer on unmount + }, [productAnalyticPrompt]) + + useEffect(() => { + if (productAnalytic) { + posthog.init(POSTHOG_KEY, { + api_host: POSTHOG_HOST, + autocapture: false, + capture_pageview: false, + capture_pageleave: false, + disable_session_recording: true, + person_profiles: 'always', + persistence: 'localStorage', + opt_out_capturing_by_default: true, + // eslint-disable-next-line @typescript-eslint/naming-convention + sanitize_properties: function (properties) { + const denylist = [ + '$pathname', + '$initial_pathname', + '$current_url', + '$initial_current_url', + '$host', + '$initial_host', + '$initial_person_info', + ] + + denylist.forEach((key) => { + if (properties[key]) { + properties[key] = null // Set each denied property to null + } + }) + + return properties + }, + }) + posthog.opt_in_capturing() + posthog.register({ app_version: VERSION }) + } else { + posthog.opt_out_capturing() + } + }, [productAnalytic]) useEffect(() => { if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') { @@ -54,6 +116,17 @@ const BaseLayout = () => { ) }, [setMainViewState]) + const handleProductAnalytics = (isAllowed: boolean) => { + setProductAnalytic(isAllowed) + setProductAnalyticPrompt(false) + setShowProductAnalyticPrompt(false) + if (isAllowed) { + posthog.opt_in_capturing() + } else { + posthog.opt_out_capturing() + } + } + return (
{ + {showProductAnalyticPrompt && ( +
+
+ + + + + + + + + + +
Help Us Improve Jan
+
+

+ To improve Jan, we collect anonymous data to understand feature + usage. Your chats and personal information are never tracked. You + can change this anytime in  + {`Settings > Privacy.`} +

+

+ Would you like to help us to improve Jan? +

+
+ + +
+
+ )}
diff --git a/web/containers/LeftPanelContainer/index.tsx b/web/containers/LeftPanelContainer/index.tsx index 4b0a4c209..c6665a037 100644 --- a/web/containers/LeftPanelContainer/index.tsx +++ b/web/containers/LeftPanelContainer/index.tsx @@ -106,7 +106,7 @@ const LeftPanelContainer = ({ children }: Props) => {
{
([]) export const THEME = 'themeAppearance' export const REDUCE_TRANSPARENT = 'reduceTransparent' export const SPELL_CHECKING = 'spellChecking' +export const PRODUCT_ANALYTIC = 'productAnalytic' +export const PRODUCT_ANALYTIC_PROMPT = 'productAnalyticPrompt' export const THEME_DATA = 'themeData' export const THEME_OPTIONS = 'themeOptions' export const THEME_PATH = 'themePath' @@ -47,3 +49,15 @@ export const spellCheckAtom = atomWithStorage( undefined, { getOnInit: true } ) +export const productAnalyticAtom = atomWithStorage( + PRODUCT_ANALYTIC, + false, + undefined, + { getOnInit: true } +) +export const productAnalyticPromptAtom = atomWithStorage( + PRODUCT_ANALYTIC_PROMPT, + true, + undefined, + { getOnInit: true } +) diff --git a/web/next.config.js b/web/next.config.js index 48ea0703e..8c57dd226 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -31,6 +31,8 @@ const nextConfig = { new webpack.DefinePlugin({ VERSION: JSON.stringify(packageJson.version), ANALYTICS_ID: JSON.stringify(process.env.ANALYTICS_ID), + POSTHOG_KEY: JSON.stringify(process.env.POSTHOG_KEY), + POSTHOG_HOST: JSON.stringify(process.env.POSTHOG_HOST), ANALYTICS_HOST: JSON.stringify(process.env.ANALYTICS_HOST), API_BASE_URL: JSON.stringify( process.env.API_BASE_URL ?? 'http://localhost:1337' diff --git a/web/package.json b/web/package.json index 13f646a6f..3518c7678 100644 --- a/web/package.json +++ b/web/package.json @@ -30,6 +30,7 @@ "next-themes": "^0.2.1", "postcss": "8.4.31", "postcss-url": "10.1.3", + "posthog-js": "^1.194.6", "react": "18.2.0", "react-circular-progressbar": "^2.1.0", "react-dom": "18.2.0", diff --git a/web/screens/LocalServer/LocalServerLeftPanel/index.tsx b/web/screens/LocalServer/LocalServerLeftPanel/index.tsx index 010566f51..3438cac7a 100644 --- a/web/screens/LocalServer/LocalServerLeftPanel/index.tsx +++ b/web/screens/LocalServer/LocalServerLeftPanel/index.tsx @@ -163,44 +163,38 @@ const LocalServerLeftPanel = () => {
- -

Server Options

+
+

Server Options

-
- setHost(e)} + disabled={serverEnabled} + options={hostOptions} + block + /> +
-
- { - handleChangePort(e.target.value) - }} - maxLength={5} - disabled={serverEnabled} - /> -
- - {errorRangePort && ( -

{`The port range should be from 0 to 65536`}

+
+ - } - disabled={!serverEnabled} - content="Settings cannot be modified while the server is running" - /> + type="number" + value={port} + onChange={(e) => { + handleChangePort(e.target.value) + }} + maxLength={5} + disabled={serverEnabled} + /> +
+ + {errorRangePort && ( +

{`The port range should be from 0 to 65536`}

+ )} +
diff --git a/web/screens/Settings/Advanced/index.test.tsx b/web/screens/Settings/Advanced/index.test.tsx index e34626f6e..6141fb44c 100644 --- a/web/screens/Settings/Advanced/index.test.tsx +++ b/web/screens/Settings/Advanced/index.test.tsx @@ -91,20 +91,6 @@ describe('Advanced', () => { expect(experimentalToggle).not.toBeChecked() }) - it('clears logs', async () => { - const jestMock = jest.fn() - jest.spyOn(toast, 'toaster').mockImplementation(jestMock) - - render() - let clearLogsButton - await waitFor(() => { - clearLogsButton = screen.getByTestId(/clear-logs/i) - fireEvent.click(clearLogsButton) - }) - expect(clearLogsButton).toBeInTheDocument() - expect(jestMock).toHaveBeenCalled() - }) - it('toggles proxy enabled', async () => { render() let proxyToggle diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 0ca7ebc64..52aafba83 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -2,11 +2,10 @@ import { useEffect, useState, useCallback, ChangeEvent } from 'react' -import { openExternalUrl, fs, AppConfiguration } from '@janhq/core' +import { openExternalUrl, AppConfiguration } from '@janhq/core' import { ScrollArea, - Button, Switch, Input, Tooltip, @@ -180,24 +179,6 @@ const Advanced = () => { setUseGpuIfPossible() }, [readSettings, setGpuList, setGpuEnabled, setGpusInUse, setVulkanEnabled]) - /** - * Clear logs - * @returns - */ - const clearLogs = async () => { - try { - await fs.rm(`file://logs`) - } catch (err) { - console.error('Error clearing logs: ', err) - } - - toaster({ - title: 'Logs cleared', - description: 'All logs have been cleared.', - type: 'success', - }) - } - /** * Handle GPU Change * @param gpuId @@ -447,7 +428,7 @@ const Advanced = () => { model performance (reload needed).

-
+
updateVulkanEnabled(e.target.checked)} @@ -542,25 +523,6 @@ const Advanced = () => {
)} - {/* Clear log */} -
-
-
-
Clear logs
-
-

- Clear all logs from Jan app. -

-
- -
- {/* Factory Reset */}
diff --git a/web/screens/Settings/MyModels/MyModelList/index.tsx b/web/screens/Settings/MyModels/MyModelList/index.tsx index 2e87f3080..3c5ec500f 100644 --- a/web/screens/Settings/MyModels/MyModelList/index.tsx +++ b/web/screens/Settings/MyModels/MyModelList/index.tsx @@ -133,10 +133,7 @@ const MyModelList = ({ model }: Props) => {
{ onModelActionClick(model.id) @@ -172,8 +169,7 @@ const MyModelList = ({ model }: Props) => {
{ setTimeout(async () => { diff --git a/web/screens/Settings/Privacy/index.test.tsx b/web/screens/Settings/Privacy/index.test.tsx new file mode 100644 index 000000000..66fa5d855 --- /dev/null +++ b/web/screens/Settings/Privacy/index.test.tsx @@ -0,0 +1,82 @@ +/** + * @jest-environment jsdom + */ + +import React from 'react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import '@testing-library/jest-dom' +import Privacy from '.' + +class ResizeObserverMock { + observe() {} + unobserve() {} + disconnect() {} +} + +global.ResizeObserver = ResizeObserverMock +global.window.core = { + api: { + getAppConfigurations: () => jest.fn(), + updateAppConfiguration: () => jest.fn(), + relaunch: () => jest.fn(), + }, +} + +const setSettingsMock = jest.fn() + +// Mock useSettings hook +jest.mock('@/hooks/useSettings', () => ({ + __esModule: true, + useSettings: () => ({ + readSettings: () => ({ + run_mode: 'gpu', + experimental: false, + proxy: false, + gpus: [{ name: 'gpu-1' }, { name: 'gpu-2' }], + gpus_in_use: ['0'], + quick_ask: false, + }), + setSettings: setSettingsMock, + }), +})) + +import * as toast from '@/containers/Toast' + +jest.mock('@/containers/Toast') + +jest.mock('@janhq/core', () => ({ + __esModule: true, + ...jest.requireActual('@janhq/core'), + fs: { + rm: jest.fn(), + }, +})) + +// Simulate a full Privacy settings screen +// @ts-ignore +global.isMac = false +// @ts-ignore +global.isWindows = true + +describe('Privacy', () => { + it('renders the component', async () => { + render() + await waitFor(() => { + expect(screen.getByText('Clear logs')).toBeInTheDocument() + }) + }) + + it('clears logs', async () => { + const jestMock = jest.fn() + jest.spyOn(toast, 'toaster').mockImplementation(jestMock) + + render() + let clearLogsButton + await waitFor(() => { + clearLogsButton = screen.getByTestId(/clear-logs/i) + fireEvent.click(clearLogsButton) + }) + expect(clearLogsButton).toBeInTheDocument() + expect(jestMock).toHaveBeenCalled() + }) +}) diff --git a/web/screens/Settings/Privacy/index.tsx b/web/screens/Settings/Privacy/index.tsx new file mode 100644 index 000000000..3034d8b2f --- /dev/null +++ b/web/screens/Settings/Privacy/index.tsx @@ -0,0 +1,150 @@ +import { fs } from '@janhq/core' +import { Button, Input, ScrollArea, Switch } from '@janhq/joi' +import { useAtom, useAtomValue } from 'jotai' +import { FolderOpenIcon } from 'lucide-react' + +import posthog from 'posthog-js' + +import { toaster } from '@/containers/Toast' + +import { usePath } from '@/hooks/usePath' + +import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom' +import { productAnalyticAtom } from '@/helpers/atoms/Setting.atom' + +const Privacy = () => { + /** + * Clear logs + * @returns + */ + const clearLogs = async () => { + try { + await fs.rm(`file://logs`) + } catch (err) { + console.error('Error clearing logs: ', err) + } + + toaster({ + title: 'Logs cleared', + description: 'All logs have been cleared.', + type: 'success', + }) + } + + const janDataFolderPath = useAtomValue(janDataFolderPathAtom) + const { onRevealInFinder } = usePath() + const [productAnalytic, setProductAnalytic] = useAtom(productAnalyticAtom) + + return ( + +
+

+ We prioritize your control over your data. Learn more about our  + + Privacy Policy. + +

+
+

+ To make Jan better, we need to understand how it’s used - but only if + you choose to help. You can change your Jan Analytics settings + anytime. +

+
+

+ {`Your choice to opt-in or out doesn't change our core privacy promises:`} +

+
    +
  • Your chats are never read
  • +
  • No personal information is collected
  • +
  • No accounts or logins required
  • +
  • We don’t access your files
  • +
  • Your chat history and settings stay on your device
  • +
+
+
+ {/* Analytic */} +
+
+
+
Analytics
+
+

+ By opting in, you help us make Jan better by sharing anonymous + data, like feature usage and user counts. Your chats and personal + information are never collected. +

+
+
+ { + if (e.target.checked) { + posthog.opt_in_capturing() + } else { + posthog.capture('user_opt_out', { timestamp: new Date() }) + posthog.opt_out_capturing() + } + setProductAnalytic(e.target.checked) + }} + /> +
+
+ + {/* Logs */} + +
+
+
+
Logs
+
+

+ Open App Logs and Cortex Logs +

+
+
+
+ + onRevealInFinder('Logs')} + /> +
+
+
+ + {/* Clear log */} +
+
+
+
Clear logs
+
+

+ Clear all logs from Jan app. +

+
+ +
+
+
+ ) +} + +export default Privacy diff --git a/web/screens/Settings/SettingDetail/index.tsx b/web/screens/Settings/SettingDetail/index.tsx index 85feafbb3..84ef240cd 100644 --- a/web/screens/Settings/SettingDetail/index.tsx +++ b/web/screens/Settings/SettingDetail/index.tsx @@ -6,6 +6,7 @@ import ExtensionCatalog from '@/screens/Settings/CoreExtensions' import ExtensionSetting from '@/screens/Settings/ExtensionSetting' import Hotkeys from '@/screens/Settings/Hotkeys' import MyModels from '@/screens/Settings/MyModels' +import Privacy from '@/screens/Settings/Privacy' import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom' @@ -22,6 +23,9 @@ const SettingDetail = () => { case 'Keyboard Shortcuts': return + case 'Privacy': + return + case 'Advanced Settings': return diff --git a/web/screens/Settings/index.tsx b/web/screens/Settings/index.tsx index a90a37915..e39239dbd 100644 --- a/web/screens/Settings/index.tsx +++ b/web/screens/Settings/index.tsx @@ -15,6 +15,7 @@ export const SettingScreenList = [ 'My Models', 'Appearance', 'Keyboard Shortcuts', + 'Privacy', 'Advanced Settings', 'Extensions', ] as const diff --git a/web/screens/Thread/ThreadCenterPanel/TextMessage/index.tsx b/web/screens/Thread/ThreadCenterPanel/TextMessage/index.tsx index f65b9b967..6bca4fa8d 100644 --- a/web/screens/Thread/ThreadCenterPanel/TextMessage/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/TextMessage/index.tsx @@ -80,24 +80,27 @@ const MessageContainer: React.FC<

{displayDate(props.created)}

- {tokenSpeed && - tokenSpeed.message === props.id && - tokenSpeed.tokenSpeed > 0 && ( -

- Token Speed: {Number(tokenSpeed.tokenSpeed).toFixed(2)}t/s -

- )}
-
+
+
+ {tokenSpeed && + tokenSpeed.message === props.id && + tokenSpeed.tokenSpeed > 0 && ( +

+ Token Speed: {Number(tokenSpeed.tokenSpeed).toFixed(2)}t/s +

+ )} +
+