commit
506cbb8834
@ -55,20 +55,6 @@ const unlinkSync = (...args: any[]) => globalThis.core.api?.unlinkSync(...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) =>
|
||||
globalThis.core.api?.copyFile(src, dest)
|
||||
|
||||
@ -95,9 +81,7 @@ export const fs = {
|
||||
rm,
|
||||
unlinkSync,
|
||||
appendFileSync,
|
||||
copyFileSync,
|
||||
copyFile,
|
||||
syncFile,
|
||||
fileStat,
|
||||
writeBlob,
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { resolve, sep } from 'path'
|
||||
import { DownloadEvent } from '../../../types/api'
|
||||
import { normalizeFilePath } from '../../helper/path'
|
||||
import { normalizeFilePath, validatePath } from '../../helper/path'
|
||||
import { getJanDataFolderPath } from '../../helper'
|
||||
import { DownloadManager } from '../../helper/download'
|
||||
import { createWriteStream, renameSync } from 'fs'
|
||||
@ -37,6 +37,7 @@ export class Downloader implements Processor {
|
||||
const modelId = array.pop() ?? ''
|
||||
|
||||
const destination = resolve(getJanDataFolderPath(), normalizedPath)
|
||||
validatePath(destination)
|
||||
const rq = request({ url, strictSSL, proxy })
|
||||
|
||||
// Put request to download manager instance
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { join } from 'path'
|
||||
import { normalizeFilePath } from '../../helper/path'
|
||||
import { join, resolve } from 'path'
|
||||
import { normalizeFilePath, validatePath } from '../../helper/path'
|
||||
import { getJanDataFolderPath } from '../../helper'
|
||||
import { Processor } from './Processor'
|
||||
import fs from 'fs'
|
||||
@ -15,17 +15,29 @@ export class FileSystem implements Processor {
|
||||
process(route: string, ...args: any): any {
|
||||
const instance = this as any
|
||||
const func = instance[route]
|
||||
|
||||
if (func) {
|
||||
return func(...args)
|
||||
} else {
|
||||
return import(FileSystem.moduleName).then((mdl) =>
|
||||
mdl[route](
|
||||
...args.map((arg: any) => {
|
||||
return typeof arg === 'string' &&
|
||||
(arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
|
||||
? join(getJanDataFolderPath(), normalizeFilePath(arg))
|
||||
: arg
|
||||
...args.map((arg: any, index: number) => {
|
||||
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:\\`))
|
||||
? join(getJanDataFolderPath(), normalizeFilePath(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))
|
||||
}
|
||||
|
||||
const absolutePath = resolve(path)
|
||||
validatePath(absolutePath)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.rm(path, { recursive: true, force: true }, (err) => {
|
||||
fs.rm(absolutePath, { recursive: true, force: true }, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
@ -63,8 +78,11 @@ export class FileSystem implements Processor {
|
||||
path = join(getJanDataFolderPath(), normalizeFilePath(path))
|
||||
}
|
||||
|
||||
const absolutePath = resolve(path)
|
||||
validatePath(absolutePath)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.mkdir(path, { recursive: true }, (err) => {
|
||||
fs.mkdir(absolutePath, { recursive: true }, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
@ -73,4 +91,5 @@ export class FileSystem implements Processor {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { join } from 'path'
|
||||
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 { Processor } from './Processor'
|
||||
import { FileStat } from '../../../types'
|
||||
@ -18,19 +18,6 @@ export class FSExt implements Processor {
|
||||
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.
|
||||
getJanDataFolderPath() {
|
||||
return Promise.resolve(getPath())
|
||||
@ -70,14 +57,18 @@ export class FSExt implements Processor {
|
||||
writeBlob(path: string, data: any) {
|
||||
try {
|
||||
const normalizedPath = normalizeFilePath(path)
|
||||
|
||||
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) {
|
||||
console.error(`writeFile ${path} result: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
copyFile(src: string, dest: string): Promise<void> {
|
||||
validatePath(dest)
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.copyFile(src, dest, (err) => {
|
||||
if (err) {
|
||||
|
||||
@ -7,7 +7,8 @@ import childProcess from 'child_process'
|
||||
const configurationFileName = 'settings.json'
|
||||
|
||||
// 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 = {
|
||||
data_folder: defaultJanDataFolder,
|
||||
quick_ask: false,
|
||||
|
||||
@ -11,7 +11,7 @@ export class DownloadManager {
|
||||
// store the download information with key is model id
|
||||
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> = {}
|
||||
|
||||
constructor() {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { join } from 'path'
|
||||
import { join, resolve } from 'path'
|
||||
import { getJanDataFolderPath } from './config'
|
||||
|
||||
/**
|
||||
* Normalize file path
|
||||
@ -33,3 +34,11 @@ export async function appResourcePath(): Promise<string> {
|
||||
// server
|
||||
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 {
|
||||
appendFileSync = 'appendFileSync',
|
||||
copyFileSync = 'copyFileSync',
|
||||
unlinkSync = 'unlinkSync',
|
||||
existsSync = 'existsSync',
|
||||
readdirSync = 'readdirSync',
|
||||
@ -100,7 +99,6 @@ export enum FileSystemRoute {
|
||||
writeFileSync = 'writeFileSync',
|
||||
}
|
||||
export enum FileManagerRoute {
|
||||
syncFile = 'syncFile',
|
||||
copyFile = 'copyFile',
|
||||
getJanDataFolderPath = 'getJanDataFolderPath',
|
||||
getResourcePath = 'getResourcePath',
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
export type AssistantTool = {
|
||||
type: string
|
||||
enabled: boolean
|
||||
useTimeWeightedRetriever?: boolean
|
||||
settings: any
|
||||
}
|
||||
|
||||
|
||||
@ -83,6 +83,8 @@ export enum MessageStatus {
|
||||
export enum ErrorCode {
|
||||
InvalidApiKey = 'invalid_api_key',
|
||||
|
||||
AuthenticationError = 'authentication_error',
|
||||
|
||||
InsufficientQuota = 'insufficient_quota',
|
||||
|
||||
InvalidRequestError = 'invalid_request_error',
|
||||
|
||||
@ -3,8 +3,9 @@
|
||||
* @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 { readdirSync } from 'fs'
|
||||
|
||||
const interfaces: { [key: string]: (...args: any[]) => any } = {}
|
||||
|
||||
@ -12,7 +13,9 @@ const interfaces: { [key: string]: (...args: any[]) => any } = {}
|
||||
APIRoutes.forEach((method) => {
|
||||
// For each method, create a function on the interfaces object
|
||||
// This function invokes the method on the ipcRenderer with any provided arguments
|
||||
|
||||
interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args)
|
||||
|
||||
})
|
||||
|
||||
// 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
|
||||
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'
|
||||
// This allows the renderer process to access these methods directly
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
|
||||
@ -126,6 +126,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
{
|
||||
type: 'retrieval',
|
||||
enabled: false,
|
||||
useTimeWeightedRetriever: false,
|
||||
settings: {
|
||||
top_k: 2,
|
||||
chunk_size: 1024,
|
||||
|
||||
@ -11,13 +11,14 @@ export function toolRetrievalUpdateTextSplitter(
|
||||
export async function toolRetrievalIngestNewDocument(
|
||||
file: string,
|
||||
model: string,
|
||||
engine: string
|
||||
engine: string,
|
||||
useTimeWeighted: boolean
|
||||
) {
|
||||
const filePath = path.join(getJanDataFolderPath(), normalizeFilePath(file))
|
||||
const threadPath = path.dirname(filePath.replace('files', ''))
|
||||
retrieval.updateEmbeddingEngine(model, engine)
|
||||
return retrieval
|
||||
.ingestAgentKnowledge(filePath, `${threadPath}/memory`)
|
||||
.ingestAgentKnowledge(filePath, `${threadPath}/memory`, useTimeWeighted)
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
@ -33,8 +34,11 @@ export async function toolRetrievalLoadThreadMemory(threadId: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function toolRetrievalQueryResult(query: string) {
|
||||
return retrieval.generateResult(query).catch((err) => {
|
||||
export async function toolRetrievalQueryResult(
|
||||
query: string,
|
||||
useTimeWeighted: boolean = false
|
||||
) {
|
||||
return retrieval.generateResult(query, useTimeWeighted).catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
@ -2,11 +2,16 @@ import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'
|
||||
import { formatDocumentsAsString } from 'langchain/util/document'
|
||||
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 { OpenAIEmbeddings } from 'langchain/embeddings/openai'
|
||||
import { readEmbeddingEngine } from './engine'
|
||||
|
||||
import path from 'path'
|
||||
|
||||
export class Retrieval {
|
||||
public chunkSize: number = 100
|
||||
public chunkOverlap?: number = 0
|
||||
@ -15,8 +20,25 @@ export class Retrieval {
|
||||
private embeddingModel?: OpenAIEmbeddings = undefined
|
||||
private textSplitter?: RecursiveCharacterTextSplitter
|
||||
|
||||
// to support time-weighted retrieval
|
||||
private timeWeightedVectorStore: MemoryVectorStore
|
||||
private timeWeightedretriever: any | TimeWeightedVectorStoreRetriever
|
||||
|
||||
constructor(chunkSize: number = 4000, chunkOverlap: number = 200) {
|
||||
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 {
|
||||
@ -44,11 +66,15 @@ export class Retrieval {
|
||||
openAIApiKey: settings.api_key,
|
||||
})
|
||||
}
|
||||
|
||||
// update time-weighted embedding model
|
||||
this.timeWeightedVectorStore.embeddings = this.embeddingModel
|
||||
}
|
||||
|
||||
public ingestAgentKnowledge = async (
|
||||
filePath: string,
|
||||
memoryPath: string
|
||||
memoryPath: string,
|
||||
useTimeWeighted: boolean
|
||||
): Promise<any> => {
|
||||
const loader = new PDFLoader(filePath, {
|
||||
splitPages: true,
|
||||
@ -57,6 +83,13 @@ export class Retrieval {
|
||||
const doc = await loader.load()
|
||||
const docs = await this.textSplitter!.splitDocuments(doc)
|
||||
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)
|
||||
}
|
||||
|
||||
@ -67,10 +100,25 @@ export class Retrieval {
|
||||
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) {
|
||||
return Promise.resolve(' ')
|
||||
}
|
||||
|
||||
// should use invoke(query) because getRelevantDocuments is deprecated
|
||||
const relevantDocs = await this.retriever.getRelevantDocuments(query)
|
||||
const serializedDoc = formatDocumentsAsString(relevantDocs)
|
||||
return Promise.resolve(serializedDoc)
|
||||
|
||||
@ -37,7 +37,8 @@ export class RetrievalTool extends InferenceTool {
|
||||
'toolRetrievalIngestNewDocument',
|
||||
docFile,
|
||||
data.model?.id,
|
||||
data.model?.engine
|
||||
data.model?.engine,
|
||||
tool?.useTimeWeightedRetriever ?? false
|
||||
)
|
||||
} else {
|
||||
return Promise.resolve(data)
|
||||
@ -78,7 +79,8 @@ export class RetrievalTool extends InferenceTool {
|
||||
const retrievalResult = await executeOnMain(
|
||||
NODE,
|
||||
'toolRetrievalQueryResult',
|
||||
prompt
|
||||
prompt,
|
||||
tool?.useTimeWeightedRetriever ?? false
|
||||
)
|
||||
console.debug('toolRetrievalQueryResult', retrievalResult)
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@janhq/inference-anthropic-extension",
|
||||
"productName": "Anthropic Inference Engine",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"description": "This extension enables Anthropic chat completion API calls",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/module.js",
|
||||
|
||||
@ -19,10 +19,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"author": "Anthropic",
|
||||
"tags": [
|
||||
"General",
|
||||
"Big Context Length"
|
||||
]
|
||||
"tags": ["General", "Big Context Length"]
|
||||
},
|
||||
"engine": "anthropic"
|
||||
},
|
||||
@ -46,10 +43,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"author": "Anthropic",
|
||||
"tags": [
|
||||
"General",
|
||||
"Big Context Length"
|
||||
]
|
||||
"tags": ["General", "Big Context Length"]
|
||||
},
|
||||
"engine": "anthropic"
|
||||
},
|
||||
@ -73,10 +67,31 @@
|
||||
},
|
||||
"metadata": {
|
||||
"author": "Anthropic",
|
||||
"tags": [
|
||||
"General",
|
||||
"Big Context Length"
|
||||
]
|
||||
"tags": ["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"
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
0.4.13
|
||||
0.4.20
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@janhq/inference-cortex-extension",
|
||||
"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.",
|
||||
"main": "dist/index.js",
|
||||
"node": "dist/node/index.cjs.js",
|
||||
@ -10,7 +10,7 @@
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"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:win32": "download.bat",
|
||||
"downloadnitro": "run-script-os",
|
||||
@ -53,7 +53,7 @@
|
||||
"fetch-retry": "^5.0.6",
|
||||
"rxjs": "^7.8.1",
|
||||
"tcp-port-used": "^1.0.2",
|
||||
"terminate": "^2.6.1",
|
||||
"terminate": "2.6.1",
|
||||
"ulidx": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"id": "gemma-2b",
|
||||
"object": "model",
|
||||
"name": "Gemma 2B Q4",
|
||||
"version": "1.2",
|
||||
"version": "1.3",
|
||||
"description": "Gemma is built from the same technology with Google's Gemini.",
|
||||
"format": "gguf",
|
||||
"settings": {
|
||||
@ -22,7 +22,7 @@
|
||||
"top_p": 0.95,
|
||||
"stream": true,
|
||||
"max_tokens": 8192,
|
||||
"stop": [],
|
||||
"stop": ["<end_of_turn>"],
|
||||
"frequency_penalty": 0,
|
||||
"presence_penalty": 0
|
||||
},
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@janhq/model-extension",
|
||||
"productName": "Model Management",
|
||||
"version": "1.0.32",
|
||||
"version": "1.0.33",
|
||||
"description": "Model Management Extension provides model exploration and seamless downloads",
|
||||
"main": "dist/index.js",
|
||||
"node": "dist/node/index.cjs.js",
|
||||
|
||||
@ -383,7 +383,12 @@ export default class JanModelExtension extends ModelExtension {
|
||||
|
||||
// model binaries (sources) are absolute path & exist
|
||||
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
|
||||
|
||||
|
||||
@ -41,12 +41,13 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
|
||||
case ErrorCode.Unknown:
|
||||
return 'Apologies, something’s amiss!'
|
||||
case ErrorCode.InvalidApiKey:
|
||||
case ErrorCode.AuthenticationError:
|
||||
case ErrorCode.InvalidRequestError:
|
||||
return (
|
||||
<span data-testid="invalid-API-key-error">
|
||||
Invalid API key. Please check your API key from{' '}
|
||||
<button
|
||||
className="font-medium text-[hsla(var(--app-link))]"
|
||||
className="font-medium text-[hsla(var(--app-link))] underline"
|
||||
onClick={() => {
|
||||
setMainState(MainViewState.Settings)
|
||||
|
||||
@ -102,7 +103,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
|
||||
<p>
|
||||
Jan’s in beta. Access
|
||||
<span
|
||||
className="cursor-pointer text-[hsla(var(--app-link))]"
|
||||
className="cursor-pointer text-[hsla(var(--app-link))] underline"
|
||||
onClick={() => setModalTroubleShooting(true)}
|
||||
>
|
||||
troubleshooting assistance
|
||||
|
||||
@ -3,12 +3,20 @@ import { Fragment, useEffect, useState } from 'react'
|
||||
import { Progress } from '@janhq/joi'
|
||||
import { useClickOutside } from '@janhq/joi'
|
||||
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 useGetSystemResources from '@/hooks/useGetSystemResources'
|
||||
|
||||
import { usePath } from '@/hooks/usePath'
|
||||
|
||||
import { toGibibytes } from '@/utils/converter'
|
||||
|
||||
import TableActiveModel from './TableActiveModel'
|
||||
@ -28,6 +36,7 @@ const SystemMonitor = () => {
|
||||
const usedRam = useAtomValue(usedRamAtom)
|
||||
const cpuUsage = useAtomValue(cpuUsageAtom)
|
||||
const gpus = useAtomValue(gpusAtom)
|
||||
const { onRevealInFinder } = usePath()
|
||||
const [showFullScreen, setShowFullScreen] = useState(false)
|
||||
const ramUtilitized = useAtomValue(ramUtilitizedAtom)
|
||||
const [showSystemMonitorPanel, setShowSystemMonitorPanel] = useAtom(
|
||||
@ -64,7 +73,7 @@ const SystemMonitor = () => {
|
||||
<div
|
||||
ref={setControl}
|
||||
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))]'
|
||||
)}
|
||||
onClick={() => {
|
||||
@ -89,6 +98,12 @@ const SystemMonitor = () => {
|
||||
Running Models
|
||||
</h6>
|
||||
<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 ? (
|
||||
<ChevronDown
|
||||
size={20}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Modal } from '@janhq/joi'
|
||||
import { Button, Modal } from '@janhq/joi'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { Maximize2 } from 'lucide-react'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import ServerLogs from '@/containers/ServerLogs'
|
||||
@ -17,6 +18,7 @@ const ModalTroubleShooting = () => {
|
||||
modalTroubleShootingAtom
|
||||
)
|
||||
const [isTabActive, setIsTabActivbe] = useState(0)
|
||||
const [showLogFullSize, setshowLogFullSize] = useState(false)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -26,59 +28,76 @@ const ModalTroubleShooting = () => {
|
||||
title="Troubleshooting Assistance"
|
||||
content={
|
||||
<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">
|
||||
{`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:`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="my-3 rounded-lg border border-[hsla(var(--app-border))] p-4 shadow">
|
||||
<h2 className="font-semibold">Step 1</h2>
|
||||
<p className="text-[hsla(var(--text-secondary)] mt-1">
|
||||
Follow our
|
||||
<a
|
||||
href="https://jan.ai/guides/troubleshooting"
|
||||
target="_blank"
|
||||
className="text-[hsla(var(--app-link))] hover:underline"
|
||||
>
|
||||
troubleshooting guide
|
||||
</a>
|
||||
for step-by-step solutions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-[hsla(var(--app-border))] pb-2 pt-4 shadow">
|
||||
<div className="px-4">
|
||||
<h2 className="font-semibold">Step 2</h2>
|
||||
{!showLogFullSize && (
|
||||
<div className="mb-3 rounded-lg border border-[hsla(var(--app-border))] p-4 shadow">
|
||||
<h2 className="font-semibold">Step 1</h2>
|
||||
<p className="text-[hsla(var(--text-secondary)] mt-1">
|
||||
{`If you can't find what you need in our troubleshooting guide, feel
|
||||
free reach out to us for extra help:`}
|
||||
Follow our
|
||||
<a
|
||||
href="https://jan.ai/guides/troubleshooting"
|
||||
target="_blank"
|
||||
className="text-[hsla(var(--app-link))] hover:underline"
|
||||
>
|
||||
troubleshooting guide
|
||||
</a>
|
||||
for step-by-step solutions.
|
||||
</p>
|
||||
<ul className="mt-2 list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<p className="font-medium">
|
||||
Copy your 2-hour logs & device specifications provided
|
||||
below.{' '}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p className="font-medium">
|
||||
Go to our
|
||||
<a
|
||||
href="https://discord.gg/AsJ8krTT3N"
|
||||
target="_blank"
|
||||
className="text-[hsla(var(--app-link))] hover:underline"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
& send it to #🆘|get-help channel for further support.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</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(
|
||||
'rounded-lg border border-[hsla(var(--app-border))] pb-2 shadow',
|
||||
!showLogFullSize && 'pt-4'
|
||||
)}
|
||||
>
|
||||
{!showLogFullSize && (
|
||||
<div className="px-4">
|
||||
<h2 className="font-semibold">Step 2</h2>
|
||||
<p className="text-[hsla(var(--text-secondary)] mt-1">
|
||||
{`If you can't find what you need in our troubleshooting guide, feel
|
||||
free reach out to us for extra help:`}
|
||||
</p>
|
||||
<ul className="mt-2 list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<p className="font-medium">
|
||||
Copy your 2-hour logs & device specifications provided
|
||||
below.{' '}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p className="font-medium">
|
||||
Go to our
|
||||
<a
|
||||
href="https://discord.gg/AsJ8krTT3N"
|
||||
target="_blank"
|
||||
className="text-[hsla(var(--app-link))] hover:underline"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
& send it to #🆘|get-help channel for further
|
||||
support.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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">
|
||||
{logOption.map((name, i) => {
|
||||
return (
|
||||
@ -103,7 +122,20 @@ const ModalTroubleShooting = () => {
|
||||
})}
|
||||
</ul>
|
||||
</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
|
||||
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 { Badge, Input, ScrollArea, Select, useClickOutside } from '@janhq/joi'
|
||||
@ -70,7 +70,7 @@ const ModelDropdown = ({
|
||||
const downloadStates = useAtomValue(modelDownloadStateAtom)
|
||||
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||
const { updateModelParameter } = useUpdateModelParameters()
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
const configuredModels = useAtomValue(configuredModelsAtom)
|
||||
const featuredModel = configuredModels.filter((x) =>
|
||||
x.metadata.tags.includes('Featured')
|
||||
@ -83,7 +83,7 @@ const ModelDropdown = ({
|
||||
|
||||
const filteredDownloadedModels = useMemo(
|
||||
() =>
|
||||
downloadedModels
|
||||
configuredModels
|
||||
.filter((e) =>
|
||||
e.name.toLowerCase().includes(searchText.toLowerCase().trim())
|
||||
)
|
||||
@ -104,10 +104,31 @@ const ModelDropdown = ({
|
||||
)
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[downloadedModels, searchText, searchFilter]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.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(() => {
|
||||
if (!activeThread) return
|
||||
let model = downloadedModels.find(
|
||||
@ -258,6 +279,7 @@ const ModelDropdown = ({
|
||||
<Input
|
||||
placeholder="Search"
|
||||
value={searchText}
|
||||
ref={searchInputRef}
|
||||
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)}
|
||||
suffixIcon={
|
||||
@ -290,58 +312,58 @@ const ModelDropdown = ({
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100%-36px)] w-full">
|
||||
{filteredDownloadedModels.filter(
|
||||
(x) => x.engine === InferenceEngine.nitro
|
||||
).length !== 0 ? (
|
||||
{searchFilter !== 'remote' && (
|
||||
<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>
|
||||
</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)
|
||||
.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>
|
||||
)
|
||||
.filter((x) => {
|
||||
if (searchText.length === 0) {
|
||||
return downloadedModels.find((c) => c.id === x.id)
|
||||
} else {
|
||||
return x
|
||||
}
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{searchFilter !== 'remote' && (
|
||||
<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>
|
||||
{searchText.length === 0 ? (
|
||||
<ul className="pb-2">
|
||||
{featuredModel.map((model) => {
|
||||
.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="line-clamp-1 text-[hsla(var(--text-secondary))]"
|
||||
className={twMerge(
|
||||
'line-clamp-1',
|
||||
!isdDownloaded &&
|
||||
'text-[hsla(var(--text-secondary))]'
|
||||
)}
|
||||
title={model.name}
|
||||
>
|
||||
{model.name}
|
||||
@ -352,10 +374,12 @@ const ModelDropdown = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[hsla(var(--text-tertiary))]">
|
||||
<span className="font-medium">
|
||||
{toGibibytes(model.metadata.size)}
|
||||
</span>
|
||||
{!isDownloading ? (
|
||||
{!isdDownloaded && (
|
||||
<span className="font-medium">
|
||||
{toGibibytes(model.metadata.size)}
|
||||
</span>
|
||||
)}
|
||||
{!isDownloading && !isdDownloaded ? (
|
||||
<DownloadCloudIcon
|
||||
size={18}
|
||||
className="cursor-pointer text-[hsla(var(--app-link))]"
|
||||
@ -382,76 +406,61 @@ const ModelDropdown = ({
|
||||
</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}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
) : (
|
||||
<ul className="pb-2">
|
||||
{featuredModel.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}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupByEngine.map((engine, i) => {
|
||||
|
||||
@ -10,6 +10,7 @@ export const janSettingScreenAtom = atom<SettingScreen[]>([])
|
||||
|
||||
export const THEME = 'themeAppearance'
|
||||
export const REDUCE_TRANSPARENT = 'reduceTransparent'
|
||||
export const SPELL_CHECKING = 'spellChecking'
|
||||
export const themesOptionsAtom = atom<{ name: string; value: string }[]>([])
|
||||
export const janThemesPathAtom = atom<string | undefined>(undefined)
|
||||
export const selectedThemeIdAtom = atomWithStorage<string>(THEME, '')
|
||||
@ -18,3 +19,4 @@ export const reduceTransparentAtom = atomWithStorage<boolean>(
|
||||
REDUCE_TRANSPARENT,
|
||||
false
|
||||
)
|
||||
export const spellCheckAtom = atomWithStorage<boolean>(SPELL_CHECKING, true)
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"lucide-react": "^0.291.0",
|
||||
"marked": "^9.1.2",
|
||||
"marked-highlight": "^2.0.6",
|
||||
"marked-katex-extension": "^5.0.1",
|
||||
"marked-katex-extension": "^5.0.2",
|
||||
"next": "14.2.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "8.4.31",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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 { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { PencilIcon, FolderOpenIcon } from 'lucide-react'
|
||||
@ -51,11 +51,10 @@ const DataFolder = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const newDestChildren: string[] = await fs.readdirSync(destFolder)
|
||||
const isNotEmpty =
|
||||
newDestChildren.filter((x) => x !== '.DS_Store').length > 0
|
||||
const isEmpty: boolean =
|
||||
await window.core?.api?.isDirectoryEmpty(destFolder)
|
||||
|
||||
if (isNotEmpty) {
|
||||
if (!isEmpty) {
|
||||
setDestinationPath(destFolder)
|
||||
showDestNotEmptyConfirm(true)
|
||||
return
|
||||
@ -74,16 +73,7 @@ const DataFolder = () => {
|
||||
if (!destinationPath) return
|
||||
try {
|
||||
setShowLoader(true)
|
||||
const appConfiguration: AppConfiguration =
|
||||
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}`
|
||||
)
|
||||
await window.core?.api?.changeDataFolder(destinationPath)
|
||||
localStorage.setItem(SUCCESS_SET_NEW_DESTINATION, 'true')
|
||||
setTimeout(() => {
|
||||
setShowLoader(false)
|
||||
|
||||
@ -3,13 +3,14 @@ import { useCallback } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
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 {
|
||||
janThemesPathAtom,
|
||||
reduceTransparentAtom,
|
||||
selectedThemeIdAtom,
|
||||
spellCheckAtom,
|
||||
themeDataAtom,
|
||||
themesOptionsAtom,
|
||||
} from '@/helpers/atoms/Setting.atom'
|
||||
@ -23,6 +24,7 @@ export default function AppearanceOptions() {
|
||||
const [reduceTransparent, setReduceTransparent] = useAtom(
|
||||
reduceTransparentAtom
|
||||
)
|
||||
const [spellCheck, setSpellCheck] = useAtom(spellCheckAtom)
|
||||
|
||||
const handleClickTheme = useCallback(
|
||||
async (e: string) => {
|
||||
@ -55,7 +57,7 @@ export default function AppearanceOptions() {
|
||||
<h6 className="font-semibold capitalize">Appearance</h6>
|
||||
</div>
|
||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||
Select of customize your interface color scheme
|
||||
Select a color theme
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
@ -70,9 +72,6 @@ export default function AppearanceOptions() {
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="font-semibold capitalize">Interface theme</h6>
|
||||
</div>
|
||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||
Choose the type of the interface
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
@ -87,15 +86,29 @@ export default function AppearanceOptions() {
|
||||
variant={reduceTransparent ? 'outline' : 'solid'}
|
||||
onClick={() => setReduceTransparent(false)}
|
||||
>
|
||||
Transparent
|
||||
Transparency
|
||||
</Button>
|
||||
</div>
|
||||
{/* <Switch
|
||||
checked={reduceTransparent}
|
||||
onChange={(e) => setReduceTransparent(e.target.checked)}
|
||||
/> */}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -32,8 +32,8 @@ const ExtensionItem: React.FC<Props> = ({ item }) => {
|
||||
)
|
||||
|
||||
const progress = isInstalling
|
||||
? installingExtensions.find((e) => e.extensionId === item.name)
|
||||
?.percentage ?? -1
|
||||
? (installingExtensions.find((e) => e.extensionId === item.name)
|
||||
?.percentage ?? -1)
|
||||
: -1
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -158,7 +158,7 @@ const ExtensionCatalog = () => {
|
||||
{engineActiveExtensions.length !== 0 && (
|
||||
<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))]">
|
||||
Model Provider
|
||||
Model Providers
|
||||
</h6>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -19,7 +19,7 @@ const SettingDetail = () => {
|
||||
case 'Appearance':
|
||||
return <AppearanceOptions />
|
||||
|
||||
case 'Hotkey & Shortcut':
|
||||
case 'Keyboard Shortcuts':
|
||||
return <Hotkeys />
|
||||
|
||||
case 'Advanced Settings':
|
||||
|
||||
@ -89,7 +89,7 @@ const SettingLeftPanel = () => {
|
||||
).length > 0 && (
|
||||
<div className="mb-1 mt-4 px-2">
|
||||
<label className="text-xs font-medium text-[hsla(var(--text-secondary))]">
|
||||
Model Provider
|
||||
Model Providers
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -14,7 +14,7 @@ import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
|
||||
export const SettingScreenList = [
|
||||
'My Models',
|
||||
'Appearance',
|
||||
'Hotkey & Shortcut',
|
||||
'Keyboard Shortcuts',
|
||||
'Advanced Settings',
|
||||
'Extensions',
|
||||
] as const
|
||||
@ -24,6 +24,7 @@ export type SettingScreen = SettingScreenTuple[number]
|
||||
|
||||
const SettingsScreen = () => {
|
||||
const setSelectedSettingScreen = useSetAtom(selectedSettingAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
|
||||
setSelectedSettingScreen('Advanced Settings')
|
||||
|
||||
@ -38,6 +38,7 @@ import ImageUploadPreview from '../ImageUploadPreview'
|
||||
import { showRightPanelAtom } from '@/helpers/atoms/App.atom'
|
||||
import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||
import { spellCheckAtom } from '@/helpers/atoms/Setting.atom'
|
||||
import {
|
||||
activeThreadAtom,
|
||||
getActiveThreadIdAtom,
|
||||
@ -52,6 +53,7 @@ const ChatInput = () => {
|
||||
const { stateModel } = useActiveModel()
|
||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||
const [activeSetting, setActiveSetting] = useState(false)
|
||||
const spellCheck = useAtomValue(spellCheckAtom)
|
||||
|
||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
||||
const { sendChatMessage } = useSendChatMessage()
|
||||
@ -162,6 +164,7 @@ const ChatInput = () => {
|
||||
experimentalFeature && 'pl-10',
|
||||
activeSetting && 'pb-14 pr-16'
|
||||
)}
|
||||
spellCheck={spellCheck}
|
||||
data-testid="txt-input-chat"
|
||||
style={{ height: activeSetting ? '100px' : '40px' }}
|
||||
ref={textareaRef}
|
||||
|
||||
@ -26,6 +26,7 @@ import {
|
||||
getCurrentChatMessagesAtom,
|
||||
setConvoMessagesAtom,
|
||||
} from '@/helpers/atoms/ChatMessage.atom'
|
||||
import { spellCheckAtom } from '@/helpers/atoms/Setting.atom'
|
||||
import {
|
||||
activeThreadAtom,
|
||||
getActiveThreadIdAtom,
|
||||
@ -45,7 +46,7 @@ const EditChatInput: React.FC<Props> = ({ message }) => {
|
||||
const { sendChatMessage } = useSendChatMessage()
|
||||
const setMessages = useSetAtom(setConvoMessagesAtom)
|
||||
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||
|
||||
const spellCheck = useAtomValue(spellCheckAtom)
|
||||
const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const setEditMessage = useSetAtom(editMessageAtom)
|
||||
@ -127,6 +128,7 @@ const EditChatInput: React.FC<Props> = ({ message }) => {
|
||||
className={twMerge('max-h-[400px] resize-none pr-20')}
|
||||
style={{ height: '40px' }}
|
||||
ref={textareaRef}
|
||||
spellCheck={spellCheck}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Enter your message..."
|
||||
disabled={stateModel.loading || !activeThread}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
ContentType,
|
||||
Thread,
|
||||
} from '@janhq/core'
|
||||
import { Tooltip } from '@janhq/joi'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import {
|
||||
RefreshCcw,
|
||||
@ -103,9 +104,14 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
||||
className="cursor-pointer rounded-lg border border-[hsla(var(--app-border))] p-2"
|
||||
onClick={onEditClick}
|
||||
>
|
||||
<PencilIcon
|
||||
size={14}
|
||||
className="text-[hsla(var(--text-secondary))]"
|
||||
<Tooltip
|
||||
trigger={
|
||||
<PencilIcon
|
||||
size={14}
|
||||
className="text-[hsla(var(--text-secondary))]"
|
||||
/>
|
||||
}
|
||||
content="Edit"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -118,9 +124,14 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
||||
className="cursor-pointer rounded-lg border border-[hsla(var(--app-border))] p-2"
|
||||
onClick={onRegenerateClick}
|
||||
>
|
||||
<RefreshCcw
|
||||
size={14}
|
||||
className="text-[hsla(var(--text-secondary))]"
|
||||
<Tooltip
|
||||
trigger={
|
||||
<RefreshCcw
|
||||
size={14}
|
||||
className="text-[hsla(var(--text-secondary))]"
|
||||
/>
|
||||
}
|
||||
content="Regenerate"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -134,9 +145,14 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
||||
{clipboard.copied ? (
|
||||
<CheckIcon size={14} className="text-[hsla(var(--success-bg))]" />
|
||||
) : (
|
||||
<CopyIcon
|
||||
size={14}
|
||||
className="text-[hsla(var(--text-secondary))]"
|
||||
<Tooltip
|
||||
trigger={
|
||||
<CopyIcon
|
||||
size={14}
|
||||
className="text-[hsla(var(--text-secondary))]"
|
||||
/>
|
||||
}
|
||||
content="Copy"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -144,9 +160,14 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
|
||||
className="cursor-pointer rounded-lg border border-[hsla(var(--app-border))] p-2"
|
||||
onClick={onDeleteClick}
|
||||
>
|
||||
<Trash2Icon
|
||||
size={14}
|
||||
className="text-[hsla(var(--text-secondary))]"
|
||||
<Tooltip
|
||||
trigger={
|
||||
<Trash2Icon
|
||||
size={14}
|
||||
className="text-[hsla(var(--text-secondary))]"
|
||||
/>
|
||||
}
|
||||
content="Delete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -178,7 +178,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||
>
|
||||
{isUser
|
||||
? props.role
|
||||
: activeThread?.assistants[0].assistant_name ?? props.role}
|
||||
: (activeThread?.assistants[0].assistant_name ?? props.role)}
|
||||
</div>
|
||||
<p className="text-xs font-medium text-gray-400">
|
||||
{displayDate(props.created)}
|
||||
@ -201,7 +201,12 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={twMerge('w-full')}>
|
||||
<div
|
||||
className={twMerge(
|
||||
'w-full',
|
||||
!isUser && !text.includes(' ') && 'break-all'
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{props.content[0]?.type === ContentType.Image && (
|
||||
<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 = {
|
||||
threadId: string
|
||||
closeContextMenu?: () => void
|
||||
}
|
||||
|
||||
const ModalCleanThread = ({ threadId }: Props) => {
|
||||
const ModalCleanThread = ({ threadId, closeContextMenu }: Props) => {
|
||||
const { cleanThread } = useDeleteThread()
|
||||
const onCleanThreadClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
@ -22,6 +23,11 @@ const ModalCleanThread = ({ threadId }: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
title="Clean Thread"
|
||||
onOpenChange={(open) => {
|
||||
if (open && closeContextMenu) {
|
||||
closeContextMenu()
|
||||
}
|
||||
}}
|
||||
trigger={
|
||||
<div
|
||||
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 = {
|
||||
threadId: string
|
||||
closeContextMenu?: () => void
|
||||
}
|
||||
|
||||
const ModalDeleteThread = ({ threadId }: Props) => {
|
||||
const ModalDeleteThread = ({ threadId, closeContextMenu }: Props) => {
|
||||
const { deleteThread } = useDeleteThread()
|
||||
|
||||
const onDeleteThreadClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation()
|
||||
@ -22,6 +24,11 @@ const ModalDeleteThread = ({ threadId }: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
title="Delete Thread"
|
||||
onOpenChange={(open) => {
|
||||
if (open && closeContextMenu) {
|
||||
closeContextMenu()
|
||||
}
|
||||
}}
|
||||
trigger={
|
||||
<div
|
||||
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 { Modal, ModalClose, Button, Input } from '@janhq/joi'
|
||||
@ -8,13 +8,19 @@ import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||
|
||||
type Props = {
|
||||
thread: Thread
|
||||
closeContextMenu?: () => void
|
||||
}
|
||||
|
||||
const ModalEditTitleThread = ({ thread }: Props) => {
|
||||
const ModalEditTitleThread = ({ thread, closeContextMenu }: Props) => {
|
||||
const [title, setTitle] = useState(thread.title)
|
||||
|
||||
const { updateThreadMetadata } = useCreateNewThread()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (thread.title) {
|
||||
setTitle(thread.title)
|
||||
}
|
||||
}, [thread.title])
|
||||
|
||||
const onUpdateTitle = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation()
|
||||
@ -30,6 +36,11 @@ const ModalEditTitleThread = ({ thread }: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
title="Edit title thread"
|
||||
onOpenChange={(open) => {
|
||||
if (open && closeContextMenu) {
|
||||
closeContextMenu()
|
||||
}
|
||||
}}
|
||||
trigger={
|
||||
<div
|
||||
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'
|
||||
|
||||
@ -43,6 +43,14 @@ const ThreadLeftPanel = () => {
|
||||
const setEditMessage = useSetAtom(editMessageAtom)
|
||||
const { recommendedModel, downloadedModels } = useRecommendedModel()
|
||||
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
visible: boolean
|
||||
thread?: Thread
|
||||
}>({
|
||||
visible: false,
|
||||
thread: undefined,
|
||||
})
|
||||
|
||||
const onThreadClick = useCallback(
|
||||
(thread: 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 (
|
||||
<LeftPanelContainer>
|
||||
{threads.length === 0 ? (
|
||||
@ -124,8 +147,10 @@ const ThreadLeftPanel = () => {
|
||||
onClick={() => {
|
||||
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
|
||||
className={twMerge(
|
||||
'line-clamp-1 pr-2 font-medium group-hover/message:pr-6',
|
||||
@ -143,10 +168,26 @@ const ThreadLeftPanel = () => {
|
||||
<Button theme="icon" className="mt-2">
|
||||
<MoreHorizontalIcon />
|
||||
</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">
|
||||
<ModalEditTitleThread thread={thread} />
|
||||
<ModalCleanThread threadId={thread.id} />
|
||||
<ModalDeleteThread threadId={thread.id} />
|
||||
<div
|
||||
className={twMerge(
|
||||
'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',
|
||||
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>
|
||||
{activeThreadId === thread.id && (
|
||||
|
||||
@ -66,6 +66,32 @@ const Tools = () => {
|
||||
[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
|
||||
|
||||
return (
|
||||
@ -140,9 +166,49 @@ const Tools = () => {
|
||||
<div className="mb-2 flex items-center">
|
||||
<label
|
||||
id="vector-database"
|
||||
className="inline-block font-medium"
|
||||
className="inline-flex items-center font-medium"
|
||||
>
|
||||
Vector Database
|
||||
<Tooltip
|
||||
trigger={
|
||||
<InfoIcon
|
||||
size={16}
|
||||
className="ml-2 flex-shrink-0 text-[hsl(var(--text-secondary))]"
|
||||
/>
|
||||
}
|
||||
content="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."
|
||||
/>
|
||||
</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 className="w-full">
|
||||
<Input value="HNSWLib" disabled readOnly />
|
||||
</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={
|
||||
@ -151,17 +217,13 @@ const Tools = () => {
|
||||
className="ml-2 flex-shrink-0 text-[hsl(var(--text-secondary))]"
|
||||
/>
|
||||
}
|
||||
content="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."
|
||||
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 className="w-full">
|
||||
<Input value="HNSWLib" disabled readOnly />
|
||||
</div>
|
||||
</div>
|
||||
<AssistantSetting
|
||||
componentData={componentDataAssistantSetting}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user