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:
parent
ec6bcf6357
commit
fa35aa6e14
@ -1,3 +1,7 @@
|
||||
import { SettingComponentProps } from '../types'
|
||||
import { getJanDataFolderPath, joinPath } from './core'
|
||||
import { fs } from './fs'
|
||||
|
||||
export enum ExtensionTypeEnum {
|
||||
Assistant = 'assistant',
|
||||
Conversational = 'conversational',
|
||||
@ -32,6 +36,38 @@ export type InstallationState = InstallationStateTuple[number]
|
||||
* This class should be extended by any class that represents an extension.
|
||||
*/
|
||||
export abstract class BaseExtension implements ExtensionType {
|
||||
protected settingFolderName = 'settings'
|
||||
protected settingFileName = 'settings.json'
|
||||
|
||||
/** @type {string} Name of the extension. */
|
||||
name?: string
|
||||
|
||||
/** @type {string} The URL of the extension to load. */
|
||||
url: string
|
||||
|
||||
/** @type {boolean} Whether the extension is activated or not. */
|
||||
active
|
||||
|
||||
/** @type {string} Extension's description. */
|
||||
description
|
||||
|
||||
/** @type {string} Extension's version. */
|
||||
version
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
name?: string,
|
||||
active?: boolean,
|
||||
description?: string,
|
||||
version?: string
|
||||
) {
|
||||
this.name = name
|
||||
this.url = url
|
||||
this.active = active
|
||||
this.description = description
|
||||
this.version = version
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the extension.
|
||||
* @returns {ExtensionType} The type of the extension
|
||||
@ -40,11 +76,13 @@ export abstract class BaseExtension implements ExtensionType {
|
||||
type(): ExtensionTypeEnum | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the extension is loaded.
|
||||
* Any initialization logic for the extension should be put here.
|
||||
*/
|
||||
abstract onLoad(): void
|
||||
|
||||
/**
|
||||
* Called when the extension is unloaded.
|
||||
* Any cleanup logic for the extension should be put here.
|
||||
@ -67,6 +105,42 @@ export abstract class BaseExtension implements ExtensionType {
|
||||
return false
|
||||
}
|
||||
|
||||
async registerSettings(settings: SettingComponentProps[]): Promise<void> {
|
||||
if (!this.name) {
|
||||
console.error('Extension name is not defined')
|
||||
return
|
||||
}
|
||||
|
||||
const extensionSettingFolderPath = await joinPath([
|
||||
await getJanDataFolderPath(),
|
||||
'settings',
|
||||
this.name,
|
||||
])
|
||||
settings.forEach((setting) => {
|
||||
setting.extensionName = this.name
|
||||
})
|
||||
try {
|
||||
await fs.mkdir(extensionSettingFolderPath)
|
||||
const settingFilePath = await joinPath([extensionSettingFolderPath, this.settingFileName])
|
||||
|
||||
if (await fs.existsSync(settingFilePath)) return
|
||||
await fs.writeFileSync(settingFilePath, JSON.stringify(settings, null, 2))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async getSetting<T>(key: string, defaultValue: T) {
|
||||
const keySetting = (await this.getSettings()).find((setting) => setting.key === key)
|
||||
|
||||
const value = keySetting?.controllerProps.value
|
||||
return (value as T) ?? defaultValue
|
||||
}
|
||||
|
||||
onSettingUpdate<T>(key: string, value: T) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the prerequisites for the extension are installed.
|
||||
*
|
||||
@ -81,8 +155,59 @@ export abstract class BaseExtension implements ExtensionType {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
async install(...args): Promise<void> {
|
||||
async install(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
async getSettings(): Promise<SettingComponentProps[]> {
|
||||
if (!this.name) return []
|
||||
|
||||
const settingPath = await joinPath([
|
||||
await getJanDataFolderPath(),
|
||||
this.settingFolderName,
|
||||
this.name,
|
||||
this.settingFileName,
|
||||
])
|
||||
|
||||
try {
|
||||
const content = await fs.readFileSync(settingPath, 'utf-8')
|
||||
const settings: SettingComponentProps[] = JSON.parse(content)
|
||||
return settings
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async updateSettings(componentProps: Partial<SettingComponentProps>[]): Promise<void> {
|
||||
if (!this.name) return
|
||||
|
||||
const settings = await this.getSettings()
|
||||
|
||||
const updatedSettings = settings.map((setting) => {
|
||||
const updatedSetting = componentProps.find(
|
||||
(componentProp) => componentProp.key === setting.key
|
||||
)
|
||||
if (updatedSetting && updatedSetting.controllerProps) {
|
||||
setting.controllerProps.value = updatedSetting.controllerProps.value
|
||||
}
|
||||
return setting
|
||||
})
|
||||
|
||||
const settingPath = await joinPath([
|
||||
await getJanDataFolderPath(),
|
||||
this.settingFolderName,
|
||||
this.name,
|
||||
this.settingFileName,
|
||||
])
|
||||
|
||||
await fs.writeFileSync(settingPath, JSON.stringify(updatedSettings, null, 2))
|
||||
|
||||
updatedSettings.forEach((setting) => {
|
||||
this.onSettingUpdate<typeof setting.controllerProps.value>(
|
||||
setting.key,
|
||||
setting.controllerProps.value
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ export abstract class OAIEngine extends AIEngine {
|
||||
/*
|
||||
* Inference request
|
||||
*/
|
||||
override inference(data: MessageRequest) {
|
||||
override async inference(data: MessageRequest) {
|
||||
if (data.model?.engine?.toString() !== this.provider) return
|
||||
|
||||
const timestamp = Date.now()
|
||||
@ -77,12 +77,14 @@ export abstract class OAIEngine extends AIEngine {
|
||||
...data.model,
|
||||
}
|
||||
|
||||
const header = await this.headers()
|
||||
|
||||
requestInference(
|
||||
this.inferenceUrl,
|
||||
data.messages ?? [],
|
||||
model,
|
||||
this.controller,
|
||||
this.headers()
|
||||
header
|
||||
).subscribe({
|
||||
next: (content: any) => {
|
||||
const messageContent: ThreadContent = {
|
||||
@ -123,7 +125,7 @@ export abstract class OAIEngine extends AIEngine {
|
||||
/**
|
||||
* Headers for the inference request
|
||||
*/
|
||||
headers(): HeadersInit {
|
||||
async headers(): Promise<HeadersInit> {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,7 @@ import { OAIEngine } from './OAIEngine'
|
||||
* Added the implementation of loading and unloading model (applicable to local inference providers)
|
||||
*/
|
||||
export abstract class RemoteOAIEngine extends OAIEngine {
|
||||
// The inference engine
|
||||
abstract apiKey: string
|
||||
apiKey?: string
|
||||
/**
|
||||
* On extension load, subscribe to events.
|
||||
*/
|
||||
@ -17,10 +16,12 @@ export abstract class RemoteOAIEngine extends OAIEngine {
|
||||
/**
|
||||
* Headers for the inference request
|
||||
*/
|
||||
override headers(): HeadersInit {
|
||||
override async headers(): Promise<HeadersInit> {
|
||||
return {
|
||||
...(this.apiKey && {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'api-key': `${this.apiKey}`,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,19 +5,16 @@ export type Handler = (route: string, args: any) => any
|
||||
|
||||
export class RequestHandler {
|
||||
handler: Handler
|
||||
adataper: RequestAdapter
|
||||
adapter: RequestAdapter
|
||||
|
||||
constructor(handler: Handler, observer?: Function) {
|
||||
this.handler = handler
|
||||
this.adataper = new RequestAdapter(observer)
|
||||
this.adapter = new RequestAdapter(observer)
|
||||
}
|
||||
|
||||
handle() {
|
||||
CoreRoutes.map((route) => {
|
||||
this.handler(route, async (...args: any[]) => {
|
||||
const values = await this.adataper.process(route, ...args)
|
||||
return values
|
||||
})
|
||||
this.handler(route, async (...args: any[]) => this.adapter.process(route, ...args))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -316,6 +316,7 @@ export const chatCompletions = async (request: any, reply: any) => {
|
||||
}
|
||||
|
||||
const requestedModel = matchedModels[0]
|
||||
|
||||
const engineConfiguration = await getEngineConfiguration(requestedModel.engine)
|
||||
|
||||
let apiKey: string | undefined = undefined
|
||||
@ -323,7 +324,7 @@ export const chatCompletions = async (request: any, reply: any) => {
|
||||
|
||||
if (engineConfiguration) {
|
||||
apiKey = engineConfiguration.api_key
|
||||
apiUrl = engineConfiguration.full_url
|
||||
apiUrl = engineConfiguration.full_url ?? DEFAULT_CHAT_COMPLETION_URL
|
||||
}
|
||||
|
||||
const headers: Record<string, any> = {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AppConfiguration } from '../../types'
|
||||
import { AppConfiguration, SettingComponentProps } from '../../types'
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
@ -125,14 +125,30 @@ const exec = async (command: string): Promise<string> => {
|
||||
})
|
||||
}
|
||||
|
||||
// a hacky way to get the api key. we should comes up with a better
|
||||
// way to handle this
|
||||
export const getEngineConfiguration = async (engineId: string) => {
|
||||
if (engineId !== 'openai' && engineId !== 'groq') {
|
||||
return undefined
|
||||
if (engineId !== 'openai' && engineId !== 'groq') return undefined
|
||||
|
||||
const settingDirectoryPath = join(
|
||||
getJanDataFolderPath(),
|
||||
'settings',
|
||||
engineId === 'openai' ? 'inference-openai-extension' : 'inference-groq-extension',
|
||||
'settings.json'
|
||||
)
|
||||
|
||||
const content = fs.readFileSync(settingDirectoryPath, 'utf-8')
|
||||
const settings: SettingComponentProps[] = JSON.parse(content)
|
||||
const apiKeyId = engineId === 'openai' ? 'openai-api-key' : 'groq-api-key'
|
||||
const keySetting = settings.find((setting) => setting.key === apiKeyId)
|
||||
|
||||
let apiKey = keySetting?.controllerProps.value
|
||||
if (typeof apiKey !== 'string') apiKey = ''
|
||||
|
||||
return {
|
||||
api_key: apiKey,
|
||||
full_url: undefined,
|
||||
}
|
||||
const directoryPath = join(getJanDataFolderPath(), 'engines')
|
||||
const filePath = join(directoryPath, `${engineId}.json`)
|
||||
const data = fs.readFileSync(filePath, 'utf-8')
|
||||
return JSON.parse(data)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -9,3 +9,4 @@ export * from './config'
|
||||
export * from './huggingface'
|
||||
export * from './miscellaneous'
|
||||
export * from './api'
|
||||
export * from './setting'
|
||||
|
||||
1
core/src/types/setting/index.ts
Normal file
1
core/src/types/setting/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './settingComponent'
|
||||
34
core/src/types/setting/settingComponent.ts
Normal file
34
core/src/types/setting/settingComponent.ts
Normal 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
|
||||
}
|
||||
@ -7,12 +7,10 @@ import replace from '@rollup/plugin-replace'
|
||||
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
const pkg = require('./package.json')
|
||||
|
||||
export default [
|
||||
{
|
||||
input: `src/index.ts`,
|
||||
output: [{ file: pkg.main, format: 'es', sourcemap: true }],
|
||||
output: [{ file: packageJson.main, format: 'es', sourcemap: true }],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: [],
|
||||
watch: {
|
||||
@ -36,7 +34,7 @@ export default [
|
||||
// https://github.com/rollup/rollup-plugin-node-resolve#usage
|
||||
resolve({
|
||||
extensions: ['.js', '.ts', '.svelte'],
|
||||
browser: true
|
||||
browser: true,
|
||||
}),
|
||||
|
||||
// Resolve source maps to the original source
|
||||
|
||||
@ -1,13 +1,36 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getJanDataFolderPath } from '@janhq/core/node'
|
||||
import { SettingComponentProps, getJanDataFolderPath } from '@janhq/core/node'
|
||||
|
||||
// Sec: Do not send engine settings over requests
|
||||
// Read it manually instead
|
||||
export const readEmbeddingEngine = (engineName: string) => {
|
||||
if (engineName !== 'openai' && engineName !== 'groq') {
|
||||
const engineSettings = fs.readFileSync(
|
||||
path.join(getJanDataFolderPath(), 'engines', `${engineName}.json`),
|
||||
'utf-8'
|
||||
)
|
||||
return JSON.parse(engineSettings)
|
||||
} else {
|
||||
const settingDirectoryPath = path.join(
|
||||
getJanDataFolderPath(),
|
||||
'settings',
|
||||
engineName === 'openai'
|
||||
? 'inference-openai-extension'
|
||||
: 'inference-groq-extension',
|
||||
'settings.json'
|
||||
)
|
||||
|
||||
const content = fs.readFileSync(settingDirectoryPath, 'utf-8')
|
||||
const settings: SettingComponentProps[] = JSON.parse(content)
|
||||
const apiKeyId = engineName === 'openai' ? 'openai-api-key' : 'groq-api-key'
|
||||
const keySetting = settings.find((setting) => setting.key === apiKeyId)
|
||||
|
||||
let apiKey = keySetting?.controllerProps.value
|
||||
if (typeof apiKey !== 'string') apiKey = ''
|
||||
|
||||
return {
|
||||
api_key: apiKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
extensions/inference-groq-extension/resources/settings.json
Normal file
23
extensions/inference-groq-extension/resources/settings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -6,78 +6,41 @@
|
||||
* @module inference-groq-extension/src/index
|
||||
*/
|
||||
|
||||
import {
|
||||
events,
|
||||
fs,
|
||||
AppConfigurationEventName,
|
||||
joinPath,
|
||||
RemoteOAIEngine,
|
||||
} from '@janhq/core'
|
||||
import { join } from 'path'
|
||||
import { RemoteOAIEngine } from '@janhq/core'
|
||||
|
||||
declare const COMPLETION_URL: string
|
||||
declare const SETTINGS: Array<any>
|
||||
enum Settings {
|
||||
apiKey = 'groq-api-key',
|
||||
chatCompletionsEndPoint = 'chat-completions-endpoint',
|
||||
}
|
||||
/**
|
||||
* A class that implements the InferenceExtension interface from the @janhq/core package.
|
||||
* The class provides methods for initializing and stopping a model, and for making inference requests.
|
||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||
*/
|
||||
export default class JanInferenceGroqExtension extends RemoteOAIEngine {
|
||||
private readonly _engineDir = 'file://engines'
|
||||
private readonly _engineMetadataFileName = 'groq.json'
|
||||
|
||||
inferenceUrl: string = COMPLETION_URL
|
||||
inferenceUrl: string = ''
|
||||
provider = 'groq'
|
||||
apiKey = ''
|
||||
|
||||
private _engineSettings = {
|
||||
full_url: COMPLETION_URL,
|
||||
api_key: 'gsk-<your key here>',
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
async onLoad() {
|
||||
override async onLoad(): Promise<void> {
|
||||
super.onLoad()
|
||||
|
||||
if (!(await fs.existsSync(this._engineDir))) {
|
||||
await fs.mkdir(this._engineDir)
|
||||
}
|
||||
// Register Settings
|
||||
this.registerSettings(SETTINGS)
|
||||
|
||||
this.writeDefaultEngineSettings()
|
||||
|
||||
const settingsFilePath = await joinPath([
|
||||
this._engineDir,
|
||||
this._engineMetadataFileName,
|
||||
])
|
||||
|
||||
// Events subscription
|
||||
events.on(
|
||||
AppConfigurationEventName.OnConfigurationUpdate,
|
||||
(settingsKey: string) => {
|
||||
// Update settings on changes
|
||||
if (settingsKey === settingsFilePath) this.writeDefaultEngineSettings()
|
||||
}
|
||||
// Retrieve API Key Setting
|
||||
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
|
||||
this.inferenceUrl = await this.getSetting<string>(
|
||||
Settings.chatCompletionsEndPoint,
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
async writeDefaultEngineSettings() {
|
||||
try {
|
||||
const engineFile = join(this._engineDir, this._engineMetadataFileName)
|
||||
if (await fs.existsSync(engineFile)) {
|
||||
const engine = await fs.readFileSync(engineFile, 'utf-8')
|
||||
this._engineSettings =
|
||||
typeof engine === 'object' ? engine : JSON.parse(engine)
|
||||
this.inferenceUrl = this._engineSettings.full_url
|
||||
this.apiKey = this._engineSettings.api_key
|
||||
} else {
|
||||
await fs.writeFileSync(
|
||||
engineFile,
|
||||
JSON.stringify(this._engineSettings, null, 2)
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
onSettingUpdate<T>(key: string, value: T): void {
|
||||
if (key === Settings.apiKey) {
|
||||
this.apiKey = value as string
|
||||
} else if (key === Settings.chatCompletionsEndPoint) {
|
||||
this.inferenceUrl = value as string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const packageJson = require('./package.json')
|
||||
const settingJson = require('./resources/settings.json')
|
||||
|
||||
module.exports = {
|
||||
experiments: { outputModule: true },
|
||||
@ -17,8 +18,8 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
SETTINGS: JSON.stringify(settingJson),
|
||||
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||
COMPLETION_URL: JSON.stringify('https://api.groq.com/openai/v1/chat/completions'),
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -5,13 +5,12 @@ import typescript from 'rollup-plugin-typescript2'
|
||||
import json from '@rollup/plugin-json'
|
||||
import replace from '@rollup/plugin-replace'
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
const pkg = require('./package.json')
|
||||
const defaultSettingJson = require('./resources/default_settings.json')
|
||||
|
||||
export default [
|
||||
{
|
||||
input: `src/index.ts`,
|
||||
output: [{ file: pkg.main, format: 'es', sourcemap: true }],
|
||||
output: [{ file: packageJson.main, format: 'es', sourcemap: true }],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: [],
|
||||
watch: {
|
||||
@ -19,7 +18,9 @@ export default [
|
||||
},
|
||||
plugins: [
|
||||
replace({
|
||||
EXTENSION_NAME: JSON.stringify(packageJson.name),
|
||||
NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`),
|
||||
DEFAULT_SETTINGS: JSON.stringify(defaultSettingJson),
|
||||
INFERENCE_URL: JSON.stringify(
|
||||
process.env.INFERENCE_URL ||
|
||||
'http://127.0.0.1:3928/inferences/llamacpp/chat_completion'
|
||||
|
||||
@ -2,6 +2,8 @@ declare const NODE: string
|
||||
declare const INFERENCE_URL: string
|
||||
declare const TROUBLESHOOTING_URL: string
|
||||
declare const JAN_SERVER_INFERENCE_URL: string
|
||||
declare const EXTENSION_NAME: string
|
||||
declare const DEFAULT_SETTINGS: Array<any>
|
||||
|
||||
/**
|
||||
* The response from the initModel function.
|
||||
|
||||
@ -58,8 +58,6 @@ export default class JanInferenceNitroExtension extends LocalOAIEngine {
|
||||
this.inferenceUrl = `${window.core?.api?.baseApiUrl}/v1/chat/completions`
|
||||
}
|
||||
|
||||
console.debug('Inference url: ', this.inferenceUrl)
|
||||
|
||||
this.getNitroProcesHealthIntervalId = setInterval(
|
||||
() => this.periodicallyGetNitroHealth(),
|
||||
JanInferenceNitroExtension._intervalHealthCheck
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -5,85 +5,41 @@
|
||||
* @version 1.0.0
|
||||
* @module inference-openai-extension/src/index
|
||||
*/
|
||||
declare const ENGINE: string
|
||||
|
||||
import {
|
||||
events,
|
||||
fs,
|
||||
AppConfigurationEventName,
|
||||
joinPath,
|
||||
RemoteOAIEngine,
|
||||
} from '@janhq/core'
|
||||
import { join } from 'path'
|
||||
|
||||
declare const COMPLETION_URL: string
|
||||
import { RemoteOAIEngine } from '@janhq/core'
|
||||
|
||||
declare const SETTINGS: Array<any>
|
||||
enum Settings {
|
||||
apiKey = 'openai-api-key',
|
||||
chatCompletionsEndPoint = 'chat-completions-endpoint',
|
||||
}
|
||||
/**
|
||||
* A class that implements the InferenceExtension interface from the @janhq/core package.
|
||||
* The class provides methods for initializing and stopping a model, and for making inference requests.
|
||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||
*/
|
||||
export default class JanInferenceOpenAIExtension extends RemoteOAIEngine {
|
||||
private static readonly _engineDir = 'file://engines'
|
||||
private static readonly _engineMetadataFileName = `${ENGINE}.json`
|
||||
|
||||
private _engineSettings = {
|
||||
full_url: COMPLETION_URL,
|
||||
api_key: 'sk-<your key here>',
|
||||
}
|
||||
|
||||
inferenceUrl: string = COMPLETION_URL
|
||||
inferenceUrl: string = ''
|
||||
provider: string = 'openai'
|
||||
apiKey: string = ''
|
||||
|
||||
// TODO: Just use registerSettings from BaseExtension
|
||||
// Remove these methods
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
async onLoad() {
|
||||
override async onLoad(): Promise<void> {
|
||||
super.onLoad()
|
||||
|
||||
if (!(await fs.existsSync(JanInferenceOpenAIExtension._engineDir))) {
|
||||
await fs.mkdir(JanInferenceOpenAIExtension._engineDir)
|
||||
}
|
||||
// Register Settings
|
||||
this.registerSettings(SETTINGS)
|
||||
|
||||
this.writeDefaultEngineSettings()
|
||||
|
||||
const settingsFilePath = await joinPath([
|
||||
JanInferenceOpenAIExtension._engineDir,
|
||||
JanInferenceOpenAIExtension._engineMetadataFileName,
|
||||
])
|
||||
|
||||
events.on(
|
||||
AppConfigurationEventName.OnConfigurationUpdate,
|
||||
(settingsKey: string) => {
|
||||
// Update settings on changes
|
||||
if (settingsKey === settingsFilePath) this.writeDefaultEngineSettings()
|
||||
}
|
||||
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
|
||||
this.inferenceUrl = await this.getSetting<string>(
|
||||
Settings.chatCompletionsEndPoint,
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
async writeDefaultEngineSettings() {
|
||||
try {
|
||||
const engineFile = join(
|
||||
JanInferenceOpenAIExtension._engineDir,
|
||||
JanInferenceOpenAIExtension._engineMetadataFileName
|
||||
)
|
||||
if (await fs.existsSync(engineFile)) {
|
||||
const engine = await fs.readFileSync(engineFile, 'utf-8')
|
||||
this._engineSettings =
|
||||
typeof engine === 'object' ? engine : JSON.parse(engine)
|
||||
this.inferenceUrl = this._engineSettings.full_url
|
||||
this.apiKey = this._engineSettings.api_key
|
||||
} else {
|
||||
await fs.writeFileSync(
|
||||
engineFile,
|
||||
JSON.stringify(this._engineSettings, null, 2)
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
onSettingUpdate<T>(key: string, value: T): void {
|
||||
if (key === Settings.apiKey) {
|
||||
this.apiKey = value as string
|
||||
} else if (key === Settings.chatCompletionsEndPoint) {
|
||||
this.inferenceUrl = value as string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const packageJson = require('./package.json')
|
||||
const settingJson = require('./resources/settings.json')
|
||||
|
||||
module.exports = {
|
||||
experiments: { outputModule: true },
|
||||
@ -17,8 +18,8 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
SETTINGS: JSON.stringify(settingJson),
|
||||
ENGINE: JSON.stringify(packageJson.engine),
|
||||
COMPLETION_URL: JSON.stringify('https://api.openai.com/v1/chat/completions'),
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -6,77 +6,44 @@
|
||||
* @module inference-nvidia-triton-trt-llm-extension/src/index
|
||||
*/
|
||||
|
||||
import {
|
||||
AppConfigurationEventName,
|
||||
events,
|
||||
fs,
|
||||
joinPath,
|
||||
Model,
|
||||
RemoteOAIEngine,
|
||||
} from '@janhq/core'
|
||||
import { join } from 'path'
|
||||
import { RemoteOAIEngine } from '@janhq/core'
|
||||
|
||||
declare const SETTINGS: Array<any>
|
||||
enum Settings {
|
||||
apiKey = 'tritonllm-api-key',
|
||||
chatCompletionsEndPoint = 'chat-completions-endpoint',
|
||||
}
|
||||
/**
|
||||
* A class that implements the InferenceExtension interface from the @janhq/core package.
|
||||
* The class provides methods for initializing and stopping a model, and for making inference requests.
|
||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||
*/
|
||||
export default class JanInferenceTritonTrtLLMExtension extends RemoteOAIEngine {
|
||||
private readonly _engineDir = 'file://engines'
|
||||
private readonly _engineMetadataFileName = 'triton_trtllm.json'
|
||||
|
||||
inferenceUrl: string = ''
|
||||
provider: string = 'triton_trtllm'
|
||||
apiKey: string = ''
|
||||
|
||||
_engineSettings: {
|
||||
base_url: ''
|
||||
api_key: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
async onLoad() {
|
||||
super.onLoad()
|
||||
if (!(await fs.existsSync(this._engineDir))) {
|
||||
await fs.mkdir(this._engineDir)
|
||||
}
|
||||
|
||||
this.writeDefaultEngineSettings()
|
||||
// Register Settings
|
||||
this.registerSettings(SETTINGS)
|
||||
|
||||
const settingsFilePath = await joinPath([
|
||||
this._engineDir,
|
||||
this._engineMetadataFileName,
|
||||
])
|
||||
|
||||
// Events subscription
|
||||
events.on(
|
||||
AppConfigurationEventName.OnConfigurationUpdate,
|
||||
(settingsKey: string) => {
|
||||
// Update settings on changes
|
||||
if (settingsKey === settingsFilePath) this.writeDefaultEngineSettings()
|
||||
}
|
||||
// Retrieve API Key Setting
|
||||
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
|
||||
this.inferenceUrl = await this.getSetting<string>(
|
||||
Settings.chatCompletionsEndPoint,
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
async writeDefaultEngineSettings() {
|
||||
try {
|
||||
const engine_json = join(this._engineDir, this._engineMetadataFileName)
|
||||
if (await fs.existsSync(engine_json)) {
|
||||
const engine = await fs.readFileSync(engine_json, 'utf-8')
|
||||
this._engineSettings =
|
||||
typeof engine === 'object' ? engine : JSON.parse(engine)
|
||||
this.inferenceUrl = this._engineSettings.base_url
|
||||
this.apiKey = this._engineSettings.api_key
|
||||
} else {
|
||||
await fs.writeFileSync(
|
||||
engine_json,
|
||||
JSON.stringify(this._engineSettings, null, 2)
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
onSettingUpdate<T>(key: string, value: T): void {
|
||||
if (key === Settings.apiKey) {
|
||||
this.apiKey = value as string
|
||||
} else if (key === Settings.chatCompletionsEndPoint) {
|
||||
this.inferenceUrl = value as string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const packageJson = require('./package.json')
|
||||
const settingJson = require('./resources/settings.json')
|
||||
|
||||
module.exports = {
|
||||
experiments: { outputModule: true },
|
||||
@ -17,6 +18,7 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
SETTINGS: JSON.stringify(settingJson),
|
||||
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||
}),
|
||||
],
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import sourceMaps from 'rollup-plugin-sourcemaps'
|
||||
import typescript from 'rollup-plugin-typescript2'
|
||||
import json from '@rollup/plugin-json'
|
||||
@ -7,12 +6,10 @@ import replace from '@rollup/plugin-replace'
|
||||
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
const pkg = require('./package.json')
|
||||
|
||||
export default [
|
||||
{
|
||||
input: `src/index.ts`,
|
||||
output: [{ file: pkg.main, format: 'es', sourcemap: true }],
|
||||
output: [{ file: packageJson.main, format: 'es', sourcemap: true }],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: [],
|
||||
watch: {
|
||||
|
||||
@ -251,7 +251,7 @@ export default class TensorRTLLMExtension extends LocalOAIEngine {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
override inference(data: MessageRequest): void {
|
||||
override async inference(data: MessageRequest) {
|
||||
if (!this.loadedModel) return
|
||||
// TensorRT LLM Extension supports streaming only
|
||||
if (data.model) data.model.parameters.stream = true
|
||||
|
||||
@ -38,7 +38,6 @@ import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
||||
import { toGibibytes } from '@/utils/converter'
|
||||
|
||||
import ModelLabel from '../ModelLabel'
|
||||
import OpenAiKeyInput from '../OpenAiKeyInput'
|
||||
|
||||
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||
@ -144,7 +143,7 @@ const DropdownListSidebar = ({
|
||||
|
||||
// Update model parameter to the thread file
|
||||
if (model)
|
||||
updateModelParameter(activeThread.id, {
|
||||
updateModelParameter(activeThread, {
|
||||
params: modelParams,
|
||||
modelId: model.id,
|
||||
engine: model.engine,
|
||||
@ -170,7 +169,6 @@ const DropdownListSidebar = ({
|
||||
stateModel.model === selectedModel?.id && stateModel.loading
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
'relative w-full overflow-hidden rounded-md',
|
||||
@ -271,10 +269,7 @@ const DropdownListSidebar = ({
|
||||
)}
|
||||
>
|
||||
<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">
|
||||
<span className="font-bold text-muted-foreground">
|
||||
{toGibibytes(x.metadata.size)}
|
||||
@ -284,7 +279,6 @@ const DropdownListSidebar = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<div
|
||||
className={twMerge(
|
||||
@ -333,9 +327,6 @@ const DropdownListSidebar = ({
|
||||
</SelectPortal>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<OpenAiKeyInput />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -26,8 +26,7 @@ const ModelConfigInput: React.FC<Props> = ({
|
||||
description,
|
||||
placeholder,
|
||||
onValueChanged,
|
||||
}) => {
|
||||
return (
|
||||
}) => (
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center gap-x-2">
|
||||
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
|
||||
@ -52,7 +51,6 @@ const ModelConfigInput: React.FC<Props> = ({
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default ModelConfigInput
|
||||
|
||||
@ -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
|
||||
@ -10,11 +10,14 @@ import useGetSystemResources from '@/hooks/useGetSystemResources'
|
||||
import useModels from '@/hooks/useModels'
|
||||
import useThreads from '@/hooks/useThreads'
|
||||
|
||||
import { SettingScreenList } from '@/screens/Settings'
|
||||
|
||||
import { defaultJanDataFolderAtom } from '@/helpers/atoms/App.atom'
|
||||
import {
|
||||
janDataFolderPathAtom,
|
||||
quickAskEnabledAtom,
|
||||
} from '@/helpers/atoms/AppConfig.atom'
|
||||
import { janSettingScreenAtom } from '@/helpers/atoms/Setting.atom'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
@ -24,6 +27,7 @@ const DataLoader: React.FC<Props> = ({ children }) => {
|
||||
const setJanDataFolderPath = useSetAtom(janDataFolderPathAtom)
|
||||
const setQuickAskEnabled = useSetAtom(quickAskEnabledAtom)
|
||||
const setJanDefaultDataFolder = useSetAtom(defaultJanDataFolderAtom)
|
||||
const setJanSettingScreen = useSetAtom(janSettingScreenAtom)
|
||||
|
||||
useModels()
|
||||
useThreads()
|
||||
@ -49,6 +53,13 @@ const DataLoader: React.FC<Props> = ({ children }) => {
|
||||
getDefaultJanDataFolder()
|
||||
}, [setJanDefaultDataFolder])
|
||||
|
||||
useEffect(() => {
|
||||
const janSettingScreen = SettingScreenList.filter(
|
||||
(screen) => window.electronAPI || screen !== 'Extensions'
|
||||
)
|
||||
setJanSettingScreen(janSettingScreen)
|
||||
}, [setJanSettingScreen])
|
||||
|
||||
console.debug('Load Data...')
|
||||
|
||||
return <Fragment>{children}</Fragment>
|
||||
|
||||
@ -119,7 +119,13 @@ export class ExtensionManager {
|
||||
) {
|
||||
this.register(
|
||||
extension.name ?? extension.url,
|
||||
new extensionClass.default()
|
||||
new extensionClass.default(
|
||||
extension.url,
|
||||
extension.name,
|
||||
extension.active,
|
||||
extension.description,
|
||||
extension.version
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
7
web/helpers/atoms/Setting.atom.ts
Normal file
7
web/helpers/atoms/Setting.atom.ts
Normal 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[]>([])
|
||||
@ -127,13 +127,6 @@ export const setThreadModelParamsAtom = atom(
|
||||
(get, set, threadId: string, params: ModelParams) => {
|
||||
const currentState = { ...get(threadModelParamsAtom) }
|
||||
currentState[threadId] = params
|
||||
console.debug(
|
||||
`Update model params for thread ${threadId}, ${JSON.stringify(
|
||||
params,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
set(threadModelParamsAtom, currentState)
|
||||
}
|
||||
)
|
||||
|
||||
@ -115,7 +115,8 @@ export function useActiveModel() {
|
||||
}
|
||||
|
||||
const stopModel = useCallback(async () => {
|
||||
if (activeModel) {
|
||||
if (!activeModel) return
|
||||
|
||||
setStateModel({ state: 'stop', loading: true, model: activeModel.id })
|
||||
const engine = EngineManager.instance().get(activeModel.engine)
|
||||
await engine
|
||||
@ -125,7 +126,6 @@ export function useActiveModel() {
|
||||
setActiveModel(undefined)
|
||||
setStateModel({ state: 'start', loading: false, model: '' })
|
||||
})
|
||||
}
|
||||
}, [activeModel, setActiveModel, setStateModel])
|
||||
|
||||
return { activeModel, startModel, stopModel, stateModel }
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import {
|
||||
Assistant,
|
||||
ConversationalExtension,
|
||||
@ -134,13 +136,16 @@ export const useCreateNewThread = () => {
|
||||
setActiveThread(thread)
|
||||
}
|
||||
|
||||
async function updateThreadMetadata(thread: Thread) {
|
||||
const updateThreadMetadata = useCallback(
|
||||
async (thread: Thread) => {
|
||||
updateThread(thread)
|
||||
|
||||
await extensionManager
|
||||
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
|
||||
?.saveThread(thread)
|
||||
}
|
||||
},
|
||||
[updateThread]
|
||||
)
|
||||
|
||||
return {
|
||||
requestCreateNewThread,
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import {
|
||||
ConversationalExtension,
|
||||
ExtensionTypeEnum,
|
||||
@ -16,10 +17,8 @@ import { toRuntimeParams, toSettingParams } from '@/utils/modelParam'
|
||||
import { extensionManager } from '@/extension'
|
||||
import {
|
||||
ModelParams,
|
||||
activeThreadStateAtom,
|
||||
getActiveThreadModelParamsAtom,
|
||||
setThreadModelParamsAtom,
|
||||
threadsAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
export type UpdateModelParameter = {
|
||||
@ -29,27 +28,12 @@ export type UpdateModelParameter = {
|
||||
}
|
||||
|
||||
export default function useUpdateModelParameters() {
|
||||
const threads = useAtomValue(threadsAtom)
|
||||
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||
const activeThreadState = useAtomValue(activeThreadStateAtom)
|
||||
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
||||
const selectedModel = useAtomValue(selectedModelAtom)
|
||||
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||
|
||||
const updateModelParameter = async (
|
||||
threadId: string,
|
||||
settings: UpdateModelParameter
|
||||
) => {
|
||||
const thread = threads.find((thread) => thread.id === threadId)
|
||||
if (!thread) {
|
||||
console.error(`Thread ${threadId} not found`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!activeThreadState) {
|
||||
console.error('No active thread')
|
||||
return
|
||||
}
|
||||
|
||||
const updateModelParameter = useCallback(
|
||||
async (thread: Thread, settings: UpdateModelParameter) => {
|
||||
const params = settings.modelId
|
||||
? settings.params
|
||||
: { ...activeModelParams, ...settings.params }
|
||||
@ -85,7 +69,9 @@ export default function useUpdateModelParameters() {
|
||||
await extensionManager
|
||||
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
|
||||
?.saveThread(updatedThread)
|
||||
}
|
||||
},
|
||||
[activeModelParams, selectedModel, setThreadModelParams]
|
||||
)
|
||||
|
||||
return { updateModelParameter }
|
||||
}
|
||||
|
||||
@ -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 SettingComponentBuilder, {
|
||||
SettingComponentData,
|
||||
} from '../ModelSetting/SettingComponent'
|
||||
import SettingComponentBuilder from '../ModelSetting/SettingComponent'
|
||||
|
||||
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
|
||||
import {
|
||||
activeThreadAtom,
|
||||
engineParamsUpdateAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
const AssistantSetting = ({
|
||||
componentData,
|
||||
}: {
|
||||
componentData: SettingComponentData[]
|
||||
}) => {
|
||||
type Props = {
|
||||
componentData: SettingComponentProps[]
|
||||
}
|
||||
|
||||
const AssistantSetting: React.FC<Props> = ({ componentData }) => {
|
||||
const activeThread = useAtomValue(activeThreadAtom)
|
||||
const { updateThreadMetadata } = useCreateNewThread()
|
||||
const { stopModel } = useActiveModel()
|
||||
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
|
||||
|
||||
const onValueChanged = useCallback(
|
||||
(key: string, value: string | number | boolean) => {
|
||||
if (!activeThread) return
|
||||
const shouldReloadModel =
|
||||
componentData.find((x) => x.key === key)?.requireModelReload ?? false
|
||||
if (shouldReloadModel) {
|
||||
setEngineParamsUpdate(true)
|
||||
stopModel()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{activeThread && componentData && (
|
||||
<SettingComponentBuilder
|
||||
componentData={componentData}
|
||||
updater={(_, name, value) => {
|
||||
if (
|
||||
activeThread.assistants[0].tools &&
|
||||
(name === 'chunk_overlap' || name === 'chunk_size')
|
||||
(key === 'chunk_overlap' || key === 'chunk_size')
|
||||
) {
|
||||
if (
|
||||
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_size
|
||||
}
|
||||
|
||||
if (
|
||||
name === 'chunk_size' &&
|
||||
value <
|
||||
activeThread.assistants[0].tools[0].settings.chunk_overlap
|
||||
key === 'chunk_size' &&
|
||||
value < activeThread.assistants[0].tools[0].settings.chunk_overlap
|
||||
) {
|
||||
activeThread.assistants[0].tools[0].settings.chunk_overlap =
|
||||
value
|
||||
activeThread.assistants[0].tools[0].settings.chunk_overlap = value
|
||||
} else if (
|
||||
name === 'chunk_overlap' &&
|
||||
key === 'chunk_overlap' &&
|
||||
value > activeThread.assistants[0].tools[0].settings.chunk_size
|
||||
) {
|
||||
activeThread.assistants[0].tools[0].settings.chunk_size = value
|
||||
}
|
||||
}
|
||||
|
||||
updateThreadMetadata({
|
||||
...activeThread,
|
||||
assistants: [
|
||||
@ -61,17 +68,31 @@ const AssistantSetting = ({
|
||||
settings: {
|
||||
...(activeThread.assistants[0].tools &&
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
30
web/screens/Chat/ChatBody/EmptyModel/index.tsx
Normal file
30
web/screens/Chat/ChatBody/EmptyModel/index.tsx
Normal 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)
|
||||
46
web/screens/Chat/ChatBody/EmptyThread/index.tsx
Normal file
46
web/screens/Chat/ChatBody/EmptyThread/index.tsx
Normal 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)
|
||||
@ -1,80 +1,26 @@
|
||||
import { Fragment } from 'react'
|
||||
|
||||
import ScrollToBottom from 'react-scroll-to-bottom'
|
||||
|
||||
import { InferenceEngine, MessageStatus } from '@janhq/core'
|
||||
import { Button } from '@janhq/uikit'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
import LogoMark from '@/containers/Brand/Logo/Mark'
|
||||
|
||||
import { MainViewState } from '@/constants/screens'
|
||||
import { MessageStatus } from '@janhq/core'
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import ChatItem from '../ChatItem'
|
||||
|
||||
import ErrorMessage from '../ErrorMessage'
|
||||
|
||||
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||
import EmptyModel from './EmptyModel'
|
||||
import EmptyThread from './EmptyThread'
|
||||
|
||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
|
||||
const ChatBody: React.FC = () => {
|
||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||
const setMainViewState = useSetAtom(mainViewStateAtom)
|
||||
|
||||
if (downloadedModels.length === 0)
|
||||
return (
|
||||
<div className="mx-auto flex h-full w-3/4 flex-col items-center justify-center text-center">
|
||||
<LogoMark
|
||||
className="mx-auto mb-4 animate-wave"
|
||||
width={56}
|
||||
height={56}
|
||||
/>
|
||||
<h1 className="text-2xl font-bold">Welcome!</h1>
|
||||
<p className="mt-1 text-base">You need to download your first model</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => setMainViewState(MainViewState.Hub)}
|
||||
>
|
||||
Explore The Hub
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const showOnboardingStep =
|
||||
downloadedModels.filter((e) => e.engine === InferenceEngine.nitro)
|
||||
.length === 0
|
||||
if (downloadedModels.length === 0) return <EmptyModel />
|
||||
if (messages.length === 0) return <EmptyThread />
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{messages.length === 0 ? (
|
||||
<div className="mx-auto flex h-full w-3/4 flex-col items-center justify-center text-center">
|
||||
<LogoMark
|
||||
className="mx-auto mb-4 animate-wave"
|
||||
width={56}
|
||||
height={56}
|
||||
/>
|
||||
{showOnboardingStep ? (
|
||||
<>
|
||||
<p className="mt-1 text-base font-medium">
|
||||
{`You don't have a local model yet.`}
|
||||
</p>
|
||||
<div className="w-auto px-4 py-2">
|
||||
<Button
|
||||
block
|
||||
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
|
||||
onClick={() => setMainViewState(MainViewState.Hub)}
|
||||
>
|
||||
Explore The Hub
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-1 text-base font-medium">How can I help you?</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollToBottom className="flex h-full w-full flex-col">
|
||||
{messages.map((message, index) => (
|
||||
<div key={message.id}>
|
||||
@ -85,14 +31,10 @@ const ChatBody: React.FC = () => {
|
||||
|
||||
{(message.status === MessageStatus.Error ||
|
||||
message.status === MessageStatus.Stopped) &&
|
||||
index === messages.length - 1 && (
|
||||
<ErrorMessage message={message} />
|
||||
)}
|
||||
index === messages.length - 1 && <ErrorMessage message={message} />}
|
||||
</div>
|
||||
))}
|
||||
</ScrollToBottom>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,25 +1,53 @@
|
||||
import SettingComponentBuilder from '../../Chat/ModelSetting/SettingComponent'
|
||||
import { SettingComponentData } from '../ModelSetting/SettingComponent'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { SettingComponentProps } from '@janhq/core/.'
|
||||
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
||||
|
||||
import SettingComponentBuilder from '../../Chat/ModelSetting/SettingComponent'
|
||||
|
||||
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||
import {
|
||||
activeThreadAtom,
|
||||
engineParamsUpdateAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
type Props = {
|
||||
componentData: SettingComponentProps[]
|
||||
}
|
||||
|
||||
const EngineSetting: React.FC<Props> = ({ componentData }) => {
|
||||
const isLocalServerRunning = useAtomValue(serverEnabledAtom)
|
||||
const activeThread = useAtomValue(activeThreadAtom)
|
||||
|
||||
const { stopModel } = useActiveModel()
|
||||
const { updateModelParameter } = useUpdateModelParameters()
|
||||
|
||||
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
|
||||
|
||||
const onValueChanged = useCallback(
|
||||
(key: string, value: string | number | boolean) => {
|
||||
if (!activeThread) return
|
||||
|
||||
setEngineParamsUpdate(true)
|
||||
stopModel()
|
||||
|
||||
updateModelParameter(activeThread, {
|
||||
params: { [key]: value },
|
||||
})
|
||||
},
|
||||
[activeThread, setEngineParamsUpdate, stopModel, updateModelParameter]
|
||||
)
|
||||
|
||||
const EngineSetting = ({
|
||||
componentData,
|
||||
enabled = true,
|
||||
}: {
|
||||
componentData: SettingComponentData[]
|
||||
enabled?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{componentData.filter((e) => e.name !== 'prompt_template').length && (
|
||||
<div className="flex flex-col">
|
||||
<SettingComponentBuilder
|
||||
componentData={componentData}
|
||||
enabled={enabled}
|
||||
selector={(e) => e.name !== 'prompt_template'}
|
||||
componentProps={componentData}
|
||||
enabled={!isLocalServerRunning}
|
||||
onValueUpdated={onValueChanged}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -12,8 +12,6 @@ import { MainViewState } from '@/constants/screens'
|
||||
import { loadModelErrorAtom } from '@/hooks/useActiveModel'
|
||||
import useSendChatMessage from '@/hooks/useSendChatMessage'
|
||||
|
||||
import { getErrorTitle } from '@/utils/errorMessage'
|
||||
|
||||
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||
|
||||
@ -31,10 +29,27 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
|
||||
resendChatMessage(message)
|
||||
}
|
||||
|
||||
const errorTitle = getErrorTitle(
|
||||
message.error_code ?? ErrorCode.Unknown,
|
||||
message.content[0]?.text?.value
|
||||
const getErrorTitle = () => {
|
||||
switch (message.error_code) {
|
||||
case ErrorCode.Unknown:
|
||||
return 'Apologies, something’s amiss!'
|
||||
case ErrorCode.InvalidApiKey:
|
||||
return (
|
||||
<span>
|
||||
Invalid API key. Please check your API key from{' '}
|
||||
<button
|
||||
className="font-medium text-primary dark:text-blue-400"
|
||||
onClick={() => setMainState(MainViewState.Settings)}
|
||||
>
|
||||
Settings
|
||||
</button>{' '}
|
||||
and try again.
|
||||
</span>
|
||||
)
|
||||
default:
|
||||
return message.content[0]?.text?.value
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-10">
|
||||
@ -84,7 +99,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
|
||||
Model is currently unavailable. Please switch to a different
|
||||
model or install the{' '}
|
||||
<button
|
||||
className="font-medium text-blue-500"
|
||||
className="font-medium text-primary dark:text-blue-400"
|
||||
onClick={() => setMainState(MainViewState.Settings)}
|
||||
>
|
||||
{loadModelError.split('::')[1] ?? ''}
|
||||
@ -97,7 +112,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
|
||||
key={message.id}
|
||||
className="flex flex-col items-center text-center text-sm font-medium text-gray-500"
|
||||
>
|
||||
<p>{errorTitle}</p>
|
||||
{getErrorTitle()}
|
||||
<p>
|
||||
Jan’s in beta. Access
|
||||
<span
|
||||
|
||||
@ -1,149 +1,78 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import {
|
||||
SettingComponentProps,
|
||||
InputComponentProps,
|
||||
CheckboxComponentProps,
|
||||
SliderComponentProps,
|
||||
} from '@janhq/core'
|
||||
|
||||
import Checkbox from '@/containers/Checkbox'
|
||||
import ModelConfigInput from '@/containers/ModelConfigInput'
|
||||
import SliderRightPanel from '@/containers/SliderRightPanel'
|
||||
|
||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
||||
|
||||
import { getConfigurationsData } from '@/utils/componentSettings'
|
||||
import { toSettingParams } from '@/utils/modelParam'
|
||||
|
||||
import {
|
||||
engineParamsUpdateAtom,
|
||||
getActiveThreadIdAtom,
|
||||
getActiveThreadModelParamsAtom,
|
||||
} from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
export type ControllerType = 'slider' | 'checkbox' | 'input'
|
||||
|
||||
export type SettingComponentData = {
|
||||
name: string
|
||||
title: string
|
||||
description: string
|
||||
controllerType: ControllerType
|
||||
controllerData: SliderData | CheckboxData | InputData
|
||||
}
|
||||
|
||||
export type InputData = {
|
||||
placeholder: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type SliderData = {
|
||||
min: number
|
||||
max: number
|
||||
|
||||
step: number
|
||||
value: number
|
||||
}
|
||||
|
||||
type CheckboxData = {
|
||||
checked: boolean
|
||||
}
|
||||
|
||||
const SettingComponent = ({
|
||||
componentData,
|
||||
enabled = true,
|
||||
selector,
|
||||
updater,
|
||||
}: {
|
||||
componentData: SettingComponentData[]
|
||||
type Props = {
|
||||
componentProps: SettingComponentProps[]
|
||||
enabled?: boolean
|
||||
selector?: (e: SettingComponentData) => boolean
|
||||
updater?: (
|
||||
threadId: string,
|
||||
name: string,
|
||||
value: string | number | boolean | string[]
|
||||
) => void
|
||||
onValueUpdated: (key: string, value: string | number | boolean) => void
|
||||
}
|
||||
|
||||
const SettingComponent: React.FC<Props> = ({
|
||||
componentProps,
|
||||
enabled = true,
|
||||
onValueUpdated,
|
||||
}) => {
|
||||
const { updateModelParameter } = useUpdateModelParameters()
|
||||
|
||||
const threadId = useAtomValue(getActiveThreadIdAtom)
|
||||
|
||||
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
||||
|
||||
const modelSettingParams = toSettingParams(activeModelParams)
|
||||
|
||||
const engineParams = getConfigurationsData(modelSettingParams)
|
||||
|
||||
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
|
||||
|
||||
const { stopModel } = useActiveModel()
|
||||
|
||||
const onValueChanged = (
|
||||
name: string,
|
||||
value: string | number | boolean | string[]
|
||||
) => {
|
||||
if (!threadId) return
|
||||
if (engineParams.some((x) => x.name.includes(name))) {
|
||||
setEngineParamsUpdate(true)
|
||||
stopModel()
|
||||
} else {
|
||||
setEngineParamsUpdate(false)
|
||||
}
|
||||
if (updater) updater(threadId, name, value)
|
||||
else {
|
||||
// Convert stop string to array
|
||||
if (name === 'stop' && typeof value === 'string') {
|
||||
value = [value]
|
||||
}
|
||||
updateModelParameter(threadId, {
|
||||
params: { [name]: value },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const components = componentData
|
||||
.filter((x) => (selector ? selector(x) : true))
|
||||
.map((data) => {
|
||||
const components = componentProps.map((data) => {
|
||||
switch (data.controllerType) {
|
||||
case 'slider':
|
||||
const { min, max, step, value } = data.controllerData as SliderData
|
||||
case 'slider': {
|
||||
const { min, max, step, value } =
|
||||
data.controllerProps as SliderComponentProps
|
||||
return (
|
||||
<SliderRightPanel
|
||||
key={data.name}
|
||||
key={data.key}
|
||||
title={data.title}
|
||||
description={data.description}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
name={data.name}
|
||||
name={data.key}
|
||||
enabled={enabled}
|
||||
onValueChanged={(value) => onValueChanged(data.name, value)}
|
||||
onValueChanged={(value) => onValueUpdated(data.key, value)}
|
||||
/>
|
||||
)
|
||||
case 'input':
|
||||
}
|
||||
|
||||
case 'input': {
|
||||
const { placeholder, value: textValue } =
|
||||
data.controllerData as InputData
|
||||
data.controllerProps as InputComponentProps
|
||||
return (
|
||||
<ModelConfigInput
|
||||
title={data.title}
|
||||
enabled={enabled}
|
||||
key={data.name}
|
||||
name={data.name}
|
||||
key={data.key}
|
||||
name={data.key}
|
||||
description={data.description}
|
||||
placeholder={placeholder}
|
||||
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 (
|
||||
<Checkbox
|
||||
key={data.name}
|
||||
key={data.key}
|
||||
enabled={enabled}
|
||||
name={data.name}
|
||||
name={data.key}
|
||||
description={data.description}
|
||||
title={data.title}
|
||||
checked={checked}
|
||||
onValueChanged={(value) => onValueChanged(data.name, value)}
|
||||
checked={value}
|
||||
onValueChanged={(value) => onValueUpdated(data.key, value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@ -1,26 +1,48 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import SettingComponentBuilder, {
|
||||
SettingComponentData,
|
||||
} from './SettingComponent'
|
||||
import { SettingComponentProps } from '@janhq/core/.'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
||||
|
||||
import SettingComponentBuilder from './SettingComponent'
|
||||
|
||||
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
|
||||
|
||||
type Props = {
|
||||
componentProps: SettingComponentProps[]
|
||||
}
|
||||
|
||||
const ModelSetting: React.FC<Props> = ({ componentProps }) => {
|
||||
const isLocalServerRunning = useAtomValue(serverEnabledAtom)
|
||||
const activeThread = useAtomValue(activeThreadAtom)
|
||||
const { updateModelParameter } = useUpdateModelParameters()
|
||||
|
||||
const onValueChanged = useCallback(
|
||||
(key: string, value: string | number | boolean) => {
|
||||
if (!activeThread) return
|
||||
|
||||
if (key === 'stop' && typeof value === 'string') {
|
||||
updateModelParameter(activeThread, {
|
||||
params: { [key]: [value] },
|
||||
})
|
||||
} else {
|
||||
updateModelParameter(activeThread, {
|
||||
params: { [key]: value },
|
||||
})
|
||||
}
|
||||
},
|
||||
[activeThread, updateModelParameter]
|
||||
)
|
||||
|
||||
const ModelSetting = ({
|
||||
componentData,
|
||||
}: {
|
||||
componentData: SettingComponentData[]
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{componentData.filter((e) => e.name !== 'prompt_template').length && (
|
||||
<div className="flex flex-col">
|
||||
<SettingComponentBuilder
|
||||
componentData={componentData}
|
||||
selector={(e) => e.name !== 'prompt_template'}
|
||||
enabled={!isLocalServerRunning}
|
||||
componentProps={componentProps}
|
||||
onValueUpdated={onValueChanged}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,192 +1,224 @@
|
||||
import { SettingComponentData } from './SettingComponent'
|
||||
import { SettingComponentProps } from '@janhq/core/.'
|
||||
|
||||
export const presetConfiguration: Record<string, SettingComponentData> = {
|
||||
export const presetConfiguration: Record<string, SettingComponentProps> = {
|
||||
prompt_template: {
|
||||
name: 'prompt_template',
|
||||
key: 'prompt_template',
|
||||
title: 'Prompt template',
|
||||
description: 'The prompt to use for internal configuration.',
|
||||
controllerType: 'input',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
placeholder: 'Prompt template',
|
||||
value: '',
|
||||
},
|
||||
requireModelReload: true,
|
||||
configType: 'setting',
|
||||
},
|
||||
stop: {
|
||||
name: 'stop',
|
||||
key: 'stop',
|
||||
title: 'Stop',
|
||||
description:
|
||||
'Defines specific tokens or phrases at which the model will stop generating further output. ',
|
||||
controllerType: 'input',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
placeholder: 'Stop',
|
||||
value: '',
|
||||
},
|
||||
requireModelReload: false,
|
||||
configType: 'runtime',
|
||||
},
|
||||
ctx_len: {
|
||||
name: 'ctx_len',
|
||||
key: 'ctx_len',
|
||||
title: 'Context Length',
|
||||
description:
|
||||
'The context length for model operations varies; the maximum depends on the specific model used.',
|
||||
controllerType: 'slider',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
min: 0,
|
||||
max: 4096,
|
||||
step: 128,
|
||||
value: 4096,
|
||||
},
|
||||
requireModelReload: true,
|
||||
configType: 'setting',
|
||||
},
|
||||
max_tokens: {
|
||||
name: 'max_tokens',
|
||||
key: 'max_tokens',
|
||||
title: 'Max Tokens',
|
||||
description:
|
||||
'The maximum number of tokens the model will generate in a single response.',
|
||||
controllerType: 'slider',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
min: 100,
|
||||
max: 4096,
|
||||
step: 10,
|
||||
value: 4096,
|
||||
},
|
||||
requireModelReload: false,
|
||||
configType: 'runtime',
|
||||
},
|
||||
ngl: {
|
||||
name: 'ngl',
|
||||
key: 'ngl',
|
||||
title: 'Number of GPU layers (ngl)',
|
||||
description: 'The number of layers to load onto the GPU for acceleration.',
|
||||
controllerType: 'slider',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
value: 100,
|
||||
},
|
||||
requireModelReload: true,
|
||||
configType: 'setting',
|
||||
},
|
||||
embedding: {
|
||||
name: 'embedding',
|
||||
key: 'embedding',
|
||||
title: 'Embedding',
|
||||
description: 'Whether to enable embedding.',
|
||||
controllerType: 'checkbox',
|
||||
controllerData: {
|
||||
checked: true,
|
||||
controllerProps: {
|
||||
value: true,
|
||||
},
|
||||
requireModelReload: true,
|
||||
configType: 'setting',
|
||||
},
|
||||
stream: {
|
||||
name: 'stream',
|
||||
key: 'stream',
|
||||
title: 'Stream',
|
||||
description: 'Enable real-time data processing for faster predictions.',
|
||||
controllerType: 'checkbox',
|
||||
controllerData: {
|
||||
checked: false,
|
||||
controllerProps: {
|
||||
value: false,
|
||||
},
|
||||
requireModelReload: false,
|
||||
configType: 'runtime',
|
||||
},
|
||||
temperature: {
|
||||
name: 'temperature',
|
||||
key: 'temperature',
|
||||
title: 'Temperature',
|
||||
description: 'Controls the randomness of the model’s output.',
|
||||
controllerType: 'slider',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
value: 0.7,
|
||||
},
|
||||
requireModelReload: false,
|
||||
configType: 'runtime',
|
||||
},
|
||||
frequency_penalty: {
|
||||
name: 'frequency_penalty',
|
||||
key: 'frequency_penalty',
|
||||
title: 'Frequency Penalty',
|
||||
description:
|
||||
'Adjusts the likelihood of the model repeating words or phrases in its output. ',
|
||||
controllerType: 'slider',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
value: 0.7,
|
||||
},
|
||||
requireModelReload: false,
|
||||
configType: 'runtime',
|
||||
},
|
||||
presence_penalty: {
|
||||
name: 'presence_penalty',
|
||||
key: 'presence_penalty',
|
||||
title: 'Presence Penalty',
|
||||
description:
|
||||
'Influences the generation of new and varied concepts in the model’s output. ',
|
||||
controllerType: 'slider',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
value: 0.7,
|
||||
},
|
||||
requireModelReload: false,
|
||||
configType: 'runtime',
|
||||
},
|
||||
top_p: {
|
||||
name: 'top_p',
|
||||
key: 'top_p',
|
||||
title: 'Top P',
|
||||
description: 'Set probability threshold for more relevant outputs.',
|
||||
controllerType: 'slider',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
value: 0.95,
|
||||
},
|
||||
requireModelReload: false,
|
||||
configType: 'runtime',
|
||||
},
|
||||
n_parallel: {
|
||||
name: 'n_parallel',
|
||||
key: 'n_parallel',
|
||||
title: 'N Parallel',
|
||||
description:
|
||||
'The number of parallel operations. Only set when enable continuous batching. ',
|
||||
controllerType: 'slider',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
min: 0,
|
||||
max: 4,
|
||||
step: 1,
|
||||
value: 1,
|
||||
},
|
||||
requireModelReload: true,
|
||||
configType: 'setting',
|
||||
},
|
||||
// assistant
|
||||
chunk_size: {
|
||||
name: 'chunk_size',
|
||||
key: 'chunk_size',
|
||||
title: 'Chunk Size',
|
||||
description: 'Maximum number of tokens in a chunk',
|
||||
controllerType: 'slider',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
min: 128,
|
||||
max: 2048,
|
||||
step: 128,
|
||||
value: 1024,
|
||||
},
|
||||
requireModelReload: true,
|
||||
configType: 'setting',
|
||||
},
|
||||
chunk_overlap: {
|
||||
name: 'chunk_overlap',
|
||||
key: 'chunk_overlap',
|
||||
title: 'Chunk Overlap',
|
||||
description: 'Number of tokens overlapping between two adjacent chunks',
|
||||
controllerType: 'slider',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
min: 32,
|
||||
max: 512,
|
||||
step: 32,
|
||||
value: 64,
|
||||
},
|
||||
requireModelReload: true,
|
||||
configType: 'setting',
|
||||
},
|
||||
top_k: {
|
||||
name: 'top_k',
|
||||
key: 'top_k',
|
||||
title: 'Top K',
|
||||
description: 'Number of top-ranked documents to retrieve',
|
||||
controllerType: 'slider',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
value: 2,
|
||||
},
|
||||
requireModelReload: false,
|
||||
configType: 'runtime',
|
||||
},
|
||||
retrieval_template: {
|
||||
name: 'retrieval_template',
|
||||
key: 'retrieval_template',
|
||||
title: 'Retrieval Template',
|
||||
description:
|
||||
'The template to use for retrieval. The following variables are available: {CONTEXT}, {QUESTION}',
|
||||
controllerType: 'input',
|
||||
controllerData: {
|
||||
controllerProps: {
|
||||
placeholder: 'Retrieval Template',
|
||||
value: '',
|
||||
},
|
||||
requireModelReload: true,
|
||||
configType: 'setting',
|
||||
},
|
||||
}
|
||||
|
||||
196
web/screens/Chat/Sidebar/AssistantTool/index.tsx
Normal file
196
web/screens/Chat/Sidebar/AssistantTool/index.tsx
Normal 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
|
||||
50
web/screens/Chat/Sidebar/PromptTemplateSetting/index.tsx
Normal file
50
web/screens/Chat/Sidebar/PromptTemplateSetting/index.tsx
Normal 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
|
||||
@ -1,19 +1,9 @@
|
||||
import React from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
|
||||
import {
|
||||
Input,
|
||||
Textarea,
|
||||
Switch,
|
||||
Tooltip,
|
||||
TooltipArrow,
|
||||
TooltipContent,
|
||||
TooltipPortal,
|
||||
TooltipTrigger,
|
||||
} from '@janhq/uikit'
|
||||
import { Input, Textarea } from '@janhq/uikit'
|
||||
|
||||
import { atom, useAtomValue } from 'jotai'
|
||||
|
||||
import { InfoIcon } from 'lucide-react'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import LogoMark from '@/containers/Brand/Logo/Mark'
|
||||
@ -28,13 +18,13 @@ import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||
import { getConfigurationsData } from '@/utils/componentSettings'
|
||||
import { toRuntimeParams, toSettingParams } from '@/utils/modelParam'
|
||||
|
||||
import AssistantSetting from '../AssistantSetting'
|
||||
import EngineSetting from '../EngineSetting'
|
||||
import ModelSetting from '../ModelSetting'
|
||||
|
||||
import SettingComponentBuilder from '../ModelSetting/SettingComponent'
|
||||
import AssistantTool from './AssistantTool'
|
||||
|
||||
import PromptTemplateSetting from './PromptTemplateSetting'
|
||||
|
||||
import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||
import {
|
||||
activeThreadAtom,
|
||||
getActiveThreadModelParamsAtom,
|
||||
@ -48,23 +38,63 @@ const Sidebar: React.FC = () => {
|
||||
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
||||
const selectedModel = useAtomValue(selectedModelAtom)
|
||||
const { updateThreadMetadata } = useCreateNewThread()
|
||||
const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom)
|
||||
|
||||
const modelEngineParams = toSettingParams(activeModelParams)
|
||||
const modelSettings = useMemo(() => {
|
||||
const modelRuntimeParams = toRuntimeParams(activeModelParams)
|
||||
const componentDataAssistantSetting = getConfigurationsData(
|
||||
(activeThread?.assistants[0]?.tools &&
|
||||
activeThread?.assistants[0]?.tools[0]?.settings) ??
|
||||
{}
|
||||
|
||||
const componentDataRuntimeSetting = getConfigurationsData(
|
||||
modelRuntimeParams,
|
||||
selectedModel
|
||||
)
|
||||
return componentDataRuntimeSetting.filter(
|
||||
(x) => x.key !== 'prompt_template'
|
||||
)
|
||||
}, [activeModelParams, selectedModel])
|
||||
|
||||
const engineSettings = useMemo(() => {
|
||||
const modelEngineParams = toSettingParams(activeModelParams)
|
||||
const componentDataEngineSetting = getConfigurationsData(
|
||||
modelEngineParams,
|
||||
selectedModel
|
||||
)
|
||||
const componentDataRuntimeSetting = getConfigurationsData(
|
||||
modelRuntimeParams,
|
||||
return componentDataEngineSetting.filter((x) => x.key !== 'prompt_template')
|
||||
}, [activeModelParams, selectedModel])
|
||||
|
||||
const promptTemplateSettings = useMemo(() => {
|
||||
const modelEngineParams = toSettingParams(activeModelParams)
|
||||
const componentDataEngineSetting = getConfigurationsData(
|
||||
modelEngineParams,
|
||||
selectedModel
|
||||
)
|
||||
return componentDataEngineSetting.filter((x) => x.key === 'prompt_template')
|
||||
}, [activeModelParams, selectedModel])
|
||||
|
||||
const onThreadTitleChanged = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (activeThread)
|
||||
updateThreadMetadata({
|
||||
...activeThread,
|
||||
title: e.target.value || '',
|
||||
})
|
||||
},
|
||||
[activeThread, updateThreadMetadata]
|
||||
)
|
||||
|
||||
const onAssistantInstructionChanged = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (activeThread)
|
||||
updateThreadMetadata({
|
||||
...activeThread,
|
||||
assistants: [
|
||||
{
|
||||
...activeThread.assistants[0],
|
||||
instructions: e.target.value || '',
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
[activeThread, updateThreadMetadata]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -92,13 +122,7 @@ const Sidebar: React.FC = () => {
|
||||
<Input
|
||||
id="thread-title"
|
||||
value={activeThread?.title}
|
||||
onChange={(e) => {
|
||||
if (activeThread)
|
||||
updateThreadMetadata({
|
||||
...activeThread,
|
||||
title: e.target.value || '',
|
||||
})
|
||||
}}
|
||||
onChange={onThreadTitleChanged}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
@ -133,18 +157,7 @@ const Sidebar: React.FC = () => {
|
||||
id="assistant-instructions"
|
||||
placeholder="Eg. You are a helpful assistant."
|
||||
value={activeThread?.assistants[0].instructions ?? ''}
|
||||
onChange={(e) => {
|
||||
if (activeThread)
|
||||
updateThreadMetadata({
|
||||
...activeThread,
|
||||
assistants: [
|
||||
{
|
||||
...activeThread.assistants[0],
|
||||
instructions: e.target.value || '',
|
||||
},
|
||||
],
|
||||
})
|
||||
}}
|
||||
onChange={onAssistantInstructionChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -154,36 +167,33 @@ const Sidebar: React.FC = () => {
|
||||
<div className="px-2 pt-4">
|
||||
<DropdownListSidebar />
|
||||
|
||||
{componentDataRuntimeSetting.length > 0 && (
|
||||
{modelSettings.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<CardSidebar title="Inference Parameters" asChild>
|
||||
<div className="px-2 py-4">
|
||||
<ModelSetting componentData={componentDataRuntimeSetting} />
|
||||
<ModelSetting componentProps={modelSettings} />
|
||||
</div>
|
||||
</CardSidebar>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{componentDataEngineSetting.filter(
|
||||
(x) => x.name === 'prompt_template'
|
||||
).length !== 0 && (
|
||||
{promptTemplateSettings.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<CardSidebar title="Model Parameters" asChild>
|
||||
<div className="px-2 py-4">
|
||||
<SettingComponentBuilder
|
||||
componentData={componentDataEngineSetting}
|
||||
selector={(x) => x.name === 'prompt_template'}
|
||||
<PromptTemplateSetting
|
||||
componentData={promptTemplateSettings}
|
||||
/>
|
||||
</div>
|
||||
</CardSidebar>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{componentDataEngineSetting.length > 0 && (
|
||||
{engineSettings.length > 0 && (
|
||||
<div className="my-4">
|
||||
<CardSidebar title="Engine Parameters" asChild>
|
||||
<div className="px-2 py-4">
|
||||
<EngineSetting componentData={componentDataEngineSetting} />
|
||||
<EngineSetting componentData={engineSettings} />
|
||||
</div>
|
||||
</CardSidebar>
|
||||
</div>
|
||||
@ -191,166 +201,7 @@ const Sidebar: React.FC = () => {
|
||||
</div>
|
||||
</CardSidebar>
|
||||
|
||||
{experimentalFeature && (
|
||||
<div>
|
||||
{activeThread?.assistants[0]?.tools &&
|
||||
componentDataAssistantSetting.length > 0 && (
|
||||
<CardSidebar title="Tools" isShow={true}>
|
||||
<div className="px-2 pt-4">
|
||||
<div className="mb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
id="retrieval"
|
||||
className="inline-flex items-center font-bold text-zinc-500 dark:text-gray-300"
|
||||
>
|
||||
Retrieval
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon
|
||||
size={16}
|
||||
className="ml-2 flex-shrink-0 text-black dark:text-gray-500"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
className="max-w-[240px]"
|
||||
>
|
||||
<span>
|
||||
Retrieval helps the assistant use information
|
||||
from files you send to it. Once you share a
|
||||
file, the assistant automatically fetches the
|
||||
relevant content based on your request.
|
||||
</span>
|
||||
<TooltipArrow />
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Switch
|
||||
name="retrieval"
|
||||
className="mr-2"
|
||||
checked={
|
||||
activeThread?.assistants[0].tools[0].enabled
|
||||
}
|
||||
onCheckedChange={(e) => {
|
||||
if (activeThread)
|
||||
updateThreadMetadata({
|
||||
...activeThread,
|
||||
assistants: [
|
||||
{
|
||||
...activeThread.assistants[0],
|
||||
tools: [
|
||||
{
|
||||
type: 'retrieval',
|
||||
enabled: e,
|
||||
settings:
|
||||
(activeThread.assistants[0].tools &&
|
||||
activeThread.assistants[0]
|
||||
.tools[0]?.settings) ??
|
||||
{},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{activeThread?.assistants[0]?.tools[0].enabled && (
|
||||
<div className="pb-4 pt-2">
|
||||
<div className="mb-4">
|
||||
<div className="item-center mb-2 flex">
|
||||
<label
|
||||
id="embedding-model"
|
||||
className="inline-flex font-bold text-zinc-500 dark:text-gray-300"
|
||||
>
|
||||
Embedding Model
|
||||
</label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon
|
||||
size={16}
|
||||
className="ml-2 flex-shrink-0 dark:text-gray-500"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
className="max-w-[240px]"
|
||||
>
|
||||
<span>
|
||||
Embedding model is crucial for understanding
|
||||
and processing the input text effectively by
|
||||
converting text to numerical
|
||||
representations. Align the model choice with
|
||||
your task, evaluate its performance, and
|
||||
consider factors like resource availability.
|
||||
Experiment to find the best fit for your
|
||||
specific use case.
|
||||
</span>
|
||||
<TooltipArrow />
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Input value={selectedModel?.name} disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center">
|
||||
<label
|
||||
id="vector-database"
|
||||
className="inline-block font-bold text-zinc-500 dark:text-gray-300"
|
||||
>
|
||||
Vector Database
|
||||
</label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon
|
||||
size={16}
|
||||
className="ml-2 flex-shrink-0 dark:text-gray-500"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
className="max-w-[240px]"
|
||||
>
|
||||
<span>
|
||||
Vector Database is crucial for efficient
|
||||
storage and retrieval of embeddings.
|
||||
Consider your specific task, available
|
||||
resources, and language requirements.
|
||||
Experiment to find the best fit for your
|
||||
specific use case.
|
||||
</span>
|
||||
<TooltipArrow />
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Input value="HNSWLib" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<AssistantSetting
|
||||
componentData={componentDataAssistantSetting}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardSidebar>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<AssistantTool />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -49,8 +49,7 @@ import { toSettingParams } from '@/utils/modelParam'
|
||||
|
||||
import EngineSetting from '../Chat/EngineSetting'
|
||||
|
||||
import SettingComponentBuilder from '../Chat/ModelSetting/SettingComponent'
|
||||
|
||||
import ModelSetting from '../Chat/ModelSetting'
|
||||
import { showRightSideBarAtom } from '../Chat/Sidebar'
|
||||
|
||||
import {
|
||||
@ -429,17 +428,22 @@ const LocalServerScreen = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{componentDataEngineSetting.filter(
|
||||
(x) => x.name === 'prompt_template'
|
||||
).length !== 0 && (
|
||||
{componentDataEngineSetting.filter((x) => x.key === 'prompt_template')
|
||||
.length !== 0 && (
|
||||
<div className="mt-4">
|
||||
<CardSidebar title="Model Parameters" asChild>
|
||||
<div className="px-2 py-4">
|
||||
<SettingComponentBuilder
|
||||
<ModelSetting componentProps={componentDataEngineSetting} />
|
||||
{/* <SettingComponentBuilder
|
||||
enabled={!serverEnabled}
|
||||
componentData={componentDataEngineSetting}
|
||||
selector={(x) => x.name === 'prompt_template'}
|
||||
/>
|
||||
componentProps={componentDataEngineSetting}
|
||||
onValueUpdated={function (
|
||||
key: string,
|
||||
value: string | number | boolean
|
||||
): void {
|
||||
throw new Error('Function not implemented.')
|
||||
}}
|
||||
/> */}
|
||||
</div>
|
||||
</CardSidebar>
|
||||
</div>
|
||||
@ -449,10 +453,7 @@ const LocalServerScreen = () => {
|
||||
<div className="my-4">
|
||||
<CardSidebar title="Engine Parameters" asChild>
|
||||
<div className="px-2 py-4">
|
||||
<EngineSetting
|
||||
enabled={!serverEnabled}
|
||||
componentData={componentDataEngineSetting}
|
||||
/>
|
||||
<EngineSetting componentData={componentDataEngineSetting} />
|
||||
</div>
|
||||
</CardSidebar>
|
||||
</div>
|
||||
|
||||
62
web/screens/Settings/ExtensionSetting/index.tsx
Normal file
62
web/screens/Settings/ExtensionSetting/index.tsx
Normal 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
|
||||
@ -66,7 +66,7 @@ const SelectingModelModal: React.FC = () => {
|
||||
</ModalHeader>
|
||||
|
||||
<div
|
||||
className={`flex h-[172px] w-full items-center justify-center rounded-md border ${borderColor} ${dragAndDropBgColor}`}
|
||||
className={`flex h-[172px] w-full cursor-pointer items-center justify-center rounded-md border ${borderColor} ${dragAndDropBgColor}`}
|
||||
{...getRootProps()}
|
||||
onClick={onSelectFileClick}
|
||||
>
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
32
web/screens/Settings/SettingDetail/index.tsx
Normal file
32
web/screens/Settings/SettingDetail/index.tsx
Normal 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
|
||||
44
web/screens/Settings/SettingMenu/SettingItem/index.tsx
Normal file
44
web/screens/Settings/SettingMenu/SettingItem/index.tsx
Normal 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
|
||||
@ -1,55 +1,69 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { ScrollArea } from '@janhq/uikit'
|
||||
import { motion as m } from 'framer-motion'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
type Props = {
|
||||
activeMenu: string
|
||||
onMenuClick: (menu: string) => void
|
||||
}
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
const SettingMenu: React.FC<Props> = ({ activeMenu, onMenuClick }) => {
|
||||
const [menus, setMenus] = useState<string[]>([])
|
||||
import SettingItem from './SettingItem'
|
||||
|
||||
import { extensionManager } from '@/extension'
|
||||
import { janSettingScreenAtom } from '@/helpers/atoms/Setting.atom'
|
||||
|
||||
const SettingMenu: React.FC = () => {
|
||||
const settingScreens = useAtomValue(janSettingScreenAtom)
|
||||
const [extensionHasSettings, setExtensionHasSettings] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setMenus([
|
||||
'My Models',
|
||||
'My Settings',
|
||||
'Advanced Settings',
|
||||
...(window.electronAPI ? ['Extensions'] : []),
|
||||
])
|
||||
const getAllSettings = async () => {
|
||||
const activeExtensions = await extensionManager.getActive()
|
||||
const extensionsMenu: string[] = []
|
||||
|
||||
for (const extension of activeExtensions) {
|
||||
const extensionName = extension.name
|
||||
if (!extensionName) continue
|
||||
|
||||
const baseExtension = extensionManager.get(extensionName)
|
||||
if (!baseExtension) continue
|
||||
|
||||
if (typeof baseExtension.getSettings === 'function') {
|
||||
const settings = await baseExtension.getSettings()
|
||||
if (settings && settings.length > 0) {
|
||||
extensionsMenu.push(extensionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
setExtensionHasSettings(extensionsMenu)
|
||||
}
|
||||
getAllSettings()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r border-border">
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="flex-shrink-0 px-6 py-4 font-medium">
|
||||
{menus.map((menu) => {
|
||||
const isActive = activeMenu === menu
|
||||
return (
|
||||
<div
|
||||
key={menu}
|
||||
className="relative my-0.5 block cursor-pointer py-1.5"
|
||||
onClick={() => onMenuClick(menu)}
|
||||
>
|
||||
<span className={twMerge(isActive && 'relative z-10')}>
|
||||
{menu}
|
||||
</span>
|
||||
{settingScreens.map((settingScreen) => (
|
||||
<SettingItem key={settingScreen} setting={settingScreen} />
|
||||
))}
|
||||
|
||||
{isActive && (
|
||||
<m.div
|
||||
className="absolute inset-0 -left-3 h-full w-[calc(100%+24px)] rounded-md bg-primary/50"
|
||||
layoutId="active-static-menu"
|
||||
/>
|
||||
)}
|
||||
{extensionHasSettings.length > 0 && (
|
||||
<div className="mb-2 mt-6">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Extensions
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)}
|
||||
|
||||
{extensionHasSettings.map((extensionName: string) => (
|
||||
<SettingItem
|
||||
key={extensionName}
|
||||
setting={extensionName}
|
||||
extension={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingMenu
|
||||
export default React.memo(SettingMenu)
|
||||
|
||||
@ -1,51 +1,40 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import Advanced from '@/screens/Settings/Advanced'
|
||||
import AppearanceOptions from '@/screens/Settings/Appearance'
|
||||
import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
|
||||
|
||||
import Models from '@/screens/Settings/Models'
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
import { SUCCESS_SET_NEW_DESTINATION } from './Advanced/DataFolder'
|
||||
|
||||
import SettingDetail from './SettingDetail'
|
||||
import SettingMenu from './SettingMenu'
|
||||
|
||||
const handleShowOptions = (menu: string) => {
|
||||
switch (menu) {
|
||||
case 'Extensions':
|
||||
return <ExtensionCatalog />
|
||||
import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
|
||||
|
||||
case 'My Settings':
|
||||
return <AppearanceOptions />
|
||||
export const SettingScreenList = [
|
||||
'My Models',
|
||||
'My Settings',
|
||||
'Advanced Settings',
|
||||
'Extensions',
|
||||
] as const
|
||||
|
||||
case 'Advanced Settings':
|
||||
return <Advanced />
|
||||
|
||||
case 'My Models':
|
||||
return <Models />
|
||||
}
|
||||
}
|
||||
export type SettingScreenTuple = typeof SettingScreenList
|
||||
export type SettingScreen = SettingScreenTuple[number]
|
||||
|
||||
const SettingsScreen: React.FC = () => {
|
||||
const [activeStaticMenu, setActiveStaticMenu] = useState('My Models')
|
||||
|
||||
const setSelectedSettingScreen = useSetAtom(selectedSettingAtom)
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
|
||||
setActiveStaticMenu('Advanced Settings')
|
||||
setSelectedSettingScreen('Advanced Settings')
|
||||
localStorage.removeItem(SUCCESS_SET_NEW_DESTINATION)
|
||||
}
|
||||
}, [])
|
||||
}, [setSelectedSettingScreen])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-full bg-background"
|
||||
data-testid="testid-setting-description"
|
||||
className="flex h-full bg-background"
|
||||
>
|
||||
<SettingMenu
|
||||
activeMenu={activeStaticMenu}
|
||||
onMenuClick={setActiveStaticMenu}
|
||||
/>
|
||||
|
||||
{handleShowOptions(activeStaticMenu)}
|
||||
<SettingMenu />
|
||||
<SettingDetail />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import { Model } from '@janhq/core'
|
||||
import { Model, SettingComponentProps } from '@janhq/core'
|
||||
|
||||
import { SettingComponentData } from '@/screens/Chat/ModelSetting/SettingComponent'
|
||||
import { presetConfiguration } from '@/screens/Chat/ModelSetting/predefinedComponent'
|
||||
|
||||
export const getConfigurationsData = (
|
||||
settings: object,
|
||||
selectedModel?: Model
|
||||
) => {
|
||||
const componentData: SettingComponentData[] = []
|
||||
): SettingComponentProps[] => {
|
||||
const componentData: SettingComponentProps[] = []
|
||||
|
||||
Object.keys(settings).forEach((key: string) => {
|
||||
const componentSetting = presetConfiguration[key]
|
||||
@ -17,20 +16,20 @@ export const getConfigurationsData = (
|
||||
}
|
||||
if ('slider' === componentSetting.controllerType) {
|
||||
const value = Number(settings[key as keyof typeof settings])
|
||||
if ('value' in componentSetting.controllerData) {
|
||||
componentSetting.controllerData.value = value
|
||||
if ('max' in componentSetting.controllerData) {
|
||||
if ('value' in componentSetting.controllerProps) {
|
||||
componentSetting.controllerProps.value = value
|
||||
if ('max' in componentSetting.controllerProps) {
|
||||
switch (key) {
|
||||
case 'max_tokens':
|
||||
componentSetting.controllerData.max =
|
||||
componentSetting.controllerProps.max =
|
||||
selectedModel?.parameters.max_tokens ||
|
||||
componentSetting.controllerData.max ||
|
||||
componentSetting.controllerProps.max ||
|
||||
4096
|
||||
break
|
||||
case 'ctx_len':
|
||||
componentSetting.controllerData.max =
|
||||
componentSetting.controllerProps.max =
|
||||
selectedModel?.settings.ctx_len ||
|
||||
componentSetting.controllerData.max ||
|
||||
componentSetting.controllerProps.max ||
|
||||
4096
|
||||
break
|
||||
}
|
||||
@ -39,15 +38,15 @@ export const getConfigurationsData = (
|
||||
} else if ('input' === componentSetting.controllerType) {
|
||||
const value = settings[key as keyof typeof settings] as string
|
||||
const placeholder = settings[key as keyof typeof settings] as string
|
||||
if ('value' in componentSetting.controllerData)
|
||||
componentSetting.controllerData.value = value
|
||||
if ('placeholder' in componentSetting.controllerData)
|
||||
componentSetting.controllerData.placeholder = placeholder
|
||||
if ('value' in componentSetting.controllerProps)
|
||||
componentSetting.controllerProps.value = value
|
||||
if ('placeholder' in componentSetting.controllerProps)
|
||||
componentSetting.controllerProps.placeholder = placeholder
|
||||
} else if ('checkbox' === componentSetting.controllerType) {
|
||||
const checked = settings[key as keyof typeof settings] as boolean
|
||||
|
||||
if ('checked' in componentSetting.controllerData)
|
||||
componentSetting.controllerData.checked = checked
|
||||
if ('value' in componentSetting.controllerProps)
|
||||
componentSetting.controllerProps.value = checked
|
||||
}
|
||||
componentData.push(componentSetting)
|
||||
})
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
import { ErrorCode } from '@janhq/core'
|
||||
|
||||
export const getErrorTitle = (
|
||||
errorCode: ErrorCode,
|
||||
errorMessage: string | undefined
|
||||
) => {
|
||||
switch (errorCode) {
|
||||
case ErrorCode.Unknown:
|
||||
return 'Apologies, something’s amiss!'
|
||||
case ErrorCode.InvalidApiKey:
|
||||
return 'Invalid API key. Please check your API key and try again.'
|
||||
default:
|
||||
return errorMessage
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user