commit
506cbb8834
@ -55,20 +55,6 @@ const unlinkSync = (...args: any[]) => globalThis.core.api?.unlinkSync(...args)
|
|||||||
*/
|
*/
|
||||||
const appendFileSync = (...args: any[]) => globalThis.core.api?.appendFileSync(...args)
|
const appendFileSync = (...args: any[]) => globalThis.core.api?.appendFileSync(...args)
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronizes a file from a source path to a destination path.
|
|
||||||
* @param {string} src - The source path of the file to be synchronized.
|
|
||||||
* @param {string} dest - The destination path where the file will be synchronized to.
|
|
||||||
* @returns {Promise<any>} - A promise that resolves when the file has been successfully synchronized.
|
|
||||||
*/
|
|
||||||
const syncFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
|
||||||
globalThis.core.api?.syncFile(src, dest)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy file sync.
|
|
||||||
*/
|
|
||||||
const copyFileSync = (...args: any[]) => globalThis.core.api?.copyFileSync(...args)
|
|
||||||
|
|
||||||
const copyFile: (src: string, dest: string) => Promise<void> = (src, dest) =>
|
const copyFile: (src: string, dest: string) => Promise<void> = (src, dest) =>
|
||||||
globalThis.core.api?.copyFile(src, dest)
|
globalThis.core.api?.copyFile(src, dest)
|
||||||
|
|
||||||
@ -95,9 +81,7 @@ export const fs = {
|
|||||||
rm,
|
rm,
|
||||||
unlinkSync,
|
unlinkSync,
|
||||||
appendFileSync,
|
appendFileSync,
|
||||||
copyFileSync,
|
|
||||||
copyFile,
|
copyFile,
|
||||||
syncFile,
|
|
||||||
fileStat,
|
fileStat,
|
||||||
writeBlob,
|
writeBlob,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { resolve, sep } from 'path'
|
import { resolve, sep } from 'path'
|
||||||
import { DownloadEvent } from '../../../types/api'
|
import { DownloadEvent } from '../../../types/api'
|
||||||
import { normalizeFilePath } from '../../helper/path'
|
import { normalizeFilePath, validatePath } from '../../helper/path'
|
||||||
import { getJanDataFolderPath } from '../../helper'
|
import { getJanDataFolderPath } from '../../helper'
|
||||||
import { DownloadManager } from '../../helper/download'
|
import { DownloadManager } from '../../helper/download'
|
||||||
import { createWriteStream, renameSync } from 'fs'
|
import { createWriteStream, renameSync } from 'fs'
|
||||||
@ -37,6 +37,7 @@ export class Downloader implements Processor {
|
|||||||
const modelId = array.pop() ?? ''
|
const modelId = array.pop() ?? ''
|
||||||
|
|
||||||
const destination = resolve(getJanDataFolderPath(), normalizedPath)
|
const destination = resolve(getJanDataFolderPath(), normalizedPath)
|
||||||
|
validatePath(destination)
|
||||||
const rq = request({ url, strictSSL, proxy })
|
const rq = request({ url, strictSSL, proxy })
|
||||||
|
|
||||||
// Put request to download manager instance
|
// Put request to download manager instance
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { join } from 'path'
|
import { join, resolve } from 'path'
|
||||||
import { normalizeFilePath } from '../../helper/path'
|
import { normalizeFilePath, validatePath } from '../../helper/path'
|
||||||
import { getJanDataFolderPath } from '../../helper'
|
import { getJanDataFolderPath } from '../../helper'
|
||||||
import { Processor } from './Processor'
|
import { Processor } from './Processor'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
@ -15,17 +15,29 @@ export class FileSystem implements Processor {
|
|||||||
process(route: string, ...args: any): any {
|
process(route: string, ...args: any): any {
|
||||||
const instance = this as any
|
const instance = this as any
|
||||||
const func = instance[route]
|
const func = instance[route]
|
||||||
|
|
||||||
if (func) {
|
if (func) {
|
||||||
return func(...args)
|
return func(...args)
|
||||||
} else {
|
} else {
|
||||||
return import(FileSystem.moduleName).then((mdl) =>
|
return import(FileSystem.moduleName).then((mdl) =>
|
||||||
mdl[route](
|
mdl[route](
|
||||||
...args.map((arg: any) => {
|
...args.map((arg: any, index: number) => {
|
||||||
return typeof arg === 'string' &&
|
if(index !== 0) {
|
||||||
|
return arg
|
||||||
|
}
|
||||||
|
if (index === 0 && typeof arg !== 'string') {
|
||||||
|
throw new Error(`Invalid argument ${JSON.stringify(args)}`)
|
||||||
|
}
|
||||||
|
const path =
|
||||||
(arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
|
(arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
|
||||||
? join(getJanDataFolderPath(), normalizeFilePath(arg))
|
? join(getJanDataFolderPath(), normalizeFilePath(arg))
|
||||||
: arg
|
: arg
|
||||||
|
|
||||||
|
if(path.startsWith(`http://`) || path.startsWith(`https://`)) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
const absolutePath = resolve(path)
|
||||||
|
validatePath(absolutePath)
|
||||||
|
return absolutePath
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -42,8 +54,11 @@ export class FileSystem implements Processor {
|
|||||||
path = join(getJanDataFolderPath(), normalizeFilePath(path))
|
path = join(getJanDataFolderPath(), normalizeFilePath(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const absolutePath = resolve(path)
|
||||||
|
validatePath(absolutePath)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.rm(path, { recursive: true, force: true }, (err) => {
|
fs.rm(absolutePath, { recursive: true, force: true }, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
@ -63,8 +78,11 @@ export class FileSystem implements Processor {
|
|||||||
path = join(getJanDataFolderPath(), normalizeFilePath(path))
|
path = join(getJanDataFolderPath(), normalizeFilePath(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const absolutePath = resolve(path)
|
||||||
|
validatePath(absolutePath)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.mkdir(path, { recursive: true }, (err) => {
|
fs.mkdir(absolutePath, { recursive: true }, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
@ -73,4 +91,5 @@ export class FileSystem implements Processor {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { appResourcePath, normalizeFilePath } from '../../helper/path'
|
import { appResourcePath, normalizeFilePath, validatePath } from '../../helper/path'
|
||||||
import { getJanDataFolderPath, getJanDataFolderPath as getPath } from '../../helper'
|
import { getJanDataFolderPath, getJanDataFolderPath as getPath } from '../../helper'
|
||||||
import { Processor } from './Processor'
|
import { Processor } from './Processor'
|
||||||
import { FileStat } from '../../../types'
|
import { FileStat } from '../../../types'
|
||||||
@ -18,19 +18,6 @@ export class FSExt implements Processor {
|
|||||||
return func(...args)
|
return func(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
|
|
||||||
syncFile(src: string, dest: string) {
|
|
||||||
const reflect = require('@alumna/reflect')
|
|
||||||
return reflect({
|
|
||||||
src,
|
|
||||||
dest,
|
|
||||||
recursive: true,
|
|
||||||
delete: false,
|
|
||||||
overwrite: true,
|
|
||||||
errorOnExist: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path.
|
// Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path.
|
||||||
getJanDataFolderPath() {
|
getJanDataFolderPath() {
|
||||||
return Promise.resolve(getPath())
|
return Promise.resolve(getPath())
|
||||||
@ -70,14 +57,18 @@ export class FSExt implements Processor {
|
|||||||
writeBlob(path: string, data: any) {
|
writeBlob(path: string, data: any) {
|
||||||
try {
|
try {
|
||||||
const normalizedPath = normalizeFilePath(path)
|
const normalizedPath = normalizeFilePath(path)
|
||||||
|
|
||||||
const dataBuffer = Buffer.from(data, 'base64')
|
const dataBuffer = Buffer.from(data, 'base64')
|
||||||
fs.writeFileSync(join(getJanDataFolderPath(), normalizedPath), dataBuffer)
|
const writePath = join(getJanDataFolderPath(), normalizedPath)
|
||||||
|
validatePath(writePath)
|
||||||
|
fs.writeFileSync(writePath, dataBuffer)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`writeFile ${path} result: ${err}`)
|
console.error(`writeFile ${path} result: ${err}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
copyFile(src: string, dest: string): Promise<void> {
|
copyFile(src: string, dest: string): Promise<void> {
|
||||||
|
validatePath(dest)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.copyFile(src, dest, (err) => {
|
fs.copyFile(src, dest, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@ -7,7 +7,8 @@ import childProcess from 'child_process'
|
|||||||
const configurationFileName = 'settings.json'
|
const configurationFileName = 'settings.json'
|
||||||
|
|
||||||
// TODO: do no specify app name in framework module
|
// TODO: do no specify app name in framework module
|
||||||
const defaultJanDataFolder = join(os.homedir(), 'jan')
|
// TODO: do not default the os.homedir
|
||||||
|
const defaultJanDataFolder = join(os?.homedir() || '', 'jan')
|
||||||
const defaultAppConfig: AppConfiguration = {
|
const defaultAppConfig: AppConfiguration = {
|
||||||
data_folder: defaultJanDataFolder,
|
data_folder: defaultJanDataFolder,
|
||||||
quick_ask: false,
|
quick_ask: false,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export class DownloadManager {
|
|||||||
// store the download information with key is model id
|
// store the download information with key is model id
|
||||||
public downloadProgressMap: Record<string, DownloadState> = {}
|
public downloadProgressMap: Record<string, DownloadState> = {}
|
||||||
|
|
||||||
// store the download infomation with key is normalized file path
|
// store the download information with key is normalized file path
|
||||||
public downloadInfo: Record<string, DownloadState> = {}
|
public downloadInfo: Record<string, DownloadState> = {}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { join } from 'path'
|
import { join, resolve } from 'path'
|
||||||
|
import { getJanDataFolderPath } from './config'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize file path
|
* Normalize file path
|
||||||
@ -33,3 +34,11 @@ export async function appResourcePath(): Promise<string> {
|
|||||||
// server
|
// server
|
||||||
return join(global.core.appPath(), '../../..')
|
return join(global.core.appPath(), '../../..')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validatePath(path: string) {
|
||||||
|
const janDataFolderPath = getJanDataFolderPath()
|
||||||
|
const absolutePath = resolve(__dirname, path)
|
||||||
|
if (!absolutePath.startsWith(janDataFolderPath)) {
|
||||||
|
throw new Error(`Invalid path: ${absolutePath}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -90,7 +90,6 @@ export enum ExtensionRoute {
|
|||||||
}
|
}
|
||||||
export enum FileSystemRoute {
|
export enum FileSystemRoute {
|
||||||
appendFileSync = 'appendFileSync',
|
appendFileSync = 'appendFileSync',
|
||||||
copyFileSync = 'copyFileSync',
|
|
||||||
unlinkSync = 'unlinkSync',
|
unlinkSync = 'unlinkSync',
|
||||||
existsSync = 'existsSync',
|
existsSync = 'existsSync',
|
||||||
readdirSync = 'readdirSync',
|
readdirSync = 'readdirSync',
|
||||||
@ -100,7 +99,6 @@ export enum FileSystemRoute {
|
|||||||
writeFileSync = 'writeFileSync',
|
writeFileSync = 'writeFileSync',
|
||||||
}
|
}
|
||||||
export enum FileManagerRoute {
|
export enum FileManagerRoute {
|
||||||
syncFile = 'syncFile',
|
|
||||||
copyFile = 'copyFile',
|
copyFile = 'copyFile',
|
||||||
getJanDataFolderPath = 'getJanDataFolderPath',
|
getJanDataFolderPath = 'getJanDataFolderPath',
|
||||||
getResourcePath = 'getResourcePath',
|
getResourcePath = 'getResourcePath',
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
export type AssistantTool = {
|
export type AssistantTool = {
|
||||||
type: string
|
type: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
useTimeWeightedRetriever?: boolean
|
||||||
settings: any
|
settings: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -83,6 +83,8 @@ export enum MessageStatus {
|
|||||||
export enum ErrorCode {
|
export enum ErrorCode {
|
||||||
InvalidApiKey = 'invalid_api_key',
|
InvalidApiKey = 'invalid_api_key',
|
||||||
|
|
||||||
|
AuthenticationError = 'authentication_error',
|
||||||
|
|
||||||
InsufficientQuota = 'insufficient_quota',
|
InsufficientQuota = 'insufficient_quota',
|
||||||
|
|
||||||
InvalidRequestError = 'invalid_request_error',
|
InvalidRequestError = 'invalid_request_error',
|
||||||
|
|||||||
@ -3,8 +3,9 @@
|
|||||||
* @module preload
|
* @module preload
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { APIEvents, APIRoutes } from '@janhq/core/node'
|
import { APIEvents, APIRoutes, AppConfiguration, getAppConfigurations, updateAppConfiguration } from '@janhq/core/node'
|
||||||
import { contextBridge, ipcRenderer } from 'electron'
|
import { contextBridge, ipcRenderer } from 'electron'
|
||||||
|
import { readdirSync } from 'fs'
|
||||||
|
|
||||||
const interfaces: { [key: string]: (...args: any[]) => any } = {}
|
const interfaces: { [key: string]: (...args: any[]) => any } = {}
|
||||||
|
|
||||||
@ -12,7 +13,9 @@ const interfaces: { [key: string]: (...args: any[]) => any } = {}
|
|||||||
APIRoutes.forEach((method) => {
|
APIRoutes.forEach((method) => {
|
||||||
// For each method, create a function on the interfaces object
|
// For each method, create a function on the interfaces object
|
||||||
// This function invokes the method on the ipcRenderer with any provided arguments
|
// This function invokes the method on the ipcRenderer with any provided arguments
|
||||||
|
|
||||||
interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args)
|
interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Loop over each method in APIEvents
|
// Loop over each method in APIEvents
|
||||||
@ -22,6 +25,33 @@ APIEvents.forEach((method) => {
|
|||||||
// The handler for the event is provided as an argument to the function
|
// The handler for the event is provided as an argument to the function
|
||||||
interfaces[method] = (handler: any) => ipcRenderer.on(method, handler)
|
interfaces[method] = (handler: any) => ipcRenderer.on(method, handler)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
interfaces['changeDataFolder'] = async path => {
|
||||||
|
const appConfiguration: AppConfiguration = await ipcRenderer.invoke('getAppConfigurations')
|
||||||
|
const currentJanDataFolder = appConfiguration.data_folder
|
||||||
|
appConfiguration.data_folder = path
|
||||||
|
const reflect = require('@alumna/reflect')
|
||||||
|
const { err } = await reflect({
|
||||||
|
src: currentJanDataFolder,
|
||||||
|
dest: path,
|
||||||
|
recursive: true,
|
||||||
|
delete: false,
|
||||||
|
overwrite: true,
|
||||||
|
errorOnExist: false,
|
||||||
|
})
|
||||||
|
if (err) {
|
||||||
|
console.error(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
await ipcRenderer.invoke('updateAppConfiguration', appConfiguration)
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces['isDirectoryEmpty'] = async path => {
|
||||||
|
const dirChildren = await readdirSync(path)
|
||||||
|
return dirChildren.filter((x) => x !== '.DS_Store').length === 0
|
||||||
|
}
|
||||||
|
|
||||||
// Expose the 'interfaces' object in the main world under the name 'electronAPI'
|
// Expose the 'interfaces' object in the main world under the name 'electronAPI'
|
||||||
// This allows the renderer process to access these methods directly
|
// This allows the renderer process to access these methods directly
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
|||||||
@ -126,6 +126,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
|||||||
{
|
{
|
||||||
type: 'retrieval',
|
type: 'retrieval',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
useTimeWeightedRetriever: false,
|
||||||
settings: {
|
settings: {
|
||||||
top_k: 2,
|
top_k: 2,
|
||||||
chunk_size: 1024,
|
chunk_size: 1024,
|
||||||
|
|||||||
@ -11,13 +11,14 @@ export function toolRetrievalUpdateTextSplitter(
|
|||||||
export async function toolRetrievalIngestNewDocument(
|
export async function toolRetrievalIngestNewDocument(
|
||||||
file: string,
|
file: string,
|
||||||
model: string,
|
model: string,
|
||||||
engine: string
|
engine: string,
|
||||||
|
useTimeWeighted: boolean
|
||||||
) {
|
) {
|
||||||
const filePath = path.join(getJanDataFolderPath(), normalizeFilePath(file))
|
const filePath = path.join(getJanDataFolderPath(), normalizeFilePath(file))
|
||||||
const threadPath = path.dirname(filePath.replace('files', ''))
|
const threadPath = path.dirname(filePath.replace('files', ''))
|
||||||
retrieval.updateEmbeddingEngine(model, engine)
|
retrieval.updateEmbeddingEngine(model, engine)
|
||||||
return retrieval
|
return retrieval
|
||||||
.ingestAgentKnowledge(filePath, `${threadPath}/memory`)
|
.ingestAgentKnowledge(filePath, `${threadPath}/memory`, useTimeWeighted)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
@ -33,8 +34,11 @@ export async function toolRetrievalLoadThreadMemory(threadId: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toolRetrievalQueryResult(query: string) {
|
export async function toolRetrievalQueryResult(
|
||||||
return retrieval.generateResult(query).catch((err) => {
|
query: string,
|
||||||
|
useTimeWeighted: boolean = false
|
||||||
|
) {
|
||||||
|
return retrieval.generateResult(query, useTimeWeighted).catch((err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,11 +2,16 @@ import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'
|
|||||||
import { formatDocumentsAsString } from 'langchain/util/document'
|
import { formatDocumentsAsString } from 'langchain/util/document'
|
||||||
import { PDFLoader } from 'langchain/document_loaders/fs/pdf'
|
import { PDFLoader } from 'langchain/document_loaders/fs/pdf'
|
||||||
|
|
||||||
|
import { TimeWeightedVectorStoreRetriever } from 'langchain/retrievers/time_weighted'
|
||||||
|
import { MemoryVectorStore } from 'langchain/vectorstores/memory'
|
||||||
|
|
||||||
import { HNSWLib } from 'langchain/vectorstores/hnswlib'
|
import { HNSWLib } from 'langchain/vectorstores/hnswlib'
|
||||||
|
|
||||||
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
|
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
|
||||||
import { readEmbeddingEngine } from './engine'
|
import { readEmbeddingEngine } from './engine'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
export class Retrieval {
|
export class Retrieval {
|
||||||
public chunkSize: number = 100
|
public chunkSize: number = 100
|
||||||
public chunkOverlap?: number = 0
|
public chunkOverlap?: number = 0
|
||||||
@ -15,8 +20,25 @@ export class Retrieval {
|
|||||||
private embeddingModel?: OpenAIEmbeddings = undefined
|
private embeddingModel?: OpenAIEmbeddings = undefined
|
||||||
private textSplitter?: RecursiveCharacterTextSplitter
|
private textSplitter?: RecursiveCharacterTextSplitter
|
||||||
|
|
||||||
|
// to support time-weighted retrieval
|
||||||
|
private timeWeightedVectorStore: MemoryVectorStore
|
||||||
|
private timeWeightedretriever: any | TimeWeightedVectorStoreRetriever
|
||||||
|
|
||||||
constructor(chunkSize: number = 4000, chunkOverlap: number = 200) {
|
constructor(chunkSize: number = 4000, chunkOverlap: number = 200) {
|
||||||
this.updateTextSplitter(chunkSize, chunkOverlap)
|
this.updateTextSplitter(chunkSize, chunkOverlap)
|
||||||
|
|
||||||
|
// declare time-weighted retriever and storage
|
||||||
|
this.timeWeightedVectorStore = new MemoryVectorStore(
|
||||||
|
new OpenAIEmbeddings(
|
||||||
|
{ openAIApiKey: 'nitro-embedding' },
|
||||||
|
{ basePath: 'http://127.0.0.1:3928/v1' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
this.timeWeightedretriever = new TimeWeightedVectorStoreRetriever({
|
||||||
|
vectorStore: this.timeWeightedVectorStore,
|
||||||
|
memoryStream: [],
|
||||||
|
searchKwargs: 2,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateTextSplitter(chunkSize: number, chunkOverlap: number): void {
|
public updateTextSplitter(chunkSize: number, chunkOverlap: number): void {
|
||||||
@ -44,11 +66,15 @@ export class Retrieval {
|
|||||||
openAIApiKey: settings.api_key,
|
openAIApiKey: settings.api_key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update time-weighted embedding model
|
||||||
|
this.timeWeightedVectorStore.embeddings = this.embeddingModel
|
||||||
}
|
}
|
||||||
|
|
||||||
public ingestAgentKnowledge = async (
|
public ingestAgentKnowledge = async (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
memoryPath: string
|
memoryPath: string,
|
||||||
|
useTimeWeighted: boolean
|
||||||
): Promise<any> => {
|
): Promise<any> => {
|
||||||
const loader = new PDFLoader(filePath, {
|
const loader = new PDFLoader(filePath, {
|
||||||
splitPages: true,
|
splitPages: true,
|
||||||
@ -57,6 +83,13 @@ export class Retrieval {
|
|||||||
const doc = await loader.load()
|
const doc = await loader.load()
|
||||||
const docs = await this.textSplitter!.splitDocuments(doc)
|
const docs = await this.textSplitter!.splitDocuments(doc)
|
||||||
const vectorStore = await HNSWLib.fromDocuments(docs, this.embeddingModel)
|
const vectorStore = await HNSWLib.fromDocuments(docs, this.embeddingModel)
|
||||||
|
|
||||||
|
// add documents with metadata by using the time-weighted retriever in order to support time-weighted retrieval
|
||||||
|
if (useTimeWeighted && this.timeWeightedretriever) {
|
||||||
|
await (
|
||||||
|
this.timeWeightedretriever as TimeWeightedVectorStoreRetriever
|
||||||
|
).addDocuments(docs)
|
||||||
|
}
|
||||||
return vectorStore.save(memoryPath)
|
return vectorStore.save(memoryPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,10 +100,25 @@ export class Retrieval {
|
|||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateResult = async (query: string): Promise<string> => {
|
public generateResult = async (
|
||||||
|
query: string,
|
||||||
|
useTimeWeighted: boolean
|
||||||
|
): Promise<string> => {
|
||||||
|
if (useTimeWeighted) {
|
||||||
|
if (!this.timeWeightedretriever) {
|
||||||
|
return Promise.resolve(' ')
|
||||||
|
}
|
||||||
|
// use invoke because getRelevantDocuments is deprecated
|
||||||
|
const relevantDocs = await this.timeWeightedretriever.invoke(query)
|
||||||
|
const serializedDoc = formatDocumentsAsString(relevantDocs)
|
||||||
|
return Promise.resolve(serializedDoc)
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.retriever) {
|
if (!this.retriever) {
|
||||||
return Promise.resolve(' ')
|
return Promise.resolve(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// should use invoke(query) because getRelevantDocuments is deprecated
|
||||||
const relevantDocs = await this.retriever.getRelevantDocuments(query)
|
const relevantDocs = await this.retriever.getRelevantDocuments(query)
|
||||||
const serializedDoc = formatDocumentsAsString(relevantDocs)
|
const serializedDoc = formatDocumentsAsString(relevantDocs)
|
||||||
return Promise.resolve(serializedDoc)
|
return Promise.resolve(serializedDoc)
|
||||||
|
|||||||
@ -37,7 +37,8 @@ export class RetrievalTool extends InferenceTool {
|
|||||||
'toolRetrievalIngestNewDocument',
|
'toolRetrievalIngestNewDocument',
|
||||||
docFile,
|
docFile,
|
||||||
data.model?.id,
|
data.model?.id,
|
||||||
data.model?.engine
|
data.model?.engine,
|
||||||
|
tool?.useTimeWeightedRetriever ?? false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(data)
|
return Promise.resolve(data)
|
||||||
@ -78,7 +79,8 @@ export class RetrievalTool extends InferenceTool {
|
|||||||
const retrievalResult = await executeOnMain(
|
const retrievalResult = await executeOnMain(
|
||||||
NODE,
|
NODE,
|
||||||
'toolRetrievalQueryResult',
|
'toolRetrievalQueryResult',
|
||||||
prompt
|
prompt,
|
||||||
|
tool?.useTimeWeightedRetriever ?? false
|
||||||
)
|
)
|
||||||
console.debug('toolRetrievalQueryResult', retrievalResult)
|
console.debug('toolRetrievalQueryResult', retrievalResult)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/inference-anthropic-extension",
|
"name": "@janhq/inference-anthropic-extension",
|
||||||
"productName": "Anthropic Inference Engine",
|
"productName": "Anthropic Inference Engine",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"description": "This extension enables Anthropic chat completion API calls",
|
"description": "This extension enables Anthropic chat completion API calls",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/module.js",
|
"module": "dist/module.js",
|
||||||
|
|||||||
@ -19,10 +19,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"author": "Anthropic",
|
"author": "Anthropic",
|
||||||
"tags": [
|
"tags": ["General", "Big Context Length"]
|
||||||
"General",
|
|
||||||
"Big Context Length"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"engine": "anthropic"
|
"engine": "anthropic"
|
||||||
},
|
},
|
||||||
@ -46,10 +43,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"author": "Anthropic",
|
"author": "Anthropic",
|
||||||
"tags": [
|
"tags": ["General", "Big Context Length"]
|
||||||
"General",
|
|
||||||
"Big Context Length"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"engine": "anthropic"
|
"engine": "anthropic"
|
||||||
},
|
},
|
||||||
@ -73,10 +67,31 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"author": "Anthropic",
|
"author": "Anthropic",
|
||||||
"tags": [
|
"tags": ["General", "Big Context Length"]
|
||||||
"General",
|
},
|
||||||
"Big Context Length"
|
"engine": "anthropic"
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"url": "https://www.anthropic.com/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "claude-3-5-sonnet-20240620",
|
||||||
|
"object": "model",
|
||||||
|
"name": "Claude 3.5 Sonnet",
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "Claude 3.5 Sonnet raises the industry bar for intelligence, outperforming competitor models and Claude 3 Opus on a wide range of evaluations, with the speed and cost of our mid-tier model, Claude 3 Sonnet.",
|
||||||
|
"format": "api",
|
||||||
|
"settings": {},
|
||||||
|
"parameters": {
|
||||||
|
"max_tokens": 4096,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"stream": true
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "Anthropic",
|
||||||
|
"tags": ["General", "Big Context Length"]
|
||||||
},
|
},
|
||||||
"engine": "anthropic"
|
"engine": "anthropic"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
0.4.13
|
0.4.20
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/inference-cortex-extension",
|
"name": "@janhq/inference-cortex-extension",
|
||||||
"productName": "Cortex Inference Engine",
|
"productName": "Cortex Inference Engine",
|
||||||
"version": "1.0.13",
|
"version": "1.0.14",
|
||||||
"description": "This extension embeds cortex.cpp, a lightweight inference engine written in C++. See https://nitro.jan.ai.\nAdditional dependencies could be installed to run without Cuda Toolkit installation.",
|
"description": "This extension embeds cortex.cpp, a lightweight inference engine written in C++. See https://nitro.jan.ai.\nAdditional dependencies could be installed to run without Cuda Toolkit installation.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"node": "dist/node/index.cjs.js",
|
"node": "dist/node/index.cjs.js",
|
||||||
@ -10,7 +10,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"build": "tsc --module commonjs && rollup -c rollup.config.ts",
|
"build": "tsc --module commonjs && rollup -c rollup.config.ts",
|
||||||
"downloadnitro:linux": "CORTEX_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-avx2.tar.gz -e --strip 1 -o ./bin/linux-cpu && chmod +x ./bin/linux-cpu/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-cuda-12-0.tar.gz -e --strip 1 -o ./bin/linux-cuda-12-0 && chmod +x ./bin/linux-cuda-12-0/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-cuda-11-7.tar.gz -e --strip 1 -o ./bin/linux-cuda-11-7 && chmod +x ./bin/linux-cuda-11-7/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-vulkan.tar.gz -e --strip 1 -o ./bin/linux-vulkan && chmod +x ./bin/linux-vulkan/cortex-cpp",
|
"downloadnitro:linux": "CORTEX_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-avx2.tar.gz -e --strip 1 -o ./bin/linux-cpu && chmod +x ./bin/linux-cpu/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-avx2-cuda-12-0.tar.gz -e --strip 1 -o ./bin/linux-cuda-12-0 && chmod +x ./bin/linux-cuda-12-0/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-avx2-cuda-11-7.tar.gz -e --strip 1 -o ./bin/linux-cuda-11-7 && chmod +x ./bin/linux-cuda-11-7/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-vulkan.tar.gz -e --strip 1 -o ./bin/linux-vulkan && chmod +x ./bin/linux-vulkan/cortex-cpp",
|
||||||
"downloadnitro:darwin": "CORTEX_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-mac-arm64.tar.gz -o ./bin/ && mkdir -p ./bin/mac-arm64 && tar -zxvf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-arm64.tar.gz --strip-components=1 -C ./bin/mac-arm64 && rm -rf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-arm64.tar.gz && chmod +x ./bin/mac-arm64/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-mac-amd64.tar.gz -o ./bin/ && mkdir -p ./bin/mac-amd64 && tar -zxvf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-amd64.tar.gz --strip-components=1 -C ./bin/mac-amd64 && rm -rf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-amd64.tar.gz && chmod +x ./bin/mac-amd64/cortex-cpp",
|
"downloadnitro:darwin": "CORTEX_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-mac-arm64.tar.gz -o ./bin/ && mkdir -p ./bin/mac-arm64 && tar -zxvf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-arm64.tar.gz --strip-components=1 -C ./bin/mac-arm64 && rm -rf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-arm64.tar.gz && chmod +x ./bin/mac-arm64/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-mac-amd64.tar.gz -o ./bin/ && mkdir -p ./bin/mac-amd64 && tar -zxvf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-amd64.tar.gz --strip-components=1 -C ./bin/mac-amd64 && rm -rf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-amd64.tar.gz && chmod +x ./bin/mac-amd64/cortex-cpp",
|
||||||
"downloadnitro:win32": "download.bat",
|
"downloadnitro:win32": "download.bat",
|
||||||
"downloadnitro": "run-script-os",
|
"downloadnitro": "run-script-os",
|
||||||
@ -53,7 +53,7 @@
|
|||||||
"fetch-retry": "^5.0.6",
|
"fetch-retry": "^5.0.6",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"tcp-port-used": "^1.0.2",
|
"tcp-port-used": "^1.0.2",
|
||||||
"terminate": "^2.6.1",
|
"terminate": "2.6.1",
|
||||||
"ulidx": "^2.3.0"
|
"ulidx": "^2.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"id": "gemma-2b",
|
"id": "gemma-2b",
|
||||||
"object": "model",
|
"object": "model",
|
||||||
"name": "Gemma 2B Q4",
|
"name": "Gemma 2B Q4",
|
||||||
"version": "1.2",
|
"version": "1.3",
|
||||||
"description": "Gemma is built from the same technology with Google's Gemini.",
|
"description": "Gemma is built from the same technology with Google's Gemini.",
|
||||||
"format": "gguf",
|
"format": "gguf",
|
||||||
"settings": {
|
"settings": {
|
||||||
@ -22,7 +22,7 @@
|
|||||||
"top_p": 0.95,
|
"top_p": 0.95,
|
||||||
"stream": true,
|
"stream": true,
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"stop": [],
|
"stop": ["<end_of_turn>"],
|
||||||
"frequency_penalty": 0,
|
"frequency_penalty": 0,
|
||||||
"presence_penalty": 0
|
"presence_penalty": 0
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/model-extension",
|
"name": "@janhq/model-extension",
|
||||||
"productName": "Model Management",
|
"productName": "Model Management",
|
||||||
"version": "1.0.32",
|
"version": "1.0.33",
|
||||||
"description": "Model Management Extension provides model exploration and seamless downloads",
|
"description": "Model Management Extension provides model exploration and seamless downloads",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"node": "dist/node/index.cjs.js",
|
"node": "dist/node/index.cjs.js",
|
||||||
|
|||||||
@ -383,7 +383,12 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
|
|
||||||
// model binaries (sources) are absolute path & exist
|
// model binaries (sources) are absolute path & exist
|
||||||
const existFiles = await Promise.all(
|
const existFiles = await Promise.all(
|
||||||
model.sources.map((source) => fs.existsSync(source.url))
|
model.sources.map(
|
||||||
|
(source) =>
|
||||||
|
// Supposed to be a local file url
|
||||||
|
!source.url.startsWith(`http://`) &&
|
||||||
|
!source.url.startsWith(`https://`)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if (existFiles.every((exist) => exist)) return true
|
if (existFiles.every((exist) => exist)) return true
|
||||||
|
|
||||||
|
|||||||
@ -41,12 +41,13 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
|
|||||||
case ErrorCode.Unknown:
|
case ErrorCode.Unknown:
|
||||||
return 'Apologies, something’s amiss!'
|
return 'Apologies, something’s amiss!'
|
||||||
case ErrorCode.InvalidApiKey:
|
case ErrorCode.InvalidApiKey:
|
||||||
|
case ErrorCode.AuthenticationError:
|
||||||
case ErrorCode.InvalidRequestError:
|
case ErrorCode.InvalidRequestError:
|
||||||
return (
|
return (
|
||||||
<span data-testid="invalid-API-key-error">
|
<span data-testid="invalid-API-key-error">
|
||||||
Invalid API key. Please check your API key from{' '}
|
Invalid API key. Please check your API key from{' '}
|
||||||
<button
|
<button
|
||||||
className="font-medium text-[hsla(var(--app-link))]"
|
className="font-medium text-[hsla(var(--app-link))] underline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMainState(MainViewState.Settings)
|
setMainState(MainViewState.Settings)
|
||||||
|
|
||||||
@ -102,7 +103,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
|
|||||||
<p>
|
<p>
|
||||||
Jan’s in beta. Access
|
Jan’s in beta. Access
|
||||||
<span
|
<span
|
||||||
className="cursor-pointer text-[hsla(var(--app-link))]"
|
className="cursor-pointer text-[hsla(var(--app-link))] underline"
|
||||||
onClick={() => setModalTroubleShooting(true)}
|
onClick={() => setModalTroubleShooting(true)}
|
||||||
>
|
>
|
||||||
troubleshooting assistance
|
troubleshooting assistance
|
||||||
|
|||||||
@ -3,12 +3,20 @@ import { Fragment, useEffect, useState } from 'react'
|
|||||||
import { Progress } from '@janhq/joi'
|
import { Progress } from '@janhq/joi'
|
||||||
import { useClickOutside } from '@janhq/joi'
|
import { useClickOutside } from '@janhq/joi'
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
import { MonitorIcon, XIcon, ChevronDown, ChevronUp } from 'lucide-react'
|
import {
|
||||||
|
MonitorIcon,
|
||||||
|
XIcon,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
FolderOpenIcon,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import useGetSystemResources from '@/hooks/useGetSystemResources'
|
import useGetSystemResources from '@/hooks/useGetSystemResources'
|
||||||
|
|
||||||
|
import { usePath } from '@/hooks/usePath'
|
||||||
|
|
||||||
import { toGibibytes } from '@/utils/converter'
|
import { toGibibytes } from '@/utils/converter'
|
||||||
|
|
||||||
import TableActiveModel from './TableActiveModel'
|
import TableActiveModel from './TableActiveModel'
|
||||||
@ -28,6 +36,7 @@ const SystemMonitor = () => {
|
|||||||
const usedRam = useAtomValue(usedRamAtom)
|
const usedRam = useAtomValue(usedRamAtom)
|
||||||
const cpuUsage = useAtomValue(cpuUsageAtom)
|
const cpuUsage = useAtomValue(cpuUsageAtom)
|
||||||
const gpus = useAtomValue(gpusAtom)
|
const gpus = useAtomValue(gpusAtom)
|
||||||
|
const { onRevealInFinder } = usePath()
|
||||||
const [showFullScreen, setShowFullScreen] = useState(false)
|
const [showFullScreen, setShowFullScreen] = useState(false)
|
||||||
const ramUtilitized = useAtomValue(ramUtilitizedAtom)
|
const ramUtilitized = useAtomValue(ramUtilitizedAtom)
|
||||||
const [showSystemMonitorPanel, setShowSystemMonitorPanel] = useAtom(
|
const [showSystemMonitorPanel, setShowSystemMonitorPanel] = useAtom(
|
||||||
@ -64,7 +73,7 @@ const SystemMonitor = () => {
|
|||||||
<div
|
<div
|
||||||
ref={setControl}
|
ref={setControl}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'flex cursor-pointer items-center gap-x-1 rounded-l px-1 py-0.5 hover:bg-[hsla(var(--secondary-bg))]',
|
'flex cursor-pointer items-center gap-x-1 rounded px-1 py-0.5 hover:bg-[hsla(var(--secondary-bg))]',
|
||||||
showSystemMonitorPanel && 'bg-[hsla(var(--secondary-bg))]'
|
showSystemMonitorPanel && 'bg-[hsla(var(--secondary-bg))]'
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -89,6 +98,12 @@ const SystemMonitor = () => {
|
|||||||
Running Models
|
Running Models
|
||||||
</h6>
|
</h6>
|
||||||
<div className="unset-drag flex cursor-pointer items-center gap-x-2">
|
<div className="unset-drag flex cursor-pointer items-center gap-x-2">
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center gap-x-1 rounded px-1 py-0.5 hover:bg-[hsla(var(--secondary-bg))]"
|
||||||
|
onClick={() => onRevealInFinder('Logs')}
|
||||||
|
>
|
||||||
|
<FolderOpenIcon size={12} /> App Log
|
||||||
|
</div>
|
||||||
{showFullScreen ? (
|
{showFullScreen ? (
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
size={20}
|
size={20}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { Modal } from '@janhq/joi'
|
import { Button, Modal } from '@janhq/joi'
|
||||||
import { atom, useAtom } from 'jotai'
|
import { atom, useAtom } from 'jotai'
|
||||||
|
import { Maximize2 } from 'lucide-react'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import ServerLogs from '@/containers/ServerLogs'
|
import ServerLogs from '@/containers/ServerLogs'
|
||||||
@ -17,6 +18,7 @@ const ModalTroubleShooting = () => {
|
|||||||
modalTroubleShootingAtom
|
modalTroubleShootingAtom
|
||||||
)
|
)
|
||||||
const [isTabActive, setIsTabActivbe] = useState(0)
|
const [isTabActive, setIsTabActivbe] = useState(0)
|
||||||
|
const [showLogFullSize, setshowLogFullSize] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@ -26,14 +28,15 @@ const ModalTroubleShooting = () => {
|
|||||||
title="Troubleshooting Assistance"
|
title="Troubleshooting Assistance"
|
||||||
content={
|
content={
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden text-sm">
|
<div className="flex h-full w-full flex-col overflow-hidden text-sm">
|
||||||
<div className="flex-shrink-0">
|
<div className="mb-3 flex-shrink-0">
|
||||||
<p className="text-[hsla(var(--text-secondary)] mt-2 pr-3 leading-relaxed">
|
<p className="text-[hsla(var(--text-secondary)] mt-2 pr-3 leading-relaxed">
|
||||||
{`We're here to help! Your report is crucial for debugging and shaping
|
{`We're here to help! Your report is crucial for debugging and shaping
|
||||||
the next version. Here’s how you can report & get further support:`}
|
the next version. Here’s how you can report & get further support:`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="my-3 rounded-lg border border-[hsla(var(--app-border))] p-4 shadow">
|
{!showLogFullSize && (
|
||||||
|
<div className="mb-3 rounded-lg border border-[hsla(var(--app-border))] p-4 shadow">
|
||||||
<h2 className="font-semibold">Step 1</h2>
|
<h2 className="font-semibold">Step 1</h2>
|
||||||
<p className="text-[hsla(var(--text-secondary)] mt-1">
|
<p className="text-[hsla(var(--text-secondary)] mt-1">
|
||||||
Follow our
|
Follow our
|
||||||
@ -47,8 +50,15 @@ const ModalTroubleShooting = () => {
|
|||||||
for step-by-step solutions.
|
for step-by-step solutions.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="rounded-lg border border-[hsla(var(--app-border))] pb-2 pt-4 shadow">
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'rounded-lg border border-[hsla(var(--app-border))] pb-2 shadow',
|
||||||
|
!showLogFullSize && 'pt-4'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!showLogFullSize && (
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<h2 className="font-semibold">Step 2</h2>
|
<h2 className="font-semibold">Step 2</h2>
|
||||||
<p className="text-[hsla(var(--text-secondary)] mt-1">
|
<p className="text-[hsla(var(--text-secondary)] mt-1">
|
||||||
@ -72,13 +82,22 @@ const ModalTroubleShooting = () => {
|
|||||||
>
|
>
|
||||||
Discord
|
Discord
|
||||||
</a>
|
</a>
|
||||||
& send it to #🆘|get-help channel for further support.
|
& send it to #🆘|get-help channel for further
|
||||||
|
support.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex h-full w-full flex-col pt-4">
|
)}
|
||||||
<div className="border-y border-[hsla(var(--app-border))] px-4 py-2 ">
|
|
||||||
|
<div
|
||||||
|
className={twMerge('relative flex h-full w-full flex-col pt-4')}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'border-y border-[hsla(var(--app-border))] px-4 py-2'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<ul className="inline-flex space-x-2 rounded-lg px-1">
|
<ul className="inline-flex space-x-2 rounded-lg px-1">
|
||||||
{logOption.map((name, i) => {
|
{logOption.map((name, i) => {
|
||||||
return (
|
return (
|
||||||
@ -103,7 +122,20 @@ const ModalTroubleShooting = () => {
|
|||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[160px] overflow-y-auto">
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'max-h-[180px] overflow-y-auto',
|
||||||
|
showLogFullSize && 'max-h-[400px]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
theme="icon"
|
||||||
|
className="absolute right-4 top-20"
|
||||||
|
autoFocus={false}
|
||||||
|
onClick={() => setshowLogFullSize(!showLogFullSize)}
|
||||||
|
>
|
||||||
|
<Maximize2 size={14} />
|
||||||
|
</Button>
|
||||||
<div
|
<div
|
||||||
className={twMerge('hidden', isTabActive === 0 && 'block')}
|
className={twMerge('hidden', isTabActive === 0 && 'block')}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo, useEffect, useCallback } from 'react'
|
import { useState, useMemo, useEffect, useCallback, useRef } from 'react'
|
||||||
|
|
||||||
import { InferenceEngine } from '@janhq/core'
|
import { InferenceEngine } from '@janhq/core'
|
||||||
import { Badge, Input, ScrollArea, Select, useClickOutside } from '@janhq/joi'
|
import { Badge, Input, ScrollArea, Select, useClickOutside } from '@janhq/joi'
|
||||||
@ -70,7 +70,7 @@ const ModelDropdown = ({
|
|||||||
const downloadStates = useAtomValue(modelDownloadStateAtom)
|
const downloadStates = useAtomValue(modelDownloadStateAtom)
|
||||||
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||||
const { updateModelParameter } = useUpdateModelParameters()
|
const { updateModelParameter } = useUpdateModelParameters()
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||||
const configuredModels = useAtomValue(configuredModelsAtom)
|
const configuredModels = useAtomValue(configuredModelsAtom)
|
||||||
const featuredModel = configuredModels.filter((x) =>
|
const featuredModel = configuredModels.filter((x) =>
|
||||||
x.metadata.tags.includes('Featured')
|
x.metadata.tags.includes('Featured')
|
||||||
@ -83,7 +83,7 @@ const ModelDropdown = ({
|
|||||||
|
|
||||||
const filteredDownloadedModels = useMemo(
|
const filteredDownloadedModels = useMemo(
|
||||||
() =>
|
() =>
|
||||||
downloadedModels
|
configuredModels
|
||||||
.filter((e) =>
|
.filter((e) =>
|
||||||
e.name.toLowerCase().includes(searchText.toLowerCase().trim())
|
e.name.toLowerCase().includes(searchText.toLowerCase().trim())
|
||||||
)
|
)
|
||||||
@ -104,9 +104,30 @@ const ModelDropdown = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
[downloadedModels, searchText, searchFilter]
|
.sort((a, b) => {
|
||||||
|
const aInDownloadedModels = downloadedModels.some(
|
||||||
|
(item) => item.id === a.id
|
||||||
)
|
)
|
||||||
|
const bInDownloadedModels = downloadedModels.some(
|
||||||
|
(item) => item.id === b.id
|
||||||
|
)
|
||||||
|
if (aInDownloadedModels && !bInDownloadedModels) {
|
||||||
|
return -1
|
||||||
|
} else if (!aInDownloadedModels && bInDownloadedModels) {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[configuredModels, searchText, searchFilter, downloadedModels]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && searchInputRef.current) {
|
||||||
|
searchInputRef.current.focus()
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeThread) return
|
if (!activeThread) return
|
||||||
@ -258,6 +279,7 @@ const ModelDropdown = ({
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
value={searchText}
|
value={searchText}
|
||||||
|
ref={searchInputRef}
|
||||||
className="rounded-none border-x-0 border-t-0 focus-within:ring-0 hover:border-b-[hsla(var(--app-border))]"
|
className="rounded-none border-x-0 border-t-0 focus-within:ring-0 hover:border-b-[hsla(var(--app-border))]"
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
suffixIcon={
|
suffixIcon={
|
||||||
@ -290,45 +312,104 @@ const ModelDropdown = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea className="h-[calc(100%-36px)] w-full">
|
<ScrollArea className="h-[calc(100%-36px)] w-full">
|
||||||
{filteredDownloadedModels.filter(
|
|
||||||
(x) => x.engine === InferenceEngine.nitro
|
|
||||||
).length !== 0 ? (
|
|
||||||
<div className="relative w-full">
|
|
||||||
<div className="mt-2">
|
|
||||||
<h6 className="mb-1 mt-3 px-3 font-medium text-[hsla(var(--text-secondary))]">
|
|
||||||
Cortex
|
|
||||||
</h6>
|
|
||||||
<ul className="pb-2">
|
|
||||||
{filteredDownloadedModels
|
|
||||||
? filteredDownloadedModels
|
|
||||||
.filter((x) => x.engine === InferenceEngine.nitro)
|
|
||||||
.map((model) => {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={model.id}
|
|
||||||
className="flex cursor-pointer items-center gap-2 px-3 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
|
|
||||||
onClick={() => onClickModelItem(model.id)}
|
|
||||||
>
|
|
||||||
<p className="line-clamp-1" title={model.name}>
|
|
||||||
{model.name}
|
|
||||||
</p>
|
|
||||||
<ModelLabel metadata={model.metadata} compact />
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{searchFilter !== 'remote' && (
|
{searchFilter !== 'remote' && (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<h6 className="mb-1 mt-3 px-3 font-medium text-[hsla(var(--text-secondary))]">
|
<h6 className="mb-1 mt-3 px-3 font-medium text-[hsla(var(--text-secondary))]">
|
||||||
Cortex
|
Cortex
|
||||||
</h6>
|
</h6>
|
||||||
{searchText.length === 0 ? (
|
</div>
|
||||||
|
{filteredDownloadedModels
|
||||||
|
.filter((x) => {
|
||||||
|
if (searchText.length === 0) {
|
||||||
|
return downloadedModels.find((c) => c.id === x.id)
|
||||||
|
} else {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((x) => x.engine === InferenceEngine.nitro).length !==
|
||||||
|
0 ? (
|
||||||
|
<ul className="pb-2">
|
||||||
|
{filteredDownloadedModels
|
||||||
|
? filteredDownloadedModels
|
||||||
|
.filter((x) => x.engine === InferenceEngine.nitro)
|
||||||
|
.filter((x) => {
|
||||||
|
if (searchText.length === 0) {
|
||||||
|
return downloadedModels.find((c) => c.id === x.id)
|
||||||
|
} else {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map((model) => {
|
||||||
|
const isDownloading = downloadingModels.some(
|
||||||
|
(md) => md.id === model.id
|
||||||
|
)
|
||||||
|
const isdDownloaded = downloadedModels.some(
|
||||||
|
(c) => c.id === model.id
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={model.id}
|
||||||
|
className="flex items-center justify-between gap-4 px-3 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
|
||||||
|
onClick={() => {
|
||||||
|
if (isdDownloaded) {
|
||||||
|
onClickModelItem(model.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p
|
||||||
|
className={twMerge(
|
||||||
|
'line-clamp-1',
|
||||||
|
!isdDownloaded &&
|
||||||
|
'text-[hsla(var(--text-secondary))]'
|
||||||
|
)}
|
||||||
|
title={model.name}
|
||||||
|
>
|
||||||
|
{model.name}
|
||||||
|
</p>
|
||||||
|
<ModelLabel
|
||||||
|
metadata={model.metadata}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-[hsla(var(--text-tertiary))]">
|
||||||
|
{!isdDownloaded && (
|
||||||
|
<span className="font-medium">
|
||||||
|
{toGibibytes(model.metadata.size)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!isDownloading && !isdDownloaded ? (
|
||||||
|
<DownloadCloudIcon
|
||||||
|
size={18}
|
||||||
|
className="cursor-pointer text-[hsla(var(--app-link))]"
|
||||||
|
onClick={() => downloadModel(model)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
Object.values(downloadStates)
|
||||||
|
.filter((x) => x.modelId === model.id)
|
||||||
|
.map((item) => (
|
||||||
|
<ProgressCircle
|
||||||
|
key={item.modelId}
|
||||||
|
percentage={
|
||||||
|
formatDownloadPercentage(
|
||||||
|
item?.percent,
|
||||||
|
{
|
||||||
|
hidePercentage: true,
|
||||||
|
}
|
||||||
|
) as number
|
||||||
|
}
|
||||||
|
size={100}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
<ul className="pb-2">
|
<ul className="pb-2">
|
||||||
{featuredModel.map((model) => {
|
{featuredModel.map((model) => {
|
||||||
const isDownloading = downloadingModels.some(
|
const isDownloading = downloadingModels.some(
|
||||||
@ -346,10 +427,7 @@ const ModelDropdown = ({
|
|||||||
>
|
>
|
||||||
{model.name}
|
{model.name}
|
||||||
</p>
|
</p>
|
||||||
<ModelLabel
|
<ModelLabel metadata={model.metadata} compact />
|
||||||
metadata={model.metadata}
|
|
||||||
compact
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-[hsla(var(--text-tertiary))]">
|
<div className="flex items-center gap-2 text-[hsla(var(--text-tertiary))]">
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
@ -368,75 +446,9 @@ const ModelDropdown = ({
|
|||||||
<ProgressCircle
|
<ProgressCircle
|
||||||
key={item.modelId}
|
key={item.modelId}
|
||||||
percentage={
|
percentage={
|
||||||
formatDownloadPercentage(
|
formatDownloadPercentage(item?.percent, {
|
||||||
item?.percent,
|
|
||||||
{
|
|
||||||
hidePercentage: true,
|
hidePercentage: true,
|
||||||
}
|
}) as number
|
||||||
) as number
|
|
||||||
}
|
|
||||||
size={100}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<ul className="pb-2">
|
|
||||||
{configuredModels
|
|
||||||
.filter((x) => x.engine === InferenceEngine.nitro)
|
|
||||||
.filter((e) =>
|
|
||||||
e.name
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchText.toLowerCase().trim())
|
|
||||||
)
|
|
||||||
.map((model) => {
|
|
||||||
const isDownloading = downloadingModels.some(
|
|
||||||
(md) => md.id === model.id
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={model.id}
|
|
||||||
className="flex items-center justify-between gap-4 px-3 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p
|
|
||||||
className="line-clamp-1 text-[hsla(var(--text-secondary))]"
|
|
||||||
title={model.name}
|
|
||||||
>
|
|
||||||
{model.name}
|
|
||||||
</p>
|
|
||||||
<ModelLabel
|
|
||||||
metadata={model.metadata}
|
|
||||||
compact
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-[hsla(var(--text-tertiary))]">
|
|
||||||
<span className="font-medium">
|
|
||||||
{toGibibytes(model.metadata.size)}
|
|
||||||
</span>
|
|
||||||
{!isDownloading ? (
|
|
||||||
<DownloadCloudIcon
|
|
||||||
size={18}
|
|
||||||
className="cursor-pointer text-[hsla(var(--app-link))]"
|
|
||||||
onClick={() => downloadModel(model)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
Object.values(downloadStates)
|
|
||||||
.filter((x) => x.modelId === model.id)
|
|
||||||
.map((item) => (
|
|
||||||
<ProgressCircle
|
|
||||||
key={item.modelId}
|
|
||||||
percentage={
|
|
||||||
formatDownloadPercentage(
|
|
||||||
item?.percent,
|
|
||||||
{
|
|
||||||
hidePercentage: true,
|
|
||||||
}
|
|
||||||
) as number
|
|
||||||
}
|
}
|
||||||
size={100}
|
size={100}
|
||||||
/>
|
/>
|
||||||
@ -449,9 +461,6 @@ const ModelDropdown = ({
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{groupByEngine.map((engine, i) => {
|
{groupByEngine.map((engine, i) => {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export const janSettingScreenAtom = atom<SettingScreen[]>([])
|
|||||||
|
|
||||||
export const THEME = 'themeAppearance'
|
export const THEME = 'themeAppearance'
|
||||||
export const REDUCE_TRANSPARENT = 'reduceTransparent'
|
export const REDUCE_TRANSPARENT = 'reduceTransparent'
|
||||||
|
export const SPELL_CHECKING = 'spellChecking'
|
||||||
export const themesOptionsAtom = atom<{ name: string; value: string }[]>([])
|
export const themesOptionsAtom = atom<{ name: string; value: string }[]>([])
|
||||||
export const janThemesPathAtom = atom<string | undefined>(undefined)
|
export const janThemesPathAtom = atom<string | undefined>(undefined)
|
||||||
export const selectedThemeIdAtom = atomWithStorage<string>(THEME, '')
|
export const selectedThemeIdAtom = atomWithStorage<string>(THEME, '')
|
||||||
@ -18,3 +19,4 @@ export const reduceTransparentAtom = atomWithStorage<boolean>(
|
|||||||
REDUCE_TRANSPARENT,
|
REDUCE_TRANSPARENT,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
export const spellCheckAtom = atomWithStorage<boolean>(SPELL_CHECKING, true)
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
"lucide-react": "^0.291.0",
|
"lucide-react": "^0.291.0",
|
||||||
"marked": "^9.1.2",
|
"marked": "^9.1.2",
|
||||||
"marked-highlight": "^2.0.6",
|
"marked-highlight": "^2.0.6",
|
||||||
"marked-katex-extension": "^5.0.1",
|
"marked-katex-extension": "^5.0.2",
|
||||||
"next": "14.2.3",
|
"next": "14.2.3",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Fragment, useCallback, useState } from 'react'
|
import { Fragment, useCallback, useState } from 'react'
|
||||||
|
|
||||||
import { fs, AppConfiguration, isSubdirectory } from '@janhq/core'
|
import { AppConfiguration, isSubdirectory } from '@janhq/core'
|
||||||
import { Button, Input } from '@janhq/joi'
|
import { Button, Input } from '@janhq/joi'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { PencilIcon, FolderOpenIcon } from 'lucide-react'
|
import { PencilIcon, FolderOpenIcon } from 'lucide-react'
|
||||||
@ -51,11 +51,10 @@ const DataFolder = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDestChildren: string[] = await fs.readdirSync(destFolder)
|
const isEmpty: boolean =
|
||||||
const isNotEmpty =
|
await window.core?.api?.isDirectoryEmpty(destFolder)
|
||||||
newDestChildren.filter((x) => x !== '.DS_Store').length > 0
|
|
||||||
|
|
||||||
if (isNotEmpty) {
|
if (!isEmpty) {
|
||||||
setDestinationPath(destFolder)
|
setDestinationPath(destFolder)
|
||||||
showDestNotEmptyConfirm(true)
|
showDestNotEmptyConfirm(true)
|
||||||
return
|
return
|
||||||
@ -74,16 +73,7 @@ const DataFolder = () => {
|
|||||||
if (!destinationPath) return
|
if (!destinationPath) return
|
||||||
try {
|
try {
|
||||||
setShowLoader(true)
|
setShowLoader(true)
|
||||||
const appConfiguration: AppConfiguration =
|
await window.core?.api?.changeDataFolder(destinationPath)
|
||||||
await window.core?.api?.getAppConfigurations()
|
|
||||||
const currentJanDataFolder = appConfiguration.data_folder
|
|
||||||
appConfiguration.data_folder = destinationPath
|
|
||||||
const { err } = await fs.syncFile(currentJanDataFolder, destinationPath)
|
|
||||||
if (err) throw err
|
|
||||||
await window.core?.api?.updateAppConfiguration(appConfiguration)
|
|
||||||
console.debug(
|
|
||||||
`File sync finished from ${currentJanDataFolder} to ${destinationPath}`
|
|
||||||
)
|
|
||||||
localStorage.setItem(SUCCESS_SET_NEW_DESTINATION, 'true')
|
localStorage.setItem(SUCCESS_SET_NEW_DESTINATION, 'true')
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setShowLoader(false)
|
setShowLoader(false)
|
||||||
|
|||||||
@ -3,13 +3,14 @@ import { useCallback } from 'react'
|
|||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
import { fs, joinPath } from '@janhq/core'
|
import { fs, joinPath } from '@janhq/core'
|
||||||
import { Button, Select } from '@janhq/joi'
|
import { Button, Select, Switch } from '@janhq/joi'
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
janThemesPathAtom,
|
janThemesPathAtom,
|
||||||
reduceTransparentAtom,
|
reduceTransparentAtom,
|
||||||
selectedThemeIdAtom,
|
selectedThemeIdAtom,
|
||||||
|
spellCheckAtom,
|
||||||
themeDataAtom,
|
themeDataAtom,
|
||||||
themesOptionsAtom,
|
themesOptionsAtom,
|
||||||
} from '@/helpers/atoms/Setting.atom'
|
} from '@/helpers/atoms/Setting.atom'
|
||||||
@ -23,6 +24,7 @@ export default function AppearanceOptions() {
|
|||||||
const [reduceTransparent, setReduceTransparent] = useAtom(
|
const [reduceTransparent, setReduceTransparent] = useAtom(
|
||||||
reduceTransparentAtom
|
reduceTransparentAtom
|
||||||
)
|
)
|
||||||
|
const [spellCheck, setSpellCheck] = useAtom(spellCheckAtom)
|
||||||
|
|
||||||
const handleClickTheme = useCallback(
|
const handleClickTheme = useCallback(
|
||||||
async (e: string) => {
|
async (e: string) => {
|
||||||
@ -55,7 +57,7 @@ export default function AppearanceOptions() {
|
|||||||
<h6 className="font-semibold capitalize">Appearance</h6>
|
<h6 className="font-semibold capitalize">Appearance</h6>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||||
Select of customize your interface color scheme
|
Select a color theme
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
@ -70,9 +72,6 @@ export default function AppearanceOptions() {
|
|||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
<h6 className="font-semibold capitalize">Interface theme</h6>
|
<h6 className="font-semibold capitalize">Interface theme</h6>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
|
||||||
Choose the type of the interface
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<Button
|
<Button
|
||||||
@ -87,15 +86,29 @@ export default function AppearanceOptions() {
|
|||||||
variant={reduceTransparent ? 'outline' : 'solid'}
|
variant={reduceTransparent ? 'outline' : 'solid'}
|
||||||
onClick={() => setReduceTransparent(false)}
|
onClick={() => setReduceTransparent(false)}
|
||||||
>
|
>
|
||||||
Transparent
|
Transparency
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* <Switch
|
|
||||||
checked={reduceTransparent}
|
|
||||||
onChange={(e) => setReduceTransparent(e.target.checked)}
|
|
||||||
/> */}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
|
||||||
|
<div className="w-full space-y-1 lg:w-3/4">
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<h6 className="font-semibold capitalize">Spell checking</h6>
|
||||||
|
</div>
|
||||||
|
<p className=" font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||||
|
Disable if you prefer to type without spell checking interruptions
|
||||||
|
or if you are using non-standard language/terminology that the spell
|
||||||
|
checker may not recognize.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Switch
|
||||||
|
checked={spellCheck}
|
||||||
|
onChange={(e) => setSpellCheck(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,8 @@ const ExtensionItem: React.FC<Props> = ({ item }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const progress = isInstalling
|
const progress = isInstalling
|
||||||
? installingExtensions.find((e) => e.extensionId === item.name)
|
? (installingExtensions.find((e) => e.extensionId === item.name)
|
||||||
?.percentage ?? -1
|
?.percentage ?? -1)
|
||||||
: -1
|
: -1
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -158,7 +158,7 @@ const ExtensionCatalog = () => {
|
|||||||
{engineActiveExtensions.length !== 0 && (
|
{engineActiveExtensions.length !== 0 && (
|
||||||
<div className="mb-3 mt-4 border-b border-[hsla(var(--app-border))] pb-4">
|
<div className="mb-3 mt-4 border-b border-[hsla(var(--app-border))] pb-4">
|
||||||
<h6 className="text-base font-semibold text-[hsla(var(--text-primary))]">
|
<h6 className="text-base font-semibold text-[hsla(var(--text-primary))]">
|
||||||
Model Provider
|
Model Providers
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const SettingDetail = () => {
|
|||||||
case 'Appearance':
|
case 'Appearance':
|
||||||
return <AppearanceOptions />
|
return <AppearanceOptions />
|
||||||
|
|
||||||
case 'Hotkey & Shortcut':
|
case 'Keyboard Shortcuts':
|
||||||
return <Hotkeys />
|
return <Hotkeys />
|
||||||
|
|
||||||
case 'Advanced Settings':
|
case 'Advanced Settings':
|
||||||
|
|||||||
@ -89,7 +89,7 @@ const SettingLeftPanel = () => {
|
|||||||
).length > 0 && (
|
).length > 0 && (
|
||||||
<div className="mb-1 mt-4 px-2">
|
<div className="mb-1 mt-4 px-2">
|
||||||
<label className="text-xs font-medium text-[hsla(var(--text-secondary))]">
|
<label className="text-xs font-medium text-[hsla(var(--text-secondary))]">
|
||||||
Model Provider
|
Model Providers
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
|
|||||||
export const SettingScreenList = [
|
export const SettingScreenList = [
|
||||||
'My Models',
|
'My Models',
|
||||||
'Appearance',
|
'Appearance',
|
||||||
'Hotkey & Shortcut',
|
'Keyboard Shortcuts',
|
||||||
'Advanced Settings',
|
'Advanced Settings',
|
||||||
'Extensions',
|
'Extensions',
|
||||||
] as const
|
] as const
|
||||||
@ -24,6 +24,7 @@ export type SettingScreen = SettingScreenTuple[number]
|
|||||||
|
|
||||||
const SettingsScreen = () => {
|
const SettingsScreen = () => {
|
||||||
const setSelectedSettingScreen = useSetAtom(selectedSettingAtom)
|
const setSelectedSettingScreen = useSetAtom(selectedSettingAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
|
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
|
||||||
setSelectedSettingScreen('Advanced Settings')
|
setSelectedSettingScreen('Advanced Settings')
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import ImageUploadPreview from '../ImageUploadPreview'
|
|||||||
import { showRightPanelAtom } from '@/helpers/atoms/App.atom'
|
import { showRightPanelAtom } from '@/helpers/atoms/App.atom'
|
||||||
import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom'
|
import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
|
import { spellCheckAtom } from '@/helpers/atoms/Setting.atom'
|
||||||
import {
|
import {
|
||||||
activeThreadAtom,
|
activeThreadAtom,
|
||||||
getActiveThreadIdAtom,
|
getActiveThreadIdAtom,
|
||||||
@ -52,6 +53,7 @@ const ChatInput = () => {
|
|||||||
const { stateModel } = useActiveModel()
|
const { stateModel } = useActiveModel()
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
const [activeSetting, setActiveSetting] = useState(false)
|
const [activeSetting, setActiveSetting] = useState(false)
|
||||||
|
const spellCheck = useAtomValue(spellCheckAtom)
|
||||||
|
|
||||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
||||||
const { sendChatMessage } = useSendChatMessage()
|
const { sendChatMessage } = useSendChatMessage()
|
||||||
@ -162,6 +164,7 @@ const ChatInput = () => {
|
|||||||
experimentalFeature && 'pl-10',
|
experimentalFeature && 'pl-10',
|
||||||
activeSetting && 'pb-14 pr-16'
|
activeSetting && 'pb-14 pr-16'
|
||||||
)}
|
)}
|
||||||
|
spellCheck={spellCheck}
|
||||||
data-testid="txt-input-chat"
|
data-testid="txt-input-chat"
|
||||||
style={{ height: activeSetting ? '100px' : '40px' }}
|
style={{ height: activeSetting ? '100px' : '40px' }}
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
getCurrentChatMessagesAtom,
|
getCurrentChatMessagesAtom,
|
||||||
setConvoMessagesAtom,
|
setConvoMessagesAtom,
|
||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
|
import { spellCheckAtom } from '@/helpers/atoms/Setting.atom'
|
||||||
import {
|
import {
|
||||||
activeThreadAtom,
|
activeThreadAtom,
|
||||||
getActiveThreadIdAtom,
|
getActiveThreadIdAtom,
|
||||||
@ -45,7 +46,7 @@ const EditChatInput: React.FC<Props> = ({ message }) => {
|
|||||||
const { sendChatMessage } = useSendChatMessage()
|
const { sendChatMessage } = useSendChatMessage()
|
||||||
const setMessages = useSetAtom(setConvoMessagesAtom)
|
const setMessages = useSetAtom(setConvoMessagesAtom)
|
||||||
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
|
const spellCheck = useAtomValue(spellCheckAtom)
|
||||||
const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
|
const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const setEditMessage = useSetAtom(editMessageAtom)
|
const setEditMessage = useSetAtom(editMessageAtom)
|
||||||
@ -127,6 +128,7 @@ const EditChatInput: React.FC<Props> = ({ message }) => {
|
|||||||
className={twMerge('max-h-[400px] resize-none pr-20')}
|
className={twMerge('max-h-[400px] resize-none pr-20')}
|
||||||
style={{ height: '40px' }}
|
style={{ height: '40px' }}
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
|
spellCheck={spellCheck}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
placeholder="Enter your message..."
|
placeholder="Enter your message..."
|
||||||
disabled={stateModel.loading || !activeThread}
|
disabled={stateModel.loading || !activeThread}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
ContentType,
|
ContentType,
|
||||||
Thread,
|
Thread,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
|
import { Tooltip } from '@janhq/joi'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import {
|
import {
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
@ -103,10 +104,15 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
|||||||
className="cursor-pointer rounded-lg border border-[hsla(var(--app-border))] p-2"
|
className="cursor-pointer rounded-lg border border-[hsla(var(--app-border))] p-2"
|
||||||
onClick={onEditClick}
|
onClick={onEditClick}
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
trigger={
|
||||||
<PencilIcon
|
<PencilIcon
|
||||||
size={14}
|
size={14}
|
||||||
className="text-[hsla(var(--text-secondary))]"
|
className="text-[hsla(var(--text-secondary))]"
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
content="Edit"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -118,10 +124,15 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
|||||||
className="cursor-pointer rounded-lg border border-[hsla(var(--app-border))] p-2"
|
className="cursor-pointer rounded-lg border border-[hsla(var(--app-border))] p-2"
|
||||||
onClick={onRegenerateClick}
|
onClick={onRegenerateClick}
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
trigger={
|
||||||
<RefreshCcw
|
<RefreshCcw
|
||||||
size={14}
|
size={14}
|
||||||
className="text-[hsla(var(--text-secondary))]"
|
className="text-[hsla(var(--text-secondary))]"
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
content="Regenerate"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -134,20 +145,30 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
|||||||
{clipboard.copied ? (
|
{clipboard.copied ? (
|
||||||
<CheckIcon size={14} className="text-[hsla(var(--success-bg))]" />
|
<CheckIcon size={14} className="text-[hsla(var(--success-bg))]" />
|
||||||
) : (
|
) : (
|
||||||
|
<Tooltip
|
||||||
|
trigger={
|
||||||
<CopyIcon
|
<CopyIcon
|
||||||
size={14}
|
size={14}
|
||||||
className="text-[hsla(var(--text-secondary))]"
|
className="text-[hsla(var(--text-secondary))]"
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
content="Copy"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer rounded-lg border border-[hsla(var(--app-border))] p-2"
|
className="cursor-pointer rounded-lg border border-[hsla(var(--app-border))] p-2"
|
||||||
onClick={onDeleteClick}
|
onClick={onDeleteClick}
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
trigger={
|
||||||
<Trash2Icon
|
<Trash2Icon
|
||||||
size={14}
|
size={14}
|
||||||
className="text-[hsla(var(--text-secondary))]"
|
className="text-[hsla(var(--text-secondary))]"
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
content="Delete"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -178,7 +178,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
>
|
>
|
||||||
{isUser
|
{isUser
|
||||||
? props.role
|
? props.role
|
||||||
: activeThread?.assistants[0].assistant_name ?? props.role}
|
: (activeThread?.assistants[0].assistant_name ?? props.role)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs font-medium text-gray-400">
|
<p className="text-xs font-medium text-gray-400">
|
||||||
{displayDate(props.created)}
|
{displayDate(props.created)}
|
||||||
@ -201,7 +201,12 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={twMerge('w-full')}>
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'w-full',
|
||||||
|
!isUser && !text.includes(' ') && 'break-all'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
{props.content[0]?.type === ContentType.Image && (
|
{props.content[0]?.type === ContentType.Image && (
|
||||||
<div className="group/image relative mb-2 inline-flex cursor-pointer overflow-hidden rounded-xl">
|
<div className="group/image relative mb-2 inline-flex cursor-pointer overflow-hidden rounded-xl">
|
||||||
|
|||||||
@ -7,9 +7,10 @@ import useDeleteThread from '@/hooks/useDeleteThread'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
threadId: string
|
threadId: string
|
||||||
|
closeContextMenu?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalCleanThread = ({ threadId }: Props) => {
|
const ModalCleanThread = ({ threadId, closeContextMenu }: Props) => {
|
||||||
const { cleanThread } = useDeleteThread()
|
const { cleanThread } = useDeleteThread()
|
||||||
const onCleanThreadClick = useCallback(
|
const onCleanThreadClick = useCallback(
|
||||||
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
@ -22,6 +23,11 @@ const ModalCleanThread = ({ threadId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Clean Thread"
|
title="Clean Thread"
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (open && closeContextMenu) {
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
}}
|
||||||
trigger={
|
trigger={
|
||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
|
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import useDeleteThread from '@/hooks/useDeleteThread'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
threadId: string
|
threadId: string
|
||||||
|
closeContextMenu?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalDeleteThread = ({ threadId }: Props) => {
|
const ModalDeleteThread = ({ threadId, closeContextMenu }: Props) => {
|
||||||
const { deleteThread } = useDeleteThread()
|
const { deleteThread } = useDeleteThread()
|
||||||
|
|
||||||
const onDeleteThreadClick = useCallback(
|
const onDeleteThreadClick = useCallback(
|
||||||
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@ -22,6 +24,11 @@ const ModalDeleteThread = ({ threadId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Delete Thread"
|
title="Delete Thread"
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (open && closeContextMenu) {
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
}}
|
||||||
trigger={
|
trigger={
|
||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
|
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, memo, useState } from 'react'
|
import { useCallback, useLayoutEffect, memo, useState } from 'react'
|
||||||
|
|
||||||
import { Thread } from '@janhq/core'
|
import { Thread } from '@janhq/core'
|
||||||
import { Modal, ModalClose, Button, Input } from '@janhq/joi'
|
import { Modal, ModalClose, Button, Input } from '@janhq/joi'
|
||||||
@ -8,13 +8,19 @@ import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
thread: Thread
|
thread: Thread
|
||||||
|
closeContextMenu?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalEditTitleThread = ({ thread }: Props) => {
|
const ModalEditTitleThread = ({ thread, closeContextMenu }: Props) => {
|
||||||
const [title, setTitle] = useState(thread.title)
|
const [title, setTitle] = useState(thread.title)
|
||||||
|
|
||||||
const { updateThreadMetadata } = useCreateNewThread()
|
const { updateThreadMetadata } = useCreateNewThread()
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (thread.title) {
|
||||||
|
setTitle(thread.title)
|
||||||
|
}
|
||||||
|
}, [thread.title])
|
||||||
|
|
||||||
const onUpdateTitle = useCallback(
|
const onUpdateTitle = useCallback(
|
||||||
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@ -30,6 +36,11 @@ const ModalEditTitleThread = ({ thread }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Edit title thread"
|
title="Edit title thread"
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (open && closeContextMenu) {
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
}}
|
||||||
trigger={
|
trigger={
|
||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
|
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { Thread } from '@janhq/core'
|
import { Thread } from '@janhq/core'
|
||||||
|
|
||||||
@ -43,6 +43,14 @@ const ThreadLeftPanel = () => {
|
|||||||
const setEditMessage = useSetAtom(editMessageAtom)
|
const setEditMessage = useSetAtom(editMessageAtom)
|
||||||
const { recommendedModel, downloadedModels } = useRecommendedModel()
|
const { recommendedModel, downloadedModels } = useRecommendedModel()
|
||||||
|
|
||||||
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
|
visible: boolean
|
||||||
|
thread?: Thread
|
||||||
|
}>({
|
||||||
|
visible: false,
|
||||||
|
thread: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
const onThreadClick = useCallback(
|
const onThreadClick = useCallback(
|
||||||
(thread: Thread) => {
|
(thread: Thread) => {
|
||||||
setActiveThread(thread)
|
setActiveThread(thread)
|
||||||
@ -91,6 +99,21 @@ const ThreadLeftPanel = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onContextMenu = (event: React.MouseEvent, thread: Thread) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setContextMenu({
|
||||||
|
visible: true,
|
||||||
|
thread,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeContextMenu = () => {
|
||||||
|
setContextMenu({
|
||||||
|
visible: false,
|
||||||
|
thread: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LeftPanelContainer>
|
<LeftPanelContainer>
|
||||||
{threads.length === 0 ? (
|
{threads.length === 0 ? (
|
||||||
@ -124,8 +147,10 @@ const ThreadLeftPanel = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onThreadClick(thread)
|
onThreadClick(thread)
|
||||||
}}
|
}}
|
||||||
|
onContextMenu={(e) => onContextMenu(e, thread)}
|
||||||
|
onMouseLeave={closeContextMenu}
|
||||||
>
|
>
|
||||||
<div className="relative z-10 p-2">
|
<div className="relative z-10 break-all p-2">
|
||||||
<h1
|
<h1
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'line-clamp-1 pr-2 font-medium group-hover/message:pr-6',
|
'line-clamp-1 pr-2 font-medium group-hover/message:pr-6',
|
||||||
@ -143,10 +168,26 @@ const ThreadLeftPanel = () => {
|
|||||||
<Button theme="icon" className="mt-2">
|
<Button theme="icon" className="mt-2">
|
||||||
<MoreHorizontalIcon />
|
<MoreHorizontalIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="invisible absolute -right-1 z-50 w-40 overflow-hidden rounded-lg border border-[hsla(var(--app-border))] bg-[hsla(var(--app-bg))] shadow-lg group-hover/icon:visible">
|
<div
|
||||||
<ModalEditTitleThread thread={thread} />
|
className={twMerge(
|
||||||
<ModalCleanThread threadId={thread.id} />
|
'invisible absolute -right-1 z-50 w-40 overflow-hidden rounded-lg border border-[hsla(var(--app-border))] bg-[hsla(var(--app-bg))] shadow-lg group-hover/icon:visible',
|
||||||
<ModalDeleteThread threadId={thread.id} />
|
contextMenu.visible &&
|
||||||
|
contextMenu.thread?.id === thread.id &&
|
||||||
|
'visible'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ModalEditTitleThread
|
||||||
|
thread={thread}
|
||||||
|
closeContextMenu={closeContextMenu}
|
||||||
|
/>
|
||||||
|
<ModalCleanThread
|
||||||
|
threadId={thread.id}
|
||||||
|
closeContextMenu={closeContextMenu}
|
||||||
|
/>
|
||||||
|
<ModalDeleteThread
|
||||||
|
threadId={thread.id}
|
||||||
|
closeContextMenu={closeContextMenu}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{activeThreadId === thread.id && (
|
{activeThreadId === thread.id && (
|
||||||
|
|||||||
@ -66,6 +66,32 @@ const Tools = () => {
|
|||||||
[activeThread, updateThreadMetadata]
|
[activeThread, updateThreadMetadata]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onTimeWeightedRetrieverSwitchUpdate = useCallback(
|
||||||
|
(enabled: boolean) => {
|
||||||
|
if (!activeThread) return
|
||||||
|
updateThreadMetadata({
|
||||||
|
...activeThread,
|
||||||
|
assistants: [
|
||||||
|
{
|
||||||
|
...activeThread.assistants[0],
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
type: 'retrieval',
|
||||||
|
enabled: true,
|
||||||
|
useTimeWeightedRetriever: enabled,
|
||||||
|
settings:
|
||||||
|
(activeThread.assistants[0].tools &&
|
||||||
|
activeThread.assistants[0].tools[0]?.settings) ??
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[activeThread, updateThreadMetadata]
|
||||||
|
)
|
||||||
|
|
||||||
if (!experimentalFeature) return null
|
if (!experimentalFeature) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -140,10 +166,9 @@ const Tools = () => {
|
|||||||
<div className="mb-2 flex items-center">
|
<div className="mb-2 flex items-center">
|
||||||
<label
|
<label
|
||||||
id="vector-database"
|
id="vector-database"
|
||||||
className="inline-block font-medium"
|
className="inline-flex items-center font-medium"
|
||||||
>
|
>
|
||||||
Vector Database
|
Vector Database
|
||||||
</label>
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
trigger={
|
trigger={
|
||||||
<InfoIcon
|
<InfoIcon
|
||||||
@ -157,12 +182,49 @@ const Tools = () => {
|
|||||||
requirements. Experiment to find the best fit for
|
requirements. Experiment to find the best fit for
|
||||||
your specific use case."
|
your specific use case."
|
||||||
/>
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="ml-auto flex items-center justify-between">
|
||||||
|
<Switch
|
||||||
|
name="use-time-weighted-retriever"
|
||||||
|
className="mr-2"
|
||||||
|
checked={
|
||||||
|
activeThread?.assistants[0].tools[0]
|
||||||
|
.useTimeWeightedRetriever || false
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
onTimeWeightedRetrieverSwitchUpdate(e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Input value="HNSWLib" disabled readOnly />
|
<Input value="HNSWLib" disabled readOnly />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="mb-2 flex items-center">
|
||||||
|
<label
|
||||||
|
id="use-time-weighted-retriever"
|
||||||
|
className="inline-block font-medium"
|
||||||
|
>
|
||||||
|
Time-Weighted Retrieval?
|
||||||
|
</label>
|
||||||
|
<Tooltip
|
||||||
|
trigger={
|
||||||
|
<InfoIcon
|
||||||
|
size={16}
|
||||||
|
className="ml-2 flex-shrink-0 text-[hsl(var(--text-secondary))]"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
content="Time-Weighted Retriever looks at how similar
|
||||||
|
they are and how new they are. It compares
|
||||||
|
documents based on their meaning like usual, but
|
||||||
|
also considers when they were added to give
|
||||||
|
newer ones more importance."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<AssistantSetting
|
<AssistantSetting
|
||||||
componentData={componentDataAssistantSetting}
|
componentData={componentDataAssistantSetting}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user