Merge branch 'dev' into feat/threads-messages-requests-to-backend

This commit is contained in:
Louis 2024-12-17 10:02:48 +07:00 committed by GitHub
commit 2410de0ba9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 557 additions and 140 deletions

53
.github/workflows/publish-npm-joi.yml vendored Normal file
View File

@ -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 }}

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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,
})
})

View File

@ -5,7 +5,6 @@
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"description": "A collection of UI component",
"private": true,
"files": [
"dist"
],

View File

@ -35,7 +35,7 @@ const BottomPanel = () => {
return (
<div
className={twMerge(
'fixed bottom-0 left-0 z-50 flex h-9 w-full items-center justify-between px-3 text-xs',
'fixed bottom-0 left-0 z-40 flex h-9 w-full items-center justify-between px-3 text-xs',
reduceTransparent &&
'border-t border-[hsla(var(--app-border))] bg-[hsla(var(--bottom-panel-bg))]'
)}

View File

@ -1,9 +1,11 @@
'use client'
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useAtomValue, useSetAtom } from 'jotai'
import { Button } from '@janhq/joi'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import posthog from 'posthog-js'
import { twMerge } from 'tailwind-merge'
import BottomPanel from '@/containers/Layout/BottomPanel'
@ -31,12 +33,72 @@ import MainViewContainer from '../MainViewContainer'
import InstallingExtensionModal from './BottomPanel/InstallingExtension/InstallingExtensionModal'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { reduceTransparentAtom } from '@/helpers/atoms/Setting.atom'
import {
productAnalyticAtom,
productAnalyticPromptAtom,
reduceTransparentAtom,
} from '@/helpers/atoms/Setting.atom'
const BaseLayout = () => {
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 (
<div
className={twMerge(
@ -76,6 +149,79 @@ const BaseLayout = () => {
<ChooseWhatToImportModal />
<InstallingExtensionModal />
<HuggingFaceRepoDetailModal />
{showProductAnalyticPrompt && (
<div className="fixed bottom-4 z-50 m-4 max-w-full rounded-xl border border-[hsla(var(--app-border))] bg-[hsla(var(--app-bg))] p-6 shadow-2xl sm:bottom-8 sm:right-4 sm:m-0 sm:max-w-[400px]">
<div className="mb-4 flex items-center gap-x-2">
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.5 12.5C5.5 11.1193 6.61929 10 8 10H24C25.3807 10 26.5 11.1193 26.5 12.5V18.5C26.5 24.299 21.799 29 16 29C10.201 29 5.5 24.299 5.5 18.5V12.5Z"
fill="#2563EB"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.20959 25.54L12.0093 10H14.0093L9.84984 27.0113C9.25274 26.579 8.70292 26.0855 8.20959 25.54ZM11.5993 28.0361C11.2955 27.8957 10.9996 27.7412 10.7124 27.5734L15.0093 10H16.0093L11.5993 28.0361Z"
fill="white"
/>
<path
d="M21 8C21 6.67392 20.4732 5.40215 19.5355 4.46447C18.5979 3.52678 17.3261 3 16 3C14.6739 3 13.4021 3.52678 12.4645 4.46447C11.5268 5.40215 11 6.67392 11 8"
stroke="#2563EB"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M27.0478 18.054C27.609 18.5733 27.609 19.4267 27.0478 19.946C25.221 21.6363 20.9622 25 16 25C11.0378 25 6.77899 21.6363 4.95219 19.946C4.39099 19.4267 4.39099 18.5733 4.95219 18.054C6.77899 16.3637 11.0378 13 16 13C20.9622 13 25.221 16.3637 27.0478 18.054Z"
fill="#C8D1EA"
/>
<circle cx="16" cy="19" r="4" fill="#2563EB" />
<path
d="M19.25 17.5C19.9404 17.5 20.5 16.9404 20.5 16.25C20.5 15.5596 19.9404 15 19.25 15C18.5596 15 18 15.5596 18 16.25C18 16.9404 18.5596 17.5 19.25 17.5Z"
fill="white"
/>
<path
d="M17.75 18.5C18.1642 18.5 18.5 18.1642 18.5 17.75C18.5 17.3358 18.1642 17 17.75 17C17.3358 17 17 17.3358 17 17.75C17 18.1642 17.3358 18.5 17.75 18.5Z"
fill="white"
/>
</svg>
<h6 className="text-base font-semibold">Help Us Improve Jan</h6>
</div>
<p className="text-[hsla(var(--text-secondary))]">
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&nbsp;
<span className="font-semibold">{`Settings > Privacy.`}</span>
</p>
<p className="mt-6 text-[hsla(var(--text-secondary))]">
Would you like to help us to improve Jan?
</p>
<div className="mt-6 flex items-center gap-x-2">
<Button
onClick={() => {
handleProductAnalytics(true)
}}
>
Allow
</Button>
<Button
data-testid="btn-deny-product-analytics"
theme="ghost"
variant="outline"
onClick={() => {
handleProductAnalytics(false)
}}
>
Deny
</Button>
</div>
</div>
)}
</div>
<BottomPanel />
</div>

View File

@ -106,7 +106,7 @@ const LeftPanelContainer = ({ children }: Props) => {
<Fragment>
<div
className={twMerge(
'group/resize absolute right-0 top-0 z-[9999] h-full w-1 flex-shrink-0 flex-grow-0 resize-x blur-sm hover:cursor-col-resize hover:bg-[hsla(var(--resize-bg))]',
'group/resize absolute right-0 top-0 z-40 h-full w-1 flex-shrink-0 flex-grow-0 resize-x blur-sm hover:cursor-col-resize hover:bg-[hsla(var(--resize-bg))]',
isResizing && 'cursor-col-resize bg-[hsla(var(--resize-bg))]',
!reduceTransparent && 'shadow-sm'
)}

View File

@ -109,7 +109,7 @@ const RightPanelContainer = ({ children }: Props) => {
<Fragment>
<div
className={twMerge(
'group/resize absolute left-0 top-0 z-[9999] h-full w-1 flex-shrink-0 flex-grow-0 resize-x blur-sm hover:cursor-col-resize hover:bg-[hsla(var(--resize-bg))]',
'group/resize absolute left-0 top-0 z-40 h-full w-1 flex-shrink-0 flex-grow-0 resize-x blur-sm hover:cursor-col-resize hover:bg-[hsla(var(--resize-bg))]',
isResizing && 'cursor-col-resize bg-[hsla(var(--resize-bg))]',
!reduceTransparent && 'shadow-sm'
)}

View File

@ -11,6 +11,8 @@ export const janSettingScreenAtom = atom<SettingScreen[]>([])
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<boolean>(
undefined,
{ getOnInit: true }
)
export const productAnalyticAtom = atomWithStorage<boolean>(
PRODUCT_ANALYTIC,
false,
undefined,
{ getOnInit: true }
)
export const productAnalyticPromptAtom = atomWithStorage<boolean>(
PRODUCT_ANALYTIC_PROMPT,
true,
undefined,
{ getOnInit: true }
)

View File

@ -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://127.0.0.1:39291'

View File

@ -33,6 +33,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",

View File

@ -163,44 +163,38 @@ const LocalServerLeftPanel = () => {
</div>
</div>
<Tooltip
trigger={
<div className="p-3">
<p className="mb-2 block font-semibold">Server Options</p>
<div className="p-3">
<p className="mb-2 block font-semibold">Server Options</p>
<div className="flex w-full">
<Select
value={host}
onValueChange={(e) => setHost(e)}
disabled={serverEnabled}
options={hostOptions}
block
/>
</div>
<div className="flex w-full">
<Select
value={host}
onValueChange={(e) => setHost(e)}
disabled={serverEnabled}
options={hostOptions}
block
/>
</div>
<div className="relative z-50 mt-2 block">
<Input
className={twMerge(
errorRangePort && 'border-[hsla(var(--destructive-bg))]'
)}
type="number"
value={port}
onChange={(e) => {
handleChangePort(e.target.value)
}}
maxLength={5}
disabled={serverEnabled}
/>
</div>
{errorRangePort && (
<p className="mt-2 text-xs text-[hsla(var(--destructive-bg))]">{`The port range should be from 0 to 65536`}</p>
<div className="relative z-50 mt-2 block">
<Input
className={twMerge(
errorRangePort && 'border-[hsla(var(--destructive-bg))]'
)}
</div>
}
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}
/>
</div>
{errorRangePort && (
<p className="mt-2 text-xs text-[hsla(var(--destructive-bg))]">{`The port range should be from 0 to 65536`}</p>
)}
</div>
<div className="space-y-4 px-3">
<div className="block">

View File

@ -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(<Advanced />)
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(<Advanced />)
let proxyToggle

View File

@ -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).
</p>
</div>
<div className="flex-sharink-0">
<div className="flex-shrink-0">
<Switch
checked={vulkanEnabled}
onChange={(e) => updateVulkanEnabled(e.target.checked)}
@ -542,25 +523,6 @@ const Advanced = () => {
</div>
)}
{/* Clear log */}
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
<div className="flex-shrink-0 space-y-1">
<div className="flex gap-x-2">
<h6 className="font-semibold capitalize">Clear logs</h6>
</div>
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
Clear all logs from Jan app.
</p>
</div>
<Button
data-testid="clear-logs"
theme="destructive"
onClick={clearLogs}
>
Clear
</Button>
</div>
{/* Factory Reset */}
<FactoryReset />
</div>

View File

@ -133,10 +133,7 @@ const MyModelList = ({ model }: Props) => {
<div
className={twMerge(
'flex items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]',
serverEnabled &&
activeModel &&
activeModel.id !== model.id &&
'pointer-events-none cursor-not-allowed opacity-40'
serverEnabled && 'cursor-not-allowed opacity-40'
)}
onClick={() => {
onModelActionClick(model.id)
@ -172,8 +169,7 @@ const MyModelList = ({ model }: Props) => {
<div
className={twMerge(
'flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]',
serverEnabled &&
'pointer-events-none cursor-not-allowed opacity-40'
serverEnabled && ' cursor-not-allowed opacity-40'
)}
onClick={() => {
setTimeout(async () => {

View File

@ -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(<Privacy />)
await waitFor(() => {
expect(screen.getByText('Clear logs')).toBeInTheDocument()
})
})
it('clears logs', async () => {
const jestMock = jest.fn()
jest.spyOn(toast, 'toaster').mockImplementation(jestMock)
render(<Privacy />)
let clearLogsButton
await waitFor(() => {
clearLogsButton = screen.getByTestId(/clear-logs/i)
fireEvent.click(clearLogsButton)
})
expect(clearLogsButton).toBeInTheDocument()
expect(jestMock).toHaveBeenCalled()
})
})

View File

@ -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 (
<ScrollArea className="h-full w-full px-4">
<div className="mb-4 mt-8 rounded-xl bg-[hsla(var(--tertiary-bg))] px-4 py-2 text-[hsla(var(--text-secondary))]">
<p>
We prioritize your control over your data. Learn more about our&nbsp;
<a
href="https://jan.ai/docs/privacy"
target="_blank"
className="text-[hsla(var(--app-link))]"
>
Privacy Policy.
</a>
</p>
<br />
<p>
To make Jan better, we need to understand how its used - but only if
you choose to help. You can change your Jan Analytics settings
anytime.
</p>
<br />
<p>
{`Your choice to opt-in or out doesn't change our core privacy promises:`}
</p>
<ul className="list-inside list-disc pl-4">
<li>Your chats are never read</li>
<li>No personal information is collected</li>
<li>No accounts or logins required</li>
<li>We dont access your files</li>
<li>Your chat history and settings stay on your device</li>
</ul>
</div>
<div className="block w-full py-4">
{/* Analytic */}
<div className="flex w-full flex-col justify-between gap-x-20 gap-y-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row sm:items-center">
<div className="space-y-1">
<div className="flex gap-x-2">
<h6 className="font-semibold capitalize">Analytics</h6>
</div>
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
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.
</p>
</div>
<div className="flex-shrink-0">
<Switch
checked={productAnalytic}
onChange={(e) => {
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)
}}
/>
</div>
</div>
{/* Logs */}
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
<div className="space-y-1">
<div className="flex gap-x-2">
<h6 className="font-semibold capitalize">Logs</h6>
</div>
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
Open App Logs and Cortex Logs
</p>
</div>
<div className="flex items-center gap-x-3">
<div className="relative">
<Input
data-testid="jan-data-folder-input"
value={janDataFolderPath + '/logs'}
className="w-full pr-8 sm:w-[240px]"
disabled
/>
<FolderOpenIcon
size={16}
className="absolute right-2 top-1/2 z-10 -translate-y-1/2 cursor-pointer"
onClick={() => onRevealInFinder('Logs')}
/>
</div>
</div>
</div>
{/* Clear log */}
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
<div className="flex-shrink-0 space-y-1">
<div className="flex gap-x-2">
<h6 className="font-semibold capitalize">Clear logs</h6>
</div>
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
Clear all logs from Jan app.
</p>
</div>
<Button
data-testid="clear-logs"
theme="destructive"
variant="soft"
onClick={clearLogs}
>
Clear
</Button>
</div>
</div>
</ScrollArea>
)
}
export default Privacy

View File

@ -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 <Hotkeys />
case 'Privacy':
return <Privacy />
case 'Advanced Settings':
return <Advanced />

View File

@ -15,6 +15,7 @@ export const SettingScreenList = [
'My Models',
'Appearance',
'Keyboard Shortcuts',
'Privacy',
'Advanced Settings',
'Extensions',
] as const

View File

@ -225,7 +225,11 @@ const RichTextEditor = ({
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
if (
event.key === 'Enter' &&
!event.shiftKey &&
event.nativeEvent.isComposing === false
) {
event.preventDefault()
if (messages[messages.length - 1]?.status !== MessageStatus.Pending) {
sendChatMessage(currentPrompt)

View File

@ -85,24 +85,27 @@ const MessageContainer: React.FC<
<p className="text-xs font-medium text-gray-400">
{props.created && displayDate(props.created ?? new Date())}
</p>
{tokenSpeed &&
tokenSpeed.message === props.id &&
tokenSpeed.tokenSpeed > 0 && (
<p className="absolute right-8 text-xs font-medium text-[hsla(var(--text-secondary))]">
Token Speed: {Number(tokenSpeed.tokenSpeed).toFixed(2)}t/s
</p>
)}
</div>
<div className="flex w-full flex-col">
<div className="flex w-full flex-col ">
<div
className={twMerge(
'absolute right-0 order-1 mt-2 flex cursor-pointer items-center justify-start gap-x-2 transition-all',
props.isCurrentMessage && !isUser
? 'relative order-2 flex justify-end'
? 'relative left-0 order-2 flex w-full justify-between'
: 'hidden group-hover:absolute group-hover:right-4 group-hover:top-4 group-hover:flex'
)}
>
<div>
{tokenSpeed &&
tokenSpeed.message === props.id &&
tokenSpeed.tokenSpeed > 0 && (
<p className="text-xs font-medium text-[hsla(var(--text-secondary))]">
Token Speed: {Number(tokenSpeed.tokenSpeed).toFixed(2)}t/s
</p>
)}
</div>
<MessageToolbar message={props} />
</div>
<div

View File

@ -6,6 +6,8 @@ export {}
declare global {
declare const VERSION: string
declare const ANALYTICS_ID: string
declare const POSTHOG_KEY: string
declare const POSTHOG_HOST: string
declare const ANALYTICS_HOST: string
declare const API_BASE_URL: string
declare const isMac: boolean