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 { export enum ExtensionTypeEnum {
Assistant = 'assistant', Assistant = 'assistant',
Conversational = 'conversational', Conversational = 'conversational',
@ -32,6 +36,38 @@ export type InstallationState = InstallationStateTuple[number]
* This class should be extended by any class that represents an extension. * This class should be extended by any class that represents an extension.
*/ */
export abstract class BaseExtension implements ExtensionType { 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 the type of the extension.
* @returns {ExtensionType} 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 { type(): ExtensionTypeEnum | undefined {
return undefined return undefined
} }
/** /**
* Called when the extension is loaded. * Called when the extension is loaded.
* Any initialization logic for the extension should be put here. * Any initialization logic for the extension should be put here.
*/ */
abstract onLoad(): void abstract onLoad(): void
/** /**
* Called when the extension is unloaded. * Called when the extension is unloaded.
* Any cleanup logic for the extension should be put here. * Any cleanup logic for the extension should be put here.
@ -67,6 +105,42 @@ export abstract class BaseExtension implements ExtensionType {
return false 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. * Determine if the prerequisites for the extension are installed.
* *
@ -81,8 +155,59 @@ export abstract class BaseExtension implements ExtensionType {
* *
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
// @ts-ignore async install(): Promise<void> {
async install(...args): Promise<void> {
return 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 * Inference request
*/ */
override inference(data: MessageRequest) { override async inference(data: MessageRequest) {
if (data.model?.engine?.toString() !== this.provider) return if (data.model?.engine?.toString() !== this.provider) return
const timestamp = Date.now() const timestamp = Date.now()
@ -77,12 +77,14 @@ export abstract class OAIEngine extends AIEngine {
...data.model, ...data.model,
} }
const header = await this.headers()
requestInference( requestInference(
this.inferenceUrl, this.inferenceUrl,
data.messages ?? [], data.messages ?? [],
model, model,
this.controller, this.controller,
this.headers() header
).subscribe({ ).subscribe({
next: (content: any) => { next: (content: any) => {
const messageContent: ThreadContent = { const messageContent: ThreadContent = {
@ -123,7 +125,7 @@ export abstract class OAIEngine extends AIEngine {
/** /**
* Headers for the inference request * Headers for the inference request
*/ */
headers(): HeadersInit { async headers(): Promise<HeadersInit> {
return {} return {}
} }
} }

View File

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

View File

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

View File

@ -316,6 +316,7 @@ export const chatCompletions = async (request: any, reply: any) => {
} }
const requestedModel = matchedModels[0] const requestedModel = matchedModels[0]
const engineConfiguration = await getEngineConfiguration(requestedModel.engine) const engineConfiguration = await getEngineConfiguration(requestedModel.engine)
let apiKey: string | undefined = undefined let apiKey: string | undefined = undefined
@ -323,7 +324,7 @@ export const chatCompletions = async (request: any, reply: any) => {
if (engineConfiguration) { if (engineConfiguration) {
apiKey = engineConfiguration.api_key apiKey = engineConfiguration.api_key
apiUrl = engineConfiguration.full_url apiUrl = engineConfiguration.full_url ?? DEFAULT_CHAT_COMPLETION_URL
} }
const headers: Record<string, any> = { 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 { join } from 'path'
import fs from 'fs' import fs from 'fs'
import os from 'os' 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) => { export const getEngineConfiguration = async (engineId: string) => {
if (engineId !== 'openai' && engineId !== 'groq') { if (engineId !== 'openai' && engineId !== 'groq') return undefined
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 './huggingface'
export * from './miscellaneous' export * from './miscellaneous'
export * from './api' 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 packageJson = require('./package.json')
const pkg = require('./package.json')
export default [ export default [
{ {
input: `src/index.ts`, 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') // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [], external: [],
watch: { watch: {
@ -36,7 +34,7 @@ export default [
// https://github.com/rollup/rollup-plugin-node-resolve#usage // https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({ resolve({
extensions: ['.js', '.ts', '.svelte'], extensions: ['.js', '.ts', '.svelte'],
browser: true browser: true,
}), }),
// Resolve source maps to the original source // Resolve source maps to the original source

View File

@ -1,13 +1,36 @@
import fs from 'fs' import fs from 'fs'
import path from 'path' 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 // Sec: Do not send engine settings over requests
// Read it manually instead // Read it manually instead
export const readEmbeddingEngine = (engineName: string) => { export const readEmbeddingEngine = (engineName: string) => {
if (engineName !== 'openai' && engineName !== 'groq') {
const engineSettings = fs.readFileSync( const engineSettings = fs.readFileSync(
path.join(getJanDataFolderPath(), 'engines', `${engineName}.json`), path.join(getJanDataFolderPath(), 'engines', `${engineName}.json`),
'utf-8' 'utf-8'
) )
return JSON.parse(engineSettings) 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 * @module inference-groq-extension/src/index
*/ */
import { import { RemoteOAIEngine } from '@janhq/core'
events,
fs,
AppConfigurationEventName,
joinPath,
RemoteOAIEngine,
} from '@janhq/core'
import { join } from 'path'
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. * 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. * 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. * It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/ */
export default class JanInferenceGroqExtension extends RemoteOAIEngine { export default class JanInferenceGroqExtension extends RemoteOAIEngine {
private readonly _engineDir = 'file://engines' inferenceUrl: string = ''
private readonly _engineMetadataFileName = 'groq.json'
inferenceUrl: string = COMPLETION_URL
provider = 'groq' provider = 'groq'
apiKey = ''
private _engineSettings = { override async onLoad(): Promise<void> {
full_url: COMPLETION_URL,
api_key: 'gsk-<your key here>',
}
/**
* Subscribes to events emitted by the @janhq/core package.
*/
async onLoad() {
super.onLoad() super.onLoad()
if (!(await fs.existsSync(this._engineDir))) { // Register Settings
await fs.mkdir(this._engineDir) this.registerSettings(SETTINGS)
}
this.writeDefaultEngineSettings() // Retrieve API Key Setting
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
const settingsFilePath = await joinPath([ this.inferenceUrl = await this.getSetting<string>(
this._engineDir, Settings.chatCompletionsEndPoint,
this._engineMetadataFileName, ''
])
// Events subscription
events.on(
AppConfigurationEventName.OnConfigurationUpdate,
(settingsKey: string) => {
// Update settings on changes
if (settingsKey === settingsFilePath) this.writeDefaultEngineSettings()
}
) )
} }
async writeDefaultEngineSettings() { onSettingUpdate<T>(key: string, value: T): void {
try { if (key === Settings.apiKey) {
const engineFile = join(this._engineDir, this._engineMetadataFileName) this.apiKey = value as string
if (await fs.existsSync(engineFile)) { } else if (key === Settings.chatCompletionsEndPoint) {
const engine = await fs.readFileSync(engineFile, 'utf-8') this.inferenceUrl = value as string
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)
} }
} }
} }

View File

@ -1,6 +1,7 @@
const path = require('path') const path = require('path')
const webpack = require('webpack') const webpack = require('webpack')
const packageJson = require('./package.json') const packageJson = require('./package.json')
const settingJson = require('./resources/settings.json')
module.exports = { module.exports = {
experiments: { outputModule: true }, experiments: { outputModule: true },
@ -17,8 +18,8 @@ module.exports = {
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
SETTINGS: JSON.stringify(settingJson),
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`), MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
COMPLETION_URL: JSON.stringify('https://api.groq.com/openai/v1/chat/completions'),
}), }),
], ],
output: { 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 json from '@rollup/plugin-json'
import replace from '@rollup/plugin-replace' import replace from '@rollup/plugin-replace'
const packageJson = require('./package.json') const packageJson = require('./package.json')
const defaultSettingJson = require('./resources/default_settings.json')
const pkg = require('./package.json')
export default [ export default [
{ {
input: `src/index.ts`, 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') // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [], external: [],
watch: { watch: {
@ -19,7 +18,9 @@ export default [
}, },
plugins: [ plugins: [
replace({ replace({
EXTENSION_NAME: JSON.stringify(packageJson.name),
NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`), NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`),
DEFAULT_SETTINGS: JSON.stringify(defaultSettingJson),
INFERENCE_URL: JSON.stringify( INFERENCE_URL: JSON.stringify(
process.env.INFERENCE_URL || process.env.INFERENCE_URL ||
'http://127.0.0.1:3928/inferences/llamacpp/chat_completion' '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 INFERENCE_URL: string
declare const TROUBLESHOOTING_URL: string declare const TROUBLESHOOTING_URL: string
declare const JAN_SERVER_INFERENCE_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. * 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` this.inferenceUrl = `${window.core?.api?.baseApiUrl}/v1/chat/completions`
} }
console.debug('Inference url: ', this.inferenceUrl)
this.getNitroProcesHealthIntervalId = setInterval( this.getNitroProcesHealthIntervalId = setInterval(
() => this.periodicallyGetNitroHealth(), () => this.periodicallyGetNitroHealth(),
JanInferenceNitroExtension._intervalHealthCheck 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 * @version 1.0.0
* @module inference-openai-extension/src/index * @module inference-openai-extension/src/index
*/ */
declare const ENGINE: string
import { import { RemoteOAIEngine } from '@janhq/core'
events,
fs,
AppConfigurationEventName,
joinPath,
RemoteOAIEngine,
} from '@janhq/core'
import { join } from 'path'
declare const COMPLETION_URL: string
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. * 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. * 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. * It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/ */
export default class JanInferenceOpenAIExtension extends RemoteOAIEngine { export default class JanInferenceOpenAIExtension extends RemoteOAIEngine {
private static readonly _engineDir = 'file://engines' inferenceUrl: string = ''
private static readonly _engineMetadataFileName = `${ENGINE}.json`
private _engineSettings = {
full_url: COMPLETION_URL,
api_key: 'sk-<your key here>',
}
inferenceUrl: string = COMPLETION_URL
provider: string = 'openai' provider: string = 'openai'
apiKey: string = ''
// TODO: Just use registerSettings from BaseExtension override async onLoad(): Promise<void> {
// Remove these methods
/**
* Subscribes to events emitted by the @janhq/core package.
*/
async onLoad() {
super.onLoad() super.onLoad()
if (!(await fs.existsSync(JanInferenceOpenAIExtension._engineDir))) { // Register Settings
await fs.mkdir(JanInferenceOpenAIExtension._engineDir) this.registerSettings(SETTINGS)
}
this.writeDefaultEngineSettings() this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
this.inferenceUrl = await this.getSetting<string>(
const settingsFilePath = await joinPath([ Settings.chatCompletionsEndPoint,
JanInferenceOpenAIExtension._engineDir, ''
JanInferenceOpenAIExtension._engineMetadataFileName,
])
events.on(
AppConfigurationEventName.OnConfigurationUpdate,
(settingsKey: string) => {
// Update settings on changes
if (settingsKey === settingsFilePath) this.writeDefaultEngineSettings()
}
) )
} }
async writeDefaultEngineSettings() { onSettingUpdate<T>(key: string, value: T): void {
try { if (key === Settings.apiKey) {
const engineFile = join( this.apiKey = value as string
JanInferenceOpenAIExtension._engineDir, } else if (key === Settings.chatCompletionsEndPoint) {
JanInferenceOpenAIExtension._engineMetadataFileName this.inferenceUrl = value as string
)
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)
} }
} }
} }

View File

@ -1,6 +1,7 @@
const path = require('path') const path = require('path')
const webpack = require('webpack') const webpack = require('webpack')
const packageJson = require('./package.json') const packageJson = require('./package.json')
const settingJson = require('./resources/settings.json')
module.exports = { module.exports = {
experiments: { outputModule: true }, experiments: { outputModule: true },
@ -17,8 +18,8 @@ module.exports = {
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
SETTINGS: JSON.stringify(settingJson),
ENGINE: JSON.stringify(packageJson.engine), ENGINE: JSON.stringify(packageJson.engine),
COMPLETION_URL: JSON.stringify('https://api.openai.com/v1/chat/completions'),
}), }),
], ],
output: { 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 * @module inference-nvidia-triton-trt-llm-extension/src/index
*/ */
import { import { RemoteOAIEngine } from '@janhq/core'
AppConfigurationEventName,
events,
fs,
joinPath,
Model,
RemoteOAIEngine,
} from '@janhq/core'
import { join } from 'path'
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. * 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. * 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. * It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/ */
export default class JanInferenceTritonTrtLLMExtension extends RemoteOAIEngine { export default class JanInferenceTritonTrtLLMExtension extends RemoteOAIEngine {
private readonly _engineDir = 'file://engines'
private readonly _engineMetadataFileName = 'triton_trtllm.json'
inferenceUrl: string = '' inferenceUrl: string = ''
provider: string = 'triton_trtllm' provider: string = 'triton_trtllm'
apiKey: string = ''
_engineSettings: {
base_url: ''
api_key: ''
}
/** /**
* Subscribes to events emitted by the @janhq/core package. * Subscribes to events emitted by the @janhq/core package.
*/ */
async onLoad() { async onLoad() {
super.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([ // Retrieve API Key Setting
this._engineDir, this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
this._engineMetadataFileName, this.inferenceUrl = await this.getSetting<string>(
]) Settings.chatCompletionsEndPoint,
''
// Events subscription
events.on(
AppConfigurationEventName.OnConfigurationUpdate,
(settingsKey: string) => {
// Update settings on changes
if (settingsKey === settingsFilePath) this.writeDefaultEngineSettings()
}
) )
} }
async writeDefaultEngineSettings() { onSettingUpdate<T>(key: string, value: T): void {
try { if (key === Settings.apiKey) {
const engine_json = join(this._engineDir, this._engineMetadataFileName) this.apiKey = value as string
if (await fs.existsSync(engine_json)) { } else if (key === Settings.chatCompletionsEndPoint) {
const engine = await fs.readFileSync(engine_json, 'utf-8') this.inferenceUrl = value as string
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)
} }
} }
} }

View File

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

View File

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

View File

@ -251,7 +251,7 @@ export default class TensorRTLLMExtension extends LocalOAIEngine {
return Promise.resolve() return Promise.resolve()
} }
override inference(data: MessageRequest): void { override async inference(data: MessageRequest) {
if (!this.loadedModel) return if (!this.loadedModel) return
// TensorRT LLM Extension supports streaming only // TensorRT LLM Extension supports streaming only
if (data.model) data.model.parameters.stream = true 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 { toGibibytes } from '@/utils/converter'
import ModelLabel from '../ModelLabel' import ModelLabel from '../ModelLabel'
import OpenAiKeyInput from '../OpenAiKeyInput'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom' import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
@ -144,7 +143,7 @@ const DropdownListSidebar = ({
// Update model parameter to the thread file // Update model parameter to the thread file
if (model) if (model)
updateModelParameter(activeThread.id, { updateModelParameter(activeThread, {
params: modelParams, params: modelParams,
modelId: model.id, modelId: model.id,
engine: model.engine, engine: model.engine,
@ -170,7 +169,6 @@ const DropdownListSidebar = ({
stateModel.model === selectedModel?.id && stateModel.loading stateModel.model === selectedModel?.id && stateModel.loading
return ( return (
<>
<div <div
className={twMerge( className={twMerge(
'relative w-full overflow-hidden rounded-md', 'relative w-full overflow-hidden rounded-md',
@ -271,10 +269,7 @@ const DropdownListSidebar = ({
)} )}
> >
<div className="relative flex w-full justify-between"> <div className="relative flex w-full justify-between">
<div> <span className="line-clamp-1 block">{x.name}</span>
<span className="line-clamp-1 block">
{x.name}
</span>
<div className="absolute right-0 top-2 space-x-2"> <div className="absolute right-0 top-2 space-x-2">
<span className="font-bold text-muted-foreground"> <span className="font-bold text-muted-foreground">
{toGibibytes(x.metadata.size)} {toGibibytes(x.metadata.size)}
@ -284,7 +279,6 @@ const DropdownListSidebar = ({
)} )}
</div> </div>
</div> </div>
</div>
</SelectItem> </SelectItem>
<div <div
className={twMerge( className={twMerge(
@ -333,9 +327,6 @@ const DropdownListSidebar = ({
</SelectPortal> </SelectPortal>
</Select> </Select>
</div> </div>
<OpenAiKeyInput />
</>
) )
} }

View File

@ -26,8 +26,7 @@ const ModelConfigInput: React.FC<Props> = ({
description, description,
placeholder, placeholder,
onValueChanged, onValueChanged,
}) => { }) => (
return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="mb-2 flex items-center gap-x-2"> <div className="mb-2 flex items-center gap-x-2">
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300"> <p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
@ -52,7 +51,6 @@ const ModelConfigInput: React.FC<Props> = ({
disabled={!enabled} disabled={!enabled}
/> />
</div> </div>
) )
}
export default ModelConfigInput 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 useModels from '@/hooks/useModels'
import useThreads from '@/hooks/useThreads' import useThreads from '@/hooks/useThreads'
import { SettingScreenList } from '@/screens/Settings'
import { defaultJanDataFolderAtom } from '@/helpers/atoms/App.atom' import { defaultJanDataFolderAtom } from '@/helpers/atoms/App.atom'
import { import {
janDataFolderPathAtom, janDataFolderPathAtom,
quickAskEnabledAtom, quickAskEnabledAtom,
} from '@/helpers/atoms/AppConfig.atom' } from '@/helpers/atoms/AppConfig.atom'
import { janSettingScreenAtom } from '@/helpers/atoms/Setting.atom'
type Props = { type Props = {
children: ReactNode children: ReactNode
@ -24,6 +27,7 @@ const DataLoader: React.FC<Props> = ({ children }) => {
const setJanDataFolderPath = useSetAtom(janDataFolderPathAtom) const setJanDataFolderPath = useSetAtom(janDataFolderPathAtom)
const setQuickAskEnabled = useSetAtom(quickAskEnabledAtom) const setQuickAskEnabled = useSetAtom(quickAskEnabledAtom)
const setJanDefaultDataFolder = useSetAtom(defaultJanDataFolderAtom) const setJanDefaultDataFolder = useSetAtom(defaultJanDataFolderAtom)
const setJanSettingScreen = useSetAtom(janSettingScreenAtom)
useModels() useModels()
useThreads() useThreads()
@ -49,6 +53,13 @@ const DataLoader: React.FC<Props> = ({ children }) => {
getDefaultJanDataFolder() getDefaultJanDataFolder()
}, [setJanDefaultDataFolder]) }, [setJanDefaultDataFolder])
useEffect(() => {
const janSettingScreen = SettingScreenList.filter(
(screen) => window.electronAPI || screen !== 'Extensions'
)
setJanSettingScreen(janSettingScreen)
}, [setJanSettingScreen])
console.debug('Load Data...') console.debug('Load Data...')
return <Fragment>{children}</Fragment> return <Fragment>{children}</Fragment>

View File

@ -119,7 +119,13 @@ export class ExtensionManager {
) { ) {
this.register( this.register(
extension.name ?? extension.url, 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) => { (get, set, threadId: string, params: ModelParams) => {
const currentState = { ...get(threadModelParamsAtom) } const currentState = { ...get(threadModelParamsAtom) }
currentState[threadId] = params currentState[threadId] = params
console.debug(
`Update model params for thread ${threadId}, ${JSON.stringify(
params,
null,
2
)}`
)
set(threadModelParamsAtom, currentState) set(threadModelParamsAtom, currentState)
} }
) )

View File

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

View File

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

View File

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

View File

@ -1,30 +1,41 @@
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 { useCreateNewThread } from '@/hooks/useCreateNewThread'
import SettingComponentBuilder, { import SettingComponentBuilder from '../ModelSetting/SettingComponent'
SettingComponentData,
} from '../ModelSetting/SettingComponent'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' import {
activeThreadAtom,
engineParamsUpdateAtom,
} from '@/helpers/atoms/Thread.atom'
const AssistantSetting = ({ type Props = {
componentData, componentData: SettingComponentProps[]
}: { }
componentData: SettingComponentData[]
}) => { const AssistantSetting: React.FC<Props> = ({ componentData }) => {
const activeThread = useAtomValue(activeThreadAtom) const activeThread = useAtomValue(activeThreadAtom)
const { updateThreadMetadata } = useCreateNewThread() 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()
}
return (
<div className="flex flex-col">
{activeThread && componentData && (
<SettingComponentBuilder
componentData={componentData}
updater={(_, name, value) => {
if ( if (
activeThread.assistants[0].tools && activeThread.assistants[0].tools &&
(name === 'chunk_overlap' || name === 'chunk_size') (key === 'chunk_overlap' || key === 'chunk_size')
) { ) {
if ( if (
activeThread.assistants[0].tools[0]?.settings.chunk_size < activeThread.assistants[0].tools[0]?.settings.chunk_size <
@ -33,22 +44,18 @@ const AssistantSetting = ({
activeThread.assistants[0].tools[0].settings.chunk_overlap = activeThread.assistants[0].tools[0].settings.chunk_overlap =
activeThread.assistants[0].tools[0].settings.chunk_size activeThread.assistants[0].tools[0].settings.chunk_size
} }
if ( if (
name === 'chunk_size' && key === 'chunk_size' &&
value < value < activeThread.assistants[0].tools[0].settings.chunk_overlap
activeThread.assistants[0].tools[0].settings.chunk_overlap
) { ) {
activeThread.assistants[0].tools[0].settings.chunk_overlap = activeThread.assistants[0].tools[0].settings.chunk_overlap = value
value
} else if ( } else if (
name === 'chunk_overlap' && key === 'chunk_overlap' &&
value > activeThread.assistants[0].tools[0].settings.chunk_size value > activeThread.assistants[0].tools[0].settings.chunk_size
) { ) {
activeThread.assistants[0].tools[0].settings.chunk_size = value activeThread.assistants[0].tools[0].settings.chunk_size = value
} }
} }
updateThreadMetadata({ updateThreadMetadata({
...activeThread, ...activeThread,
assistants: [ assistants: [
@ -61,17 +68,31 @@ const AssistantSetting = ({
settings: { settings: {
...(activeThread.assistants[0].tools && ...(activeThread.assistants[0].tools &&
activeThread.assistants[0].tools[0]?.settings), activeThread.assistants[0].tools[0]?.settings),
[name]: value, [key]: value,
}, },
}, },
], ],
}, },
], ],
}) })
}} },
[
activeThread,
componentData,
setEngineParamsUpdate,
stopModel,
updateThreadMetadata,
]
)
if (!activeThread) return null
if (componentData.length === 0) return null
return (
<SettingComponentBuilder
componentProps={componentData}
onValueUpdated={onValueChanged}
/> />
)}
</div>
) )
} }

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,80 +1,26 @@
import { Fragment } from 'react'
import ScrollToBottom from 'react-scroll-to-bottom' import ScrollToBottom from 'react-scroll-to-bottom'
import { InferenceEngine, MessageStatus } from '@janhq/core' import { MessageStatus } from '@janhq/core'
import { Button } from '@janhq/uikit' import { useAtomValue } from 'jotai'
import { useAtomValue, useSetAtom } from 'jotai'
import LogoMark from '@/containers/Brand/Logo/Mark'
import { MainViewState } from '@/constants/screens'
import ChatItem from '../ChatItem' import ChatItem from '../ChatItem'
import ErrorMessage from '../ErrorMessage' 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 { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const ChatBody: React.FC = () => { const ChatBody: React.FC = () => {
const messages = useAtomValue(getCurrentChatMessagesAtom) const messages = useAtomValue(getCurrentChatMessagesAtom)
const downloadedModels = useAtomValue(downloadedModelsAtom) const downloadedModels = useAtomValue(downloadedModelsAtom)
const setMainViewState = useSetAtom(mainViewStateAtom)
if (downloadedModels.length === 0) if (downloadedModels.length === 0) return <EmptyModel />
return ( if (messages.length === 0) return <EmptyThread />
<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
return ( 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"> <ScrollToBottom className="flex h-full w-full flex-col">
{messages.map((message, index) => ( {messages.map((message, index) => (
<div key={message.id}> <div key={message.id}>
@ -85,14 +31,10 @@ const ChatBody: React.FC = () => {
{(message.status === MessageStatus.Error || {(message.status === MessageStatus.Error ||
message.status === MessageStatus.Stopped) && message.status === MessageStatus.Stopped) &&
index === messages.length - 1 && ( index === messages.length - 1 && <ErrorMessage message={message} />}
<ErrorMessage message={message} />
)}
</div> </div>
))} ))}
</ScrollToBottom> </ScrollToBottom>
)}
</Fragment>
) )
} }

View File

@ -1,25 +1,53 @@
import SettingComponentBuilder from '../../Chat/ModelSetting/SettingComponent' import { useCallback } from 'react'
import { SettingComponentData } from '../ModelSetting/SettingComponent'
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 ( return (
<>
{componentData.filter((e) => e.name !== 'prompt_template').length && (
<div className="flex flex-col">
<SettingComponentBuilder <SettingComponentBuilder
componentData={componentData} componentProps={componentData}
enabled={enabled} enabled={!isLocalServerRunning}
selector={(e) => e.name !== 'prompt_template'} onValueUpdated={onValueChanged}
/> />
</div>
)}
</>
) )
} }

View File

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

View File

@ -1,149 +1,78 @@
/* eslint-disable no-case-declarations */ import {
import { useAtomValue, useSetAtom } from 'jotai' SettingComponentProps,
InputComponentProps,
CheckboxComponentProps,
SliderComponentProps,
} from '@janhq/core'
import Checkbox from '@/containers/Checkbox' import Checkbox from '@/containers/Checkbox'
import ModelConfigInput from '@/containers/ModelConfigInput' import ModelConfigInput from '@/containers/ModelConfigInput'
import SliderRightPanel from '@/containers/SliderRightPanel' import SliderRightPanel from '@/containers/SliderRightPanel'
import { useActiveModel } from '@/hooks/useActiveModel' type Props = {
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters' componentProps: SettingComponentProps[]
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[]
enabled?: boolean enabled?: boolean
selector?: (e: SettingComponentData) => boolean onValueUpdated: (key: string, value: string | number | boolean) => void
updater?: ( }
threadId: string,
name: string, const SettingComponent: React.FC<Props> = ({
value: string | number | boolean | string[] componentProps,
) => void enabled = true,
onValueUpdated,
}) => { }) => {
const { updateModelParameter } = useUpdateModelParameters() const components = componentProps.map((data) => {
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]
}
updateModelParameter(threadId, {
params: { [name]: value },
})
}
}
const components = componentData
.filter((x) => (selector ? selector(x) : true))
.map((data) => {
switch (data.controllerType) { switch (data.controllerType) {
case 'slider': case 'slider': {
const { min, max, step, value } = data.controllerData as SliderData const { min, max, step, value } =
data.controllerProps as SliderComponentProps
return ( return (
<SliderRightPanel <SliderRightPanel
key={data.name} key={data.key}
title={data.title} title={data.title}
description={data.description} description={data.description}
min={min} min={min}
max={max} max={max}
step={step} step={step}
value={value} value={value}
name={data.name} name={data.key}
enabled={enabled} enabled={enabled}
onValueChanged={(value) => onValueChanged(data.name, value)} onValueChanged={(value) => onValueUpdated(data.key, value)}
/> />
) )
case 'input': }
case 'input': {
const { placeholder, value: textValue } = const { placeholder, value: textValue } =
data.controllerData as InputData data.controllerProps as InputComponentProps
return ( return (
<ModelConfigInput <ModelConfigInput
title={data.title} title={data.title}
enabled={enabled} enabled={enabled}
key={data.name} key={data.key}
name={data.name} name={data.key}
description={data.description} description={data.description}
placeholder={placeholder} placeholder={placeholder}
value={textValue} value={textValue}
onValueChanged={(value) => onValueChanged(data.name, value)} onValueChanged={(value) => onValueUpdated(data.key, value)}
/> />
) )
case 'checkbox': }
const { checked } = data.controllerData as CheckboxData
case 'checkbox': {
const { value } = data.controllerProps as CheckboxComponentProps
return ( return (
<Checkbox <Checkbox
key={data.name} key={data.key}
enabled={enabled} enabled={enabled}
name={data.name} name={data.key}
description={data.description} description={data.description}
title={data.title} title={data.title}
checked={checked} checked={value}
onValueChanged={(value) => onValueChanged(data.name, value)} onValueChanged={(value) => onValueUpdated(data.key, value)}
/> />
) )
}
default: default:
return null return null
} }

View File

@ -1,26 +1,48 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { useCallback } from 'react'
import React from 'react'
import SettingComponentBuilder, { import { SettingComponentProps } from '@janhq/core/.'
SettingComponentData,
} from './SettingComponent' 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 ( return (
<>
{componentData.filter((e) => e.name !== 'prompt_template').length && (
<div className="flex flex-col">
<SettingComponentBuilder <SettingComponentBuilder
componentData={componentData} enabled={!isLocalServerRunning}
selector={(e) => e.name !== 'prompt_template'} componentProps={componentProps}
onValueUpdated={onValueChanged}
/> />
</div>
)}
</>
) )
} }

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: { prompt_template: {
name: 'prompt_template', key: 'prompt_template',
title: 'Prompt template', title: 'Prompt template',
description: 'The prompt to use for internal configuration.', description: 'The prompt to use for internal configuration.',
controllerType: 'input', controllerType: 'input',
controllerData: { controllerProps: {
placeholder: 'Prompt template', placeholder: 'Prompt template',
value: '', value: '',
}, },
requireModelReload: true,
configType: 'setting',
}, },
stop: { stop: {
name: 'stop', key: 'stop',
title: 'Stop', title: 'Stop',
description: description:
'Defines specific tokens or phrases at which the model will stop generating further output. ', 'Defines specific tokens or phrases at which the model will stop generating further output. ',
controllerType: 'input', controllerType: 'input',
controllerData: { controllerProps: {
placeholder: 'Stop', placeholder: 'Stop',
value: '', value: '',
}, },
requireModelReload: false,
configType: 'runtime',
}, },
ctx_len: { ctx_len: {
name: 'ctx_len', key: 'ctx_len',
title: 'Context Length', title: 'Context Length',
description: description:
'The context length for model operations varies; the maximum depends on the specific model used.', 'The context length for model operations varies; the maximum depends on the specific model used.',
controllerType: 'slider', controllerType: 'slider',
controllerData: { controllerProps: {
min: 0, min: 0,
max: 4096, max: 4096,
step: 128, step: 128,
value: 4096, value: 4096,
}, },
requireModelReload: true,
configType: 'setting',
}, },
max_tokens: { max_tokens: {
name: 'max_tokens', key: 'max_tokens',
title: 'Max Tokens', title: 'Max Tokens',
description: description:
'The maximum number of tokens the model will generate in a single response.', 'The maximum number of tokens the model will generate in a single response.',
controllerType: 'slider', controllerType: 'slider',
controllerData: { controllerProps: {
min: 100, min: 100,
max: 4096, max: 4096,
step: 10, step: 10,
value: 4096, value: 4096,
}, },
requireModelReload: false,
configType: 'runtime',
}, },
ngl: { ngl: {
name: 'ngl', key: 'ngl',
title: 'Number of GPU layers (ngl)', title: 'Number of GPU layers (ngl)',
description: 'The number of layers to load onto the GPU for acceleration.', description: 'The number of layers to load onto the GPU for acceleration.',
controllerType: 'slider', controllerType: 'slider',
controllerData: { controllerProps: {
min: 1, min: 1,
max: 100, max: 100,
step: 1, step: 1,
value: 100, value: 100,
}, },
requireModelReload: true,
configType: 'setting',
}, },
embedding: { embedding: {
name: 'embedding', key: 'embedding',
title: 'Embedding', title: 'Embedding',
description: 'Whether to enable embedding.', description: 'Whether to enable embedding.',
controllerType: 'checkbox', controllerType: 'checkbox',
controllerData: { controllerProps: {
checked: true, value: true,
}, },
requireModelReload: true,
configType: 'setting',
}, },
stream: { stream: {
name: 'stream', key: 'stream',
title: 'Stream', title: 'Stream',
description: 'Enable real-time data processing for faster predictions.', description: 'Enable real-time data processing for faster predictions.',
controllerType: 'checkbox', controllerType: 'checkbox',
controllerData: { controllerProps: {
checked: false, value: false,
}, },
requireModelReload: false,
configType: 'runtime',
}, },
temperature: { temperature: {
name: 'temperature', key: 'temperature',
title: 'Temperature', title: 'Temperature',
description: 'Controls the randomness of the models output.', description: 'Controls the randomness of the models output.',
controllerType: 'slider', controllerType: 'slider',
controllerData: { controllerProps: {
min: 0, min: 0,
max: 2, max: 2,
step: 0.1, step: 0.1,
value: 0.7, value: 0.7,
}, },
requireModelReload: false,
configType: 'runtime',
}, },
frequency_penalty: { frequency_penalty: {
name: 'frequency_penalty', key: 'frequency_penalty',
title: 'Frequency Penalty', title: 'Frequency Penalty',
description: description:
'Adjusts the likelihood of the model repeating words or phrases in its output. ', 'Adjusts the likelihood of the model repeating words or phrases in its output. ',
controllerType: 'slider', controllerType: 'slider',
controllerData: { controllerProps: {
min: 0, min: 0,
max: 1, max: 1,
step: 0.1, step: 0.1,
value: 0.7, value: 0.7,
}, },
requireModelReload: false,
configType: 'runtime',
}, },
presence_penalty: { presence_penalty: {
name: 'presence_penalty', key: 'presence_penalty',
title: 'Presence Penalty', title: 'Presence Penalty',
description: description:
'Influences the generation of new and varied concepts in the models output. ', 'Influences the generation of new and varied concepts in the models output. ',
controllerType: 'slider', controllerType: 'slider',
controllerData: { controllerProps: {
min: 0, min: 0,
max: 1, max: 1,
step: 0.1, step: 0.1,
value: 0.7, value: 0.7,
}, },
requireModelReload: false,
configType: 'runtime',
}, },
top_p: { top_p: {
name: 'top_p', key: 'top_p',
title: 'Top P', title: 'Top P',
description: 'Set probability threshold for more relevant outputs.', description: 'Set probability threshold for more relevant outputs.',
controllerType: 'slider', controllerType: 'slider',
controllerData: { controllerProps: {
min: 0, min: 0,
max: 1, max: 1,
step: 0.1, step: 0.1,
value: 0.95, value: 0.95,
}, },
requireModelReload: false,
configType: 'runtime',
}, },
n_parallel: { n_parallel: {
name: 'n_parallel', key: 'n_parallel',
title: 'N Parallel', title: 'N Parallel',
description: description:
'The number of parallel operations. Only set when enable continuous batching. ', 'The number of parallel operations. Only set when enable continuous batching. ',
controllerType: 'slider', controllerType: 'slider',
controllerData: { controllerProps: {
min: 0, min: 0,
max: 4, max: 4,
step: 1, step: 1,
value: 1, value: 1,
}, },
requireModelReload: true,
configType: 'setting',
}, },
// assistant // assistant
chunk_size: { chunk_size: {
name: 'chunk_size', key: 'chunk_size',
title: 'Chunk Size', title: 'Chunk Size',
description: 'Maximum number of tokens in a chunk', description: 'Maximum number of tokens in a chunk',
controllerType: 'slider', controllerType: 'slider',
controllerData: { controllerProps: {
min: 128, min: 128,
max: 2048, max: 2048,
step: 128, step: 128,
value: 1024, value: 1024,
}, },
requireModelReload: true,
configType: 'setting',
}, },
chunk_overlap: { chunk_overlap: {
name: 'chunk_overlap', key: 'chunk_overlap',
title: 'Chunk Overlap', title: 'Chunk Overlap',
description: 'Number of tokens overlapping between two adjacent chunks', description: 'Number of tokens overlapping between two adjacent chunks',
controllerType: 'slider', controllerType: 'slider',
controllerData: { controllerProps: {
min: 32, min: 32,
max: 512, max: 512,
step: 32, step: 32,
value: 64, value: 64,
}, },
requireModelReload: true,
configType: 'setting',
}, },
top_k: { top_k: {
name: 'top_k', key: 'top_k',
title: 'Top K', title: 'Top K',
description: 'Number of top-ranked documents to retrieve', description: 'Number of top-ranked documents to retrieve',
controllerType: 'slider', controllerType: 'slider',
controllerData: { controllerProps: {
min: 1, min: 1,
max: 5, max: 5,
step: 1, step: 1,
value: 2, value: 2,
}, },
requireModelReload: false,
configType: 'runtime',
}, },
retrieval_template: { retrieval_template: {
name: 'retrieval_template', key: 'retrieval_template',
title: 'Retrieval Template', title: 'Retrieval Template',
description: description:
'The template to use for retrieval. The following variables are available: {CONTEXT}, {QUESTION}', 'The template to use for retrieval. The following variables are available: {CONTEXT}, {QUESTION}',
controllerType: 'input', controllerType: 'input',
controllerData: { controllerProps: {
placeholder: 'Retrieval Template', placeholder: 'Retrieval Template',
value: '', 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 { import { Input, Textarea } from '@janhq/uikit'
Input,
Textarea,
Switch,
Tooltip,
TooltipArrow,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from '@janhq/uikit'
import { atom, useAtomValue } from 'jotai' import { atom, useAtomValue } from 'jotai'
import { InfoIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import LogoMark from '@/containers/Brand/Logo/Mark' import LogoMark from '@/containers/Brand/Logo/Mark'
@ -28,13 +18,13 @@ import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import { getConfigurationsData } from '@/utils/componentSettings' import { getConfigurationsData } from '@/utils/componentSettings'
import { toRuntimeParams, toSettingParams } from '@/utils/modelParam' import { toRuntimeParams, toSettingParams } from '@/utils/modelParam'
import AssistantSetting from '../AssistantSetting'
import EngineSetting from '../EngineSetting' import EngineSetting from '../EngineSetting'
import ModelSetting from '../ModelSetting' 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 { import {
activeThreadAtom, activeThreadAtom,
getActiveThreadModelParamsAtom, getActiveThreadModelParamsAtom,
@ -48,23 +38,63 @@ const Sidebar: React.FC = () => {
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const selectedModel = useAtomValue(selectedModelAtom) const selectedModel = useAtomValue(selectedModelAtom)
const { updateThreadMetadata } = useCreateNewThread() const { updateThreadMetadata } = useCreateNewThread()
const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom)
const modelEngineParams = toSettingParams(activeModelParams) const modelSettings = useMemo(() => {
const modelRuntimeParams = toRuntimeParams(activeModelParams) const modelRuntimeParams = toRuntimeParams(activeModelParams)
const componentDataAssistantSetting = getConfigurationsData(
(activeThread?.assistants[0]?.tools && const componentDataRuntimeSetting = getConfigurationsData(
activeThread?.assistants[0]?.tools[0]?.settings) ?? modelRuntimeParams,
{} selectedModel
) )
return componentDataRuntimeSetting.filter(
(x) => x.key !== 'prompt_template'
)
}, [activeModelParams, selectedModel])
const engineSettings = useMemo(() => {
const modelEngineParams = toSettingParams(activeModelParams)
const componentDataEngineSetting = getConfigurationsData( const componentDataEngineSetting = getConfigurationsData(
modelEngineParams, modelEngineParams,
selectedModel selectedModel
) )
const componentDataRuntimeSetting = getConfigurationsData( return componentDataEngineSetting.filter((x) => x.key !== 'prompt_template')
modelRuntimeParams, }, [activeModelParams, selectedModel])
const promptTemplateSettings = useMemo(() => {
const modelEngineParams = toSettingParams(activeModelParams)
const componentDataEngineSetting = getConfigurationsData(
modelEngineParams,
selectedModel 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 onAssistantInstructionChanged = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (activeThread)
updateThreadMetadata({
...activeThread,
assistants: [
{
...activeThread.assistants[0],
instructions: e.target.value || '',
},
],
})
},
[activeThread, updateThreadMetadata]
)
return ( return (
<div <div
@ -92,13 +122,7 @@ const Sidebar: React.FC = () => {
<Input <Input
id="thread-title" id="thread-title"
value={activeThread?.title} value={activeThread?.title}
onChange={(e) => { onChange={onThreadTitleChanged}
if (activeThread)
updateThreadMetadata({
...activeThread,
title: e.target.value || '',
})
}}
/> />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
@ -133,18 +157,7 @@ const Sidebar: React.FC = () => {
id="assistant-instructions" id="assistant-instructions"
placeholder="Eg. You are a helpful assistant." placeholder="Eg. You are a helpful assistant."
value={activeThread?.assistants[0].instructions ?? ''} value={activeThread?.assistants[0].instructions ?? ''}
onChange={(e) => { onChange={onAssistantInstructionChanged}
if (activeThread)
updateThreadMetadata({
...activeThread,
assistants: [
{
...activeThread.assistants[0],
instructions: e.target.value || '',
},
],
})
}}
/> />
</div> </div>
</div> </div>
@ -154,36 +167,33 @@ const Sidebar: React.FC = () => {
<div className="px-2 pt-4"> <div className="px-2 pt-4">
<DropdownListSidebar /> <DropdownListSidebar />
{componentDataRuntimeSetting.length > 0 && ( {modelSettings.length > 0 && (
<div className="mt-6"> <div className="mt-6">
<CardSidebar title="Inference Parameters" asChild> <CardSidebar title="Inference Parameters" asChild>
<div className="px-2 py-4"> <div className="px-2 py-4">
<ModelSetting componentData={componentDataRuntimeSetting} /> <ModelSetting componentProps={modelSettings} />
</div> </div>
</CardSidebar> </CardSidebar>
</div> </div>
)} )}
{componentDataEngineSetting.filter( {promptTemplateSettings.length > 0 && (
(x) => x.name === 'prompt_template'
).length !== 0 && (
<div className="mt-4"> <div className="mt-4">
<CardSidebar title="Model Parameters" asChild> <CardSidebar title="Model Parameters" asChild>
<div className="px-2 py-4"> <div className="px-2 py-4">
<SettingComponentBuilder <PromptTemplateSetting
componentData={componentDataEngineSetting} componentData={promptTemplateSettings}
selector={(x) => x.name === 'prompt_template'}
/> />
</div> </div>
</CardSidebar> </CardSidebar>
</div> </div>
)} )}
{componentDataEngineSetting.length > 0 && ( {engineSettings.length > 0 && (
<div className="my-4"> <div className="my-4">
<CardSidebar title="Engine Parameters" asChild> <CardSidebar title="Engine Parameters" asChild>
<div className="px-2 py-4"> <div className="px-2 py-4">
<EngineSetting componentData={componentDataEngineSetting} /> <EngineSetting componentData={engineSettings} />
</div> </div>
</CardSidebar> </CardSidebar>
</div> </div>
@ -191,166 +201,7 @@ const Sidebar: React.FC = () => {
</div> </div>
</CardSidebar> </CardSidebar>
{experimentalFeature && ( <AssistantTool />
<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>
)}
</div> </div>
</div> </div>
) )

View File

@ -49,8 +49,7 @@ import { toSettingParams } from '@/utils/modelParam'
import EngineSetting from '../Chat/EngineSetting' import EngineSetting from '../Chat/EngineSetting'
import SettingComponentBuilder from '../Chat/ModelSetting/SettingComponent' import ModelSetting from '../Chat/ModelSetting'
import { showRightSideBarAtom } from '../Chat/Sidebar' import { showRightSideBarAtom } from '../Chat/Sidebar'
import { import {
@ -429,17 +428,22 @@ const LocalServerScreen = () => {
</div> </div>
)} )}
{componentDataEngineSetting.filter( {componentDataEngineSetting.filter((x) => x.key === 'prompt_template')
(x) => x.name === 'prompt_template' .length !== 0 && (
).length !== 0 && (
<div className="mt-4"> <div className="mt-4">
<CardSidebar title="Model Parameters" asChild> <CardSidebar title="Model Parameters" asChild>
<div className="px-2 py-4"> <div className="px-2 py-4">
<SettingComponentBuilder <ModelSetting componentProps={componentDataEngineSetting} />
{/* <SettingComponentBuilder
enabled={!serverEnabled} enabled={!serverEnabled}
componentData={componentDataEngineSetting} componentProps={componentDataEngineSetting}
selector={(x) => x.name === 'prompt_template'} onValueUpdated={function (
/> key: string,
value: string | number | boolean
): void {
throw new Error('Function not implemented.')
}}
/> */}
</div> </div>
</CardSidebar> </CardSidebar>
</div> </div>
@ -449,10 +453,7 @@ const LocalServerScreen = () => {
<div className="my-4"> <div className="my-4">
<CardSidebar title="Engine Parameters" asChild> <CardSidebar title="Engine Parameters" asChild>
<div className="px-2 py-4"> <div className="px-2 py-4">
<EngineSetting <EngineSetting componentData={componentDataEngineSetting} />
enabled={!serverEnabled}
componentData={componentDataEngineSetting}
/>
</div> </div>
</CardSidebar> </CardSidebar>
</div> </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> </ModalHeader>
<div <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()} {...getRootProps()}
onClick={onSelectFileClick} 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 { ScrollArea } from '@janhq/uikit'
import { motion as m } from 'framer-motion'
import { twMerge } from 'tailwind-merge'
type Props = { import { useAtomValue } from 'jotai'
activeMenu: string
onMenuClick: (menu: string) => void
}
const SettingMenu: React.FC<Props> = ({ activeMenu, onMenuClick }) => { import SettingItem from './SettingItem'
const [menus, setMenus] = useState<string[]>([])
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(() => { useEffect(() => {
setMenus([ const getAllSettings = async () => {
'My Models', const activeExtensions = await extensionManager.getActive()
'My Settings', const extensionsMenu: string[] = []
'Advanced Settings',
...(window.electronAPI ? ['Extensions'] : []), 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 ( return (
<div className="flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r border-border"> <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"> <ScrollArea className="h-full w-full">
<div className="flex-shrink-0 px-6 py-4 font-medium"> <div className="flex-shrink-0 px-6 py-4 font-medium">
{menus.map((menu) => { {settingScreens.map((settingScreen) => (
const isActive = activeMenu === menu <SettingItem key={settingScreen} setting={settingScreen} />
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>
{isActive && ( {extensionHasSettings.length > 0 && (
<m.div <div className="mb-2 mt-6">
className="absolute inset-0 -left-3 h-full w-[calc(100%+24px)] rounded-md bg-primary/50" <label className="text-xs font-medium text-muted-foreground">
layoutId="active-static-menu" Extensions
/> </label>
)}
</div> </div>
) )}
})}
{extensionHasSettings.map((extensionName: string) => (
<SettingItem
key={extensionName}
setting={extensionName}
extension={true}
/>
))}
</div> </div>
</ScrollArea> </ScrollArea>
</div> </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 { useSetAtom } from 'jotai'
import AppearanceOptions from '@/screens/Settings/Appearance'
import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
import Models from '@/screens/Settings/Models'
import { SUCCESS_SET_NEW_DESTINATION } from './Advanced/DataFolder' import { SUCCESS_SET_NEW_DESTINATION } from './Advanced/DataFolder'
import SettingDetail from './SettingDetail'
import SettingMenu from './SettingMenu' import SettingMenu from './SettingMenu'
const handleShowOptions = (menu: string) => { import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
switch (menu) {
case 'Extensions':
return <ExtensionCatalog />
case 'My Settings': export const SettingScreenList = [
return <AppearanceOptions /> 'My Models',
'My Settings',
'Advanced Settings',
'Extensions',
] as const
case 'Advanced Settings': export type SettingScreenTuple = typeof SettingScreenList
return <Advanced /> export type SettingScreen = SettingScreenTuple[number]
case 'My Models':
return <Models />
}
}
const SettingsScreen: React.FC = () => { const SettingsScreen: React.FC = () => {
const [activeStaticMenu, setActiveStaticMenu] = useState('My Models') const setSelectedSettingScreen = useSetAtom(selectedSettingAtom)
useEffect(() => { useEffect(() => {
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') { if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
setActiveStaticMenu('Advanced Settings') setSelectedSettingScreen('Advanced Settings')
localStorage.removeItem(SUCCESS_SET_NEW_DESTINATION) localStorage.removeItem(SUCCESS_SET_NEW_DESTINATION)
} }
}, []) }, [setSelectedSettingScreen])
return ( return (
<div <div
className="flex h-full bg-background"
data-testid="testid-setting-description" data-testid="testid-setting-description"
className="flex h-full bg-background"
> >
<SettingMenu <SettingMenu />
activeMenu={activeStaticMenu} <SettingDetail />
onMenuClick={setActiveStaticMenu}
/>
{handleShowOptions(activeStaticMenu)}
</div> </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' import { presetConfiguration } from '@/screens/Chat/ModelSetting/predefinedComponent'
export const getConfigurationsData = ( export const getConfigurationsData = (
settings: object, settings: object,
selectedModel?: Model selectedModel?: Model
) => { ): SettingComponentProps[] => {
const componentData: SettingComponentData[] = [] const componentData: SettingComponentProps[] = []
Object.keys(settings).forEach((key: string) => { Object.keys(settings).forEach((key: string) => {
const componentSetting = presetConfiguration[key] const componentSetting = presetConfiguration[key]
@ -17,20 +16,20 @@ export const getConfigurationsData = (
} }
if ('slider' === componentSetting.controllerType) { if ('slider' === componentSetting.controllerType) {
const value = Number(settings[key as keyof typeof settings]) const value = Number(settings[key as keyof typeof settings])
if ('value' in componentSetting.controllerData) { if ('value' in componentSetting.controllerProps) {
componentSetting.controllerData.value = value componentSetting.controllerProps.value = value
if ('max' in componentSetting.controllerData) { if ('max' in componentSetting.controllerProps) {
switch (key) { switch (key) {
case 'max_tokens': case 'max_tokens':
componentSetting.controllerData.max = componentSetting.controllerProps.max =
selectedModel?.parameters.max_tokens || selectedModel?.parameters.max_tokens ||
componentSetting.controllerData.max || componentSetting.controllerProps.max ||
4096 4096
break break
case 'ctx_len': case 'ctx_len':
componentSetting.controllerData.max = componentSetting.controllerProps.max =
selectedModel?.settings.ctx_len || selectedModel?.settings.ctx_len ||
componentSetting.controllerData.max || componentSetting.controllerProps.max ||
4096 4096
break break
} }
@ -39,15 +38,15 @@ export const getConfigurationsData = (
} else if ('input' === componentSetting.controllerType) { } else if ('input' === componentSetting.controllerType) {
const value = settings[key as keyof typeof settings] as string const value = settings[key as keyof typeof settings] as string
const placeholder = settings[key as keyof typeof settings] as string const placeholder = settings[key as keyof typeof settings] as string
if ('value' in componentSetting.controllerData) if ('value' in componentSetting.controllerProps)
componentSetting.controllerData.value = value componentSetting.controllerProps.value = value
if ('placeholder' in componentSetting.controllerData) if ('placeholder' in componentSetting.controllerProps)
componentSetting.controllerData.placeholder = placeholder componentSetting.controllerProps.placeholder = placeholder
} else if ('checkbox' === componentSetting.controllerType) { } else if ('checkbox' === componentSetting.controllerType) {
const checked = settings[key as keyof typeof settings] as boolean const checked = settings[key as keyof typeof settings] as boolean
if ('checked' in componentSetting.controllerData) if ('value' in componentSetting.controllerProps)
componentSetting.controllerData.checked = checked componentSetting.controllerProps.value = checked
} }
componentData.push(componentSetting) 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
}
}