feat: dynamically register extension settings (#2494)

* feat: add extesion settings

Signed-off-by: James <james@jan.ai>

---------

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
Co-authored-by: Louis <louis@jan.ai>
This commit is contained in:
NamH 2024-03-29 15:44:46 +07:00 committed by GitHub
parent ec6bcf6357
commit fa35aa6e14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 1744 additions and 1334 deletions

View File

@ -1,3 +1,7 @@
import { SettingComponentProps } from '../types'
import { getJanDataFolderPath, joinPath } from './core'
import { fs } from './fs'
export enum ExtensionTypeEnum {
Assistant = 'assistant',
Conversational = 'conversational',
@ -32,6 +36,38 @@ export type InstallationState = InstallationStateTuple[number]
* This class should be extended by any class that represents an extension.
*/
export abstract class BaseExtension implements ExtensionType {
protected settingFolderName = 'settings'
protected settingFileName = 'settings.json'
/** @type {string} Name of the extension. */
name?: string
/** @type {string} The URL of the extension to load. */
url: string
/** @type {boolean} Whether the extension is activated or not. */
active
/** @type {string} Extension's description. */
description
/** @type {string} Extension's version. */
version
constructor(
url: string,
name?: string,
active?: boolean,
description?: string,
version?: string
) {
this.name = name
this.url = url
this.active = active
this.description = description
this.version = version
}
/**
* Returns the type of the extension.
* @returns {ExtensionType} The type of the extension
@ -40,11 +76,13 @@ export abstract class BaseExtension implements ExtensionType {
type(): ExtensionTypeEnum | undefined {
return undefined
}
/**
* Called when the extension is loaded.
* Any initialization logic for the extension should be put here.
*/
abstract onLoad(): void
/**
* Called when the extension is unloaded.
* Any cleanup logic for the extension should be put here.
@ -67,6 +105,42 @@ export abstract class BaseExtension implements ExtensionType {
return false
}
async registerSettings(settings: SettingComponentProps[]): Promise<void> {
if (!this.name) {
console.error('Extension name is not defined')
return
}
const extensionSettingFolderPath = await joinPath([
await getJanDataFolderPath(),
'settings',
this.name,
])
settings.forEach((setting) => {
setting.extensionName = this.name
})
try {
await fs.mkdir(extensionSettingFolderPath)
const settingFilePath = await joinPath([extensionSettingFolderPath, this.settingFileName])
if (await fs.existsSync(settingFilePath)) return
await fs.writeFileSync(settingFilePath, JSON.stringify(settings, null, 2))
} catch (err) {
console.error(err)
}
}
async getSetting<T>(key: string, defaultValue: T) {
const keySetting = (await this.getSettings()).find((setting) => setting.key === key)
const value = keySetting?.controllerProps.value
return (value as T) ?? defaultValue
}
onSettingUpdate<T>(key: string, value: T) {
return
}
/**
* Determine if the prerequisites for the extension are installed.
*
@ -81,8 +155,59 @@ export abstract class BaseExtension implements ExtensionType {
*
* @returns {Promise<void>}
*/
// @ts-ignore
async install(...args): Promise<void> {
async install(): Promise<void> {
return
}
async getSettings(): Promise<SettingComponentProps[]> {
if (!this.name) return []
const settingPath = await joinPath([
await getJanDataFolderPath(),
this.settingFolderName,
this.name,
this.settingFileName,
])
try {
const content = await fs.readFileSync(settingPath, 'utf-8')
const settings: SettingComponentProps[] = JSON.parse(content)
return settings
} catch (err) {
console.warn(err)
return []
}
}
async updateSettings(componentProps: Partial<SettingComponentProps>[]): Promise<void> {
if (!this.name) return
const settings = await this.getSettings()
const updatedSettings = settings.map((setting) => {
const updatedSetting = componentProps.find(
(componentProp) => componentProp.key === setting.key
)
if (updatedSetting && updatedSetting.controllerProps) {
setting.controllerProps.value = updatedSetting.controllerProps.value
}
return setting
})
const settingPath = await joinPath([
await getJanDataFolderPath(),
this.settingFolderName,
this.name,
this.settingFileName,
])
await fs.writeFileSync(settingPath, JSON.stringify(updatedSettings, null, 2))
updatedSettings.forEach((setting) => {
this.onSettingUpdate<typeof setting.controllerProps.value>(
setting.key,
setting.controllerProps.value
)
})
}
}

View File

@ -48,7 +48,7 @@ export abstract class OAIEngine extends AIEngine {
/*
* Inference request
*/
override inference(data: MessageRequest) {
override async inference(data: MessageRequest) {
if (data.model?.engine?.toString() !== this.provider) return
const timestamp = Date.now()
@ -77,12 +77,14 @@ export abstract class OAIEngine extends AIEngine {
...data.model,
}
const header = await this.headers()
requestInference(
this.inferenceUrl,
data.messages ?? [],
model,
this.controller,
this.headers()
header
).subscribe({
next: (content: any) => {
const messageContent: ThreadContent = {
@ -123,7 +125,7 @@ export abstract class OAIEngine extends AIEngine {
/**
* Headers for the inference request
*/
headers(): HeadersInit {
async headers(): Promise<HeadersInit> {
return {}
}
}

View File

@ -5,8 +5,7 @@ import { OAIEngine } from './OAIEngine'
* Added the implementation of loading and unloading model (applicable to local inference providers)
*/
export abstract class RemoteOAIEngine extends OAIEngine {
// The inference engine
abstract apiKey: string
apiKey?: string
/**
* On extension load, subscribe to events.
*/
@ -17,10 +16,12 @@ export abstract class RemoteOAIEngine extends OAIEngine {
/**
* Headers for the inference request
*/
override headers(): HeadersInit {
override async headers(): Promise<HeadersInit> {
return {
'Authorization': `Bearer ${this.apiKey}`,
'api-key': `${this.apiKey}`,
...(this.apiKey && {
'Authorization': `Bearer ${this.apiKey}`,
'api-key': `${this.apiKey}`,
}),
}
}
}

View File

@ -5,19 +5,16 @@ export type Handler = (route: string, args: any) => any
export class RequestHandler {
handler: Handler
adataper: RequestAdapter
adapter: RequestAdapter
constructor(handler: Handler, observer?: Function) {
this.handler = handler
this.adataper = new RequestAdapter(observer)
this.adapter = new RequestAdapter(observer)
}
handle() {
CoreRoutes.map((route) => {
this.handler(route, async (...args: any[]) => {
const values = await this.adataper.process(route, ...args)
return values
})
this.handler(route, async (...args: any[]) => this.adapter.process(route, ...args))
})
}
}

View File

@ -316,6 +316,7 @@ export const chatCompletions = async (request: any, reply: any) => {
}
const requestedModel = matchedModels[0]
const engineConfiguration = await getEngineConfiguration(requestedModel.engine)
let apiKey: string | undefined = undefined
@ -323,7 +324,7 @@ export const chatCompletions = async (request: any, reply: any) => {
if (engineConfiguration) {
apiKey = engineConfiguration.api_key
apiUrl = engineConfiguration.full_url
apiUrl = engineConfiguration.full_url ?? DEFAULT_CHAT_COMPLETION_URL
}
const headers: Record<string, any> = {

View File

@ -1,4 +1,4 @@
import { AppConfiguration } from '../../types'
import { AppConfiguration, SettingComponentProps } from '../../types'
import { join } from 'path'
import fs from 'fs'
import os from 'os'
@ -125,14 +125,30 @@ const exec = async (command: string): Promise<string> => {
})
}
// a hacky way to get the api key. we should comes up with a better
// way to handle this
export const getEngineConfiguration = async (engineId: string) => {
if (engineId !== 'openai' && engineId !== 'groq') {
return undefined
if (engineId !== 'openai' && engineId !== 'groq') return undefined
const settingDirectoryPath = join(
getJanDataFolderPath(),
'settings',
engineId === 'openai' ? 'inference-openai-extension' : 'inference-groq-extension',
'settings.json'
)
const content = fs.readFileSync(settingDirectoryPath, 'utf-8')
const settings: SettingComponentProps[] = JSON.parse(content)
const apiKeyId = engineId === 'openai' ? 'openai-api-key' : 'groq-api-key'
const keySetting = settings.find((setting) => setting.key === apiKeyId)
let apiKey = keySetting?.controllerProps.value
if (typeof apiKey !== 'string') apiKey = ''
return {
api_key: apiKey,
full_url: undefined,
}
const directoryPath = join(getJanDataFolderPath(), 'engines')
const filePath = join(directoryPath, `${engineId}.json`)
const data = fs.readFileSync(filePath, 'utf-8')
return JSON.parse(data)
}
/**

View File

@ -9,3 +9,4 @@ export * from './config'
export * from './huggingface'
export * from './miscellaneous'
export * from './api'
export * from './setting'

View File

@ -0,0 +1 @@
export * from './settingComponent'

View File

@ -0,0 +1,34 @@
export type SettingComponentProps = {
key: string
title: string
description: string
controllerType: ControllerType
controllerProps: SliderComponentProps | CheckboxComponentProps | InputComponentProps
extensionName?: string
requireModelReload?: boolean
configType?: ConfigType
}
export type ConfigType = 'runtime' | 'setting'
export type ControllerType = 'slider' | 'checkbox' | 'input'
export type InputType = 'password' | 'text' | 'email' | 'number' | 'tel' | 'url'
export type InputComponentProps = {
placeholder: string
value: string
type?: InputType
}
export type SliderComponentProps = {
min: number
max: number
step: number
value: number
}
export type CheckboxComponentProps = {
value: boolean
}

View File

@ -7,12 +7,10 @@ import replace from '@rollup/plugin-replace'
const packageJson = require('./package.json')
const pkg = require('./package.json')
export default [
{
input: `src/index.ts`,
output: [{ file: pkg.main, format: 'es', sourcemap: true }],
output: [{ file: packageJson.main, format: 'es', sourcemap: true }],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
@ -36,7 +34,7 @@ export default [
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({
extensions: ['.js', '.ts', '.svelte'],
browser: true
browser: true,
}),
// Resolve source maps to the original source

View File

@ -1,13 +1,36 @@
import fs from 'fs'
import path from 'path'
import { getJanDataFolderPath } from '@janhq/core/node'
import { SettingComponentProps, getJanDataFolderPath } from '@janhq/core/node'
// Sec: Do not send engine settings over requests
// Read it manually instead
export const readEmbeddingEngine = (engineName: string) => {
const engineSettings = fs.readFileSync(
path.join(getJanDataFolderPath(), 'engines', `${engineName}.json`),
'utf-8'
)
return JSON.parse(engineSettings)
if (engineName !== 'openai' && engineName !== 'groq') {
const engineSettings = fs.readFileSync(
path.join(getJanDataFolderPath(), 'engines', `${engineName}.json`),
'utf-8'
)
return JSON.parse(engineSettings)
} else {
const settingDirectoryPath = path.join(
getJanDataFolderPath(),
'settings',
engineName === 'openai'
? 'inference-openai-extension'
: 'inference-groq-extension',
'settings.json'
)
const content = fs.readFileSync(settingDirectoryPath, 'utf-8')
const settings: SettingComponentProps[] = JSON.parse(content)
const apiKeyId = engineName === 'openai' ? 'openai-api-key' : 'groq-api-key'
const keySetting = settings.find((setting) => setting.key === apiKeyId)
let apiKey = keySetting?.controllerProps.value
if (typeof apiKey !== 'string') apiKey = ''
return {
api_key: apiKey,
}
}
}

View File

@ -0,0 +1,23 @@
[
{
"key": "chat-completions-endpoint",
"title": "Chat Completions Endpoint",
"description": "The endpoint to use for chat completions. See the [Groq Documentation](https://console.groq.com/docs/openai) for more information.",
"controllerType": "input",
"controllerProps": {
"placeholder": "Chat Completions Endpoint",
"value": "https://api.groq.com/openai/v1/chat/completions"
}
},
{
"key": "groq-api-key",
"title": "API Key",
"description": "The Groq API uses API keys for authentication. Visit your [API Keys](https://console.groq.com/keys) page to retrieve the API key you'll use in your requests.",
"controllerType": "input",
"controllerProps": {
"placeholder": "API Key",
"value": "",
"type": "password"
}
}
]

View File

@ -6,78 +6,41 @@
* @module inference-groq-extension/src/index
*/
import {
events,
fs,
AppConfigurationEventName,
joinPath,
RemoteOAIEngine,
} from '@janhq/core'
import { join } from 'path'
import { RemoteOAIEngine } from '@janhq/core'
declare const COMPLETION_URL: string
declare const SETTINGS: Array<any>
enum Settings {
apiKey = 'groq-api-key',
chatCompletionsEndPoint = 'chat-completions-endpoint',
}
/**
* A class that implements the InferenceExtension interface from the @janhq/core package.
* The class provides methods for initializing and stopping a model, and for making inference requests.
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/
export default class JanInferenceGroqExtension extends RemoteOAIEngine {
private readonly _engineDir = 'file://engines'
private readonly _engineMetadataFileName = 'groq.json'
inferenceUrl: string = COMPLETION_URL
inferenceUrl: string = ''
provider = 'groq'
apiKey = ''
private _engineSettings = {
full_url: COMPLETION_URL,
api_key: 'gsk-<your key here>',
}
/**
* Subscribes to events emitted by the @janhq/core package.
*/
async onLoad() {
override async onLoad(): Promise<void> {
super.onLoad()
if (!(await fs.existsSync(this._engineDir))) {
await fs.mkdir(this._engineDir)
}
// Register Settings
this.registerSettings(SETTINGS)
this.writeDefaultEngineSettings()
const settingsFilePath = await joinPath([
this._engineDir,
this._engineMetadataFileName,
])
// Events subscription
events.on(
AppConfigurationEventName.OnConfigurationUpdate,
(settingsKey: string) => {
// Update settings on changes
if (settingsKey === settingsFilePath) this.writeDefaultEngineSettings()
}
// Retrieve API Key Setting
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
this.inferenceUrl = await this.getSetting<string>(
Settings.chatCompletionsEndPoint,
''
)
}
async writeDefaultEngineSettings() {
try {
const engineFile = join(this._engineDir, this._engineMetadataFileName)
if (await fs.existsSync(engineFile)) {
const engine = await fs.readFileSync(engineFile, 'utf-8')
this._engineSettings =
typeof engine === 'object' ? engine : JSON.parse(engine)
this.inferenceUrl = this._engineSettings.full_url
this.apiKey = this._engineSettings.api_key
} else {
await fs.writeFileSync(
engineFile,
JSON.stringify(this._engineSettings, null, 2)
)
}
} catch (err) {
console.error(err)
onSettingUpdate<T>(key: string, value: T): void {
if (key === Settings.apiKey) {
this.apiKey = value as string
} else if (key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = value as string
}
}
}

View File

@ -1,6 +1,7 @@
const path = require('path')
const webpack = require('webpack')
const packageJson = require('./package.json')
const settingJson = require('./resources/settings.json')
module.exports = {
experiments: { outputModule: true },
@ -17,8 +18,8 @@ module.exports = {
},
plugins: [
new webpack.DefinePlugin({
SETTINGS: JSON.stringify(settingJson),
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
COMPLETION_URL: JSON.stringify('https://api.groq.com/openai/v1/chat/completions'),
}),
],
output: {

View File

@ -0,0 +1,33 @@
[
{
"key": "test",
"title": "Test",
"description": "Test",
"controllerType": "input",
"controllerProps": {
"placeholder": "Test",
"value": ""
}
},
{
"key": "embedding",
"title": "Embedding",
"description": "Whether to enable embedding.",
"controllerType": "checkbox",
"controllerProps": {
"value": true
}
},
{
"key": "ctx_len",
"title": "Context Length",
"description": "The context length for model operations varies; the maximum depends on the specific model used.",
"controllerType": "slider",
"controllerProps": {
"min": 0,
"max": 4096,
"step": 128,
"value": 4096
}
}
]

View File

@ -5,13 +5,12 @@ import typescript from 'rollup-plugin-typescript2'
import json from '@rollup/plugin-json'
import replace from '@rollup/plugin-replace'
const packageJson = require('./package.json')
const pkg = require('./package.json')
const defaultSettingJson = require('./resources/default_settings.json')
export default [
{
input: `src/index.ts`,
output: [{ file: pkg.main, format: 'es', sourcemap: true }],
output: [{ file: packageJson.main, format: 'es', sourcemap: true }],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
@ -19,7 +18,9 @@ export default [
},
plugins: [
replace({
EXTENSION_NAME: JSON.stringify(packageJson.name),
NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`),
DEFAULT_SETTINGS: JSON.stringify(defaultSettingJson),
INFERENCE_URL: JSON.stringify(
process.env.INFERENCE_URL ||
'http://127.0.0.1:3928/inferences/llamacpp/chat_completion'

View File

@ -2,6 +2,8 @@ declare const NODE: string
declare const INFERENCE_URL: string
declare const TROUBLESHOOTING_URL: string
declare const JAN_SERVER_INFERENCE_URL: string
declare const EXTENSION_NAME: string
declare const DEFAULT_SETTINGS: Array<any>
/**
* The response from the initModel function.

View File

@ -58,8 +58,6 @@ export default class JanInferenceNitroExtension extends LocalOAIEngine {
this.inferenceUrl = `${window.core?.api?.baseApiUrl}/v1/chat/completions`
}
console.debug('Inference url: ', this.inferenceUrl)
this.getNitroProcesHealthIntervalId = setInterval(
() => this.periodicallyGetNitroHealth(),
JanInferenceNitroExtension._intervalHealthCheck

View File

@ -0,0 +1,23 @@
[
{
"key": "chat-completions-endpoint",
"title": "Chat Completions Endpoint",
"description": "The endpoint to use for chat completions. See the [OpenAI API documentation](https://platform.openai.com/docs/api-reference/chat/create) for more information.",
"controllerType": "input",
"controllerProps": {
"placeholder": "Chat Completions Endpoint",
"value": "https://api.openai.com/v1/chat/completions"
}
},
{
"key": "openai-api-key",
"title": "API Key",
"description": "The OpenAI API uses API keys for authentication. Visit your [API Keys](https://platform.openai.com/account/api-keys) page to retrieve the API key you'll use in your requests.",
"controllerType": "input",
"controllerProps": {
"placeholder": "API Key",
"value": "",
"type": "password"
}
}
]

View File

@ -5,85 +5,41 @@
* @version 1.0.0
* @module inference-openai-extension/src/index
*/
declare const ENGINE: string
import {
events,
fs,
AppConfigurationEventName,
joinPath,
RemoteOAIEngine,
} from '@janhq/core'
import { join } from 'path'
declare const COMPLETION_URL: string
import { RemoteOAIEngine } from '@janhq/core'
declare const SETTINGS: Array<any>
enum Settings {
apiKey = 'openai-api-key',
chatCompletionsEndPoint = 'chat-completions-endpoint',
}
/**
* A class that implements the InferenceExtension interface from the @janhq/core package.
* The class provides methods for initializing and stopping a model, and for making inference requests.
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/
export default class JanInferenceOpenAIExtension extends RemoteOAIEngine {
private static readonly _engineDir = 'file://engines'
private static readonly _engineMetadataFileName = `${ENGINE}.json`
private _engineSettings = {
full_url: COMPLETION_URL,
api_key: 'sk-<your key here>',
}
inferenceUrl: string = COMPLETION_URL
inferenceUrl: string = ''
provider: string = 'openai'
apiKey: string = ''
// TODO: Just use registerSettings from BaseExtension
// Remove these methods
/**
* Subscribes to events emitted by the @janhq/core package.
*/
async onLoad() {
override async onLoad(): Promise<void> {
super.onLoad()
if (!(await fs.existsSync(JanInferenceOpenAIExtension._engineDir))) {
await fs.mkdir(JanInferenceOpenAIExtension._engineDir)
}
// Register Settings
this.registerSettings(SETTINGS)
this.writeDefaultEngineSettings()
const settingsFilePath = await joinPath([
JanInferenceOpenAIExtension._engineDir,
JanInferenceOpenAIExtension._engineMetadataFileName,
])
events.on(
AppConfigurationEventName.OnConfigurationUpdate,
(settingsKey: string) => {
// Update settings on changes
if (settingsKey === settingsFilePath) this.writeDefaultEngineSettings()
}
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
this.inferenceUrl = await this.getSetting<string>(
Settings.chatCompletionsEndPoint,
''
)
}
async writeDefaultEngineSettings() {
try {
const engineFile = join(
JanInferenceOpenAIExtension._engineDir,
JanInferenceOpenAIExtension._engineMetadataFileName
)
if (await fs.existsSync(engineFile)) {
const engine = await fs.readFileSync(engineFile, 'utf-8')
this._engineSettings =
typeof engine === 'object' ? engine : JSON.parse(engine)
this.inferenceUrl = this._engineSettings.full_url
this.apiKey = this._engineSettings.api_key
} else {
await fs.writeFileSync(
engineFile,
JSON.stringify(this._engineSettings, null, 2)
)
}
} catch (err) {
console.error(err)
onSettingUpdate<T>(key: string, value: T): void {
if (key === Settings.apiKey) {
this.apiKey = value as string
} else if (key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = value as string
}
}
}

View File

@ -1,6 +1,7 @@
const path = require('path')
const webpack = require('webpack')
const packageJson = require('./package.json')
const settingJson = require('./resources/settings.json')
module.exports = {
experiments: { outputModule: true },
@ -17,8 +18,8 @@ module.exports = {
},
plugins: [
new webpack.DefinePlugin({
SETTINGS: JSON.stringify(settingJson),
ENGINE: JSON.stringify(packageJson.engine),
COMPLETION_URL: JSON.stringify('https://api.openai.com/v1/chat/completions'),
}),
],
output: {

View File

@ -0,0 +1,23 @@
[
{
"key": "chat-completions-endpoint",
"title": "Chat Completions Endpoint",
"description": "The endpoint to use for chat completions.",
"controllerType": "input",
"controllerProps": {
"placeholder": "Chat Completions Endpoint",
"value": "http://localhost:8000/v2/models/tensorrt_llm_bls/generate"
}
},
{
"key": "tritonllm-api-key",
"title": "Triton LLM API Key",
"description": "The Triton LLM API uses API keys for authentication.",
"controllerType": "input",
"controllerProps": {
"placeholder": "API Key",
"value": "",
"type": "password"
}
}
]

View File

@ -6,77 +6,44 @@
* @module inference-nvidia-triton-trt-llm-extension/src/index
*/
import {
AppConfigurationEventName,
events,
fs,
joinPath,
Model,
RemoteOAIEngine,
} from '@janhq/core'
import { join } from 'path'
import { RemoteOAIEngine } from '@janhq/core'
declare const SETTINGS: Array<any>
enum Settings {
apiKey = 'tritonllm-api-key',
chatCompletionsEndPoint = 'chat-completions-endpoint',
}
/**
* A class that implements the InferenceExtension interface from the @janhq/core package.
* The class provides methods for initializing and stopping a model, and for making inference requests.
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/
export default class JanInferenceTritonTrtLLMExtension extends RemoteOAIEngine {
private readonly _engineDir = 'file://engines'
private readonly _engineMetadataFileName = 'triton_trtllm.json'
inferenceUrl: string = ''
provider: string = 'triton_trtllm'
apiKey: string = ''
_engineSettings: {
base_url: ''
api_key: ''
}
/**
* Subscribes to events emitted by the @janhq/core package.
*/
async onLoad() {
super.onLoad()
if (!(await fs.existsSync(this._engineDir))) {
await fs.mkdir(this._engineDir)
}
this.writeDefaultEngineSettings()
// Register Settings
this.registerSettings(SETTINGS)
const settingsFilePath = await joinPath([
this._engineDir,
this._engineMetadataFileName,
])
// Events subscription
events.on(
AppConfigurationEventName.OnConfigurationUpdate,
(settingsKey: string) => {
// Update settings on changes
if (settingsKey === settingsFilePath) this.writeDefaultEngineSettings()
}
// Retrieve API Key Setting
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
this.inferenceUrl = await this.getSetting<string>(
Settings.chatCompletionsEndPoint,
''
)
}
async writeDefaultEngineSettings() {
try {
const engine_json = join(this._engineDir, this._engineMetadataFileName)
if (await fs.existsSync(engine_json)) {
const engine = await fs.readFileSync(engine_json, 'utf-8')
this._engineSettings =
typeof engine === 'object' ? engine : JSON.parse(engine)
this.inferenceUrl = this._engineSettings.base_url
this.apiKey = this._engineSettings.api_key
} else {
await fs.writeFileSync(
engine_json,
JSON.stringify(this._engineSettings, null, 2)
)
}
} catch (err) {
console.error(err)
onSettingUpdate<T>(key: string, value: T): void {
if (key === Settings.apiKey) {
this.apiKey = value as string
} else if (key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = value as string
}
}
}

View File

@ -1,6 +1,7 @@
const path = require('path')
const webpack = require('webpack')
const packageJson = require('./package.json')
const settingJson = require('./resources/settings.json')
module.exports = {
experiments: { outputModule: true },
@ -17,6 +18,7 @@ module.exports = {
},
plugins: [
new webpack.DefinePlugin({
SETTINGS: JSON.stringify(settingJson),
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
}),
],

View File

@ -1,5 +1,4 @@
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import sourceMaps from 'rollup-plugin-sourcemaps'
import typescript from 'rollup-plugin-typescript2'
import json from '@rollup/plugin-json'
@ -7,12 +6,10 @@ import replace from '@rollup/plugin-replace'
const packageJson = require('./package.json')
const pkg = require('./package.json')
export default [
{
input: `src/index.ts`,
output: [{ file: pkg.main, format: 'es', sourcemap: true }],
output: [{ file: packageJson.main, format: 'es', sourcemap: true }],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {

View File

@ -251,7 +251,7 @@ export default class TensorRTLLMExtension extends LocalOAIEngine {
return Promise.resolve()
}
override inference(data: MessageRequest): void {
override async inference(data: MessageRequest) {
if (!this.loadedModel) return
// TensorRT LLM Extension supports streaming only
if (data.model) data.model.parameters.stream = true

View File

@ -38,7 +38,6 @@ import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
import { toGibibytes } from '@/utils/converter'
import ModelLabel from '../ModelLabel'
import OpenAiKeyInput from '../OpenAiKeyInput'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
@ -144,7 +143,7 @@ const DropdownListSidebar = ({
// Update model parameter to the thread file
if (model)
updateModelParameter(activeThread.id, {
updateModelParameter(activeThread, {
params: modelParams,
modelId: model.id,
engine: model.engine,
@ -170,172 +169,164 @@ const DropdownListSidebar = ({
stateModel.model === selectedModel?.id && stateModel.loading
return (
<>
<div
className={twMerge(
'relative w-full overflow-hidden rounded-md',
stateModel.loading && 'pointer-events-none',
selectedModelLoading && 'bg-blue-200 text-blue-600'
)}
<div
className={twMerge(
'relative w-full overflow-hidden rounded-md',
stateModel.loading && 'pointer-events-none',
selectedModelLoading && 'bg-blue-200 text-blue-600'
)}
>
<Select
value={selectedModel?.id}
onValueChange={onValueSelected}
disabled={serverEnabled}
>
<Select
value={selectedModel?.id}
onValueChange={onValueSelected}
disabled={serverEnabled}
>
<SelectTrigger className="relative w-full">
<SelectValue placeholder="Choose model to start">
{selectedModelLoading && (
<div
className="z-5 absolute left-0 top-0 h-full w-full rounded-md bg-blue-100/80"
style={{ width: `${loader}%` }}
/>
)}
<span
className={twMerge(
'relative z-20',
selectedModelLoading && 'font-medium'
)}
>
{selectedModel?.name}
</span>
</SelectValue>
</SelectTrigger>
<SelectPortal>
<SelectContent
<SelectTrigger className="relative w-full">
<SelectValue placeholder="Choose model to start">
{selectedModelLoading && (
<div
className="z-5 absolute left-0 top-0 h-full w-full rounded-md bg-blue-100/80"
style={{ width: `${loader}%` }}
/>
)}
<span
className={twMerge(
'right-2 block w-full min-w-[450px] pr-0',
isTabActive === 1 && '[&_.select-scroll-down-button]:hidden'
'relative z-20',
selectedModelLoading && 'font-medium'
)}
>
<div className="relative px-2 py-2 dark:bg-secondary/50">
<ul className="inline-flex w-full space-x-2 rounded-lg bg-zinc-100 px-1 dark:bg-secondary">
{engineOptions.map((name, i) => {
return (
<li
{selectedModel?.name}
</span>
</SelectValue>
</SelectTrigger>
<SelectPortal>
<SelectContent
className={twMerge(
'right-2 block w-full min-w-[450px] pr-0',
isTabActive === 1 && '[&_.select-scroll-down-button]:hidden'
)}
>
<div className="relative px-2 py-2 dark:bg-secondary/50">
<ul className="inline-flex w-full space-x-2 rounded-lg bg-zinc-100 px-1 dark:bg-secondary">
{engineOptions.map((name, i) => {
return (
<li
className={twMerge(
'relative my-1 flex w-full cursor-pointer items-center justify-center space-x-2 px-2 py-2',
isTabActive === i &&
'rounded-md bg-background dark:bg-white'
)}
key={i}
onClick={() => setIsTabActive(i)}
>
{i === 0 ? (
<MonitorIcon
size={20}
className="z-50 text-muted-foreground"
/>
) : (
<GlobeIcon
size={20}
className="z-50 text-muted-foreground"
/>
)}
<span
className={twMerge(
'relative my-1 flex w-full cursor-pointer items-center justify-center space-x-2 px-2 py-2',
'relative z-50 font-medium text-muted-foreground',
isTabActive === i &&
'rounded-md bg-background dark:bg-white'
'font-bold text-foreground dark:text-black'
)}
key={i}
onClick={() => setIsTabActive(i)}
>
{i === 0 ? (
<MonitorIcon
size={20}
className="z-50 text-muted-foreground"
/>
) : (
<GlobeIcon
size={20}
className="z-50 text-muted-foreground"
/>
)}
<span
className={twMerge(
'relative z-50 font-medium text-muted-foreground',
isTabActive === i &&
'font-bold text-foreground dark:text-black'
)}
>
{name}
</span>
</li>
)
})}
</ul>
</div>
{name}
</span>
</li>
)
})}
</ul>
</div>
<div className="border-b border-border" />
{downloadedModels.length === 0 ? (
<div className="px-4 py-2">
<p>{`Oops, you don't have a model yet.`}</p>
</div>
) : (
<SelectGroup className="py-2">
<>
{modelOptions.map((x, i) => (
<div
key={i}
<div className="border-b border-border" />
{downloadedModels.length === 0 ? (
<div className="px-4 py-2">
<p>{`Oops, you don't have a model yet.`}</p>
</div>
) : (
<SelectGroup className="py-2">
<>
{modelOptions.map((x, i) => (
<div
key={i}
className={twMerge(
x.id === selectedModel?.id && 'bg-secondary',
'hover:bg-secondary'
)}
>
<SelectItem
value={x.id}
className={twMerge(
x.id === selectedModel?.id && 'bg-secondary',
'hover:bg-secondary'
'my-0 pb-8 pt-4'
)}
>
<SelectItem
value={x.id}
className={twMerge(
x.id === selectedModel?.id && 'bg-secondary',
'my-0 pb-8 pt-4'
)}
>
<div className="relative flex w-full justify-between">
<div>
<span className="line-clamp-1 block">
{x.name}
</span>
<div className="absolute right-0 top-2 space-x-2">
<span className="font-bold text-muted-foreground">
{toGibibytes(x.metadata.size)}
</span>
{x.metadata.size && (
<ModelLabel size={x.metadata.size} />
)}
</div>
</div>
<div className="relative flex w-full justify-between">
<span className="line-clamp-1 block">{x.name}</span>
<div className="absolute right-0 top-2 space-x-2">
<span className="font-bold text-muted-foreground">
{toGibibytes(x.metadata.size)}
</span>
{x.metadata.size && (
<ModelLabel size={x.metadata.size} />
)}
</div>
</SelectItem>
<div
className={twMerge(
'absolute -mt-6 inline-flex items-center space-x-2 px-4 pb-2 text-muted-foreground'
)}
>
<span className="text-xs">{x.id}</span>
{clipboard.copied && copyId === x.id ? (
<CheckIcon size={16} className="text-green-600" />
) : (
<CopyIcon
size={16}
className="z-20 cursor-pointer"
onClick={() => {
clipboard.copy(x.id)
setCopyId(x.id)
}}
/>
)}
</div>
</SelectItem>
<div
className={twMerge(
'absolute -mt-6 inline-flex items-center space-x-2 px-4 pb-2 text-muted-foreground'
)}
>
<span className="text-xs">{x.id}</span>
{clipboard.copied && copyId === x.id ? (
<CheckIcon size={16} className="text-green-600" />
) : (
<CopyIcon
size={16}
className="z-20 cursor-pointer"
onClick={() => {
clipboard.copy(x.id)
setCopyId(x.id)
}}
/>
)}
</div>
))}
</>
</SelectGroup>
)}
<div className="border-b border-border" />
<div className="flex w-full space-x-2 px-4 py-2">
<Button
block
themes="secondary"
onClick={() => setMainViewState(MainViewState.Settings)}
>
<FoldersIcon size={20} className="mr-2" />
<span>My Models</span>
</Button>
<Button
block
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
onClick={() => setMainViewState(MainViewState.Hub)}
>
<LayoutGridIcon size={20} className="mr-2" />
<span>Explore The Hub</span>
</Button>
</div>
</SelectContent>
</SelectPortal>
</Select>
</div>
<OpenAiKeyInput />
</>
</div>
))}
</>
</SelectGroup>
)}
<div className="border-b border-border" />
<div className="flex w-full space-x-2 px-4 py-2">
<Button
block
themes="secondary"
onClick={() => setMainViewState(MainViewState.Settings)}
>
<FoldersIcon size={20} className="mr-2" />
<span>My Models</span>
</Button>
<Button
block
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
onClick={() => setMainViewState(MainViewState.Hub)}
>
<LayoutGridIcon size={20} className="mr-2" />
<span>Explore The Hub</span>
</Button>
</div>
</SelectContent>
</SelectPortal>
</Select>
</div>
)
}

View File

@ -26,33 +26,31 @@ const ModelConfigInput: React.FC<Props> = ({
description,
placeholder,
onValueChanged,
}) => {
return (
<div className="flex flex-col">
<div className="mb-2 flex items-center gap-x-2">
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
{title}
</p>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" />
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]">
<span>{description}</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
<Textarea
placeholder={placeholder}
onChange={(e) => onValueChanged?.(e.target.value)}
value={value}
disabled={!enabled}
/>
}) => (
<div className="flex flex-col">
<div className="mb-2 flex items-center gap-x-2">
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
{title}
</p>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" />
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]">
<span>{description}</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
)
}
<Textarea
placeholder={placeholder}
onChange={(e) => onValueChanged?.(e.target.value)}
value={value}
disabled={!enabled}
/>
</div>
)
export default ModelConfigInput

View File

@ -1,84 +0,0 @@
import React, { useEffect, useState } from 'react'
import { InferenceEngine } from '@janhq/core'
import { Input } from '@janhq/uikit'
import { useAtomValue } from 'jotai'
import { useEngineSettings } from '@/hooks/useEngineSettings'
import { selectedModelAtom } from '../DropdownListSidebar'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
const OpenAiKeyInput: React.FC = () => {
const selectedModel = useAtomValue(selectedModelAtom)
const serverEnabled = useAtomValue(serverEnabledAtom)
const [openAISettings, setOpenAISettings] = useState<
{ api_key: string } | undefined
>(undefined)
const { readOpenAISettings, saveOpenAISettings } = useEngineSettings()
const [groqSettings, setGroqSettings] = useState<
{ api_key: string } | undefined
>(undefined)
const { readGroqSettings, saveGroqSettings } = useEngineSettings()
useEffect(() => {
readOpenAISettings().then((settings) => {
setOpenAISettings(settings)
})
}, [readOpenAISettings])
useEffect(() => {
readGroqSettings().then((settings) => {
setGroqSettings(settings)
})
}, [readGroqSettings])
if (
!selectedModel ||
(selectedModel.engine !== InferenceEngine.openai &&
selectedModel.engine !== InferenceEngine.groq)
) {
return null
}
const getCurrentApiKey = () => {
if (selectedModel.engine === InferenceEngine.openai) {
return openAISettings?.api_key
} else if (selectedModel.engine === InferenceEngine.groq) {
return groqSettings?.api_key
}
return '' // Default return value
}
const handleApiKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newApiKey = e.target.value
if (selectedModel.engine === InferenceEngine.openai) {
saveOpenAISettings({ apiKey: newApiKey })
} else if (selectedModel.engine === InferenceEngine.groq) {
saveGroqSettings({ apiKey: newApiKey })
}
}
return (
<div className="my-4">
<label
id="thread-title"
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
>
API Key
</label>
<Input
disabled={serverEnabled}
id="assistant-instructions"
placeholder={getCurrentApiKey()}
defaultValue={getCurrentApiKey()}
onChange={handleApiKeyChange}
/>
</div>
)
}
export default OpenAiKeyInput

View File

@ -10,11 +10,14 @@ import useGetSystemResources from '@/hooks/useGetSystemResources'
import useModels from '@/hooks/useModels'
import useThreads from '@/hooks/useThreads'
import { SettingScreenList } from '@/screens/Settings'
import { defaultJanDataFolderAtom } from '@/helpers/atoms/App.atom'
import {
janDataFolderPathAtom,
quickAskEnabledAtom,
} from '@/helpers/atoms/AppConfig.atom'
import { janSettingScreenAtom } from '@/helpers/atoms/Setting.atom'
type Props = {
children: ReactNode
@ -24,6 +27,7 @@ const DataLoader: React.FC<Props> = ({ children }) => {
const setJanDataFolderPath = useSetAtom(janDataFolderPathAtom)
const setQuickAskEnabled = useSetAtom(quickAskEnabledAtom)
const setJanDefaultDataFolder = useSetAtom(defaultJanDataFolderAtom)
const setJanSettingScreen = useSetAtom(janSettingScreenAtom)
useModels()
useThreads()
@ -49,6 +53,13 @@ const DataLoader: React.FC<Props> = ({ children }) => {
getDefaultJanDataFolder()
}, [setJanDefaultDataFolder])
useEffect(() => {
const janSettingScreen = SettingScreenList.filter(
(screen) => window.electronAPI || screen !== 'Extensions'
)
setJanSettingScreen(janSettingScreen)
}, [setJanSettingScreen])
console.debug('Load Data...')
return <Fragment>{children}</Fragment>

View File

@ -119,7 +119,13 @@ export class ExtensionManager {
) {
this.register(
extension.name ?? extension.url,
new extensionClass.default()
new extensionClass.default(
extension.url,
extension.name,
extension.active,
extension.description,
extension.version
)
)
}
}

View File

@ -0,0 +1,7 @@
import { atom } from 'jotai'
import { SettingScreen } from '@/screens/Settings'
export const selectedSettingAtom = atom<SettingScreen | string>('My Models')
export const janSettingScreenAtom = atom<SettingScreen[]>([])

View File

@ -127,13 +127,6 @@ export const setThreadModelParamsAtom = atom(
(get, set, threadId: string, params: ModelParams) => {
const currentState = { ...get(threadModelParamsAtom) }
currentState[threadId] = params
console.debug(
`Update model params for thread ${threadId}, ${JSON.stringify(
params,
null,
2
)}`
)
set(threadModelParamsAtom, currentState)
}
)

View File

@ -115,17 +115,17 @@ export function useActiveModel() {
}
const stopModel = useCallback(async () => {
if (activeModel) {
setStateModel({ state: 'stop', loading: true, model: activeModel.id })
const engine = EngineManager.instance().get(activeModel.engine)
await engine
?.unloadModel(activeModel)
.catch()
.then(() => {
setActiveModel(undefined)
setStateModel({ state: 'start', loading: false, model: '' })
})
}
if (!activeModel) return
setStateModel({ state: 'stop', loading: true, model: activeModel.id })
const engine = EngineManager.instance().get(activeModel.engine)
await engine
?.unloadModel(activeModel)
.catch()
.then(() => {
setActiveModel(undefined)
setStateModel({ state: 'start', loading: false, model: '' })
})
}, [activeModel, setActiveModel, setStateModel])
return { activeModel, startModel, stopModel, stateModel }

View File

@ -1,3 +1,5 @@
import { useCallback } from 'react'
import {
Assistant,
ConversationalExtension,
@ -134,13 +136,16 @@ export const useCreateNewThread = () => {
setActiveThread(thread)
}
async function updateThreadMetadata(thread: Thread) {
updateThread(thread)
const updateThreadMetadata = useCallback(
async (thread: Thread) => {
updateThread(thread)
await extensionManager
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
?.saveThread(thread)
}
await extensionManager
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
?.saveThread(thread)
},
[updateThread]
)
return {
requestCreateNewThread,

View File

@ -1,78 +0,0 @@
import { useCallback } from 'react'
import { fs, joinPath, events, AppConfigurationEventName } from '@janhq/core'
export const useEngineSettings = () => {
const readOpenAISettings = useCallback(async () => {
if (
!(await fs.existsSync(await joinPath(['file://engines', 'openai.json'])))
)
return {}
const settings = await fs.readFileSync(
await joinPath(['file://engines', 'openai.json']),
'utf-8'
)
if (settings) {
return typeof settings === 'object' ? settings : JSON.parse(settings)
}
return {}
}, [])
const saveOpenAISettings = async ({
apiKey,
}: {
apiKey: string | undefined
}) => {
const settings = await readOpenAISettings()
const settingFilePath = await joinPath(['file://engines', 'openai.json'])
settings.api_key = apiKey
await fs.writeFileSync(settingFilePath, JSON.stringify(settings))
// Sec: Don't attach the settings data to the event
events.emit(
AppConfigurationEventName.OnConfigurationUpdate,
settingFilePath
)
}
const readGroqSettings = useCallback(async () => {
if (!(await fs.existsSync(await joinPath(['file://engines', 'groq.json']))))
return {}
const settings = await fs.readFileSync(
await joinPath(['file://engines', 'groq.json']),
'utf-8'
)
if (settings) {
return typeof settings === 'object' ? settings : JSON.parse(settings)
}
return {}
}, [])
const saveGroqSettings = async ({
apiKey,
}: {
apiKey: string | undefined
}) => {
const settings = await readGroqSettings()
const settingFilePath = await joinPath(['file://engines', 'groq.json'])
settings.api_key = apiKey
await fs.writeFileSync(settingFilePath, JSON.stringify(settings))
// Sec: Don't attach the settings data to the event
events.emit(
AppConfigurationEventName.OnConfigurationUpdate,
settingFilePath
)
}
return {
readOpenAISettings,
saveOpenAISettings,
readGroqSettings,
saveGroqSettings,
}
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useRef } from 'react'
import {

View File

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useCallback } from 'react'
import {
ConversationalExtension,
ExtensionTypeEnum,
@ -16,10 +17,8 @@ import { toRuntimeParams, toSettingParams } from '@/utils/modelParam'
import { extensionManager } from '@/extension'
import {
ModelParams,
activeThreadStateAtom,
getActiveThreadModelParamsAtom,
setThreadModelParamsAtom,
threadsAtom,
} from '@/helpers/atoms/Thread.atom'
export type UpdateModelParameter = {
@ -29,63 +28,50 @@ export type UpdateModelParameter = {
}
export default function useUpdateModelParameters() {
const threads = useAtomValue(threadsAtom)
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
const activeThreadState = useAtomValue(activeThreadStateAtom)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const selectedModel = useAtomValue(selectedModelAtom)
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
const updateModelParameter = async (
threadId: string,
settings: UpdateModelParameter
) => {
const thread = threads.find((thread) => thread.id === threadId)
if (!thread) {
console.error(`Thread ${threadId} not found`)
return
}
const updateModelParameter = useCallback(
async (thread: Thread, settings: UpdateModelParameter) => {
const params = settings.modelId
? settings.params
: { ...activeModelParams, ...settings.params }
if (!activeThreadState) {
console.error('No active thread')
return
}
const params = settings.modelId
? settings.params
: { ...activeModelParams, ...settings.params }
const updatedModelParams: ModelParams = {
...params,
}
// update the state
setThreadModelParams(thread.id, updatedModelParams)
const assistants = thread.assistants.map(
(assistant: ThreadAssistantInfo) => {
const runtimeParams = toRuntimeParams(updatedModelParams)
const settingParams = toSettingParams(updatedModelParams)
assistant.model.parameters = runtimeParams
assistant.model.settings = settingParams
if (selectedModel) {
assistant.model.id = settings.modelId ?? selectedModel?.id
assistant.model.engine = settings.engine ?? selectedModel?.engine
}
return assistant
const updatedModelParams: ModelParams = {
...params,
}
)
// update thread
const updatedThread: Thread = {
...thread,
assistants,
}
// update the state
setThreadModelParams(thread.id, updatedModelParams)
await extensionManager
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
?.saveThread(updatedThread)
}
const assistants = thread.assistants.map(
(assistant: ThreadAssistantInfo) => {
const runtimeParams = toRuntimeParams(updatedModelParams)
const settingParams = toSettingParams(updatedModelParams)
assistant.model.parameters = runtimeParams
assistant.model.settings = settingParams
if (selectedModel) {
assistant.model.id = settings.modelId ?? selectedModel?.id
assistant.model.engine = settings.engine ?? selectedModel?.engine
}
return assistant
}
)
// update thread
const updatedThread: Thread = {
...thread,
assistants,
}
await extensionManager
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
?.saveThread(updatedThread)
},
[activeModelParams, selectedModel, setThreadModelParams]
)
return { updateModelParameter }
}

View File

@ -1,77 +1,98 @@
import { useAtomValue } from 'jotai'
import { useCallback } from 'react'
import { SettingComponentProps } from '@janhq/core'
import { useAtomValue, useSetAtom } from 'jotai'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import SettingComponentBuilder, {
SettingComponentData,
} from '../ModelSetting/SettingComponent'
import SettingComponentBuilder from '../ModelSetting/SettingComponent'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
import {
activeThreadAtom,
engineParamsUpdateAtom,
} from '@/helpers/atoms/Thread.atom'
const AssistantSetting = ({
componentData,
}: {
componentData: SettingComponentData[]
}) => {
type Props = {
componentData: SettingComponentProps[]
}
const AssistantSetting: React.FC<Props> = ({ componentData }) => {
const activeThread = useAtomValue(activeThreadAtom)
const { updateThreadMetadata } = useCreateNewThread()
const { stopModel } = useActiveModel()
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
const onValueChanged = useCallback(
(key: string, value: string | number | boolean) => {
if (!activeThread) return
const shouldReloadModel =
componentData.find((x) => x.key === key)?.requireModelReload ?? false
if (shouldReloadModel) {
setEngineParamsUpdate(true)
stopModel()
}
if (
activeThread.assistants[0].tools &&
(key === 'chunk_overlap' || key === 'chunk_size')
) {
if (
activeThread.assistants[0].tools[0]?.settings.chunk_size <
activeThread.assistants[0].tools[0]?.settings.chunk_overlap
) {
activeThread.assistants[0].tools[0].settings.chunk_overlap =
activeThread.assistants[0].tools[0].settings.chunk_size
}
if (
key === 'chunk_size' &&
value < activeThread.assistants[0].tools[0].settings.chunk_overlap
) {
activeThread.assistants[0].tools[0].settings.chunk_overlap = value
} else if (
key === 'chunk_overlap' &&
value > activeThread.assistants[0].tools[0].settings.chunk_size
) {
activeThread.assistants[0].tools[0].settings.chunk_size = value
}
}
updateThreadMetadata({
...activeThread,
assistants: [
{
...activeThread.assistants[0],
tools: [
{
type: 'retrieval',
enabled: true,
settings: {
...(activeThread.assistants[0].tools &&
activeThread.assistants[0].tools[0]?.settings),
[key]: value,
},
},
],
},
],
})
},
[
activeThread,
componentData,
setEngineParamsUpdate,
stopModel,
updateThreadMetadata,
]
)
if (!activeThread) return null
if (componentData.length === 0) return null
return (
<div className="flex flex-col">
{activeThread && componentData && (
<SettingComponentBuilder
componentData={componentData}
updater={(_, name, value) => {
if (
activeThread.assistants[0].tools &&
(name === 'chunk_overlap' || name === 'chunk_size')
) {
if (
activeThread.assistants[0].tools[0]?.settings.chunk_size <
activeThread.assistants[0].tools[0]?.settings.chunk_overlap
) {
activeThread.assistants[0].tools[0].settings.chunk_overlap =
activeThread.assistants[0].tools[0].settings.chunk_size
}
if (
name === 'chunk_size' &&
value <
activeThread.assistants[0].tools[0].settings.chunk_overlap
) {
activeThread.assistants[0].tools[0].settings.chunk_overlap =
value
} else if (
name === 'chunk_overlap' &&
value > activeThread.assistants[0].tools[0].settings.chunk_size
) {
activeThread.assistants[0].tools[0].settings.chunk_size = value
}
}
updateThreadMetadata({
...activeThread,
assistants: [
{
...activeThread.assistants[0],
tools: [
{
type: 'retrieval',
enabled: true,
settings: {
...(activeThread.assistants[0].tools &&
activeThread.assistants[0].tools[0]?.settings),
[name]: value,
},
},
],
},
],
})
}}
/>
)}
</div>
<SettingComponentBuilder
componentProps={componentData}
onValueUpdated={onValueChanged}
/>
)
}

View File

@ -0,0 +1,30 @@
import React from 'react'
import { Button } from '@janhq/uikit'
import { useSetAtom } from 'jotai'
import LogoMark from '@/containers/Brand/Logo/Mark'
import { MainViewState } from '@/constants/screens'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
const EmptyModel: React.FC = () => {
const setMainViewState = useSetAtom(mainViewStateAtom)
return (
<div className="mx-auto flex h-full w-3/4 flex-col items-center justify-center text-center">
<LogoMark className="mx-auto mb-4 animate-wave" width={56} height={56} />
<h1 className="text-2xl font-bold">Welcome!</h1>
<p className="mt-1 text-base">You need to download your first model</p>
<Button
className="mt-4"
onClick={() => setMainViewState(MainViewState.Hub)}
>
Explore The Hub
</Button>
</div>
)
}
export default React.memo(EmptyModel)

View File

@ -0,0 +1,46 @@
import React from 'react'
import { InferenceEngine } from '@janhq/core'
import { Button } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
import LogoMark from '@/containers/Brand/Logo/Mark'
import { MainViewState } from '@/constants/screens'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const EmptyThread: React.FC = () => {
const downloadedModels = useAtomValue(downloadedModelsAtom)
const setMainViewState = useSetAtom(mainViewStateAtom)
const showOnboardingStep =
downloadedModels.filter((e) => e.engine === InferenceEngine.nitro)
.length === 0
return (
<div className="mx-auto flex h-full w-3/4 flex-col items-center justify-center text-center">
<LogoMark className="mx-auto mb-4 animate-wave" width={56} height={56} />
{showOnboardingStep ? (
<>
<p className="mt-1 text-base font-medium">
{`You don't have a local model yet.`}
</p>
<div className="w-auto px-4 py-2">
<Button
block
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
onClick={() => setMainViewState(MainViewState.Hub)}
>
Explore The Hub
</Button>
</div>
</>
) : (
<p className="mt-1 text-base font-medium">How can I help you?</p>
)}
</div>
)
}
export default React.memo(EmptyThread)

View File

@ -1,98 +1,40 @@
import { Fragment } from 'react'
import ScrollToBottom from 'react-scroll-to-bottom'
import { InferenceEngine, MessageStatus } from '@janhq/core'
import { Button } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
import LogoMark from '@/containers/Brand/Logo/Mark'
import { MainViewState } from '@/constants/screens'
import { MessageStatus } from '@janhq/core'
import { useAtomValue } from 'jotai'
import ChatItem from '../ChatItem'
import ErrorMessage from '../ErrorMessage'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import EmptyModel from './EmptyModel'
import EmptyThread from './EmptyThread'
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const ChatBody: React.FC = () => {
const messages = useAtomValue(getCurrentChatMessagesAtom)
const downloadedModels = useAtomValue(downloadedModelsAtom)
const setMainViewState = useSetAtom(mainViewStateAtom)
if (downloadedModels.length === 0)
return (
<div className="mx-auto flex h-full w-3/4 flex-col items-center justify-center text-center">
<LogoMark
className="mx-auto mb-4 animate-wave"
width={56}
height={56}
/>
<h1 className="text-2xl font-bold">Welcome!</h1>
<p className="mt-1 text-base">You need to download your first model</p>
<Button
className="mt-4"
onClick={() => setMainViewState(MainViewState.Hub)}
>
Explore The Hub
</Button>
</div>
)
const showOnboardingStep =
downloadedModels.filter((e) => e.engine === InferenceEngine.nitro)
.length === 0
if (downloadedModels.length === 0) return <EmptyModel />
if (messages.length === 0) return <EmptyThread />
return (
<Fragment>
{messages.length === 0 ? (
<div className="mx-auto flex h-full w-3/4 flex-col items-center justify-center text-center">
<LogoMark
className="mx-auto mb-4 animate-wave"
width={56}
height={56}
/>
{showOnboardingStep ? (
<>
<p className="mt-1 text-base font-medium">
{`You don't have a local model yet.`}
</p>
<div className="w-auto px-4 py-2">
<Button
block
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
onClick={() => setMainViewState(MainViewState.Hub)}
>
Explore The Hub
</Button>
</div>
</>
) : (
<p className="mt-1 text-base font-medium">How can I help you?</p>
)}
</div>
) : (
<ScrollToBottom className="flex h-full w-full flex-col">
{messages.map((message, index) => (
<div key={message.id}>
{message.status !== MessageStatus.Error &&
message.content.length > 0 && (
<ChatItem {...message} key={message.id} />
)}
<ScrollToBottom className="flex h-full w-full flex-col">
{messages.map((message, index) => (
<div key={message.id}>
{message.status !== MessageStatus.Error &&
message.content.length > 0 && (
<ChatItem {...message} key={message.id} />
)}
{(message.status === MessageStatus.Error ||
message.status === MessageStatus.Stopped) &&
index === messages.length - 1 && (
<ErrorMessage message={message} />
)}
</div>
))}
</ScrollToBottom>
)}
</Fragment>
{(message.status === MessageStatus.Error ||
message.status === MessageStatus.Stopped) &&
index === messages.length - 1 && <ErrorMessage message={message} />}
</div>
))}
</ScrollToBottom>
)
}

View File

@ -1,25 +1,53 @@
import SettingComponentBuilder from '../../Chat/ModelSetting/SettingComponent'
import { SettingComponentData } from '../ModelSetting/SettingComponent'
import { useCallback } from 'react'
import { SettingComponentProps } from '@janhq/core/.'
import { useAtomValue, useSetAtom } from 'jotai'
import { useActiveModel } from '@/hooks/useActiveModel'
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
import SettingComponentBuilder from '../../Chat/ModelSetting/SettingComponent'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import {
activeThreadAtom,
engineParamsUpdateAtom,
} from '@/helpers/atoms/Thread.atom'
type Props = {
componentData: SettingComponentProps[]
}
const EngineSetting: React.FC<Props> = ({ componentData }) => {
const isLocalServerRunning = useAtomValue(serverEnabledAtom)
const activeThread = useAtomValue(activeThreadAtom)
const { stopModel } = useActiveModel()
const { updateModelParameter } = useUpdateModelParameters()
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
const onValueChanged = useCallback(
(key: string, value: string | number | boolean) => {
if (!activeThread) return
setEngineParamsUpdate(true)
stopModel()
updateModelParameter(activeThread, {
params: { [key]: value },
})
},
[activeThread, setEngineParamsUpdate, stopModel, updateModelParameter]
)
const EngineSetting = ({
componentData,
enabled = true,
}: {
componentData: SettingComponentData[]
enabled?: boolean
}) => {
return (
<>
{componentData.filter((e) => e.name !== 'prompt_template').length && (
<div className="flex flex-col">
<SettingComponentBuilder
componentData={componentData}
enabled={enabled}
selector={(e) => e.name !== 'prompt_template'}
/>
</div>
)}
</>
<SettingComponentBuilder
componentProps={componentData}
enabled={!isLocalServerRunning}
onValueUpdated={onValueChanged}
/>
)
}

View File

@ -12,8 +12,6 @@ import { MainViewState } from '@/constants/screens'
import { loadModelErrorAtom } from '@/hooks/useActiveModel'
import useSendChatMessage from '@/hooks/useSendChatMessage'
import { getErrorTitle } from '@/utils/errorMessage'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
@ -31,10 +29,27 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
resendChatMessage(message)
}
const errorTitle = getErrorTitle(
message.error_code ?? ErrorCode.Unknown,
message.content[0]?.text?.value
)
const getErrorTitle = () => {
switch (message.error_code) {
case ErrorCode.Unknown:
return 'Apologies, somethings amiss!'
case ErrorCode.InvalidApiKey:
return (
<span>
Invalid API key. Please check your API key from{' '}
<button
className="font-medium text-primary dark:text-blue-400"
onClick={() => setMainState(MainViewState.Settings)}
>
Settings
</button>{' '}
and try again.
</span>
)
default:
return message.content[0]?.text?.value
}
}
return (
<div className="mt-10">
@ -84,7 +99,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
Model is currently unavailable. Please switch to a different
model or install the{' '}
<button
className="font-medium text-blue-500"
className="font-medium text-primary dark:text-blue-400"
onClick={() => setMainState(MainViewState.Settings)}
>
{loadModelError.split('::')[1] ?? ''}
@ -97,7 +112,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
key={message.id}
className="flex flex-col items-center text-center text-sm font-medium text-gray-500"
>
<p>{errorTitle}</p>
{getErrorTitle()}
<p>
Jans in beta. Access&nbsp;
<span

View File

@ -1,153 +1,82 @@
/* eslint-disable no-case-declarations */
import { useAtomValue, useSetAtom } from 'jotai'
import {
SettingComponentProps,
InputComponentProps,
CheckboxComponentProps,
SliderComponentProps,
} from '@janhq/core'
import Checkbox from '@/containers/Checkbox'
import ModelConfigInput from '@/containers/ModelConfigInput'
import SliderRightPanel from '@/containers/SliderRightPanel'
import { useActiveModel } from '@/hooks/useActiveModel'
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
import { getConfigurationsData } from '@/utils/componentSettings'
import { toSettingParams } from '@/utils/modelParam'
import {
engineParamsUpdateAtom,
getActiveThreadIdAtom,
getActiveThreadModelParamsAtom,
} from '@/helpers/atoms/Thread.atom'
export type ControllerType = 'slider' | 'checkbox' | 'input'
export type SettingComponentData = {
name: string
title: string
description: string
controllerType: ControllerType
controllerData: SliderData | CheckboxData | InputData
}
export type InputData = {
placeholder: string
value: string
}
export type SliderData = {
min: number
max: number
step: number
value: number
}
type CheckboxData = {
checked: boolean
}
const SettingComponent = ({
componentData,
enabled = true,
selector,
updater,
}: {
componentData: SettingComponentData[]
type Props = {
componentProps: SettingComponentProps[]
enabled?: boolean
selector?: (e: SettingComponentData) => boolean
updater?: (
threadId: string,
name: string,
value: string | number | boolean | string[]
) => void
onValueUpdated: (key: string, value: string | number | boolean) => void
}
const SettingComponent: React.FC<Props> = ({
componentProps,
enabled = true,
onValueUpdated,
}) => {
const { updateModelParameter } = useUpdateModelParameters()
const threadId = useAtomValue(getActiveThreadIdAtom)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const modelSettingParams = toSettingParams(activeModelParams)
const engineParams = getConfigurationsData(modelSettingParams)
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
const { stopModel } = useActiveModel()
const onValueChanged = (
name: string,
value: string | number | boolean | string[]
) => {
if (!threadId) return
if (engineParams.some((x) => x.name.includes(name))) {
setEngineParamsUpdate(true)
stopModel()
} else {
setEngineParamsUpdate(false)
}
if (updater) updater(threadId, name, value)
else {
// Convert stop string to array
if (name === 'stop' && typeof value === 'string') {
value = [value]
const components = componentProps.map((data) => {
switch (data.controllerType) {
case 'slider': {
const { min, max, step, value } =
data.controllerProps as SliderComponentProps
return (
<SliderRightPanel
key={data.key}
title={data.title}
description={data.description}
min={min}
max={max}
step={step}
value={value}
name={data.key}
enabled={enabled}
onValueChanged={(value) => onValueUpdated(data.key, value)}
/>
)
}
updateModelParameter(threadId, {
params: { [name]: value },
})
}
}
const components = componentData
.filter((x) => (selector ? selector(x) : true))
.map((data) => {
switch (data.controllerType) {
case 'slider':
const { min, max, step, value } = data.controllerData as SliderData
return (
<SliderRightPanel
key={data.name}
title={data.title}
description={data.description}
min={min}
max={max}
step={step}
value={value}
name={data.name}
enabled={enabled}
onValueChanged={(value) => onValueChanged(data.name, value)}
/>
)
case 'input':
const { placeholder, value: textValue } =
data.controllerData as InputData
return (
<ModelConfigInput
title={data.title}
enabled={enabled}
key={data.name}
name={data.name}
description={data.description}
placeholder={placeholder}
value={textValue}
onValueChanged={(value) => onValueChanged(data.name, value)}
/>
)
case 'checkbox':
const { checked } = data.controllerData as CheckboxData
return (
<Checkbox
key={data.name}
enabled={enabled}
name={data.name}
description={data.description}
title={data.title}
checked={checked}
onValueChanged={(value) => onValueChanged(data.name, value)}
/>
)
default:
return null
case 'input': {
const { placeholder, value: textValue } =
data.controllerProps as InputComponentProps
return (
<ModelConfigInput
title={data.title}
enabled={enabled}
key={data.key}
name={data.key}
description={data.description}
placeholder={placeholder}
value={textValue}
onValueChanged={(value) => onValueUpdated(data.key, value)}
/>
)
}
})
case 'checkbox': {
const { value } = data.controllerProps as CheckboxComponentProps
return (
<Checkbox
key={data.key}
enabled={enabled}
name={data.key}
description={data.description}
title={data.title}
checked={value}
onValueChanged={(value) => onValueUpdated(data.key, value)}
/>
)
}
default:
return null
}
})
return <div className="flex flex-col gap-y-4">{components}</div>
}

View File

@ -1,26 +1,48 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react'
import React, { useCallback } from 'react'
import SettingComponentBuilder, {
SettingComponentData,
} from './SettingComponent'
import { SettingComponentProps } from '@janhq/core/.'
import { useAtomValue } from 'jotai'
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
import SettingComponentBuilder from './SettingComponent'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
type Props = {
componentProps: SettingComponentProps[]
}
const ModelSetting: React.FC<Props> = ({ componentProps }) => {
const isLocalServerRunning = useAtomValue(serverEnabledAtom)
const activeThread = useAtomValue(activeThreadAtom)
const { updateModelParameter } = useUpdateModelParameters()
const onValueChanged = useCallback(
(key: string, value: string | number | boolean) => {
if (!activeThread) return
if (key === 'stop' && typeof value === 'string') {
updateModelParameter(activeThread, {
params: { [key]: [value] },
})
} else {
updateModelParameter(activeThread, {
params: { [key]: value },
})
}
},
[activeThread, updateModelParameter]
)
const ModelSetting = ({
componentData,
}: {
componentData: SettingComponentData[]
}) => {
return (
<>
{componentData.filter((e) => e.name !== 'prompt_template').length && (
<div className="flex flex-col">
<SettingComponentBuilder
componentData={componentData}
selector={(e) => e.name !== 'prompt_template'}
/>
</div>
)}
</>
<SettingComponentBuilder
enabled={!isLocalServerRunning}
componentProps={componentProps}
onValueUpdated={onValueChanged}
/>
)
}

View File

@ -1,192 +1,224 @@
import { SettingComponentData } from './SettingComponent'
import { SettingComponentProps } from '@janhq/core/.'
export const presetConfiguration: Record<string, SettingComponentData> = {
export const presetConfiguration: Record<string, SettingComponentProps> = {
prompt_template: {
name: 'prompt_template',
key: 'prompt_template',
title: 'Prompt template',
description: 'The prompt to use for internal configuration.',
controllerType: 'input',
controllerData: {
controllerProps: {
placeholder: 'Prompt template',
value: '',
},
requireModelReload: true,
configType: 'setting',
},
stop: {
name: 'stop',
key: 'stop',
title: 'Stop',
description:
'Defines specific tokens or phrases at which the model will stop generating further output. ',
controllerType: 'input',
controllerData: {
controllerProps: {
placeholder: 'Stop',
value: '',
},
requireModelReload: false,
configType: 'runtime',
},
ctx_len: {
name: 'ctx_len',
key: 'ctx_len',
title: 'Context Length',
description:
'The context length for model operations varies; the maximum depends on the specific model used.',
controllerType: 'slider',
controllerData: {
controllerProps: {
min: 0,
max: 4096,
step: 128,
value: 4096,
},
requireModelReload: true,
configType: 'setting',
},
max_tokens: {
name: 'max_tokens',
key: 'max_tokens',
title: 'Max Tokens',
description:
'The maximum number of tokens the model will generate in a single response.',
controllerType: 'slider',
controllerData: {
controllerProps: {
min: 100,
max: 4096,
step: 10,
value: 4096,
},
requireModelReload: false,
configType: 'runtime',
},
ngl: {
name: 'ngl',
key: 'ngl',
title: 'Number of GPU layers (ngl)',
description: 'The number of layers to load onto the GPU for acceleration.',
controllerType: 'slider',
controllerData: {
controllerProps: {
min: 1,
max: 100,
step: 1,
value: 100,
},
requireModelReload: true,
configType: 'setting',
},
embedding: {
name: 'embedding',
key: 'embedding',
title: 'Embedding',
description: 'Whether to enable embedding.',
controllerType: 'checkbox',
controllerData: {
checked: true,
controllerProps: {
value: true,
},
requireModelReload: true,
configType: 'setting',
},
stream: {
name: 'stream',
key: 'stream',
title: 'Stream',
description: 'Enable real-time data processing for faster predictions.',
controllerType: 'checkbox',
controllerData: {
checked: false,
controllerProps: {
value: false,
},
requireModelReload: false,
configType: 'runtime',
},
temperature: {
name: 'temperature',
key: 'temperature',
title: 'Temperature',
description: 'Controls the randomness of the models output.',
controllerType: 'slider',
controllerData: {
controllerProps: {
min: 0,
max: 2,
step: 0.1,
value: 0.7,
},
requireModelReload: false,
configType: 'runtime',
},
frequency_penalty: {
name: 'frequency_penalty',
key: 'frequency_penalty',
title: 'Frequency Penalty',
description:
'Adjusts the likelihood of the model repeating words or phrases in its output. ',
controllerType: 'slider',
controllerData: {
controllerProps: {
min: 0,
max: 1,
step: 0.1,
value: 0.7,
},
requireModelReload: false,
configType: 'runtime',
},
presence_penalty: {
name: 'presence_penalty',
key: 'presence_penalty',
title: 'Presence Penalty',
description:
'Influences the generation of new and varied concepts in the models output. ',
controllerType: 'slider',
controllerData: {
controllerProps: {
min: 0,
max: 1,
step: 0.1,
value: 0.7,
},
requireModelReload: false,
configType: 'runtime',
},
top_p: {
name: 'top_p',
key: 'top_p',
title: 'Top P',
description: 'Set probability threshold for more relevant outputs.',
controllerType: 'slider',
controllerData: {
controllerProps: {
min: 0,
max: 1,
step: 0.1,
value: 0.95,
},
requireModelReload: false,
configType: 'runtime',
},
n_parallel: {
name: 'n_parallel',
key: 'n_parallel',
title: 'N Parallel',
description:
'The number of parallel operations. Only set when enable continuous batching. ',
controllerType: 'slider',
controllerData: {
controllerProps: {
min: 0,
max: 4,
step: 1,
value: 1,
},
requireModelReload: true,
configType: 'setting',
},
// assistant
chunk_size: {
name: 'chunk_size',
key: 'chunk_size',
title: 'Chunk Size',
description: 'Maximum number of tokens in a chunk',
controllerType: 'slider',
controllerData: {
controllerProps: {
min: 128,
max: 2048,
step: 128,
value: 1024,
},
requireModelReload: true,
configType: 'setting',
},
chunk_overlap: {
name: 'chunk_overlap',
key: 'chunk_overlap',
title: 'Chunk Overlap',
description: 'Number of tokens overlapping between two adjacent chunks',
controllerType: 'slider',
controllerData: {
controllerProps: {
min: 32,
max: 512,
step: 32,
value: 64,
},
requireModelReload: true,
configType: 'setting',
},
top_k: {
name: 'top_k',
key: 'top_k',
title: 'Top K',
description: 'Number of top-ranked documents to retrieve',
controllerType: 'slider',
controllerData: {
controllerProps: {
min: 1,
max: 5,
step: 1,
value: 2,
},
requireModelReload: false,
configType: 'runtime',
},
retrieval_template: {
name: 'retrieval_template',
key: 'retrieval_template',
title: 'Retrieval Template',
description:
'The template to use for retrieval. The following variables are available: {CONTEXT}, {QUESTION}',
controllerType: 'input',
controllerData: {
controllerProps: {
placeholder: 'Retrieval Template',
value: '',
},
requireModelReload: true,
configType: 'setting',
},
}

View File

@ -0,0 +1,196 @@
import { Fragment, useCallback } from 'react'
import {
Tooltip,
TooltipTrigger,
TooltipPortal,
TooltipContent,
TooltipArrow,
Switch,
Input,
} from '@janhq/uikit'
import { useAtomValue } from 'jotai'
import { InfoIcon } from 'lucide-react'
import CardSidebar from '@/containers/CardSidebar'
import { selectedModelAtom } from '@/containers/DropdownListSidebar'
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import { getConfigurationsData } from '@/utils/componentSettings'
import AssistantSetting from '../../AssistantSetting'
import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
const AssistantTool: React.FC = () => {
const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom)
const activeThread = useAtomValue(activeThreadAtom)
const selectedModel = useAtomValue(selectedModelAtom)
const { updateThreadMetadata } = useCreateNewThread()
const componentDataAssistantSetting = getConfigurationsData(
(activeThread?.assistants[0]?.tools &&
activeThread?.assistants[0]?.tools[0]?.settings) ??
{}
)
const onRetrievalSwitchUpdate = useCallback(
(enabled: boolean) => {
if (!activeThread) return
updateThreadMetadata({
...activeThread,
assistants: [
{
...activeThread.assistants[0],
tools: [
{
type: 'retrieval',
enabled: enabled,
settings:
(activeThread.assistants[0].tools &&
activeThread.assistants[0].tools[0]?.settings) ??
{},
},
],
},
],
})
},
[activeThread, updateThreadMetadata]
)
if (!experimentalFeature) return null
return (
<Fragment>
{activeThread?.assistants[0]?.tools &&
componentDataAssistantSetting.length > 0 && (
<CardSidebar title="Tools" isShow={true}>
<div className="px-2 pt-4">
<div className="mb-2">
<div className="flex items-center justify-between">
<label
id="retrieval"
className="inline-flex items-center font-bold text-zinc-500 dark:text-gray-300"
>
Retrieval
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="ml-2 flex-shrink-0 text-black dark:text-gray-500"
/>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]">
<span>
Retrieval helps the assistant use information from
files you send to it. Once you share a file, the
assistant automatically fetches the relevant content
based on your request.
</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</label>
<div className="flex items-center justify-between">
<Switch
name="retrieval"
className="mr-2"
checked={activeThread?.assistants[0].tools[0].enabled}
onCheckedChange={onRetrievalSwitchUpdate}
/>
</div>
</div>
</div>
{activeThread?.assistants[0]?.tools[0].enabled && (
<div className="pb-4 pt-2">
<div className="mb-4">
<div className="item-center mb-2 flex">
<label
id="embedding-model"
className="inline-flex font-bold text-zinc-500 dark:text-gray-300"
>
Embedding Model
</label>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="ml-2 flex-shrink-0 dark:text-gray-500"
/>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]">
<span>
Embedding model is crucial for understanding and
processing the input text effectively by
converting text to numerical representations.
Align the model choice with your task, evaluate
its performance, and consider factors like
resource availability. Experiment to find the best
fit for your specific use case.
</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
<div className="flex items-center justify-between">
<Input value={selectedModel?.name} disabled />
</div>
</div>
<div className="mb-4">
<div className="mb-2 flex items-center">
<label
id="vector-database"
className="inline-block font-bold text-zinc-500 dark:text-gray-300"
>
Vector Database
</label>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="ml-2 flex-shrink-0 dark:text-gray-500"
/>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]">
<span>
Vector Database is crucial for efficient storage
and retrieval of embeddings. Consider your
specific task, available resources, and language
requirements. Experiment to find the best fit for
your specific use case.
</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
<div className="flex items-center justify-between">
<Input value="HNSWLib" disabled />
</div>
</div>
<AssistantSetting
componentData={componentDataAssistantSetting}
/>
</div>
)}
</div>
</CardSidebar>
)}
</Fragment>
)
}
export default AssistantTool

View File

@ -0,0 +1,50 @@
import { useCallback } from 'react'
import { SettingComponentProps } from '@janhq/core'
import { useAtomValue, useSetAtom } from 'jotai'
import { useActiveModel } from '@/hooks/useActiveModel'
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
import SettingComponent from '../../ModelSetting/SettingComponent'
import {
activeThreadAtom,
engineParamsUpdateAtom,
} from '@/helpers/atoms/Thread.atom'
type Props = {
componentData: SettingComponentProps[]
}
const PromptTemplateSetting: React.FC<Props> = ({ componentData }) => {
const activeThread = useAtomValue(activeThreadAtom)
const { stopModel } = useActiveModel()
const { updateModelParameter } = useUpdateModelParameters()
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
const onValueChanged = useCallback(
(key: string, value: string | number | boolean) => {
if (!activeThread) return
setEngineParamsUpdate(true)
stopModel()
updateModelParameter(activeThread, {
params: { [key]: value },
})
},
[activeThread, setEngineParamsUpdate, stopModel, updateModelParameter]
)
return (
<SettingComponent
componentProps={componentData}
onValueUpdated={onValueChanged}
/>
)
}
export default PromptTemplateSetting

View File

@ -1,19 +1,9 @@
import React from 'react'
import React, { useCallback, useMemo } from 'react'
import {
Input,
Textarea,
Switch,
Tooltip,
TooltipArrow,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from '@janhq/uikit'
import { Input, Textarea } from '@janhq/uikit'
import { atom, useAtomValue } from 'jotai'
import { InfoIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import LogoMark from '@/containers/Brand/Logo/Mark'
@ -28,13 +18,13 @@ import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import { getConfigurationsData } from '@/utils/componentSettings'
import { toRuntimeParams, toSettingParams } from '@/utils/modelParam'
import AssistantSetting from '../AssistantSetting'
import EngineSetting from '../EngineSetting'
import ModelSetting from '../ModelSetting'
import SettingComponentBuilder from '../ModelSetting/SettingComponent'
import AssistantTool from './AssistantTool'
import PromptTemplateSetting from './PromptTemplateSetting'
import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom'
import {
activeThreadAtom,
getActiveThreadModelParamsAtom,
@ -48,22 +38,62 @@ const Sidebar: React.FC = () => {
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const selectedModel = useAtomValue(selectedModelAtom)
const { updateThreadMetadata } = useCreateNewThread()
const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom)
const modelEngineParams = toSettingParams(activeModelParams)
const modelRuntimeParams = toRuntimeParams(activeModelParams)
const componentDataAssistantSetting = getConfigurationsData(
(activeThread?.assistants[0]?.tools &&
activeThread?.assistants[0]?.tools[0]?.settings) ??
{}
const modelSettings = useMemo(() => {
const modelRuntimeParams = toRuntimeParams(activeModelParams)
const componentDataRuntimeSetting = getConfigurationsData(
modelRuntimeParams,
selectedModel
)
return componentDataRuntimeSetting.filter(
(x) => x.key !== 'prompt_template'
)
}, [activeModelParams, selectedModel])
const engineSettings = useMemo(() => {
const modelEngineParams = toSettingParams(activeModelParams)
const componentDataEngineSetting = getConfigurationsData(
modelEngineParams,
selectedModel
)
return componentDataEngineSetting.filter((x) => x.key !== 'prompt_template')
}, [activeModelParams, selectedModel])
const promptTemplateSettings = useMemo(() => {
const modelEngineParams = toSettingParams(activeModelParams)
const componentDataEngineSetting = getConfigurationsData(
modelEngineParams,
selectedModel
)
return componentDataEngineSetting.filter((x) => x.key === 'prompt_template')
}, [activeModelParams, selectedModel])
const onThreadTitleChanged = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (activeThread)
updateThreadMetadata({
...activeThread,
title: e.target.value || '',
})
},
[activeThread, updateThreadMetadata]
)
const componentDataEngineSetting = getConfigurationsData(
modelEngineParams,
selectedModel
)
const componentDataRuntimeSetting = getConfigurationsData(
modelRuntimeParams,
selectedModel
const onAssistantInstructionChanged = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (activeThread)
updateThreadMetadata({
...activeThread,
assistants: [
{
...activeThread.assistants[0],
instructions: e.target.value || '',
},
],
})
},
[activeThread, updateThreadMetadata]
)
return (
@ -92,13 +122,7 @@ const Sidebar: React.FC = () => {
<Input
id="thread-title"
value={activeThread?.title}
onChange={(e) => {
if (activeThread)
updateThreadMetadata({
...activeThread,
title: e.target.value || '',
})
}}
onChange={onThreadTitleChanged}
/>
</div>
<div className="flex flex-col">
@ -133,18 +157,7 @@ const Sidebar: React.FC = () => {
id="assistant-instructions"
placeholder="Eg. You are a helpful assistant."
value={activeThread?.assistants[0].instructions ?? ''}
onChange={(e) => {
if (activeThread)
updateThreadMetadata({
...activeThread,
assistants: [
{
...activeThread.assistants[0],
instructions: e.target.value || '',
},
],
})
}}
onChange={onAssistantInstructionChanged}
/>
</div>
</div>
@ -154,36 +167,33 @@ const Sidebar: React.FC = () => {
<div className="px-2 pt-4">
<DropdownListSidebar />
{componentDataRuntimeSetting.length > 0 && (
{modelSettings.length > 0 && (
<div className="mt-6">
<CardSidebar title="Inference Parameters" asChild>
<div className="px-2 py-4">
<ModelSetting componentData={componentDataRuntimeSetting} />
<ModelSetting componentProps={modelSettings} />
</div>
</CardSidebar>
</div>
)}
{componentDataEngineSetting.filter(
(x) => x.name === 'prompt_template'
).length !== 0 && (
{promptTemplateSettings.length > 0 && (
<div className="mt-4">
<CardSidebar title="Model Parameters" asChild>
<div className="px-2 py-4">
<SettingComponentBuilder
componentData={componentDataEngineSetting}
selector={(x) => x.name === 'prompt_template'}
<PromptTemplateSetting
componentData={promptTemplateSettings}
/>
</div>
</CardSidebar>
</div>
)}
{componentDataEngineSetting.length > 0 && (
{engineSettings.length > 0 && (
<div className="my-4">
<CardSidebar title="Engine Parameters" asChild>
<div className="px-2 py-4">
<EngineSetting componentData={componentDataEngineSetting} />
<EngineSetting componentData={engineSettings} />
</div>
</CardSidebar>
</div>
@ -191,166 +201,7 @@ const Sidebar: React.FC = () => {
</div>
</CardSidebar>
{experimentalFeature && (
<div>
{activeThread?.assistants[0]?.tools &&
componentDataAssistantSetting.length > 0 && (
<CardSidebar title="Tools" isShow={true}>
<div className="px-2 pt-4">
<div className="mb-2">
<div className="flex items-center justify-between">
<label
id="retrieval"
className="inline-flex items-center font-bold text-zinc-500 dark:text-gray-300"
>
Retrieval
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="ml-2 flex-shrink-0 text-black dark:text-gray-500"
/>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent
side="top"
className="max-w-[240px]"
>
<span>
Retrieval helps the assistant use information
from files you send to it. Once you share a
file, the assistant automatically fetches the
relevant content based on your request.
</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</label>
<div className="flex items-center justify-between">
<Switch
name="retrieval"
className="mr-2"
checked={
activeThread?.assistants[0].tools[0].enabled
}
onCheckedChange={(e) => {
if (activeThread)
updateThreadMetadata({
...activeThread,
assistants: [
{
...activeThread.assistants[0],
tools: [
{
type: 'retrieval',
enabled: e,
settings:
(activeThread.assistants[0].tools &&
activeThread.assistants[0]
.tools[0]?.settings) ??
{},
},
],
},
],
})
}}
/>
</div>
</div>
</div>
{activeThread?.assistants[0]?.tools[0].enabled && (
<div className="pb-4 pt-2">
<div className="mb-4">
<div className="item-center mb-2 flex">
<label
id="embedding-model"
className="inline-flex font-bold text-zinc-500 dark:text-gray-300"
>
Embedding Model
</label>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="ml-2 flex-shrink-0 dark:text-gray-500"
/>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent
side="top"
className="max-w-[240px]"
>
<span>
Embedding model is crucial for understanding
and processing the input text effectively by
converting text to numerical
representations. Align the model choice with
your task, evaluate its performance, and
consider factors like resource availability.
Experiment to find the best fit for your
specific use case.
</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
<div className="flex items-center justify-between">
<Input value={selectedModel?.name} disabled />
</div>
</div>
<div className="mb-4">
<div className="mb-2 flex items-center">
<label
id="vector-database"
className="inline-block font-bold text-zinc-500 dark:text-gray-300"
>
Vector Database
</label>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="ml-2 flex-shrink-0 dark:text-gray-500"
/>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent
side="top"
className="max-w-[240px]"
>
<span>
Vector Database is crucial for efficient
storage and retrieval of embeddings.
Consider your specific task, available
resources, and language requirements.
Experiment to find the best fit for your
specific use case.
</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
<div className="flex items-center justify-between">
<Input value="HNSWLib" disabled />
</div>
</div>
<AssistantSetting
componentData={componentDataAssistantSetting}
/>
</div>
)}
</div>
</CardSidebar>
)}
</div>
)}
<AssistantTool />
</div>
</div>
)

View File

@ -49,8 +49,7 @@ import { toSettingParams } from '@/utils/modelParam'
import EngineSetting from '../Chat/EngineSetting'
import SettingComponentBuilder from '../Chat/ModelSetting/SettingComponent'
import ModelSetting from '../Chat/ModelSetting'
import { showRightSideBarAtom } from '../Chat/Sidebar'
import {
@ -429,17 +428,22 @@ const LocalServerScreen = () => {
</div>
)}
{componentDataEngineSetting.filter(
(x) => x.name === 'prompt_template'
).length !== 0 && (
{componentDataEngineSetting.filter((x) => x.key === 'prompt_template')
.length !== 0 && (
<div className="mt-4">
<CardSidebar title="Model Parameters" asChild>
<div className="px-2 py-4">
<SettingComponentBuilder
<ModelSetting componentProps={componentDataEngineSetting} />
{/* <SettingComponentBuilder
enabled={!serverEnabled}
componentData={componentDataEngineSetting}
selector={(x) => x.name === 'prompt_template'}
/>
componentProps={componentDataEngineSetting}
onValueUpdated={function (
key: string,
value: string | number | boolean
): void {
throw new Error('Function not implemented.')
}}
/> */}
</div>
</CardSidebar>
</div>
@ -449,10 +453,7 @@ const LocalServerScreen = () => {
<div className="my-4">
<CardSidebar title="Engine Parameters" asChild>
<div className="px-2 py-4">
<EngineSetting
enabled={!serverEnabled}
componentData={componentDataEngineSetting}
/>
<EngineSetting componentData={componentDataEngineSetting} />
</div>
</CardSidebar>
</div>

View File

@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react'
import { SettingComponentProps } from '@janhq/core/.'
import { useAtomValue } from 'jotai'
import SettingDetailItem from '../SettingDetail/SettingDetailItem'
import { extensionManager } from '@/extension'
import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
const ExtensionSetting: React.FC = () => {
const selectedExtensionName = useAtomValue(selectedSettingAtom)
const [settings, setSettings] = useState<SettingComponentProps[]>([])
useEffect(() => {
const getExtensionSettings = async () => {
if (!selectedExtensionName) return
const allSettings: SettingComponentProps[] = []
const baseExtension = extensionManager.get(selectedExtensionName)
if (!baseExtension) return
if (typeof baseExtension.getSettings === 'function') {
const setting = await baseExtension.getSettings()
if (setting) allSettings.push(...setting)
}
setSettings(allSettings)
}
getExtensionSettings()
}, [selectedExtensionName])
const onValueChanged = async (
key: string,
value: string | number | boolean
) => {
// find the key in settings state, update it and set the state back
const newSettings = settings.map((setting) => {
if (setting.key !== key) return setting
setting.controllerProps.value = value
const extensionName = setting.extensionName
if (extensionName) {
extensionManager.get(extensionName)?.updateSettings([setting])
}
return setting
})
setSettings(newSettings)
}
if (settings.length === 0) return null
return (
<SettingDetailItem
componentProps={settings}
onValueUpdated={onValueChanged}
/>
)
}
export default ExtensionSetting

View File

@ -66,7 +66,7 @@ const SelectingModelModal: React.FC = () => {
</ModalHeader>
<div
className={`flex h-[172px] w-full items-center justify-center rounded-md border ${borderColor} ${dragAndDropBgColor}`}
className={`flex h-[172px] w-full cursor-pointer items-center justify-center rounded-md border ${borderColor} ${dragAndDropBgColor}`}
{...getRootProps()}
onClick={onSelectFileClick}
>

View File

@ -0,0 +1,54 @@
import { InputComponentProps, SettingComponentProps } from '@janhq/core'
import { Input } from '@janhq/uikit'
import { Marked, Renderer } from 'marked'
type Props = {
settingProps: SettingComponentProps
onValueChanged?: (e: string) => void
}
const marked: Marked = new Marked({
renderer: {
link: (href, title, text) => {
return Renderer.prototype.link
?.apply(this, [href, title, text])
.replace('<a', "<a class='text-blue-500' target='_blank'")
},
},
})
const SettingDetailTextInputItem: React.FC<Props> = ({
settingProps,
onValueChanged,
}) => {
const { value, type, placeholder } =
settingProps.controllerProps as InputComponentProps
const description = marked.parse(settingProps.description ?? '', {
async: false,
})
return (
<div className="flex w-full justify-between py-6">
<div className="flex flex-1 flex-col space-y-1">
<h1 className="text-base font-bold">{settingProps.title}</h1>
{
<div
// eslint-disable-next-line @typescript-eslint/naming-convention
dangerouslySetInnerHTML={{ __html: description }}
className="text-sm font-normal text-muted-foreground"
/>
}
</div>
<Input
placeholder={placeholder}
type={type}
value={value}
className="ml-4 w-[360px]"
onChange={(e) => onValueChanged?.(e.target.value)}
/>
</div>
)
}
export default SettingDetailTextInputItem

View File

@ -0,0 +1,45 @@
import { SettingComponentProps } from '@janhq/core'
import SettingDetailTextInputItem from './SettingDetailTextInputItem'
type Props = {
componentProps: SettingComponentProps[]
onValueUpdated: (key: string, value: string | number | boolean) => void
}
const SettingDetailItem: React.FC<Props> = ({
componentProps,
onValueUpdated,
}) => {
const components = componentProps.map((data) => {
switch (data.controllerType) {
case 'input': {
return (
<SettingDetailTextInputItem
key={data.key}
settingProps={data}
onValueChanged={(value) => onValueUpdated(data.key, value)}
/>
)
}
default:
return null
}
})
return (
<div className="flex w-full flex-col">
{components.map((component, index) => (
<div
className={`mx-6 ${index === components.length - 1 ? '' : 'border-b border-border'}`}
key={index}
>
{component}
</div>
))}
</div>
)
}
export default SettingDetailItem

View File

@ -0,0 +1,32 @@
import { useAtomValue } from 'jotai'
import Advanced from '../Advanced'
import AppearanceOptions from '../Appearance'
import ExtensionCatalog from '../CoreExtensions'
import ExtensionSetting from '../ExtensionSetting'
import Models from '../Models'
import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
const SettingDetail: React.FC = () => {
const selectedSetting = useAtomValue(selectedSettingAtom)
switch (selectedSetting) {
case 'Extensions':
return <ExtensionCatalog />
case 'My Settings':
return <AppearanceOptions />
case 'Advanced Settings':
return <Advanced />
case 'My Models':
return <Models />
default:
return <ExtensionSetting />
}
}
export default SettingDetail

View File

@ -0,0 +1,44 @@
import { useCallback } from 'react'
import { motion as m } from 'framer-motion'
import { useAtom } from 'jotai'
import { twMerge } from 'tailwind-merge'
import { formatExtensionsName } from '@/utils/converter'
import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
type Props = {
setting: string
extension?: boolean
}
const SettingItem: React.FC<Props> = ({ setting, extension = false }) => {
const [selectedSetting, setSelectedSetting] = useAtom(selectedSettingAtom)
const isActive = selectedSetting === setting
const onSettingItemClick = useCallback(() => {
setSelectedSetting(setting)
}, [setting, setSelectedSetting])
return (
<div
className="relative block cursor-pointer py-1.5"
onClick={onSettingItemClick}
>
<span className={twMerge(isActive && 'relative z-10', 'capitalize')}>
{extension ? formatExtensionsName(setting) : setting}
</span>
{isActive && (
<m.div
className="absolute inset-0 -left-3 h-full w-[calc(100%+24px)] rounded-md bg-primary/50"
layoutId="active-static-menu"
/>
)}
</div>
)
}
export default SettingItem

View File

@ -1,55 +1,69 @@
import { useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { ScrollArea } from '@janhq/uikit'
import { motion as m } from 'framer-motion'
import { twMerge } from 'tailwind-merge'
type Props = {
activeMenu: string
onMenuClick: (menu: string) => void
}
import { useAtomValue } from 'jotai'
const SettingMenu: React.FC<Props> = ({ activeMenu, onMenuClick }) => {
const [menus, setMenus] = useState<string[]>([])
import SettingItem from './SettingItem'
import { extensionManager } from '@/extension'
import { janSettingScreenAtom } from '@/helpers/atoms/Setting.atom'
const SettingMenu: React.FC = () => {
const settingScreens = useAtomValue(janSettingScreenAtom)
const [extensionHasSettings, setExtensionHasSettings] = useState<string[]>([])
useEffect(() => {
setMenus([
'My Models',
'My Settings',
'Advanced Settings',
...(window.electronAPI ? ['Extensions'] : []),
])
const getAllSettings = async () => {
const activeExtensions = await extensionManager.getActive()
const extensionsMenu: string[] = []
for (const extension of activeExtensions) {
const extensionName = extension.name
if (!extensionName) continue
const baseExtension = extensionManager.get(extensionName)
if (!baseExtension) continue
if (typeof baseExtension.getSettings === 'function') {
const settings = await baseExtension.getSettings()
if (settings && settings.length > 0) {
extensionsMenu.push(extensionName)
}
}
}
setExtensionHasSettings(extensionsMenu)
}
getAllSettings()
}, [])
return (
<div className="flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r border-border">
<ScrollArea className="h-full w-full">
<div className="flex-shrink-0 px-6 py-4 font-medium">
{menus.map((menu) => {
const isActive = activeMenu === menu
return (
<div
key={menu}
className="relative my-0.5 block cursor-pointer py-1.5"
onClick={() => onMenuClick(menu)}
>
<span className={twMerge(isActive && 'relative z-10')}>
{menu}
</span>
{settingScreens.map((settingScreen) => (
<SettingItem key={settingScreen} setting={settingScreen} />
))}
{isActive && (
<m.div
className="absolute inset-0 -left-3 h-full w-[calc(100%+24px)] rounded-md bg-primary/50"
layoutId="active-static-menu"
/>
)}
</div>
)
})}
{extensionHasSettings.length > 0 && (
<div className="mb-2 mt-6">
<label className="text-xs font-medium text-muted-foreground">
Extensions
</label>
</div>
)}
{extensionHasSettings.map((extensionName: string) => (
<SettingItem
key={extensionName}
setting={extensionName}
extension={true}
/>
))}
</div>
</ScrollArea>
</div>
)
}
export default SettingMenu
export default React.memo(SettingMenu)

View File

@ -1,51 +1,40 @@
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import Advanced from '@/screens/Settings/Advanced'
import AppearanceOptions from '@/screens/Settings/Appearance'
import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
import Models from '@/screens/Settings/Models'
import { useSetAtom } from 'jotai'
import { SUCCESS_SET_NEW_DESTINATION } from './Advanced/DataFolder'
import SettingDetail from './SettingDetail'
import SettingMenu from './SettingMenu'
const handleShowOptions = (menu: string) => {
switch (menu) {
case 'Extensions':
return <ExtensionCatalog />
import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
case 'My Settings':
return <AppearanceOptions />
export const SettingScreenList = [
'My Models',
'My Settings',
'Advanced Settings',
'Extensions',
] as const
case 'Advanced Settings':
return <Advanced />
case 'My Models':
return <Models />
}
}
export type SettingScreenTuple = typeof SettingScreenList
export type SettingScreen = SettingScreenTuple[number]
const SettingsScreen: React.FC = () => {
const [activeStaticMenu, setActiveStaticMenu] = useState('My Models')
const setSelectedSettingScreen = useSetAtom(selectedSettingAtom)
useEffect(() => {
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
setActiveStaticMenu('Advanced Settings')
setSelectedSettingScreen('Advanced Settings')
localStorage.removeItem(SUCCESS_SET_NEW_DESTINATION)
}
}, [])
}, [setSelectedSettingScreen])
return (
<div
className="flex h-full bg-background"
data-testid="testid-setting-description"
className="flex h-full bg-background"
>
<SettingMenu
activeMenu={activeStaticMenu}
onMenuClick={setActiveStaticMenu}
/>
{handleShowOptions(activeStaticMenu)}
<SettingMenu />
<SettingDetail />
</div>
)
}

View File

@ -1,13 +1,12 @@
import { Model } from '@janhq/core'
import { Model, SettingComponentProps } from '@janhq/core'
import { SettingComponentData } from '@/screens/Chat/ModelSetting/SettingComponent'
import { presetConfiguration } from '@/screens/Chat/ModelSetting/predefinedComponent'
export const getConfigurationsData = (
settings: object,
selectedModel?: Model
) => {
const componentData: SettingComponentData[] = []
): SettingComponentProps[] => {
const componentData: SettingComponentProps[] = []
Object.keys(settings).forEach((key: string) => {
const componentSetting = presetConfiguration[key]
@ -17,20 +16,20 @@ export const getConfigurationsData = (
}
if ('slider' === componentSetting.controllerType) {
const value = Number(settings[key as keyof typeof settings])
if ('value' in componentSetting.controllerData) {
componentSetting.controllerData.value = value
if ('max' in componentSetting.controllerData) {
if ('value' in componentSetting.controllerProps) {
componentSetting.controllerProps.value = value
if ('max' in componentSetting.controllerProps) {
switch (key) {
case 'max_tokens':
componentSetting.controllerData.max =
componentSetting.controllerProps.max =
selectedModel?.parameters.max_tokens ||
componentSetting.controllerData.max ||
componentSetting.controllerProps.max ||
4096
break
case 'ctx_len':
componentSetting.controllerData.max =
componentSetting.controllerProps.max =
selectedModel?.settings.ctx_len ||
componentSetting.controllerData.max ||
componentSetting.controllerProps.max ||
4096
break
}
@ -39,15 +38,15 @@ export const getConfigurationsData = (
} else if ('input' === componentSetting.controllerType) {
const value = settings[key as keyof typeof settings] as string
const placeholder = settings[key as keyof typeof settings] as string
if ('value' in componentSetting.controllerData)
componentSetting.controllerData.value = value
if ('placeholder' in componentSetting.controllerData)
componentSetting.controllerData.placeholder = placeholder
if ('value' in componentSetting.controllerProps)
componentSetting.controllerProps.value = value
if ('placeholder' in componentSetting.controllerProps)
componentSetting.controllerProps.placeholder = placeholder
} else if ('checkbox' === componentSetting.controllerType) {
const checked = settings[key as keyof typeof settings] as boolean
if ('checked' in componentSetting.controllerData)
componentSetting.controllerData.checked = checked
if ('value' in componentSetting.controllerProps)
componentSetting.controllerProps.value = checked
}
componentData.push(componentSetting)
})

View File

@ -1,15 +0,0 @@
import { ErrorCode } from '@janhq/core'
export const getErrorTitle = (
errorCode: ErrorCode,
errorMessage: string | undefined
) => {
switch (errorCode) {
case ErrorCode.Unknown:
return 'Apologies, somethings amiss!'
case ErrorCode.InvalidApiKey:
return 'Invalid API key. Please check your API key and try again.'
default:
return errorMessage
}
}