fix: update new api from cortex to support 0.5.0 (#3221)
* fix: update new api from cortex to support 0.5.0 Signed-off-by: James <namnh0122@gmail.com> * fix stop button for streaming Signed-off-by: James <namnh0122@gmail.com> * fix stop inference for nonstreaming Signed-off-by: James <namnh0122@gmail.com> * chore: remove umami prevent tracking call to vercel Signed-off-by: James <namnh0122@gmail.com> * add warning modal when running more than 2 model concurrently Signed-off-by: James <namnh0122@gmail.com> * fix: skip summarize if abort Signed-off-by: James <namnh0122@gmail.com> * 0.5.0-3 * add inference error popup Signed-off-by: James <namnh0122@gmail.com> * add back import local model Signed-off-by: James <namnh0122@gmail.com> * fix: max token issue (#3225) Signed-off-by: James <namnh0122@gmail.com> * format status Signed-off-by: James <namnh0122@gmail.com> * fix migration missing instructions Signed-off-by: James <namnh0122@gmail.com> * fix: wait for cortex process overlay should be on top (#3224) * fix: wait for cortex process overlay should be on top * chore: update cortex.js * Cortex 0.5.0-5 * add import model to my model screen Signed-off-by: James <namnh0122@gmail.com> * fix: should migrate symlink models (#3226) * fix import on windows (#3229) Signed-off-by: James <namnh0122@gmail.com> * fix yarn lint Signed-off-by: James <namnh0122@gmail.com> * fix: clean up port before start jan (#3232) Signed-off-by: James <namnh0122@gmail.com> --------- Signed-off-by: James <namnh0122@gmail.com> Co-authored-by: Van Pham <64197333+Van-QA@users.noreply.github.com> Co-authored-by: Louis <louis@jan.ai>
This commit is contained in:
parent
e8ee694abd
commit
ec9b5bf682
@ -69,6 +69,11 @@ export interface DownloadState2 {
|
|||||||
*/
|
*/
|
||||||
type: DownloadType2
|
type: DownloadType2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Percentage of the download.
|
||||||
|
*/
|
||||||
|
progress: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The status of the download.
|
* The status of the download.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -34,5 +34,5 @@ module.exports = {
|
|||||||
{ name: 'Link', linkAttribute: 'to' },
|
{ name: 'Link', linkAttribute: 'to' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ignorePatterns: ['build', 'renderer', 'node_modules', '@global'],
|
ignorePatterns: ['build', 'renderer', 'node_modules', '@global', 'playwright-report'],
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,24 @@
|
|||||||
@echo off
|
@echo off
|
||||||
set /p CORTEX_VERSION=<./resources/version.txt
|
setlocal
|
||||||
set DOWNLOAD_URL=https://github.com/janhq/cortex/releases/download/v%CORTEX_VERSION%/cortex-%CORTEX_VERSION%-amd64-windows.tar.gz
|
|
||||||
echo Downloading from %DOWNLOAD_URL%
|
|
||||||
|
|
||||||
.\node_modules\.bin\download %DOWNLOAD_URL% -e -o ./resources/win
|
:: Read the version from the version.txt file
|
||||||
|
set /p CORTEX_VERSION=<./resources/version.txt
|
||||||
|
|
||||||
|
:: Set the download URL
|
||||||
|
set DOWNLOAD_URL=https://github.com/janhq/cortex/releases/download/v%CORTEX_VERSION%/cortex-%CORTEX_VERSION%-amd64-windows.tar.gz
|
||||||
|
|
||||||
|
:: Set the output directory and file name
|
||||||
|
set OUTPUT_DIR=./resources/win
|
||||||
|
set OUTPUT_FILE=%OUTPUT_DIR%/cortex.exe
|
||||||
|
|
||||||
|
echo %OUTPUT_FILE%
|
||||||
|
|
||||||
|
:: Check if the file already exists
|
||||||
|
if exist %OUTPUT_FILE% (
|
||||||
|
echo File %OUTPUT_FILE% already exists. Skipping download.
|
||||||
|
) else (
|
||||||
|
echo Downloading from %DOWNLOAD_URL%
|
||||||
|
.\node_modules\.bin\download %DOWNLOAD_URL% -e -o %OUTPUT_DIR%
|
||||||
|
)
|
||||||
|
|
||||||
|
endlocal
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
writeFileSync,
|
writeFileSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
existsSync,
|
existsSync,
|
||||||
mkdirSync,
|
mkdirSync
|
||||||
} from 'fs'
|
} from 'fs'
|
||||||
import { dump } from 'js-yaml'
|
import { dump } from 'js-yaml'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
@ -229,25 +229,20 @@ export function handleAppIPCs() {
|
|||||||
|
|
||||||
const cortexHomeDir = join(os.homedir(), 'cortex')
|
const cortexHomeDir = join(os.homedir(), 'cortex')
|
||||||
const cortexModelFolderPath = join(cortexHomeDir, 'models')
|
const cortexModelFolderPath = join(cortexHomeDir, 'models')
|
||||||
|
|
||||||
|
if(!existsSync(cortexModelFolderPath))
|
||||||
|
mkdirSync(cortexModelFolderPath)
|
||||||
console.log('cortexModelFolderPath', cortexModelFolderPath)
|
console.log('cortexModelFolderPath', cortexModelFolderPath)
|
||||||
const reflect = require('@alumna/reflect')
|
const reflect = require('@alumna/reflect')
|
||||||
|
|
||||||
for (const modelName of allModelFolders) {
|
for (const modelName of allModelFolders) {
|
||||||
const modelFolderPath = join(janModelFolderPath, modelName)
|
const modelFolderPath = join(janModelFolderPath, modelName)
|
||||||
const filesInModelFolder = readdirSync(modelFolderPath)
|
|
||||||
if (filesInModelFolder.length <= 1) {
|
|
||||||
// if only have model.json file or empty folder, we skip it
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const destinationPath = join(cortexModelFolderPath, modelName)
|
|
||||||
|
|
||||||
// create folder if not exist
|
|
||||||
if (!existsSync(destinationPath)) {
|
|
||||||
mkdirSync(destinationPath, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
const filesInModelFolder = readdirSync(modelFolderPath)
|
||||||
|
|
||||||
|
const destinationPath = join(cortexModelFolderPath, modelName)
|
||||||
|
|
||||||
const modelJsonFullPath = join(
|
const modelJsonFullPath = join(
|
||||||
janModelFolderPath,
|
janModelFolderPath,
|
||||||
modelName,
|
modelName,
|
||||||
@ -256,12 +251,25 @@ export function handleAppIPCs() {
|
|||||||
|
|
||||||
const model = JSON.parse(readFileSync(modelJsonFullPath, 'utf-8'))
|
const model = JSON.parse(readFileSync(modelJsonFullPath, 'utf-8'))
|
||||||
const fileNames: string[] = model.sources.map((x: any) => x.filename)
|
const fileNames: string[] = model.sources.map((x: any) => x.filename)
|
||||||
// prepend fileNames with cortexModelFolderPath
|
let files: string[] = []
|
||||||
const files = fileNames.map((x: string) =>
|
|
||||||
join(cortexModelFolderPath, model.id, x)
|
|
||||||
)
|
|
||||||
|
|
||||||
const engine = 'cortex.llamacpp'
|
if(filesInModelFolder.length > 1) {
|
||||||
|
// prepend fileNames with cortexModelFolderPath
|
||||||
|
files = fileNames.map((x: string) =>
|
||||||
|
join(cortexModelFolderPath, model.id, x)
|
||||||
|
)
|
||||||
|
} else if(model.sources.length && !/^(http|https):\/\/[^/]+\/.*/.test(model.sources[0].url)) {
|
||||||
|
// Symlink case
|
||||||
|
files = [ model.sources[0].url ]
|
||||||
|
} else continue;
|
||||||
|
|
||||||
|
// create folder if not exist
|
||||||
|
// only for local model files
|
||||||
|
if (!existsSync(destinationPath) && filesInModelFolder.length > 1) {
|
||||||
|
mkdirSync(destinationPath, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const engine = (model.engine === 'nitro' || model.engine === 'cortex') ? 'cortex.llamacpp' : (model.engine ?? 'cortex.llamacpp')
|
||||||
|
|
||||||
const updatedModelFormat = {
|
const updatedModelFormat = {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
@ -288,24 +296,27 @@ export function handleAppIPCs() {
|
|||||||
max_tokens: model.parameters?.max_tokens ?? 2048,
|
max_tokens: model.parameters?.max_tokens ?? 2048,
|
||||||
stream: model.parameters?.stream ?? true,
|
stream: model.parameters?.stream ?? true,
|
||||||
}
|
}
|
||||||
|
if(filesInModelFolder.length > 1 ) {
|
||||||
|
const { err } = await reflect({
|
||||||
|
src: modelFolderPath,
|
||||||
|
dest: destinationPath,
|
||||||
|
recursive: true,
|
||||||
|
exclude: ['model.json'],
|
||||||
|
delete: false,
|
||||||
|
overwrite: true,
|
||||||
|
errorOnExist: false,
|
||||||
|
})
|
||||||
|
|
||||||
const { err } = await reflect({
|
if (err) {
|
||||||
src: modelFolderPath,
|
console.error(err);
|
||||||
dest: destinationPath,
|
continue;
|
||||||
recursive: true,
|
}
|
||||||
exclude: ['model.json'],
|
|
||||||
delete: false,
|
|
||||||
overwrite: true,
|
|
||||||
errorOnExist: false,
|
|
||||||
})
|
|
||||||
if (err) console.error(err)
|
|
||||||
else {
|
|
||||||
// create the model.yml file
|
|
||||||
const modelYamlData = dump(updatedModelFormat)
|
|
||||||
const modelYamlPath = join(cortexModelFolderPath, `${modelName}.yaml`)
|
|
||||||
|
|
||||||
writeFileSync(modelYamlPath, modelYamlData)
|
|
||||||
}
|
}
|
||||||
|
// create the model.yml file
|
||||||
|
const modelYamlData = dump(updatedModelFormat)
|
||||||
|
const modelYamlPath = join(cortexModelFolderPath, `${modelName}.yaml`)
|
||||||
|
|
||||||
|
writeFileSync(modelYamlPath, modelYamlData)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -316,6 +327,13 @@ export function handleAppIPCs() {
|
|||||||
NativeRoute.getAllMessagesAndThreads,
|
NativeRoute.getAllMessagesAndThreads,
|
||||||
async (_event): Promise<any> => {
|
async (_event): Promise<any> => {
|
||||||
const janThreadFolderPath = join(getJanDataFolderPath(), 'threads')
|
const janThreadFolderPath = join(getJanDataFolderPath(), 'threads')
|
||||||
|
// check if exist
|
||||||
|
if (!existsSync(janThreadFolderPath)) {
|
||||||
|
return {
|
||||||
|
threads: [],
|
||||||
|
messages: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
// get children of thread folder
|
// get children of thread folder
|
||||||
const allThreadFolders = readdirSync(janThreadFolderPath)
|
const allThreadFolders = readdirSync(janThreadFolderPath)
|
||||||
const threads: any[] = []
|
const threads: any[] = []
|
||||||
@ -335,10 +353,12 @@ export function handleAppIPCs() {
|
|||||||
threadFolder,
|
threadFolder,
|
||||||
'messages.jsonl'
|
'messages.jsonl'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(!existsSync(messageFullPath)) continue;
|
||||||
const lines = readFileSync(messageFullPath, 'utf-8')
|
const lines = readFileSync(messageFullPath, 'utf-8')
|
||||||
.toString()
|
.toString()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((line: any) => line !== '')
|
.filter((line: any) => line !== '')
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
messages.push(JSON.parse(line))
|
messages.push(JSON.parse(line))
|
||||||
}
|
}
|
||||||
@ -357,6 +377,10 @@ export function handleAppIPCs() {
|
|||||||
NativeRoute.getAllLocalModels,
|
NativeRoute.getAllLocalModels,
|
||||||
async (_event): Promise<boolean> => {
|
async (_event): Promise<boolean> => {
|
||||||
const janModelsFolderPath = join(getJanDataFolderPath(), 'models')
|
const janModelsFolderPath = join(getJanDataFolderPath(), 'models')
|
||||||
|
|
||||||
|
if (!existsSync(janModelsFolderPath)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
// get children of thread folder
|
// get children of thread folder
|
||||||
const allModelsFolders = readdirSync(janModelsFolderPath)
|
const allModelsFolders = readdirSync(janModelsFolderPath)
|
||||||
let hasLocalModels = false
|
let hasLocalModels = false
|
||||||
|
|||||||
121
electron/main.ts
121
electron/main.ts
@ -1,7 +1,7 @@
|
|||||||
import { app, BrowserWindow } from 'electron'
|
import { app, BrowserWindow } from 'electron'
|
||||||
|
|
||||||
import { join, resolve } from 'path'
|
import { join, resolve } from 'path'
|
||||||
import { exec } from 'child_process'
|
import { exec, execSync, ChildProcess } from 'child_process'
|
||||||
import { cortexPath } from './cortex-runner'
|
import { cortexPath } from './cortex-runner'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,13 +56,18 @@ log.info('Log from the main process')
|
|||||||
// replace all console.log to log
|
// replace all console.log to log
|
||||||
Object.assign(console, log.functions)
|
Object.assign(console, log.functions)
|
||||||
|
|
||||||
|
let cortexService: ChildProcess | undefined = undefined
|
||||||
|
|
||||||
app
|
app
|
||||||
.whenReady()
|
.whenReady()
|
||||||
|
.then(() => killProcessesOnPort(3929))
|
||||||
|
.then(() => killProcessesOnPort(1337))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
log.info('Starting cortex with path:', cortexPath)
|
const command = `${cortexPath} -a 127.0.0.1 -p 1337`
|
||||||
|
|
||||||
|
log.info('Starting cortex with command:', command)
|
||||||
// init cortex
|
// init cortex
|
||||||
// running shell command cortex init -s
|
cortexService = exec(`${command}`, (error, stdout, stderr) => {
|
||||||
exec(`${cortexPath}`, (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
if (error) {
|
||||||
log.error(`error: ${error.message}`)
|
log.error(`error: ${error.message}`)
|
||||||
return
|
return
|
||||||
@ -123,25 +128,37 @@ app.on('open-url', (_event, url) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.once('quit', async () => {
|
app.once('quit', async () => {
|
||||||
await stopApiServer()
|
|
||||||
cleanUpAndQuit()
|
cleanUpAndQuit()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.once('window-all-closed', async () => {
|
app.once('window-all-closed', async () => {
|
||||||
await stopApiServer()
|
await stopApiServer()
|
||||||
|
await stopCortexService()
|
||||||
cleanUpAndQuit()
|
cleanUpAndQuit()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function stopApiServer() {
|
async function stopCortexService() {
|
||||||
try {
|
try {
|
||||||
console.log('Stopping API server')
|
const pid = cortexService?.pid
|
||||||
const response = await fetch('http://localhost:1337/v1/process', {
|
if (!pid) {
|
||||||
|
console.log('No cortex service to stop.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
process.kill(pid)
|
||||||
|
console.log(`Service with PID ${pid} has been terminated.`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error killing service:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopApiServer() {
|
||||||
|
// this function is not meant to be success. It will throw an error.
|
||||||
|
try {
|
||||||
|
await fetch('http://localhost:1337/v1/system', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Response status:', response.status)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error stopping API server:', error)
|
// do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,6 +171,88 @@ function handleIPCs() {
|
|||||||
handleAppIPCs()
|
handleAppIPCs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function killProcessesOnPort(port: number): void {
|
||||||
|
try {
|
||||||
|
console.log(`Killing processes on port ${port}...`)
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
killProcessesOnWindowsPort(port)
|
||||||
|
} else {
|
||||||
|
killProcessesOnUnixPort(port)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to kill process(es) on port ${port}: ${(error as Error).message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function killProcessesOnWindowsPort(port: number): void {
|
||||||
|
let result: string
|
||||||
|
try {
|
||||||
|
result = execSync(`netstat -ano | findstr :${port}`).toString()
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`No processes found on port ${port}.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = result.split('\n').filter(Boolean)
|
||||||
|
|
||||||
|
if (lines.length === 0) {
|
||||||
|
console.log(`No processes found on port ${port}.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pids = lines
|
||||||
|
.map((line) => {
|
||||||
|
const parts = line.trim().split(/\s+/)
|
||||||
|
return parts[parts.length - 1]
|
||||||
|
})
|
||||||
|
.filter((pid): pid is string => Boolean(pid) && !isNaN(Number(pid)))
|
||||||
|
|
||||||
|
if (pids.length === 0) {
|
||||||
|
console.log(`No valid PIDs found for port ${port}.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const uniquePids = Array.from(new Set(pids))
|
||||||
|
console.log('uniquePids', uniquePids)
|
||||||
|
|
||||||
|
uniquePids.forEach((pid) => {
|
||||||
|
try {
|
||||||
|
execSync(`taskkill /PID ${pid} /F`)
|
||||||
|
console.log(
|
||||||
|
`Process with PID ${pid} on port ${port} has been terminated.`
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to kill process with PID ${pid}: ${(error as Error).message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function killProcessesOnUnixPort(port: number): void {
|
||||||
|
let pids: string[]
|
||||||
|
|
||||||
|
try {
|
||||||
|
pids = execSync(`lsof -ti tcp:${port}`)
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter(Boolean)
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as { status?: number }).status === 1) {
|
||||||
|
console.log(`No processes found on port ${port}.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw error // Re-throw if it's not the "no processes found" error
|
||||||
|
}
|
||||||
|
|
||||||
|
pids.forEach((pid) => {
|
||||||
|
process.kill(parseInt(pid), 'SIGTERM')
|
||||||
|
console.log(`Process with PID ${pid} on port ${port} has been terminated.`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Suppress Node error messages
|
* Suppress Node error messages
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -32,6 +32,7 @@ class WindowManager {
|
|||||||
x: bounds.x,
|
x: bounds.x,
|
||||||
y: bounds.y,
|
y: bounds.y,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
allowRunningInsecureContent: true,
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
preload: preloadPath,
|
preload: preloadPath,
|
||||||
webSecurity: false,
|
webSecurity: false,
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
0.5.0-1
|
0.5.0-5
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
import { Model } from '@janhq/core'
|
import { Model } from '@janhq/core'
|
||||||
import { Button, Badge } from '@janhq/joi'
|
import { Button, Badge } from '@janhq/joi'
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import useModels from '@/hooks/useModels'
|
import useModelStop from '@/hooks/useModelStop'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
activeModelsAtom,
|
activeModelsAtom,
|
||||||
@ -13,7 +15,7 @@ import {
|
|||||||
const Column = ['Name', 'Engine', '']
|
const Column = ['Name', 'Engine', '']
|
||||||
|
|
||||||
const TableActiveModel: React.FC = () => {
|
const TableActiveModel: React.FC = () => {
|
||||||
const { stopModel } = useModels()
|
const stopModelMutation = useModelStop()
|
||||||
const activeModels = useAtomValue(activeModelsAtom)
|
const activeModels = useAtomValue(activeModelsAtom)
|
||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
|
|
||||||
@ -25,6 +27,13 @@ const TableActiveModel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const onStopModelClick = useCallback(
|
||||||
|
(modelId: string) => {
|
||||||
|
stopModelMutation.mutate(modelId)
|
||||||
|
},
|
||||||
|
[stopModelMutation]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="m-4 mr-0 w-1/2">
|
<div className="m-4 mr-0 w-1/2">
|
||||||
<div className="overflow-hidden rounded-lg border border-[hsla(var(--app-border))]">
|
<div className="overflow-hidden rounded-lg border border-[hsla(var(--app-border))]">
|
||||||
@ -58,7 +67,7 @@ const TableActiveModel: React.FC = () => {
|
|||||||
<td className="px-4 py-2 text-center">
|
<td className="px-4 py-2 text-center">
|
||||||
<Button
|
<Button
|
||||||
theme="destructive"
|
theme="destructive"
|
||||||
onClick={() => stopModel(model.model)}
|
onClick={() => onStopModelClick(model.model)}
|
||||||
>
|
>
|
||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -57,7 +57,7 @@ const SystemMonitor: React.FC = () => {
|
|||||||
const register = useCallback(async () => {
|
const register = useCallback(async () => {
|
||||||
if (abortControllerRef.current) return
|
if (abortControllerRef.current) return
|
||||||
abortControllerRef.current = new AbortController()
|
abortControllerRef.current = new AbortController()
|
||||||
await fetchEventSource(`${host}/events/resources`, {
|
await fetchEventSource(`${host}/system/events/resources`, {
|
||||||
onmessage(ev) {
|
onmessage(ev) {
|
||||||
if (!ev.data || ev.data === '') return
|
if (!ev.data || ev.data === '') return
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -16,8 +16,10 @@ import TopPanel from '@/containers/Layout/TopPanel'
|
|||||||
import { getImportModelStageAtom } from '@/hooks/useImportModel'
|
import { getImportModelStageAtom } from '@/hooks/useImportModel'
|
||||||
|
|
||||||
import DownloadLocalModelModal from '@/screens/HubScreen2/components/DownloadLocalModelModal'
|
import DownloadLocalModelModal from '@/screens/HubScreen2/components/DownloadLocalModelModal'
|
||||||
|
import InferenceErrorModal from '@/screens/HubScreen2/components/InferenceErrorModal'
|
||||||
import SetUpApiKeyModal from '@/screens/HubScreen2/components/SetUpApiKeyModal'
|
import SetUpApiKeyModal from '@/screens/HubScreen2/components/SetUpApiKeyModal'
|
||||||
import SetUpRemoteModelModal from '@/screens/HubScreen2/components/SetUpRemoteModelModal'
|
import SetUpRemoteModelModal from '@/screens/HubScreen2/components/SetUpRemoteModelModal'
|
||||||
|
import WarningMultipleModelModal from '@/screens/HubScreen2/components/WarningMultipleModelModal'
|
||||||
import { SUCCESS_SET_NEW_DESTINATION } from '@/screens/Settings/Advanced/DataFolder'
|
import { SUCCESS_SET_NEW_DESTINATION } from '@/screens/Settings/Advanced/DataFolder'
|
||||||
import CancelModelImportModal from '@/screens/Settings/CancelModelImportModal'
|
import CancelModelImportModal from '@/screens/Settings/CancelModelImportModal'
|
||||||
import ChooseWhatToImportModal from '@/screens/Settings/ChooseWhatToImportModal'
|
import ChooseWhatToImportModal from '@/screens/Settings/ChooseWhatToImportModal'
|
||||||
@ -82,6 +84,8 @@ const BaseLayout = () => {
|
|||||||
{importModelStage === 'EDIT_MODEL_INFO' && <EditModelInfoModal />}
|
{importModelStage === 'EDIT_MODEL_INFO' && <EditModelInfoModal />}
|
||||||
{importModelStage === 'CONFIRM_CANCEL' && <CancelModelImportModal />}
|
{importModelStage === 'CONFIRM_CANCEL' && <CancelModelImportModal />}
|
||||||
|
|
||||||
|
<InferenceErrorModal />
|
||||||
|
<WarningMultipleModelModal />
|
||||||
<DownloadLocalModelModal />
|
<DownloadLocalModelModal />
|
||||||
<SetUpRemoteModelModal />
|
<SetUpRemoteModelModal />
|
||||||
<SetUpApiKeyModal />
|
<SetUpApiKeyModal />
|
||||||
|
|||||||
@ -6,8 +6,14 @@ import { useAtomValue, useSetAtom } from 'jotai'
|
|||||||
|
|
||||||
import { downloadStateListAtom } from '@/hooks/useDownloadState'
|
import { downloadStateListAtom } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
|
import useModels from '@/hooks/useModels'
|
||||||
|
|
||||||
import { waitingForCortexAtom } from '@/helpers/atoms/App.atom'
|
import { waitingForCortexAtom } from '@/helpers/atoms/App.atom'
|
||||||
import { hostAtom } from '@/helpers/atoms/AppConfig.atom'
|
import { hostAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||||
|
import {
|
||||||
|
setImportingModelSuccessAtom,
|
||||||
|
updateImportingModelProgressAtom,
|
||||||
|
} from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
const DownloadEventListener: React.FC = () => {
|
const DownloadEventListener: React.FC = () => {
|
||||||
const host = useAtomValue(hostAtom)
|
const host = useAtomValue(hostAtom)
|
||||||
@ -15,15 +21,52 @@ const DownloadEventListener: React.FC = () => {
|
|||||||
const abortController = useRef(new AbortController())
|
const abortController = useRef(new AbortController())
|
||||||
const setDownloadStateList = useSetAtom(downloadStateListAtom)
|
const setDownloadStateList = useSetAtom(downloadStateListAtom)
|
||||||
const setWaitingForCortex = useSetAtom(waitingForCortexAtom)
|
const setWaitingForCortex = useSetAtom(waitingForCortexAtom)
|
||||||
|
const { getModels } = useModels()
|
||||||
|
|
||||||
|
const updateImportingModelProgress = useSetAtom(
|
||||||
|
updateImportingModelProgressAtom
|
||||||
|
)
|
||||||
|
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
|
||||||
|
|
||||||
|
const handleLocalImportModels = useCallback(
|
||||||
|
(events: DownloadState2[]) => {
|
||||||
|
if (events.length === 0) return
|
||||||
|
for (const event of events) {
|
||||||
|
if (event.progress === 100) {
|
||||||
|
setImportingModelSuccess(event.id)
|
||||||
|
} else {
|
||||||
|
updateImportingModelProgress(event.id, event.progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getModels()
|
||||||
|
},
|
||||||
|
[setImportingModelSuccess, updateImportingModelProgress, getModels]
|
||||||
|
)
|
||||||
|
|
||||||
const subscribeDownloadEvent = useCallback(async () => {
|
const subscribeDownloadEvent = useCallback(async () => {
|
||||||
if (isRegistered.current) return
|
if (isRegistered.current) return
|
||||||
await fetchEventSource(`${host}/events/download`, {
|
await fetchEventSource(`${host}/system/events/download`, {
|
||||||
onmessage(ev) {
|
onmessage(ev) {
|
||||||
if (!ev.data || ev.data === '') return
|
if (!ev.data || ev.data === '') return
|
||||||
try {
|
try {
|
||||||
const downloadEvent = JSON.parse(ev.data) as DownloadState2[]
|
const downloadEvents = JSON.parse(ev.data) as DownloadState2[]
|
||||||
setDownloadStateList(downloadEvent)
|
const remoteDownloadEvents: DownloadState2[] = []
|
||||||
|
const localImportEvents: DownloadState2[] = []
|
||||||
|
// filter out the import local events
|
||||||
|
for (const event of downloadEvents) {
|
||||||
|
console.debug('Receiving event', event)
|
||||||
|
if (
|
||||||
|
isAbsolutePath(event.id) &&
|
||||||
|
event.type === 'model' &&
|
||||||
|
event.children.length === 0
|
||||||
|
) {
|
||||||
|
localImportEvents.push(event)
|
||||||
|
} else {
|
||||||
|
remoteDownloadEvents.push(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleLocalImportModels(localImportEvents)
|
||||||
|
setDownloadStateList(remoteDownloadEvents)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -40,7 +83,7 @@ const DownloadEventListener: React.FC = () => {
|
|||||||
})
|
})
|
||||||
console.log('Download event subscribed')
|
console.log('Download event subscribed')
|
||||||
isRegistered.current = true
|
isRegistered.current = true
|
||||||
}, [host, setDownloadStateList, setWaitingForCortex])
|
}, [host, setDownloadStateList, setWaitingForCortex, handleLocalImportModels])
|
||||||
|
|
||||||
const unsubscribeDownloadEvent = useCallback(() => {
|
const unsubscribeDownloadEvent = useCallback(() => {
|
||||||
if (!isRegistered.current) return
|
if (!isRegistered.current) return
|
||||||
@ -60,4 +103,22 @@ const DownloadEventListener: React.FC = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isAbsolutePath = (path: string): boolean => {
|
||||||
|
// Trim any leading or trailing whitespace
|
||||||
|
const trimmedPath = path.trim()
|
||||||
|
|
||||||
|
// Check for Unix-like absolute path
|
||||||
|
if (trimmedPath.startsWith('/')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Windows absolute path (with drive letter)
|
||||||
|
if (/^[A-Za-z]:[/\\]/.test(trimmedPath)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other paths are not considered absolute local paths
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export default DownloadEventListener
|
export default DownloadEventListener
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import DownloadEventListener from './DownloadEventListener'
|
|||||||
|
|
||||||
import KeyListener from './KeyListener'
|
import KeyListener from './KeyListener'
|
||||||
import ModelEventListener from './ModelEventListener'
|
import ModelEventListener from './ModelEventListener'
|
||||||
import ModelImportListener from './ModelImportListener'
|
|
||||||
|
|
||||||
const EventListenerWrapper: React.FC = () => (
|
const EventListenerWrapper: React.FC = () => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@ -19,7 +18,6 @@ const EventListenerWrapper: React.FC = () => (
|
|||||||
<ModelEventListener />
|
<ModelEventListener />
|
||||||
<ClipboardListener />
|
<ClipboardListener />
|
||||||
<DeepLinkListener />
|
<DeepLinkListener />
|
||||||
<ModelImportListener />
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -91,7 +91,7 @@ function ModelEventListener() {
|
|||||||
if (abortController.current) return
|
if (abortController.current) return
|
||||||
abortController.current = new AbortController()
|
abortController.current = new AbortController()
|
||||||
|
|
||||||
await fetchEventSource(`${host}/events/model`, {
|
await fetchEventSource(`${host}/system/events/model`, {
|
||||||
onmessage(ev) {
|
onmessage(ev) {
|
||||||
if (!ev.data || ev.data === '') return
|
if (!ev.data || ev.data === '') return
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,103 +0,0 @@
|
|||||||
import { useCallback, useEffect } from 'react'
|
|
||||||
|
|
||||||
import { ImportingModel, Model } from '@janhq/core'
|
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
|
|
||||||
import { snackbar } from '../Toast'
|
|
||||||
|
|
||||||
import {
|
|
||||||
setImportingModelErrorAtom,
|
|
||||||
setImportingModelSuccessAtom,
|
|
||||||
updateImportingModelProgressAtom,
|
|
||||||
} from '@/helpers/atoms/Model.atom'
|
|
||||||
|
|
||||||
const ModelImportListener: React.FC = () => {
|
|
||||||
const updateImportingModelProgress = useSetAtom(
|
|
||||||
updateImportingModelProgressAtom
|
|
||||||
)
|
|
||||||
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
|
|
||||||
const setImportingModelFailed = useSetAtom(setImportingModelErrorAtom)
|
|
||||||
|
|
||||||
const onImportModelUpdate = useCallback(
|
|
||||||
async (state: ImportingModel) => {
|
|
||||||
if (!state.importId) return
|
|
||||||
updateImportingModelProgress(state.importId, state.percentage ?? 0)
|
|
||||||
},
|
|
||||||
[updateImportingModelProgress]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onImportModelFailed = useCallback(
|
|
||||||
async (state: ImportingModel) => {
|
|
||||||
if (!state.importId) return
|
|
||||||
setImportingModelFailed(state.importId, state.error ?? '')
|
|
||||||
},
|
|
||||||
[setImportingModelFailed]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onImportModelSuccess = useCallback(
|
|
||||||
(state: ImportingModel) => {
|
|
||||||
if (!state.modelId) return
|
|
||||||
// events.emit(ModelEvent.OnModelsUpdate, {})
|
|
||||||
setImportingModelSuccess(state.importId, state.modelId)
|
|
||||||
},
|
|
||||||
[setImportingModelSuccess]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onImportModelFinished = useCallback((importedModels: Model[]) => {
|
|
||||||
const modelText = importedModels.length === 1 ? 'model' : 'models'
|
|
||||||
snackbar({
|
|
||||||
description: `Successfully imported ${importedModels.length} ${modelText}`,
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.debug('ModelImportListener: registering event listeners..')
|
|
||||||
|
|
||||||
// events.on(
|
|
||||||
// LocalImportModelEvent.onLocalImportModelUpdate,
|
|
||||||
// onImportModelUpdate
|
|
||||||
// )
|
|
||||||
// events.on(
|
|
||||||
// LocalImportModelEvent.onLocalImportModelSuccess,
|
|
||||||
// onImportModelSuccess
|
|
||||||
// )
|
|
||||||
// events.on(
|
|
||||||
// LocalImportModelEvent.onLocalImportModelFinished,
|
|
||||||
// onImportModelFinished
|
|
||||||
// )
|
|
||||||
// events.on(
|
|
||||||
// LocalImportModelEvent.onLocalImportModelFailed,
|
|
||||||
// onImportModelFailed
|
|
||||||
// )
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// console.debug('ModelImportListener: unregistering event listeners...')
|
|
||||||
// events.off(
|
|
||||||
// LocalImportModelEvent.onLocalImportModelUpdate,
|
|
||||||
// onImportModelUpdate
|
|
||||||
// )
|
|
||||||
// events.off(
|
|
||||||
// LocalImportModelEvent.onLocalImportModelSuccess,
|
|
||||||
// onImportModelSuccess
|
|
||||||
// )
|
|
||||||
// events.off(
|
|
||||||
// LocalImportModelEvent.onLocalImportModelFinished,
|
|
||||||
// onImportModelFinished
|
|
||||||
// )
|
|
||||||
// events.off(
|
|
||||||
// LocalImportModelEvent.onLocalImportModelFailed,
|
|
||||||
// onImportModelFailed
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
}, [
|
|
||||||
onImportModelUpdate,
|
|
||||||
onImportModelSuccess,
|
|
||||||
onImportModelFinished,
|
|
||||||
onImportModelFailed,
|
|
||||||
])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModelImportListener
|
|
||||||
@ -13,8 +13,6 @@ import ThemeWrapper from '@/containers/Providers/Theme'
|
|||||||
|
|
||||||
import { setupCoreServices } from '@/services/coreService'
|
import { setupCoreServices } from '@/services/coreService'
|
||||||
|
|
||||||
import Umami from '@/utils/umami'
|
|
||||||
|
|
||||||
import DataLoader from './DataLoader'
|
import DataLoader from './DataLoader'
|
||||||
import ModalMigrations from './ModalMigrations'
|
import ModalMigrations from './ModalMigrations'
|
||||||
|
|
||||||
@ -35,7 +33,7 @@ const Providers = ({ children }: PropsWithChildren) => {
|
|||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
<JotaiWrapper>
|
<JotaiWrapper>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Umami />
|
{/* <Umami /> */}
|
||||||
{setupCore && (
|
{setupCore && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<DataLoader />
|
<DataLoader />
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
|
import { useCallback, useEffect } from 'react'
|
||||||
|
|
||||||
import { Modal } from '@janhq/joi'
|
import { Modal } from '@janhq/joi'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import useCortex from '@/hooks/useCortex'
|
||||||
|
|
||||||
import Spinner from '../Loader/Spinner'
|
import Spinner from '../Loader/Spinner'
|
||||||
|
|
||||||
@ -8,12 +12,22 @@ import { hostAtom } from '@/helpers/atoms/AppConfig.atom'
|
|||||||
|
|
||||||
const WaitingForCortexModal: React.FC = () => {
|
const WaitingForCortexModal: React.FC = () => {
|
||||||
const host = useAtomValue(hostAtom)
|
const host = useAtomValue(hostAtom)
|
||||||
const open = useAtomValue(waitingForCortexAtom)
|
const [waitingForCortex, setWaitingForCortex] = useAtom(waitingForCortexAtom)
|
||||||
|
const { isSystemAlive } = useCortex()
|
||||||
|
|
||||||
|
const checkSystemAlive = useCallback(async () => {
|
||||||
|
setWaitingForCortex(!(await isSystemAlive()))
|
||||||
|
}, [setWaitingForCortex, isSystemAlive])
|
||||||
|
|
||||||
|
// Check health for the first time on mount
|
||||||
|
useEffect(() => {
|
||||||
|
checkSystemAlive()
|
||||||
|
}, [checkSystemAlive])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
hideClose
|
hideClose
|
||||||
open={open}
|
open={waitingForCortex}
|
||||||
title={'Waiting for cortex'}
|
title={'Waiting for cortex'}
|
||||||
content={
|
content={
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export const mainViewStateAtom = atom<MainViewState>(MainViewState.Thread)
|
|||||||
|
|
||||||
export const defaultJanDataFolderAtom = atom<string>('')
|
export const defaultJanDataFolderAtom = atom<string>('')
|
||||||
|
|
||||||
export const waitingForCortexAtom = atom<boolean>(false)
|
export const waitingForCortexAtom = atom<boolean>(true)
|
||||||
|
|
||||||
// Store panel atom
|
// Store panel atom
|
||||||
export const showLeftPanelAtom = atom<boolean>(true)
|
export const showLeftPanelAtom = atom<boolean>(true)
|
||||||
|
|||||||
@ -61,14 +61,14 @@ export const setImportingModelErrorAtom = atom(
|
|||||||
|
|
||||||
export const setImportingModelSuccessAtom = atom(
|
export const setImportingModelSuccessAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, importId: string, modelId: string) => {
|
(get, set, importId: string) => {
|
||||||
const model = get(importingModelsAtom).find((x) => x.importId === importId)
|
const model = get(importingModelsAtom).find((x) => x.importId === importId)
|
||||||
if (!model) return
|
if (!model) return
|
||||||
const newModel: ImportingModel = {
|
const newModel: ImportingModel = {
|
||||||
...model,
|
...model,
|
||||||
modelId,
|
modelId: undefined,
|
||||||
status: 'IMPORTED',
|
status: 'IMPORTED',
|
||||||
percentage: 1,
|
percentage: 100,
|
||||||
}
|
}
|
||||||
const newList = get(importingModelsAtom).map((x) =>
|
const newList = get(importingModelsAtom).map((x) =>
|
||||||
x.importId === importId ? newModel : x
|
x.importId === importId ? newModel : x
|
||||||
|
|||||||
@ -61,17 +61,16 @@ export const setActiveThreadIdAtom = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const waitingToSendMessage = atom<boolean | undefined>(undefined)
|
|
||||||
export const isLoadingModelAtom = atom<boolean | undefined>(undefined)
|
export const isLoadingModelAtom = atom<boolean | undefined>(undefined)
|
||||||
|
|
||||||
export const isGeneratingResponseAtom = atom<boolean | undefined>(undefined)
|
export const isGeneratingResponseAtom = atom<boolean>(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all threads for the current user
|
* Stores all threads for the current user
|
||||||
*/
|
*/
|
||||||
export const threadsAtom = atom<Thread[]>([])
|
export const threadsAtom = atom<Thread[]>([])
|
||||||
|
|
||||||
export const deleteThreadAtom = atom(null, (get, set, threadId: string) => {
|
export const deleteThreadAtom = atom(null, (_get, set, threadId: string) => {
|
||||||
set(threadsAtom, (threads) => {
|
set(threadsAtom, (threads) => {
|
||||||
// set active thread to the latest
|
// set active thread to the latest
|
||||||
const allThreads = threads.filter((c) => c.id !== threadId)
|
const allThreads = threads.filter((c) => c.id !== threadId)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'cortexso-node/shims/web'
|
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
import { Cortex } from '@cortexso/cortex.js'
|
||||||
|
import { Engine } from '@cortexso/cortex.js/resources'
|
||||||
import {
|
import {
|
||||||
Assistant,
|
Assistant,
|
||||||
Model,
|
Model,
|
||||||
@ -11,10 +12,9 @@ import {
|
|||||||
AssistantCreateParams,
|
AssistantCreateParams,
|
||||||
AssistantUpdateParams,
|
AssistantUpdateParams,
|
||||||
LlmEngine,
|
LlmEngine,
|
||||||
|
LlmEngines,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
|
|
||||||
import { Cortex } from 'cortexso-node'
|
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import { UpdateConfigMutationVariables } from './useEngineMutation'
|
import { UpdateConfigMutationVariables } from './useEngineMutation'
|
||||||
@ -24,22 +24,6 @@ import { MessageUpdateMutationVariables } from './useMessageUpdateMutation'
|
|||||||
|
|
||||||
import { hostAtom } from '@/helpers/atoms/AppConfig.atom'
|
import { hostAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||||
|
|
||||||
const EngineInitStatuses = [
|
|
||||||
'ready',
|
|
||||||
'not_initialized',
|
|
||||||
'missing_configuration',
|
|
||||||
'not_supported',
|
|
||||||
] as const
|
|
||||||
export type EngineInitStatus = (typeof EngineInitStatuses)[number]
|
|
||||||
|
|
||||||
export type EngineStatus = {
|
|
||||||
name: LlmEngine
|
|
||||||
description: string
|
|
||||||
version: string
|
|
||||||
productName: string
|
|
||||||
status: EngineInitStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
const useCortex = () => {
|
const useCortex = () => {
|
||||||
const host = useAtomValue(hostAtom)
|
const host = useAtomValue(hostAtom)
|
||||||
|
|
||||||
@ -49,50 +33,33 @@ const useCortex = () => {
|
|||||||
dangerouslyAllowBrowser: true,
|
dangerouslyAllowBrowser: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: put in to cortexso-node?
|
const getEngineStatuses = useCallback(async (): Promise<Engine[]> => {
|
||||||
const getEngineStatuses = useCallback(async (): Promise<EngineStatus[]> => {
|
const engineResponse = await cortex.engines.list()
|
||||||
const response = await fetch(`${host}/engines`, {
|
// @ts-expect-error incompatible types
|
||||||
method: 'GET',
|
const engineStatuses: Engine[] = engineResponse.body.data.map(
|
||||||
})
|
(engine: Engine) => {
|
||||||
const data = await response.json()
|
return {
|
||||||
const engineStatuses: EngineStatus[] = []
|
name: engine.name,
|
||||||
data.data.forEach((engineStatus: EngineStatus) => {
|
description: engine.description,
|
||||||
engineStatuses.push(engineStatus)
|
version: engine.version,
|
||||||
})
|
productName: engine.productName,
|
||||||
return engineStatuses
|
status: engine.status,
|
||||||
}, [host])
|
}
|
||||||
|
|
||||||
// TODO: put in to cortexso-node?
|
|
||||||
const getEngineStatus = useCallback(
|
|
||||||
async (engine: LlmEngine): Promise<EngineStatus | undefined> => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${host}/engines/${engine}`, {
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
const data = (await response.json()) as EngineStatus
|
|
||||||
return data
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
[host]
|
|
||||||
)
|
return engineStatuses
|
||||||
|
}, [cortex.engines])
|
||||||
|
|
||||||
// TODO: put in to cortexso-node?
|
|
||||||
const initializeEngine = useCallback(
|
const initializeEngine = useCallback(
|
||||||
async (engine: LlmEngine) => {
|
async (engine: LlmEngine) => {
|
||||||
try {
|
try {
|
||||||
await fetch(`${host}/engines/${engine}/init/`, {
|
await cortex.engines.init(engine)
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
accept: 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[host]
|
[cortex.engines]
|
||||||
)
|
)
|
||||||
|
|
||||||
const fetchAssistants = useCallback(async () => {
|
const fetchAssistants = useCallback(async () => {
|
||||||
@ -132,8 +99,15 @@ const useCortex = () => {
|
|||||||
console.debug('Model id is empty, skipping', model)
|
console.debug('Model id is empty, skipping', model)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
const engine = LlmEngines.find((engine) => engine === model.engine)
|
||||||
|
if (!engine) {
|
||||||
|
console.error(`Model ${modelId} has an invalid engine ${model.engine}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
models.push({
|
models.push({
|
||||||
...model,
|
...model,
|
||||||
|
engine: engine,
|
||||||
model: modelId,
|
model: modelId,
|
||||||
// @ts-expect-error each model must have associated files
|
// @ts-expect-error each model must have associated files
|
||||||
files: model['files'],
|
files: model['files'],
|
||||||
@ -263,26 +237,18 @@ const useCortex = () => {
|
|||||||
const updateModel = useCallback(
|
const updateModel = useCallback(
|
||||||
async (modelId: string, options: Record<string, unknown>) => {
|
async (modelId: string, options: Record<string, unknown>) => {
|
||||||
try {
|
try {
|
||||||
return await fetch(`${host}/models/${modelId}`, {
|
return await cortex.models.update(modelId, options)
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
'accept': 'application/json',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(options),
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[host]
|
[cortex.models]
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: put this into cortexso-node
|
|
||||||
const downloadModel = useCallback(
|
const downloadModel = useCallback(
|
||||||
async (modelId: string, fileName?: string, persistedModelId?: string) => {
|
async (modelId: string, fileName?: string, persistedModelId?: string) => {
|
||||||
try {
|
try {
|
||||||
|
// return await cortex.models.download(modelId)
|
||||||
return await fetch(`${host}/models/${modelId}/pull`, {
|
return await fetch(`${host}/models/${modelId}/pull`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -305,19 +271,12 @@ const useCortex = () => {
|
|||||||
const abortDownload = useCallback(
|
const abortDownload = useCallback(
|
||||||
async (downloadId: string) => {
|
async (downloadId: string) => {
|
||||||
try {
|
try {
|
||||||
return await fetch(`${host}/models/${downloadId}/pull`, {
|
return await cortex.models.abortDownload(downloadId)
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'accept': 'application/json',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[host]
|
[cortex.models]
|
||||||
)
|
)
|
||||||
|
|
||||||
const createAssistant = useCallback(
|
const createAssistant = useCallback(
|
||||||
@ -335,22 +294,14 @@ const useCortex = () => {
|
|||||||
// TODO: add this to cortex-node
|
// TODO: add this to cortex-node
|
||||||
const registerEngineConfig = useCallback(
|
const registerEngineConfig = useCallback(
|
||||||
async (variables: UpdateConfigMutationVariables) => {
|
async (variables: UpdateConfigMutationVariables) => {
|
||||||
|
const { engine, config } = variables
|
||||||
try {
|
try {
|
||||||
const { engine, config } = variables
|
await cortex.engines.update(engine, config)
|
||||||
await fetch(`${host}/engines/${engine}`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
'accept': 'application/json',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(config),
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[host]
|
[cortex.engines]
|
||||||
)
|
)
|
||||||
|
|
||||||
// add this to cortex-node?
|
// add this to cortex-node?
|
||||||
@ -368,6 +319,15 @@ const useCortex = () => {
|
|||||||
[host]
|
[host]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isSystemAlive = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await cortex.system.status()
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [cortex.system])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fetchAssistants,
|
fetchAssistants,
|
||||||
fetchThreads,
|
fetchThreads,
|
||||||
@ -392,9 +352,9 @@ const useCortex = () => {
|
|||||||
chatCompletionNonStreaming,
|
chatCompletionNonStreaming,
|
||||||
registerEngineConfig,
|
registerEngineConfig,
|
||||||
createModel,
|
createModel,
|
||||||
getEngineStatus,
|
|
||||||
initializeEngine,
|
initializeEngine,
|
||||||
getEngineStatuses,
|
getEngineStatuses,
|
||||||
|
isSystemAlive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export const addDownloadModelStateAtom = atom(
|
|||||||
id: modelId,
|
id: modelId,
|
||||||
title: modelId,
|
title: modelId,
|
||||||
type: DownloadType2.Model,
|
type: DownloadType2.Model,
|
||||||
|
progress: 0,
|
||||||
status: DownloadStatus.Downloading,
|
status: DownloadStatus.Downloading,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,8 +3,6 @@ import { useCallback } from 'react'
|
|||||||
import { ImportingModel } from '@janhq/core'
|
import { ImportingModel } from '@janhq/core'
|
||||||
import { useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
|
|
||||||
import { snackbar } from '@/containers/Toast'
|
import { snackbar } from '@/containers/Toast'
|
||||||
|
|
||||||
import { getFileInfoFromFile } from '@/utils/file'
|
import { getFileInfoFromFile } from '@/utils/file'
|
||||||
@ -26,17 +24,23 @@ export default function useDropModelBinaries() {
|
|||||||
)
|
)
|
||||||
const supportedFiles = files.filter((file) => file.path.endsWith('.gguf'))
|
const supportedFiles = files.filter((file) => file.path.endsWith('.gguf'))
|
||||||
|
|
||||||
const importingModels: ImportingModel[] = supportedFiles.map((file) => ({
|
const importingModels: ImportingModel[] = supportedFiles.map((file) => {
|
||||||
importId: uuidv4(),
|
const normalizedPath = isWindows
|
||||||
modelId: undefined,
|
? file.path.replace(/\\/g, '/')
|
||||||
name: file.name.replace('.gguf', ''),
|
: file.path
|
||||||
description: '',
|
|
||||||
path: file.path,
|
return {
|
||||||
tags: [],
|
importId: normalizedPath,
|
||||||
size: file.size,
|
modelId: undefined,
|
||||||
status: 'PREPARING',
|
name: normalizedPath.replace('.gguf', ''),
|
||||||
format: 'gguf',
|
description: '',
|
||||||
}))
|
path: file.path,
|
||||||
|
tags: [],
|
||||||
|
size: file.size,
|
||||||
|
status: 'PREPARING',
|
||||||
|
format: 'gguf',
|
||||||
|
}
|
||||||
|
})
|
||||||
if (unsupportedFiles.length > 0) {
|
if (unsupportedFiles.length > 0) {
|
||||||
snackbar({
|
snackbar({
|
||||||
description: `Only files with .gguf extension can be imported.`,
|
description: `Only files with .gguf extension can be imported.`,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
import { Engine } from '@cortexso/cortex.js/resources'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
import useCortex, { EngineStatus } from './useCortex'
|
import useCortex from './useCortex'
|
||||||
import { engineQueryKey } from './useEngineQuery'
|
import { engineQueryKey } from './useEngineQuery'
|
||||||
|
|
||||||
const useEngineInit = () => {
|
const useEngineInit = () => {
|
||||||
@ -10,21 +11,21 @@ const useEngineInit = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: initializeEngine,
|
mutationFn: initializeEngine,
|
||||||
|
|
||||||
onSuccess: async (data, variables) => {
|
onSuccess: async (data, engineName) => {
|
||||||
console.debug(`Engine ${variables} initialized`, data)
|
console.debug(`Engine ${engineName} initialized`, data)
|
||||||
|
|
||||||
// optimistically set the engine status to 'ready'
|
// optimistically set the engine status to 'ready'
|
||||||
const queryCacheData = await queryClient.getQueryData(engineQueryKey)
|
const queryCacheData = await queryClient.getQueryData(engineQueryKey)
|
||||||
if (!queryCacheData) {
|
if (!queryCacheData) {
|
||||||
return queryClient.invalidateQueries({ queryKey: engineQueryKey })
|
return queryClient.invalidateQueries({ queryKey: engineQueryKey })
|
||||||
}
|
}
|
||||||
const engineStatuses = queryCacheData as EngineStatus[]
|
const engineStatuses = queryCacheData as Engine[]
|
||||||
engineStatuses.forEach((engine) => {
|
engineStatuses.forEach((engine) => {
|
||||||
if (engine.name === variables) {
|
if (engine.name === engineName) {
|
||||||
engine.status = 'ready'
|
engine.status = 'ready'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
console.log(`Updated engine status: ${engineStatuses}`)
|
console.debug(`Updated engine status: ${engineStatuses}`)
|
||||||
await queryClient.setQueryData(engineQueryKey, engineStatuses)
|
await queryClient.setQueryData(engineQueryKey, engineStatuses)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -68,13 +68,12 @@ const useMigratingData = () => {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const threadTitle: string = thread.title ?? 'New Thread'
|
const threadTitle: string = thread.title ?? 'New Thread'
|
||||||
const instruction: string = thread.assistants[0]?.instruction ?? ''
|
const instructions: string = thread.assistants[0]?.instructions ?? ''
|
||||||
|
|
||||||
// currently, we don't have api support for creating thread with messages
|
// currently, we don't have api support for creating thread with messages
|
||||||
const cortexThread = await createThread(modelId, assistants[0])
|
const cortexThread = await createThread(modelId, assistants[0])
|
||||||
console.log('createThread', cortexThread)
|
console.log('createThread', cortexThread)
|
||||||
// update instruction
|
// update instruction
|
||||||
cortexThread.assistants[0].instructions = instruction
|
cortexThread.assistants[0].instructions = instructions
|
||||||
cortexThread.title = threadTitle
|
cortexThread.title = threadTitle
|
||||||
|
|
||||||
// update thread name
|
// update thread name
|
||||||
|
|||||||
21
web/hooks/useModelStop.ts
Normal file
21
web/hooks/useModelStop.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import useCortex from './useCortex'
|
||||||
|
|
||||||
|
const useModelStop = () => {
|
||||||
|
const { stopModel } = useCortex()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: stopModel,
|
||||||
|
|
||||||
|
onSuccess: (data, modelId) => {
|
||||||
|
console.debug(`Model ${modelId} stopped successfully`, data)
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (error, modelId) => {
|
||||||
|
console.debug(`Stop model ${modelId} error`, error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useModelStop
|
||||||
@ -16,7 +16,6 @@ const useModels = () => {
|
|||||||
const removeDownloadedModel = useSetAtom(removeDownloadedModelAtom)
|
const removeDownloadedModel = useSetAtom(removeDownloadedModelAtom)
|
||||||
const {
|
const {
|
||||||
fetchModels,
|
fetchModels,
|
||||||
stopModel: cortexStopModel,
|
|
||||||
deleteModel: cortexDeleteModel,
|
deleteModel: cortexDeleteModel,
|
||||||
updateModel: cortexUpdateModel,
|
updateModel: cortexUpdateModel,
|
||||||
} = useCortex()
|
} = useCortex()
|
||||||
@ -29,11 +28,6 @@ const useModels = () => {
|
|||||||
getDownloadedModels()
|
getDownloadedModels()
|
||||||
}, [setDownloadedModels, fetchModels])
|
}, [setDownloadedModels, fetchModels])
|
||||||
|
|
||||||
const stopModel = useCallback(
|
|
||||||
async (modelId: string) => cortexStopModel(modelId),
|
|
||||||
[cortexStopModel]
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteModel = useCallback(
|
const deleteModel = useCallback(
|
||||||
async (modelId: string) => {
|
async (modelId: string) => {
|
||||||
await cortexDeleteModel(modelId)
|
await cortexDeleteModel(modelId)
|
||||||
@ -54,7 +48,7 @@ const useModels = () => {
|
|||||||
[cortexUpdateModel]
|
[cortexUpdateModel]
|
||||||
)
|
)
|
||||||
|
|
||||||
return { getModels, stopModel, deleteModel, updateModel }
|
return { getModels, deleteModel, updateModel }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useModels
|
export default useModels
|
||||||
|
|||||||
@ -17,6 +17,11 @@ import { currentPromptAtom, editPromptAtom } from '@/containers/Providers/Jotai'
|
|||||||
|
|
||||||
import { toaster } from '@/containers/Toast'
|
import { toaster } from '@/containers/Toast'
|
||||||
|
|
||||||
|
import { inferenceErrorAtom } from '@/screens/HubScreen2/components/InferenceErrorModal'
|
||||||
|
|
||||||
|
import { showWarningMultipleModelModalAtom } from '@/screens/HubScreen2/components/WarningMultipleModelModal'
|
||||||
|
import { concurrentModelWarningThreshold } from '@/screens/Settings/MyModels/ModelItem'
|
||||||
|
|
||||||
import useCortex from './useCortex'
|
import useCortex from './useCortex'
|
||||||
|
|
||||||
import useEngineInit from './useEngineInit'
|
import useEngineInit from './useEngineInit'
|
||||||
@ -89,6 +94,11 @@ const useSendMessage = () => {
|
|||||||
const startModel = useModelStart()
|
const startModel = useModelStart()
|
||||||
|
|
||||||
const abortControllerRef = useRef<AbortController | undefined>(undefined)
|
const abortControllerRef = useRef<AbortController | undefined>(undefined)
|
||||||
|
const didUserAborted = useRef<boolean>(false)
|
||||||
|
const setInferenceErrorAtom = useSetAtom(inferenceErrorAtom)
|
||||||
|
const setShowWarningMultipleModelModal = useSetAtom(
|
||||||
|
showWarningMultipleModelModalAtom
|
||||||
|
)
|
||||||
|
|
||||||
const validatePrerequisite = useCallback(async (): Promise<boolean> => {
|
const validatePrerequisite = useCallback(async (): Promise<boolean> => {
|
||||||
const errorTitle = 'Failed to send message'
|
const errorTitle = 'Failed to send message'
|
||||||
@ -195,10 +205,17 @@ const useSendMessage = () => {
|
|||||||
|
|
||||||
const stopInference = useCallback(() => {
|
const stopInference = useCallback(() => {
|
||||||
abortControllerRef.current?.abort()
|
abortControllerRef.current?.abort()
|
||||||
|
didUserAborted.current = true
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const summarizeThread = useCallback(
|
const summarizeThread = useCallback(
|
||||||
async (messages: string[], modelId: string, thread: Thread) => {
|
async (messages: string[], modelId: string, thread: Thread) => {
|
||||||
|
// if its a local model, and is not started, skip summarization
|
||||||
|
if (LocalEngines.find((e) => e === selectedModel!.engine) != null) {
|
||||||
|
if (!activeModels.map((model) => model.model).includes(modelId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
const maxWordForThreadTitle = 10
|
const maxWordForThreadTitle = 10
|
||||||
const summarizeMessages: ChatCompletionMessageParam[] = [
|
const summarizeMessages: ChatCompletionMessageParam[] = [
|
||||||
{
|
{
|
||||||
@ -223,6 +240,8 @@ const useSendMessage = () => {
|
|||||||
updateThreadTitle(thread.id, summarizedText)
|
updateThreadTitle(thread.id, summarizedText)
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
activeModels,
|
||||||
|
selectedModel,
|
||||||
addThreadIdShouldAnimateTitle,
|
addThreadIdShouldAnimateTitle,
|
||||||
chatCompletionNonStreaming,
|
chatCompletionNonStreaming,
|
||||||
updateThreadTitle,
|
updateThreadTitle,
|
||||||
@ -241,6 +260,11 @@ const useSendMessage = () => {
|
|||||||
if (LocalEngines.find((e) => e === selectedModel!.engine) != null) {
|
if (LocalEngines.find((e) => e === selectedModel!.engine) != null) {
|
||||||
// start model if local and not started
|
// start model if local and not started
|
||||||
if (!activeModels.map((model) => model.model).includes(modelId)) {
|
if (!activeModels.map((model) => model.model).includes(modelId)) {
|
||||||
|
if (activeModels.length >= concurrentModelWarningThreshold) {
|
||||||
|
// if max concurrent models reached, stop the first model
|
||||||
|
// display popup
|
||||||
|
setShowWarningMultipleModelModal(true)
|
||||||
|
}
|
||||||
await startModel.mutateAsync(modelId)
|
await startModel.mutateAsync(modelId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -268,7 +292,10 @@ const useSendMessage = () => {
|
|||||||
case 'assistant':
|
case 'assistant':
|
||||||
return {
|
return {
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: (msg.content[0] as TextContentBlock).text.value,
|
content:
|
||||||
|
msg.content[0] != null
|
||||||
|
? (msg.content[0] as TextContentBlock).text.value
|
||||||
|
: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
// we will need to support other roles in the future
|
// we will need to support other roles in the future
|
||||||
@ -300,6 +327,7 @@ const useSendMessage = () => {
|
|||||||
...modelOptions,
|
...modelOptions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
didUserAborted.current = false
|
||||||
abortControllerRef.current = stream.controller
|
abortControllerRef.current = stream.controller
|
||||||
|
|
||||||
const assistantMessage = await createMessage.mutateAsync({
|
const assistantMessage = await createMessage.mutateAsync({
|
||||||
@ -366,6 +394,7 @@ const useSendMessage = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
didUserAborted.current = false
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
const response = await chatCompletionNonStreaming(
|
const response = await chatCompletionNonStreaming(
|
||||||
{
|
{
|
||||||
@ -427,9 +456,18 @@ const useSendMessage = () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
// @ts-expect-error error message should be there
|
||||||
|
const errorMessage = err['message']
|
||||||
|
if (errorMessage != null) {
|
||||||
|
setInferenceErrorAtom({
|
||||||
|
engine: selectedModel!.engine,
|
||||||
|
message: errorMessage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
toaster({
|
toaster({
|
||||||
title: 'Failed to generate response',
|
title: `Error with ${selectedModel!.model}`,
|
||||||
|
description: 'Failed to generate response',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -442,13 +480,15 @@ const useSendMessage = () => {
|
|||||||
selectedModel,
|
selectedModel,
|
||||||
updateMessage,
|
updateMessage,
|
||||||
createMessage,
|
createMessage,
|
||||||
validatePrerequisite,
|
|
||||||
startModel,
|
startModel,
|
||||||
|
setInferenceErrorAtom,
|
||||||
|
validatePrerequisite,
|
||||||
updateMessageState,
|
updateMessageState,
|
||||||
addNewMessage,
|
addNewMessage,
|
||||||
chatCompletionNonStreaming,
|
chatCompletionNonStreaming,
|
||||||
chatCompletionStreaming,
|
chatCompletionStreaming,
|
||||||
setIsGeneratingResponse,
|
setIsGeneratingResponse,
|
||||||
|
setShowWarningMultipleModelModal,
|
||||||
])
|
])
|
||||||
|
|
||||||
const sendMessage = useCallback(
|
const sendMessage = useCallback(
|
||||||
@ -479,6 +519,11 @@ const useSendMessage = () => {
|
|||||||
if (LocalEngines.find((e) => e === selectedModel!.engine) != null) {
|
if (LocalEngines.find((e) => e === selectedModel!.engine) != null) {
|
||||||
// start model if local and not started
|
// start model if local and not started
|
||||||
if (!activeModels.map((model) => model.model).includes(modelId)) {
|
if (!activeModels.map((model) => model.model).includes(modelId)) {
|
||||||
|
if (activeModels.length >= concurrentModelWarningThreshold) {
|
||||||
|
// if max concurrent models reached, stop the first model
|
||||||
|
// display popup
|
||||||
|
setShowWarningMultipleModelModal(true)
|
||||||
|
}
|
||||||
await startModel.mutateAsync(modelId)
|
await startModel.mutateAsync(modelId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -502,7 +547,10 @@ const useSendMessage = () => {
|
|||||||
case 'assistant':
|
case 'assistant':
|
||||||
return {
|
return {
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: (msg.content[0] as TextContentBlock).text.value,
|
content:
|
||||||
|
msg.content[0] != null
|
||||||
|
? (msg.content[0] as TextContentBlock).text.value
|
||||||
|
: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
// we will need to support other roles in the future
|
// we will need to support other roles in the future
|
||||||
@ -536,7 +584,7 @@ const useSendMessage = () => {
|
|||||||
top_p: selectedModel!.top_p ?? 1,
|
top_p: selectedModel!.top_p ?? 1,
|
||||||
...modelOptions,
|
...modelOptions,
|
||||||
})
|
})
|
||||||
|
didUserAborted.current = false
|
||||||
abortControllerRef.current = stream.controller
|
abortControllerRef.current = stream.controller
|
||||||
|
|
||||||
const assistantMessage = await createMessage.mutateAsync({
|
const assistantMessage = await createMessage.mutateAsync({
|
||||||
@ -606,7 +654,10 @@ const useSendMessage = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
didUserAborted.current = false
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
|
abortControllerRef.current = abortController
|
||||||
|
|
||||||
const response = await chatCompletionNonStreaming(
|
const response = await chatCompletionNonStreaming(
|
||||||
{
|
{
|
||||||
messages,
|
messages,
|
||||||
@ -663,7 +714,7 @@ const useSendMessage = () => {
|
|||||||
content: responseMessage.content,
|
content: responseMessage.content,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
abortControllerRef.current = undefined
|
||||||
if (responseMessage) {
|
if (responseMessage) {
|
||||||
setIsGeneratingResponse(false)
|
setIsGeneratingResponse(false)
|
||||||
}
|
}
|
||||||
@ -672,17 +723,27 @@ const useSendMessage = () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
// @ts-expect-error error message should be there
|
||||||
|
const errorMessage = err['message']
|
||||||
|
if (errorMessage != null) {
|
||||||
|
setInferenceErrorAtom({
|
||||||
|
engine: selectedModel!.engine,
|
||||||
|
message: errorMessage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
setIsGeneratingResponse(false)
|
setIsGeneratingResponse(false)
|
||||||
shouldSummarize = false
|
shouldSummarize = false
|
||||||
|
|
||||||
toaster({
|
toaster({
|
||||||
title: 'Failed to generate response',
|
title: `Error with ${selectedModel!.model}`,
|
||||||
|
description: 'Failed to generate response',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!shouldSummarize) return
|
if (!shouldSummarize || didUserAborted.current === true) return
|
||||||
// summarize if needed
|
// summarize if needed
|
||||||
const textMessages: string[] = messages
|
const textMessages: string[] = messages
|
||||||
.map((msg) => {
|
.map((msg) => {
|
||||||
@ -702,16 +763,18 @@ const useSendMessage = () => {
|
|||||||
selectedModel,
|
selectedModel,
|
||||||
updateMessage,
|
updateMessage,
|
||||||
createMessage,
|
createMessage,
|
||||||
|
startModel,
|
||||||
|
setInferenceErrorAtom,
|
||||||
validatePrerequisite,
|
validatePrerequisite,
|
||||||
setCurrentPrompt,
|
setCurrentPrompt,
|
||||||
setEditPrompt,
|
setEditPrompt,
|
||||||
setIsGeneratingResponse,
|
setIsGeneratingResponse,
|
||||||
updateMessageState,
|
updateMessageState,
|
||||||
addNewMessage,
|
addNewMessage,
|
||||||
startModel,
|
|
||||||
chatCompletionNonStreaming,
|
chatCompletionNonStreaming,
|
||||||
chatCompletionStreaming,
|
chatCompletionStreaming,
|
||||||
summarizeThread,
|
summarizeThread,
|
||||||
|
setShowWarningMultipleModelModal,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
"yaml": "^2.4.5",
|
"yaml": "^2.4.5",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"embla-carousel-react": "^8.1.5",
|
"embla-carousel-react": "^8.1.5",
|
||||||
"cortexso-node": "^0.0.4",
|
"@cortexso/cortex.js": "^0.1.6",
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
"@janhq/core": "link:./core",
|
"@janhq/core": "link:./core",
|
||||||
"@janhq/joi": "link:./joi",
|
"@janhq/joi": "link:./joi",
|
||||||
@ -44,7 +44,6 @@
|
|||||||
"sass": "^1.69.4",
|
"sass": "^1.69.4",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"tailwindcss": "3.3.5",
|
"tailwindcss": "3.3.5",
|
||||||
"uuid": "^9.0.1",
|
|
||||||
"use-debounce": "^10.0.0"
|
"use-debounce": "^10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
45
web/screens/HubScreen2/components/InferenceErrorModal.tsx
Normal file
45
web/screens/HubScreen2/components/InferenceErrorModal.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Fragment, useCallback } from 'react'
|
||||||
|
|
||||||
|
import { LlmEngine } from '@janhq/core'
|
||||||
|
import { Button, Modal, ModalClose } from '@janhq/joi'
|
||||||
|
import { atom, useAtom } from 'jotai'
|
||||||
|
|
||||||
|
export type InferenceError = {
|
||||||
|
message: string
|
||||||
|
engine?: LlmEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
export const inferenceErrorAtom = atom<InferenceError | undefined>(undefined)
|
||||||
|
|
||||||
|
const InferenceErrorModal: React.FC = () => {
|
||||||
|
const [inferenceError, setInferenceError] = useAtom(inferenceErrorAtom)
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
setInferenceError(undefined)
|
||||||
|
}, [setInferenceError])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
hideClose
|
||||||
|
open={inferenceError != null}
|
||||||
|
onOpenChange={onClose}
|
||||||
|
title={'Inference error'}
|
||||||
|
content={
|
||||||
|
<Fragment>
|
||||||
|
<p className="text-[hsla(var(--text-secondary))]">
|
||||||
|
{inferenceError?.message}
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 flex justify-end">
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button onClick={onClose} autoFocus theme="destructive">
|
||||||
|
OK
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InferenceErrorModal
|
||||||
@ -2,7 +2,7 @@ import React, { useCallback, useState } from 'react'
|
|||||||
|
|
||||||
import { Button, Input } from '@janhq/joi'
|
import { Button, Input } from '@janhq/joi'
|
||||||
import { useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
import { SearchIcon } from 'lucide-react'
|
import { ImportIcon, SearchIcon } from 'lucide-react'
|
||||||
import { FoldersIcon } from 'lucide-react'
|
import { FoldersIcon } from 'lucide-react'
|
||||||
import { useDebouncedCallback } from 'use-debounce'
|
import { useDebouncedCallback } from 'use-debounce'
|
||||||
|
|
||||||
@ -10,6 +10,8 @@ import { toaster } from '@/containers/Toast'
|
|||||||
|
|
||||||
import { useGetHFRepoData } from '@/hooks/useGetHFRepoData'
|
import { useGetHFRepoData } from '@/hooks/useGetHFRepoData'
|
||||||
|
|
||||||
|
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
||||||
|
|
||||||
import { MainViewState, mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
import { MainViewState, mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||||
import {
|
import {
|
||||||
importHuggingFaceModelStageAtom,
|
importHuggingFaceModelStageAtom,
|
||||||
@ -26,6 +28,7 @@ const ModelSearchBar: React.FC<Props> = ({ onSearchChanged }) => {
|
|||||||
const { getHfRepoData } = useGetHFRepoData()
|
const { getHfRepoData } = useGetHFRepoData()
|
||||||
const setMainViewState = useSetAtom(mainViewStateAtom)
|
const setMainViewState = useSetAtom(mainViewStateAtom)
|
||||||
const setSelectedSetting = useSetAtom(selectedSettingAtom)
|
const setSelectedSetting = useSetAtom(selectedSettingAtom)
|
||||||
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
|
||||||
const setImportingHuggingFaceRepoData = useSetAtom(
|
const setImportingHuggingFaceRepoData = useSetAtom(
|
||||||
importingHuggingFaceRepoDataAtom
|
importingHuggingFaceRepoDataAtom
|
||||||
@ -34,6 +37,10 @@ const ModelSearchBar: React.FC<Props> = ({ onSearchChanged }) => {
|
|||||||
importHuggingFaceModelStageAtom
|
importHuggingFaceModelStageAtom
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onImportModelClick = useCallback(() => {
|
||||||
|
setImportModelStage('SELECTING_MODEL')
|
||||||
|
}, [setImportModelStage])
|
||||||
|
|
||||||
const debounced = useDebouncedCallback(async (searchText: string) => {
|
const debounced = useDebouncedCallback(async (searchText: string) => {
|
||||||
if (searchText.indexOf('/') === -1) {
|
if (searchText.indexOf('/') === -1) {
|
||||||
// If we don't find / in the text, perform a local search
|
// If we don't find / in the text, perform a local search
|
||||||
@ -90,6 +97,14 @@ const ModelSearchBar: React.FC<Props> = ({ onSearchChanged }) => {
|
|||||||
<FoldersIcon size={16} />
|
<FoldersIcon size={16} />
|
||||||
<span>My models</span>
|
<span>My models</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="flex gap-2 bg-[hsla(var(--app-bg))] text-[hsla(var(--text-primary))]"
|
||||||
|
theme="ghost"
|
||||||
|
onClick={onImportModelClick}
|
||||||
|
>
|
||||||
|
<ImportIcon size={16} />
|
||||||
|
<span>Import model</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,9 @@ const SetUpApiKeyModal: React.FC = () => {
|
|||||||
alert('Does not have engine')
|
alert('Does not have engine')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const normalizedApiKey = apiKey.trim().replaceAll('*', '')
|
||||||
|
if (normalizedApiKey.length === 0) return
|
||||||
|
|
||||||
updateEngineConfig.mutate({
|
updateEngineConfig.mutate({
|
||||||
engine: remoteEngine,
|
engine: remoteEngine,
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { Fragment, useCallback, useMemo } from 'react'
|
||||||
|
|
||||||
|
import { Button, Modal, ModalClose } from '@janhq/joi'
|
||||||
|
import { atom, useAtom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { activeModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
export const showWarningMultipleModelModalAtom = atom<boolean>(false)
|
||||||
|
|
||||||
|
const WarningMultipleModelModal: React.FC = () => {
|
||||||
|
const [showWarningMultipleModelModal, setShowWarningMultipleModelModal] =
|
||||||
|
useAtom(showWarningMultipleModelModalAtom)
|
||||||
|
const activeModels = useAtomValue(activeModelsAtom)
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
setShowWarningMultipleModelModal(false)
|
||||||
|
}, [setShowWarningMultipleModelModal])
|
||||||
|
|
||||||
|
const title = useMemo(
|
||||||
|
() => `${activeModels.length} models running`,
|
||||||
|
[activeModels]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
hideClose
|
||||||
|
open={showWarningMultipleModelModal}
|
||||||
|
onOpenChange={onClose}
|
||||||
|
title={title}
|
||||||
|
content={
|
||||||
|
<Fragment>
|
||||||
|
<p className="text-[hsla(var(--text-secondary))]">
|
||||||
|
This may affect performance. Please review them via System Monitor
|
||||||
|
in the lower right conner of Jan app.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 flex justify-end">
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button onClick={onClose} autoFocus theme="destructive">
|
||||||
|
OK
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WarningMultipleModelModal
|
||||||
@ -10,7 +10,7 @@ import { useAtom, useAtomValue } from 'jotai'
|
|||||||
|
|
||||||
import { toaster } from '@/containers/Toast'
|
import { toaster } from '@/containers/Toast'
|
||||||
|
|
||||||
import useModels from '@/hooks/useModels'
|
import useModelStop from '@/hooks/useModelStop'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
import { useSettings } from '@/hooks/useSettings'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -53,7 +53,7 @@ const Advanced = () => {
|
|||||||
const { readSettings, saveSettings } = useSettings()
|
const { readSettings, saveSettings } = useSettings()
|
||||||
const activeModels = useAtomValue(activeModelsAtom)
|
const activeModels = useAtomValue(activeModelsAtom)
|
||||||
// const [open, setOpen] = useState(false)
|
// const [open, setOpen] = useState(false)
|
||||||
const { stopModel } = useModels()
|
const stopModel = useModelStop()
|
||||||
|
|
||||||
// const selectedGpu = gpuList
|
// const selectedGpu = gpuList
|
||||||
// .filter((x) => gpusInUse.includes(x.id))
|
// .filter((x) => gpusInUse.includes(x.id))
|
||||||
@ -92,7 +92,7 @@ const Advanced = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for (const model of activeModels) {
|
for (const model of activeModels) {
|
||||||
await stopModel(model.model)
|
await stopModel.mutateAsync(model.model)
|
||||||
}
|
}
|
||||||
|
|
||||||
setVulkanEnabled(e)
|
setVulkanEnabled(e)
|
||||||
|
|||||||
@ -1,18 +1,22 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
import { SelectFileOption } from '@janhq/core'
|
import { ImportingModel, SelectFileOption } from '@janhq/core'
|
||||||
import { Button, Modal } from '@janhq/joi'
|
import { Button, Modal } from '@janhq/joi'
|
||||||
import { useSetAtom, useAtomValue } from 'jotai'
|
import { useSetAtom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import useImportModel, {
|
import { snackbar } from '@/containers/Toast'
|
||||||
|
|
||||||
|
import {
|
||||||
setImportModelStageAtom,
|
setImportModelStageAtom,
|
||||||
getImportModelStageAtom,
|
getImportModelStageAtom,
|
||||||
} from '@/hooks/useImportModel'
|
} from '@/hooks/useImportModel'
|
||||||
|
|
||||||
|
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
const ChooseWhatToImportModal = () => {
|
const ChooseWhatToImportModal = () => {
|
||||||
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
const setImportingModels = useSetAtom(importingModelsAtom)
|
||||||
const importModelStage = useAtomValue(getImportModelStageAtom)
|
const importModelStage = useAtomValue(getImportModelStageAtom)
|
||||||
const { sanitizeFilePaths } = useImportModel()
|
|
||||||
|
|
||||||
const onImportFileClick = useCallback(async () => {
|
const onImportFileClick = useCallback(async () => {
|
||||||
const options: SelectFileOption = {
|
const options: SelectFileOption = {
|
||||||
@ -24,10 +28,36 @@ const ChooseWhatToImportModal = () => {
|
|||||||
{ name: 'All Files', extensions: ['*'] },
|
{ name: 'All Files', extensions: ['*'] },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
const filePaths = await window.core?.api?.selectFiles(options)
|
const filePaths: string[] = await window.core?.api?.selectFiles(options)
|
||||||
if (!filePaths || filePaths.length === 0) return
|
if (!filePaths || filePaths.length === 0) return
|
||||||
sanitizeFilePaths(filePaths)
|
|
||||||
}, [sanitizeFilePaths])
|
const importingModels: ImportingModel[] = filePaths
|
||||||
|
.filter((path) => path.endsWith('.gguf'))
|
||||||
|
.map((path) => {
|
||||||
|
const normalizedPath = isWindows ? path.replace(/\\/g, '/') : path
|
||||||
|
|
||||||
|
return {
|
||||||
|
importId: normalizedPath,
|
||||||
|
modelId: undefined,
|
||||||
|
name: normalizedPath.replace('.gguf', ''),
|
||||||
|
description: '',
|
||||||
|
path: path,
|
||||||
|
tags: [],
|
||||||
|
size: 0,
|
||||||
|
status: 'PREPARING',
|
||||||
|
format: 'gguf',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (importingModels.length < 1) {
|
||||||
|
snackbar({
|
||||||
|
description: `Only files with .gguf extension can be imported.`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setImportingModels(importingModels)
|
||||||
|
setImportModelStage('MODEL_SELECTED')
|
||||||
|
}, [setImportingModels, setImportModelStage])
|
||||||
|
|
||||||
const onImportFolderClick = useCallback(async () => {
|
const onImportFolderClick = useCallback(async () => {
|
||||||
const options: SelectFileOption = {
|
const options: SelectFileOption = {
|
||||||
@ -36,10 +66,37 @@ const ChooseWhatToImportModal = () => {
|
|||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
selectDirectory: true,
|
selectDirectory: true,
|
||||||
}
|
}
|
||||||
const filePaths = await window.core?.api?.selectFiles(options)
|
const filePaths: string[] = await window.core?.api?.selectFiles(options)
|
||||||
if (!filePaths || filePaths.length === 0) return
|
if (!filePaths || filePaths.length === 0) return
|
||||||
sanitizeFilePaths(filePaths)
|
|
||||||
}, [sanitizeFilePaths])
|
console.log('filePaths folder', filePaths)
|
||||||
|
const importingModels: ImportingModel[] = filePaths
|
||||||
|
.filter((path) => path.endsWith('.gguf'))
|
||||||
|
.map((path) => {
|
||||||
|
const normalizedPath = isWindows ? path.replace(/\\/g, '/') : path
|
||||||
|
|
||||||
|
return {
|
||||||
|
importId: normalizedPath,
|
||||||
|
modelId: undefined,
|
||||||
|
name: normalizedPath.replace('.gguf', ''),
|
||||||
|
description: '',
|
||||||
|
path: path,
|
||||||
|
tags: [],
|
||||||
|
size: 0,
|
||||||
|
status: 'PREPARING',
|
||||||
|
format: 'gguf',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (importingModels.length < 1) {
|
||||||
|
snackbar({
|
||||||
|
description: `Only files with .gguf extension can be imported.`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setImportingModels(importingModels)
|
||||||
|
setImportModelStage('MODEL_SELECTED')
|
||||||
|
}, [setImportingModels, setImportModelStage])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@ -13,6 +13,11 @@ import useEngineQuery from '@/hooks/useEngineQuery'
|
|||||||
|
|
||||||
import LoadingIndicator from '@/screens/HubScreen2/components/LoadingIndicator'
|
import LoadingIndicator from '@/screens/HubScreen2/components/LoadingIndicator'
|
||||||
|
|
||||||
|
const getStatusTitle = (status: string) => {
|
||||||
|
const normalized = status.charAt(0).toUpperCase() + status.slice(1)
|
||||||
|
return normalized.replaceAll('_', ' ')
|
||||||
|
}
|
||||||
|
|
||||||
const EngineSetting: React.FC = () => {
|
const EngineSetting: React.FC = () => {
|
||||||
const { isLoading, data } = useEngineQuery()
|
const { isLoading, data } = useEngineQuery()
|
||||||
|
|
||||||
@ -56,7 +61,7 @@ const EngineSetting: React.FC = () => {
|
|||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{engineStatus.version}
|
{engineStatus.version}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{engineStatus.status}</TableCell>
|
<TableCell>{getStatusTitle(engineStatus.status)}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -20,12 +20,12 @@ const importOptions: ModelImportOption[] = [
|
|||||||
description:
|
description:
|
||||||
'You maintain your model files outside of Jan. Keeping your files where they are, and Jan will create a smart link to them.',
|
'You maintain your model files outside of Jan. Keeping your files where they are, and Jan will create a smart link to them.',
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
type: 'MOVE_BINARY_FILE',
|
// type: 'MOVE_BINARY_FILE',
|
||||||
title: 'Move model binary file',
|
// title: 'Move model binary file',
|
||||||
description:
|
// description:
|
||||||
'Jan will move your model binary file from your current folder into Jan Data Folder.',
|
// 'Jan will move your model binary file from your current folder into Jan Data Folder.',
|
||||||
},
|
// },
|
||||||
]
|
]
|
||||||
|
|
||||||
const ImportModelOptionModal = () => {
|
const ImportModelOptionModal = () => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import { Check, Pencil } from 'lucide-react'
|
import { Check } from 'lucide-react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onEditModelClick: () => void
|
onEditModelClick: () => void
|
||||||
@ -9,6 +9,8 @@ type Props = {
|
|||||||
const ImportSuccessIcon: React.FC<Props> = ({ onEditModelClick }) => {
|
const ImportSuccessIcon: React.FC<Props> = ({ onEditModelClick }) => {
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
|
||||||
|
console.log(isHovered, onEditModelClick)
|
||||||
|
|
||||||
const onMouseOver = () => {
|
const onMouseOver = () => {
|
||||||
setIsHovered(true)
|
setIsHovered(true)
|
||||||
}
|
}
|
||||||
@ -19,34 +21,34 @@ const ImportSuccessIcon: React.FC<Props> = ({ onEditModelClick }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
|
<div onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
|
||||||
{isHovered ? (
|
{/* {isHovered ? (
|
||||||
<EditIcon onEditModelClick={onEditModelClick} />
|
<EditIcon onEditModelClick={onEditModelClick} />
|
||||||
) : (
|
) : ( */}
|
||||||
<SuccessIcon />
|
<SuccessIcon />
|
||||||
)}
|
{/* )} */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SuccessIcon = React.memo(() => (
|
const SuccessIcon = React.memo(() => (
|
||||||
<div className="bg-primary flex h-8 w-8 items-center justify-center rounded-full text-white">
|
<div className="bg-primary flex h-8 w-8 items-center justify-center rounded-full">
|
||||||
<Check size={20} />
|
<Check size={20} />
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|
||||||
const EditIcon: React.FC<Props> = React.memo(({ onEditModelClick }) => {
|
// const EditIcon: React.FC<Props> = React.memo(({ onEditModelClick }) => {
|
||||||
const onClick = useCallback(() => {
|
// const onClick = useCallback(() => {
|
||||||
onEditModelClick()
|
// onEditModelClick()
|
||||||
}, [onEditModelClick])
|
// }, [onEditModelClick])
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div
|
// <div
|
||||||
className="bg-secondary flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg"
|
// className="bg-secondary flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg"
|
||||||
onClick={onClick}
|
// onClick={onClick}
|
||||||
>
|
// >
|
||||||
<Pencil size={20} />
|
// <Pencil size={20} />
|
||||||
</div>
|
// </div>
|
||||||
)
|
// )
|
||||||
})
|
// })
|
||||||
|
|
||||||
export default ImportSuccessIcon
|
export default ImportSuccessIcon
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
|
|
||||||
import { ImportingModel } from '@janhq/core'
|
import { ImportingModel } from '@janhq/core'
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
|
|
||||||
import { AlertCircle } from 'lucide-react'
|
import { AlertCircle } from 'lucide-react'
|
||||||
|
|
||||||
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
|
||||||
|
|
||||||
import { toGibibytes } from '@/utils/converter'
|
import { toGibibytes } from '@/utils/converter'
|
||||||
|
|
||||||
import { editingModelIdAtom } from '../EditModelInfoModal'
|
|
||||||
import ImportInProgressIcon from '../ImportInProgressIcon'
|
import ImportInProgressIcon from '../ImportInProgressIcon'
|
||||||
import ImportSuccessIcon from '../ImportSuccessIcon'
|
import ImportSuccessIcon from '../ImportSuccessIcon'
|
||||||
|
|
||||||
@ -18,16 +14,13 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ImportingModelItem = ({ model }: Props) => {
|
const ImportingModelItem = ({ model }: Props) => {
|
||||||
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
|
||||||
const setEditingModelId = useSetAtom(editingModelIdAtom)
|
|
||||||
|
|
||||||
const onEditModelInfoClick = useCallback(() => {
|
const onEditModelInfoClick = useCallback(() => {
|
||||||
setEditingModelId(model.importId)
|
// setEditingModelId(model.importId)
|
||||||
setImportModelStage('EDIT_MODEL_INFO')
|
// setImportModelStage('EDIT_MODEL_INFO')
|
||||||
}, [setImportModelStage, setEditingModelId, model.importId])
|
}, [])
|
||||||
|
|
||||||
const onDeleteModelClick = useCallback(() => {}, [])
|
const onDeleteModelClick = useCallback(() => {}, [])
|
||||||
|
console.log('namh model', model)
|
||||||
const displayStatus = useMemo(() => {
|
const displayStatus = useMemo(() => {
|
||||||
if (model.status === 'FAILED') {
|
if (model.status === 'FAILED') {
|
||||||
return 'Failed'
|
return 'Failed'
|
||||||
|
|||||||
@ -1,50 +1,47 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import { Button, Modal } from '@janhq/joi'
|
import { Modal } from '@janhq/joi'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { AlertCircle } from 'lucide-react'
|
import { AlertCircle } from 'lucide-react'
|
||||||
|
|
||||||
|
import useCortex from '@/hooks/useCortex'
|
||||||
import {
|
import {
|
||||||
getImportModelStageAtom,
|
getImportModelStageAtom,
|
||||||
setImportModelStageAtom,
|
setImportModelStageAtom,
|
||||||
} from '@/hooks/useImportModel'
|
} from '@/hooks/useImportModel'
|
||||||
|
|
||||||
import { openFileTitle } from '@/utils/titleUtils'
|
|
||||||
|
|
||||||
import ImportingModelItem from './ImportingModelItem'
|
import ImportingModelItem from './ImportingModelItem'
|
||||||
|
|
||||||
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
|
|
||||||
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
const ImportingModelModal = () => {
|
const ImportingModelModal = () => {
|
||||||
|
const { downloadModel } = useCortex()
|
||||||
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
const importingModels = useAtomValue(importingModelsAtom)
|
const importingModels = useAtomValue(importingModelsAtom)
|
||||||
const importModelStage = useAtomValue(getImportModelStageAtom)
|
const importModelStage = useAtomValue(getImportModelStageAtom)
|
||||||
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
|
||||||
const janDataFolder = useAtomValue(janDataFolderPathAtom)
|
|
||||||
|
|
||||||
const [modelFolder, setModelFolder] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getModelPath = async () => {
|
|
||||||
// const modelPath = await joinPath([janDataFolder, 'models'])
|
|
||||||
setModelFolder('')
|
|
||||||
}
|
|
||||||
getModelPath()
|
|
||||||
}, [janDataFolder])
|
|
||||||
|
|
||||||
const finishedImportModel = importingModels.filter(
|
const finishedImportModel = importingModels.filter(
|
||||||
(model) => model.status === 'IMPORTED'
|
(model) => model.status === 'IMPORTED'
|
||||||
).length
|
).length
|
||||||
|
|
||||||
const onOpenModelFolderClick = useCallback(
|
useEffect(() => {
|
||||||
() => {
|
const importModels = async () => {
|
||||||
// openFileExplorer(modelFolder)
|
for (const model of importingModels) {
|
||||||
},
|
await downloadModel(model.path)
|
||||||
[
|
// const parsedResult = await result?.json()
|
||||||
/*modelFolder*/
|
// if (
|
||||||
]
|
// parsedResult['message'] &&
|
||||||
)
|
// parsedResult['message'] === 'Download model started successfully.'
|
||||||
|
// ) {
|
||||||
|
// // update importingModels
|
||||||
|
// }
|
||||||
|
// console.log(`NamH result ${JSON.stringify(parsedResult)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
importModels()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [downloadModel])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@ -54,7 +51,7 @@ const ImportingModelModal = () => {
|
|||||||
content={
|
content={
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row items-center space-x-2 pb-3">
|
<div className="flex flex-row items-center space-x-2 pb-3">
|
||||||
<label className="text-[hsla(var(--text-secondary)] text-xs">
|
{/* <label className="text-[hsla(var(--text-secondary)] text-xs">
|
||||||
{modelFolder}
|
{modelFolder}
|
||||||
</label>
|
</label>
|
||||||
<Button
|
<Button
|
||||||
@ -64,10 +61,10 @@ const ImportingModelModal = () => {
|
|||||||
onClick={onOpenModelFolderClick}
|
onClick={onOpenModelFolderClick}
|
||||||
>
|
>
|
||||||
{openFileTitle()}
|
{openFileTitle()}
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="mb-2 space-y-3">
|
||||||
{importingModels.map((model) => (
|
{importingModels.map((model) => (
|
||||||
<ImportingModelItem key={model.importId} model={model} />
|
<ImportingModelItem key={model.importId} model={model} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { memo, useCallback, useMemo, useState } from 'react'
|
|||||||
import { LocalEngines, Model } from '@janhq/core'
|
import { LocalEngines, Model } from '@janhq/core'
|
||||||
import { Badge, Button, useClickOutside } from '@janhq/joi'
|
import { Badge, Button, useClickOutside } from '@janhq/joi'
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import {
|
import {
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
@ -13,22 +13,32 @@ import {
|
|||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import useModelStart from '@/hooks/useModelStart'
|
import useModelStart from '@/hooks/useModelStart'
|
||||||
|
import useModelStop from '@/hooks/useModelStop'
|
||||||
import useModels from '@/hooks/useModels'
|
import useModels from '@/hooks/useModels'
|
||||||
|
|
||||||
|
import { showWarningMultipleModelModalAtom } from '@/screens/HubScreen2/components/WarningMultipleModelModal'
|
||||||
|
|
||||||
import { activeModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { activeModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
model: Model
|
model: Model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If more than this number of models are running, show a warning modal.
|
||||||
|
export const concurrentModelWarningThreshold = 2
|
||||||
|
|
||||||
const ModelItem: React.FC<Props> = ({ model }) => {
|
const ModelItem: React.FC<Props> = ({ model }) => {
|
||||||
const activeModels = useAtomValue(activeModelsAtom)
|
const activeModels = useAtomValue(activeModelsAtom)
|
||||||
const startModel = useModelStart()
|
const startModel = useModelStart()
|
||||||
|
const stopModel = useModelStop()
|
||||||
const [more, setMore] = useState(false)
|
const [more, setMore] = useState(false)
|
||||||
const { stopModel, deleteModel } = useModels()
|
const { deleteModel } = useModels()
|
||||||
|
|
||||||
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
|
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
|
||||||
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
|
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
|
||||||
|
const setShowWarningMultipleModelModal = useSetAtom(
|
||||||
|
showWarningMultipleModelModalAtom
|
||||||
|
)
|
||||||
useClickOutside(() => setMore(false), null, [menu, toggle])
|
useClickOutside(() => setMore(false), null, [menu, toggle])
|
||||||
|
|
||||||
const isActive = useMemo(
|
const isActive = useMemo(
|
||||||
@ -39,17 +49,30 @@ const ModelItem: React.FC<Props> = ({ model }) => {
|
|||||||
const onModelActionClick = useCallback(
|
const onModelActionClick = useCallback(
|
||||||
(modelId: string) => {
|
(modelId: string) => {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
stopModel(modelId)
|
// if model already active, stop it
|
||||||
} else {
|
stopModel.mutate(modelId)
|
||||||
startModel.mutate(modelId)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeModels.length >= concurrentModelWarningThreshold) {
|
||||||
|
// if max concurrent models reached, stop the first model
|
||||||
|
// display popup
|
||||||
|
setShowWarningMultipleModelModal(true)
|
||||||
|
}
|
||||||
|
startModel.mutate(modelId)
|
||||||
},
|
},
|
||||||
[isActive, startModel, stopModel]
|
[
|
||||||
|
isActive,
|
||||||
|
startModel,
|
||||||
|
stopModel,
|
||||||
|
activeModels.length,
|
||||||
|
setShowWarningMultipleModelModal,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onDeleteModelClicked = useCallback(
|
const onDeleteModelClicked = useCallback(
|
||||||
async (modelId: string) => {
|
async (modelId: string) => {
|
||||||
await stopModel(modelId)
|
await stopModel.mutateAsync(modelId)
|
||||||
await deleteModel(modelId)
|
await deleteModel(modelId)
|
||||||
},
|
},
|
||||||
[stopModel, deleteModel]
|
[stopModel, deleteModel]
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { LlmEngines } from '@janhq/core'
|
|||||||
import { Button, ScrollArea } from '@janhq/joi'
|
import { Button, ScrollArea } from '@janhq/joi'
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { UploadCloudIcon } from 'lucide-react'
|
import { ImportIcon, UploadCloudIcon } from 'lucide-react'
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
@ -16,6 +16,8 @@ import ModelSearch from '@/containers/ModelSearch'
|
|||||||
|
|
||||||
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
|
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
|
||||||
|
|
||||||
|
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
||||||
|
|
||||||
import ModelItem from './ModelItem'
|
import ModelItem from './ModelItem'
|
||||||
|
|
||||||
import { MainViewState, mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
import { MainViewState, mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||||
@ -26,6 +28,11 @@ const MyModels = () => {
|
|||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
const { onDropModels } = useDropModelBinaries()
|
const { onDropModels } = useDropModelBinaries()
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
|
||||||
|
const onImportModelClick = useCallback(() => {
|
||||||
|
setImportModelStage('SELECTING_MODEL')
|
||||||
|
}, [setImportModelStage])
|
||||||
|
|
||||||
const filteredDownloadedModels = useMemo(
|
const filteredDownloadedModels = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -75,14 +82,14 @@ const MyModels = () => {
|
|||||||
<div className="w-full sm:w-[300px]">
|
<div className="w-full sm:w-[300px]">
|
||||||
<ModelSearch onSearchLocal={onSearchChange} />
|
<ModelSearch onSearchLocal={onSearchChange} />
|
||||||
</div>
|
</div>
|
||||||
{/* <Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
theme="ghost"
|
theme="ghost"
|
||||||
onClick={onImportModelClick}
|
onClick={onImportModelClick}
|
||||||
>
|
>
|
||||||
<UploadIcon size={16} className="mr-2" />
|
<ImportIcon size={16} className="mr-2" />
|
||||||
<p>Import Model</p>
|
<p>Import Model</p>
|
||||||
</Button> */}
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!filteredDownloadedModels.length ? (
|
{!filteredDownloadedModels.length ? (
|
||||||
|
|||||||
@ -1,39 +1,109 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useDropzone } from 'react-dropzone'
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
|
||||||
|
import { ImportingModel, SelectFileOption } from '@janhq/core'
|
||||||
import { Modal } from '@janhq/joi'
|
import { Modal } from '@janhq/joi'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { UploadCloudIcon } from 'lucide-react'
|
import { UploadCloudIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
import { snackbar } from '@/containers/Toast'
|
||||||
|
|
||||||
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
|
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
|
||||||
import {
|
import {
|
||||||
getImportModelStageAtom,
|
getImportModelStageAtom,
|
||||||
setImportModelStageAtom,
|
setImportModelStageAtom,
|
||||||
} from '@/hooks/useImportModel'
|
} from '@/hooks/useImportModel'
|
||||||
|
|
||||||
const SelectingModelModal = () => {
|
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
const SelectingModelModal: React.FC = () => {
|
||||||
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
|
const setImportingModels = useSetAtom(importingModelsAtom)
|
||||||
const importModelStage = useAtomValue(getImportModelStageAtom)
|
const importModelStage = useAtomValue(getImportModelStageAtom)
|
||||||
const { onDropModels } = useDropModelBinaries()
|
const { onDropModels } = useDropModelBinaries()
|
||||||
// const { sanitizeFilePaths } = useImportModel()
|
|
||||||
|
const onImportFileWindowsClick = useCallback(async () => {
|
||||||
|
const options: SelectFileOption = {
|
||||||
|
title: 'Select model files',
|
||||||
|
buttonLabel: 'Select',
|
||||||
|
allowMultiple: true,
|
||||||
|
filters: [
|
||||||
|
{ name: 'GGUF Files', extensions: ['gguf'] },
|
||||||
|
{ name: 'All Files', extensions: ['*'] },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const filePaths: string[] = await window.core?.api?.selectFiles(options)
|
||||||
|
if (!filePaths || filePaths.length === 0) return
|
||||||
|
|
||||||
|
const importingModels: ImportingModel[] = filePaths
|
||||||
|
.filter((path) => path.endsWith('.gguf'))
|
||||||
|
.map((path) => {
|
||||||
|
const normalizedPath = isWindows ? path.replace(/\\/g, '/') : path
|
||||||
|
|
||||||
|
return {
|
||||||
|
importId: normalizedPath,
|
||||||
|
modelId: undefined,
|
||||||
|
name: normalizedPath.replace('.gguf', ''),
|
||||||
|
description: '',
|
||||||
|
path: path,
|
||||||
|
tags: [],
|
||||||
|
size: 0,
|
||||||
|
status: 'PREPARING',
|
||||||
|
format: 'gguf',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (importingModels.length < 1) {
|
||||||
|
snackbar({
|
||||||
|
description: `Only files with .gguf extension can be imported.`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setImportingModels(importingModels)
|
||||||
|
setImportModelStage('MODEL_SELECTED')
|
||||||
|
}, [setImportingModels, setImportModelStage])
|
||||||
|
|
||||||
const onSelectFileClick = useCallback(async () => {
|
const onSelectFileClick = useCallback(async () => {
|
||||||
// const platform = (await systemInformation()).osInfo?.platform
|
if (isWindows) {
|
||||||
// if (platform === 'win32') {
|
return onImportFileWindowsClick()
|
||||||
// setImportModelStage('CHOOSE_WHAT_TO_IMPORT')
|
}
|
||||||
// return
|
const options: SelectFileOption = {
|
||||||
// }
|
title: 'Select model folders',
|
||||||
// const options: SelectFileOption = {
|
buttonLabel: 'Select',
|
||||||
// title: 'Select model folders',
|
allowMultiple: true,
|
||||||
// buttonLabel: 'Select',
|
selectDirectory: true,
|
||||||
// allowMultiple: true,
|
}
|
||||||
// selectDirectory: true,
|
const filePaths: string[] = await window.core?.api?.selectFiles(options)
|
||||||
// }
|
if (!filePaths || filePaths.length === 0) return
|
||||||
// const filePaths = await window.core?.api?.selectFiles(options)
|
|
||||||
// if (!filePaths || filePaths.length === 0) return
|
const importingModels: ImportingModel[] = filePaths
|
||||||
// sanitizeFilePaths(filePaths)
|
.filter((path) => path.endsWith('.gguf'))
|
||||||
}, [])
|
.map((path) => {
|
||||||
|
const normalizedPath = isWindows ? path.replace(/\\/g, '/') : path
|
||||||
|
|
||||||
|
return {
|
||||||
|
importId: normalizedPath,
|
||||||
|
modelId: undefined,
|
||||||
|
name: normalizedPath.replace('.gguf', ''),
|
||||||
|
description: '',
|
||||||
|
path: path,
|
||||||
|
tags: [],
|
||||||
|
size: 0,
|
||||||
|
status: 'PREPARING',
|
||||||
|
format: 'gguf',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (importingModels.length < 1) {
|
||||||
|
snackbar({
|
||||||
|
description: `Only files with .gguf extension can be imported.`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setImportingModels(importingModels)
|
||||||
|
setImportModelStage('MODEL_SELECTED')
|
||||||
|
}, [setImportModelStage, setImportingModels, onImportFileWindowsClick])
|
||||||
|
|
||||||
const { isDragActive, getRootProps } = useDropzone({
|
const { isDragActive, getRootProps } = useDropzone({
|
||||||
noClick: true,
|
noClick: true,
|
||||||
@ -52,9 +122,7 @@ const SelectingModelModal = () => {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={importModelStage === 'SELECTING_MODEL'}
|
open={importModelStage === 'SELECTING_MODEL'}
|
||||||
onOpenChange={() => {
|
onOpenChange={() => setImportModelStage('NONE')}
|
||||||
setImportModelStage('NONE')
|
|
||||||
}}
|
|
||||||
title="Import Model"
|
title="Import Model"
|
||||||
content={
|
content={
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useCallback } from 'react'
|
|||||||
import { SettingComponentProps } from '@janhq/core'
|
import { SettingComponentProps } from '@janhq/core'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import useModels from '@/hooks/useModels'
|
import useModelStop from '@/hooks/useModelStop'
|
||||||
|
|
||||||
import SettingComponentBuilder from '../../../../containers/ModelSetting/SettingComponent'
|
import SettingComponentBuilder from '../../../../containers/ModelSetting/SettingComponent'
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ type Props = {
|
|||||||
const AssistantSetting: React.FC<Props> = ({ componentData }) => {
|
const AssistantSetting: React.FC<Props> = ({ componentData }) => {
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const activeModels = useAtomValue(activeModelsAtom)
|
const activeModels = useAtomValue(activeModelsAtom)
|
||||||
const { stopModel } = useModels()
|
const stopModel = useModelStop()
|
||||||
|
|
||||||
const onValueChanged = useCallback(
|
const onValueChanged = useCallback(
|
||||||
(key: string, value: string | number | boolean) => {
|
(key: string, value: string | number | boolean) => {
|
||||||
@ -29,7 +29,7 @@ const AssistantSetting: React.FC<Props> = ({ componentData }) => {
|
|||||||
const model = activeModels.find(
|
const model = activeModels.find(
|
||||||
(model) => activeThread.assistants[0]?.model === model.model
|
(model) => activeThread.assistants[0]?.model === model.model
|
||||||
)
|
)
|
||||||
if (model) stopModel(model.model)
|
if (model) stopModel.mutate(model.model)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (
|
// if (
|
||||||
|
|||||||
@ -8,7 +8,11 @@ import EmptyThread from './EmptyThread'
|
|||||||
|
|
||||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
|
|
||||||
const ChatBody: React.FC = () => {
|
type Props = {
|
||||||
|
onResendMessage: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatBody: React.FC<Props> = ({ onResendMessage }) => {
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
|
|
||||||
if (!messages.length) return <EmptyThread />
|
if (!messages.length) return <EmptyThread />
|
||||||
@ -22,6 +26,7 @@ const ChatBody: React.FC = () => {
|
|||||||
key={message.id}
|
key={message.id}
|
||||||
msg={message}
|
msg={message}
|
||||||
isLatestMessage={isLatestMessage}
|
isLatestMessage={isLatestMessage}
|
||||||
|
onResendMessage={onResendMessage}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@janhq/joi'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSendMessageClick: (message: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SendMessageButton: React.FC<Props> = ({ onSendMessageClick }) => {
|
||||||
|
const currentPrompt = useAtomValue(currentPromptAtom)
|
||||||
|
|
||||||
|
const showSendButton = useMemo(() => {
|
||||||
|
if (currentPrompt.trim().length === 0) return false
|
||||||
|
return true
|
||||||
|
}, [currentPrompt])
|
||||||
|
|
||||||
|
if (!showSendButton) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className="h-8 w-8 rounded-lg p-0"
|
||||||
|
data-testid="btn-send-chat"
|
||||||
|
onClick={() => onSendMessageClick(currentPrompt)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="fill-white stroke-white"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3.93098 4.26171L3.93108 4.26168L12.9041 1.27032C12.9041 1.27031 12.9041 1.27031 12.9041 1.27031C13.7983 0.972243 14.3972 0.77445 14.8316 0.697178C15.0428 0.659595 15.1663 0.660546 15.2355 0.671861C15.2855 0.680033 15.296 0.690905 15.3015 0.696542C15.3018 0.696895 15.3022 0.697228 15.3025 0.697538C15.3028 0.697847 15.3031 0.698168 15.3035 0.698509C15.3091 0.703965 15.32 0.71449 15.3282 0.764538C15.3395 0.8338 15.3405 0.957246 15.3029 1.16844C15.2258 1.60268 15.0282 2.20131 14.7307 3.09505L11.7383 12.0689L11.7383 12.069C11.3184 13.3293 11.0242 14.2078 10.7465 14.7789C10.6083 15.063 10.4994 15.2158 10.4215 15.292C10.3948 15.3182 10.3774 15.3295 10.3698 15.3338C10.3622 15.3295 10.3449 15.3181 10.3184 15.2921C10.2404 15.2158 10.1314 15.0629 9.99319 14.7788C9.71539 14.2077 9.42091 13.3291 9.00105 12.069L9.00094 12.0687L8.34059 10.0903L12.6391 5.79172L12.6392 5.7918L12.6472 5.78348C12.9604 5.45927 13.1337 5.02503 13.1297 4.57431C13.1258 4.12358 12.945 3.69242 12.6263 3.3737C12.3076 3.05497 11.8764 2.87418 11.4257 2.87027C10.975 2.86635 10.5407 3.03962 10.2165 3.35276L10.2165 3.35268L10.2083 3.36086L5.9106 7.65853L3.93098 6.99895C2.67072 6.57904 1.79218 6.28485 1.22115 6.00715C0.937001 5.86898 0.784237 5.76011 0.707981 5.68215C0.681839 5.65542 0.670463 5.63807 0.666163 5.63051C0.670529 5.62288 0.681934 5.60558 0.707909 5.57904C0.784233 5.50103 0.937088 5.3921 1.22125 5.25386C1.79226 4.97606 2.67087 4.68157 3.93098 4.26171Z"
|
||||||
|
strokeWidth="1.33"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(SendMessageButton)
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@janhq/joi'
|
||||||
|
|
||||||
|
import { StopCircle } from 'lucide-react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onStopInferenceClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const StopInferenceButton: React.FC<Props> = ({ onStopInferenceClick }) => (
|
||||||
|
<Button
|
||||||
|
theme="destructive"
|
||||||
|
onClick={onStopInferenceClick}
|
||||||
|
className="h-8 w-8 rounded-lg p-0"
|
||||||
|
>
|
||||||
|
<StopCircle size={20} />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default React.memo(StopInferenceButton)
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import SendMessageButton from './SendMessageButton'
|
||||||
|
import StopInferenceButton from './StopInferenceButton'
|
||||||
|
|
||||||
|
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
|
|
||||||
|
import { isGeneratingResponseAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onStopInferenceClick: () => void
|
||||||
|
onSendMessageClick: (message: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatActionButton: React.FC<Props> = ({
|
||||||
|
onStopInferenceClick,
|
||||||
|
onSendMessageClick,
|
||||||
|
}) => {
|
||||||
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
|
const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom)
|
||||||
|
|
||||||
|
const showStopButton = useMemo(() => {
|
||||||
|
if (isGeneratingResponse) return true
|
||||||
|
|
||||||
|
const lastMessage = messages[messages.length - 1]
|
||||||
|
if (!lastMessage) return false
|
||||||
|
if (lastMessage.status === 'in_progress') return true
|
||||||
|
return false
|
||||||
|
}, [isGeneratingResponse, messages])
|
||||||
|
|
||||||
|
if (showStopButton) {
|
||||||
|
return <StopInferenceButton onStopInferenceClick={onStopInferenceClick} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SendMessageButton onSendMessageClick={onSendMessageClick} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatActionButton
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
|
|
||||||
|
import { TextArea } from '@janhq/joi'
|
||||||
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||||
|
|
||||||
|
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
|
|
||||||
|
import { spellCheckAtom } from '@/helpers/atoms/Setting.atom'
|
||||||
|
import {
|
||||||
|
getActiveThreadIdAtom,
|
||||||
|
isGeneratingResponseAtom,
|
||||||
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isSettingActive: boolean
|
||||||
|
onSendMessageClick: (message: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatTextInput: React.FC<Props> = ({
|
||||||
|
isSettingActive,
|
||||||
|
onSendMessageClick,
|
||||||
|
}) => {
|
||||||
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
|
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
|
const spellCheck = useAtomValue(spellCheckAtom)
|
||||||
|
|
||||||
|
const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom)
|
||||||
|
|
||||||
|
const disabled = useMemo(() => {
|
||||||
|
return !activeThreadId
|
||||||
|
}, [activeThreadId])
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setCurrentPrompt(e.target.value)
|
||||||
|
},
|
||||||
|
[setCurrentPrompt]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.focus()
|
||||||
|
}
|
||||||
|
}, [activeThreadId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current?.clientHeight) {
|
||||||
|
textareaRef.current.style.height = isSettingActive ? '100px' : '40px'
|
||||||
|
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'
|
||||||
|
textareaRef.current.style.overflow =
|
||||||
|
textareaRef.current.clientHeight >= 390 ? 'auto' : 'hidden'
|
||||||
|
}
|
||||||
|
}, [textareaRef.current?.clientHeight, currentPrompt, isSettingActive])
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (isGeneratingResponse) return
|
||||||
|
const lastMessage = messages[messages.length - 1]
|
||||||
|
if (!lastMessage || lastMessage.status !== 'in_progress') {
|
||||||
|
onSendMessageClick(currentPrompt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[messages, isGeneratingResponse, currentPrompt, onSendMessageClick]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextArea
|
||||||
|
className={twMerge(
|
||||||
|
'relative max-h-[400px] resize-none pr-20',
|
||||||
|
isSettingActive && 'pb-14 pr-16'
|
||||||
|
)}
|
||||||
|
spellCheck={spellCheck}
|
||||||
|
data-testid="txt-input-chat"
|
||||||
|
style={{ height: isSettingActive ? '100px' : '40px' }}
|
||||||
|
ref={textareaRef}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
placeholder="Ask me anything"
|
||||||
|
disabled={disabled}
|
||||||
|
value={currentPrompt}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatTextInput
|
||||||
@ -1,28 +1,19 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { TextArea, Button, useMediaQuery } from '@janhq/joi'
|
import { Button, useMediaQuery } from '@janhq/joi'
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
import {
|
import { SettingsIcon, ChevronUpIcon, Settings2Icon } from 'lucide-react'
|
||||||
StopCircle,
|
|
||||||
SettingsIcon,
|
|
||||||
ChevronUpIcon,
|
|
||||||
Settings2Icon,
|
|
||||||
} from 'lucide-react'
|
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import ModelDropdown from '@/containers/ModelDropdown'
|
import ModelDropdown from '@/containers/ModelDropdown'
|
||||||
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
|
||||||
|
import ChatActionButton from './ChatActionButton'
|
||||||
|
import ChatTextInput from './ChatTextInput'
|
||||||
|
|
||||||
import { showRightPanelAtom } from '@/helpers/atoms/App.atom'
|
import { showRightPanelAtom } from '@/helpers/atoms/App.atom'
|
||||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
|
||||||
import { spellCheckAtom } from '@/helpers/atoms/Setting.atom'
|
import { getActiveThreadIdAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
import {
|
|
||||||
activeThreadAtom,
|
|
||||||
getActiveThreadIdAtom,
|
|
||||||
isGeneratingResponseAtom,
|
|
||||||
waitingToSendMessage,
|
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
|
||||||
import { activeTabThreadRightPanelAtom } from '@/helpers/atoms/ThreadRightPanel.atom'
|
import { activeTabThreadRightPanelAtom } from '@/helpers/atoms/ThreadRightPanel.atom'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -31,170 +22,20 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ChatInput: React.FC<Props> = ({ sendMessage, stopInference }) => {
|
const ChatInput: React.FC<Props> = ({ sendMessage, stopInference }) => {
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
|
||||||
const [activeSetting, setActiveSetting] = useState(false)
|
|
||||||
const spellCheck = useAtomValue(spellCheckAtom)
|
|
||||||
|
|
||||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
|
||||||
|
|
||||||
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
|
||||||
const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
|
|
||||||
// const [fileUpload, setFileUpload] = useAtom(fileUploadAtom)
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
||||||
const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom)
|
|
||||||
|
|
||||||
const setActiveTabThreadRightPanel = useSetAtom(activeTabThreadRightPanelAtom)
|
const setActiveTabThreadRightPanel = useSetAtom(activeTabThreadRightPanelAtom)
|
||||||
|
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
const onPromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const [activeSetting, setActiveSetting] = useState(false)
|
||||||
setCurrentPrompt(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom)
|
const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom)
|
||||||
|
|
||||||
const matches = useMediaQuery('(max-width: 880px)')
|
const matches = useMediaQuery('(max-width: 880px)')
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isWaitingToSend && activeThreadId) {
|
|
||||||
setIsWaitingToSend(false)
|
|
||||||
sendMessage(currentPrompt)
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
activeThreadId,
|
|
||||||
isWaitingToSend,
|
|
||||||
currentPrompt,
|
|
||||||
setIsWaitingToSend,
|
|
||||||
sendMessage,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (textareaRef.current) {
|
|
||||||
textareaRef.current.focus()
|
|
||||||
}
|
|
||||||
}, [activeThreadId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (textareaRef.current?.clientHeight) {
|
|
||||||
textareaRef.current.style.height = activeSetting ? '100px' : '40px'
|
|
||||||
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'
|
|
||||||
textareaRef.current.style.overflow =
|
|
||||||
textareaRef.current.clientHeight >= 390 ? 'auto' : 'hidden'
|
|
||||||
}
|
|
||||||
}, [textareaRef.current?.clientHeight, currentPrompt, activeSetting])
|
|
||||||
|
|
||||||
const onKeyDown = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (isGeneratingResponse) return
|
|
||||||
if (messages[messages.length - 1]?.status !== 'in_progress')
|
|
||||||
sendMessage(currentPrompt)
|
|
||||||
else stopInference()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the change event of the extension file input element by setting the file name state.
|
|
||||||
* Its to be used to display the extension file name of the selected file.
|
|
||||||
* @param event - The change event object.
|
|
||||||
*/
|
|
||||||
// const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// const file = event.target.files?.[0]
|
|
||||||
// if (!file) return
|
|
||||||
// setFileUpload([{ file: file, type: 'pdf' }])
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// const file = event.target.files?.[0]
|
|
||||||
// if (!file) return
|
|
||||||
// setFileUpload([{ file: file, type: 'image' }])
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const renderPreview = (fileUpload: any) => {
|
|
||||||
// if (fileUpload.length > 0) {
|
|
||||||
// if (fileUpload[0].type === 'image') {
|
|
||||||
// return <ImageUploadPreview file={fileUpload[0].file} />
|
|
||||||
// } else {
|
|
||||||
// return <FileUploadPreview />
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative p-4 pb-2">
|
<div className="relative p-4 pb-2">
|
||||||
<div className="relative flex w-full flex-col">
|
<div className="relative flex w-full flex-col">
|
||||||
{/* {renderPreview(fileUpload)} */}
|
<ChatTextInput
|
||||||
<TextArea
|
isSettingActive={activeSetting}
|
||||||
className={twMerge(
|
onSendMessageClick={sendMessage}
|
||||||
'relative max-h-[400px] resize-none pr-20',
|
|
||||||
// fileUpload.length && 'rounded-t-none',
|
|
||||||
activeSetting && 'pb-14 pr-16'
|
|
||||||
)}
|
|
||||||
spellCheck={spellCheck}
|
|
||||||
data-testid="txt-input-chat"
|
|
||||||
style={{ height: activeSetting ? '100px' : '40px' }}
|
|
||||||
ref={textareaRef}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
placeholder="Ask me anything"
|
|
||||||
disabled={!activeThread}
|
|
||||||
value={currentPrompt}
|
|
||||||
onChange={onPromptChange}
|
|
||||||
/>
|
/>
|
||||||
{/* {experimentalFeature && (
|
|
||||||
<Tooltip
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
theme="icon"
|
|
||||||
className="absolute left-3 top-2.5"
|
|
||||||
onClick={(e) => {
|
|
||||||
if (
|
|
||||||
fileUpload.length > 0 ||
|
|
||||||
(activeThread?.assistants[0].tools &&
|
|
||||||
!activeThread?.assistants[0].tools[0]?.enabled &&
|
|
||||||
!isVisionModel)
|
|
||||||
) {
|
|
||||||
e.stopPropagation()
|
|
||||||
} else {
|
|
||||||
setShowAttacmentMenus(!showAttacmentMenus)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PaperclipIcon
|
|
||||||
size={18}
|
|
||||||
className="text-[hsla(var(--text-secondary))]"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
activeThread?.assistants[0].tools &&
|
|
||||||
activeThread?.assistants[0].tools[0]?.enabled
|
|
||||||
}
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
{fileUpload.length > 0 ||
|
|
||||||
(activeThread?.assistants[0].tools &&
|
|
||||||
!activeThread?.assistants[0].tools[0]?.enabled &&
|
|
||||||
!isVisionModel && (
|
|
||||||
<>
|
|
||||||
{fileUpload.length !== 0 && (
|
|
||||||
<span>
|
|
||||||
Currently, we only support 1 attachment at the same
|
|
||||||
time.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{activeThread?.assistants[0].tools &&
|
|
||||||
activeThread?.assistants[0].tools[0]?.enabled ===
|
|
||||||
false && (
|
|
||||||
<span>
|
|
||||||
Turn on Retrieval in Assistant Settings to use
|
|
||||||
this feature.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
<div className={twMerge('absolute right-3 top-1.5')}>
|
<div className={twMerge('absolute right-3 top-1.5')}>
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
@ -202,9 +43,7 @@ const ChatInput: React.FC<Props> = ({ sendMessage, stopInference }) => {
|
|||||||
<div className="flex h-8 items-center">
|
<div className="flex h-8 items-center">
|
||||||
<Button
|
<Button
|
||||||
theme="icon"
|
theme="icon"
|
||||||
onClick={() => {
|
onClick={() => setActiveSetting(!activeSetting)}
|
||||||
setActiveSetting(!activeSetting)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<SettingsIcon
|
<SettingsIcon
|
||||||
size={18}
|
size={18}
|
||||||
@ -213,43 +52,10 @@ const ChatInput: React.FC<Props> = ({ sendMessage, stopInference }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{messages[messages.length - 1]?.status !== 'in_progress' &&
|
<ChatActionButton
|
||||||
isGeneratingResponse ? (
|
onStopInferenceClick={stopInference}
|
||||||
<Button
|
onSendMessageClick={sendMessage}
|
||||||
theme="destructive"
|
/>
|
||||||
onClick={stopInference}
|
|
||||||
className="h-8 w-8 rounded-lg p-0"
|
|
||||||
>
|
|
||||||
<StopCircle size={20} />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{currentPrompt.length !== 0 && (
|
|
||||||
<Button
|
|
||||||
disabled={
|
|
||||||
!activeThread || currentPrompt.trim().length === 0
|
|
||||||
}
|
|
||||||
className="h-8 w-8 rounded-lg p-0"
|
|
||||||
data-testid="btn-send-chat"
|
|
||||||
onClick={() => sendMessage(currentPrompt)}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="fill-white stroke-white"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3.93098 4.26171L3.93108 4.26168L12.9041 1.27032C12.9041 1.27031 12.9041 1.27031 12.9041 1.27031C13.7983 0.972243 14.3972 0.77445 14.8316 0.697178C15.0428 0.659595 15.1663 0.660546 15.2355 0.671861C15.2855 0.680033 15.296 0.690905 15.3015 0.696542C15.3018 0.696895 15.3022 0.697228 15.3025 0.697538C15.3028 0.697847 15.3031 0.698168 15.3035 0.698509C15.3091 0.703965 15.32 0.71449 15.3282 0.764538C15.3395 0.8338 15.3405 0.957246 15.3029 1.16844C15.2258 1.60268 15.0282 2.20131 14.7307 3.09505L11.7383 12.0689L11.7383 12.069C11.3184 13.3293 11.0242 14.2078 10.7465 14.7789C10.6083 15.063 10.4994 15.2158 10.4215 15.292C10.3948 15.3182 10.3774 15.3295 10.3698 15.3338C10.3622 15.3295 10.3449 15.3181 10.3184 15.2921C10.2404 15.2158 10.1314 15.0629 9.99319 14.7788C9.71539 14.2077 9.42091 13.3291 9.00105 12.069L9.00094 12.0687L8.34059 10.0903L12.6391 5.79172L12.6392 5.7918L12.6472 5.78348C12.9604 5.45927 13.1337 5.02503 13.1297 4.57431C13.1258 4.12358 12.945 3.69242 12.6263 3.3737C12.3076 3.05497 11.8764 2.87418 11.4257 2.87027C10.975 2.86635 10.5407 3.03962 10.2165 3.35276L10.2165 3.35268L10.2083 3.36086L5.9106 7.65853L3.93098 6.99895C2.67072 6.57904 1.79218 6.28485 1.22115 6.00715C0.937001 5.86898 0.784237 5.76011 0.707981 5.68215C0.681839 5.65542 0.670463 5.63807 0.666163 5.63051C0.670529 5.62288 0.681934 5.60558 0.707909 5.57904C0.784233 5.50103 0.937088 5.3921 1.22125 5.25386C1.79226 4.97606 2.67087 4.68157 3.93098 4.26171Z"
|
|
||||||
strokeWidth="1.33"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -257,7 +63,7 @@ const ChatInput: React.FC<Props> = ({ sendMessage, stopInference }) => {
|
|||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'absolute bottom-[6px] left-[1px] flex w-[calc(100%-2px)] items-center justify-between rounded-lg bg-[hsla(var(--textarea-bg))] p-3',
|
'absolute bottom-[6px] left-[1px] flex w-[calc(100%-2px)] items-center justify-between rounded-lg bg-[hsla(var(--textarea-bg))] p-3',
|
||||||
!activeThread && 'bg-transparent'
|
!activeThreadId && 'bg-transparent'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
@ -278,26 +84,6 @@ const ChatInput: React.FC<Props> = ({ sendMessage, stopInference }) => {
|
|||||||
className="flex-shrink-0 cursor-pointer text-[hsla(var(--text-secondary))]"
|
className="flex-shrink-0 cursor-pointer text-[hsla(var(--text-secondary))]"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
{/* {experimentalFeature && (
|
|
||||||
<Badge
|
|
||||||
className="flex cursor-pointer items-center gap-x-1"
|
|
||||||
theme="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setActiveTabThreadRightPanel('tools')
|
|
||||||
if (matches) {
|
|
||||||
setShowRightPanel(!showRightPanel)
|
|
||||||
} else if (!showRightPanel) {
|
|
||||||
setShowRightPanel(true)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShapesIcon
|
|
||||||
size={16}
|
|
||||||
className="flex-shrink-0 text-[hsla(var(--text-secondary))]"
|
|
||||||
/>
|
|
||||||
<span>Tools</span>
|
|
||||||
</Badge>
|
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
<Button theme="icon" onClick={() => setActiveSetting(false)}>
|
<Button theme="icon" onClick={() => setActiveSetting(false)}>
|
||||||
<ChevronUpIcon
|
<ChevronUpIcon
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import {
|
|||||||
import { useClipboard } from '@/hooks/useClipboard'
|
import { useClipboard } from '@/hooks/useClipboard'
|
||||||
|
|
||||||
import useMessageDeleteMutation from '@/hooks/useMessageDeleteMutation'
|
import useMessageDeleteMutation from '@/hooks/useMessageDeleteMutation'
|
||||||
import useSendMessage from '@/hooks/useSendMessage'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deleteMessageAtom,
|
deleteMessageAtom,
|
||||||
@ -24,12 +23,16 @@ import {
|
|||||||
type Props = {
|
type Props = {
|
||||||
isLastMessage: boolean
|
isLastMessage: boolean
|
||||||
message: Message
|
message: Message
|
||||||
|
onResendMessage: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageToolbar: React.FC<Props> = ({ isLastMessage, message }) => {
|
const MessageToolbar: React.FC<Props> = ({
|
||||||
|
isLastMessage,
|
||||||
|
message,
|
||||||
|
onResendMessage,
|
||||||
|
}) => {
|
||||||
const deleteMessage = useSetAtom(deleteMessageAtom)
|
const deleteMessage = useSetAtom(deleteMessageAtom)
|
||||||
const setEditMessage = useSetAtom(editMessageAtom)
|
const setEditMessage = useSetAtom(editMessageAtom)
|
||||||
const { resendMessage } = useSendMessage()
|
|
||||||
const clipboard = useClipboard({ timeout: 1000 })
|
const clipboard = useClipboard({ timeout: 1000 })
|
||||||
const deleteCortexMessage = useMessageDeleteMutation()
|
const deleteCortexMessage = useMessageDeleteMutation()
|
||||||
|
|
||||||
@ -61,8 +64,8 @@ const MessageToolbar: React.FC<Props> = ({ isLastMessage, message }) => {
|
|||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
})
|
})
|
||||||
deleteMessage(message.id)
|
deleteMessage(message.id)
|
||||||
await resendMessage()
|
onResendMessage()
|
||||||
}, [deleteCortexMessage, deleteMessage, resendMessage, message])
|
}, [deleteCortexMessage, deleteMessage, onResendMessage, message])
|
||||||
|
|
||||||
const allowRegenerate = useMemo(
|
const allowRegenerate = useMemo(
|
||||||
() => isLastMessage && message.role === 'assistant',
|
() => isLastMessage && message.role === 'assistant',
|
||||||
|
|||||||
@ -33,9 +33,14 @@ import { editMessageAtom } from '@/helpers/atoms/ChatMessage.atom'
|
|||||||
type Props = {
|
type Props = {
|
||||||
isLatestMessage: boolean
|
isLatestMessage: boolean
|
||||||
msg: Message
|
msg: Message
|
||||||
|
onResendMessage: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const SimpleTextMessage: React.FC<Props> = ({ isLatestMessage, msg }) => {
|
const SimpleTextMessage: React.FC<Props> = ({
|
||||||
|
isLatestMessage,
|
||||||
|
msg,
|
||||||
|
onResendMessage,
|
||||||
|
}) => {
|
||||||
const [text, setText] = useState('')
|
const [text, setText] = useState('')
|
||||||
const { data: assistants } = useAssistantQuery()
|
const { data: assistants } = useAssistantQuery()
|
||||||
const editMessage = useAtomValue(editMessageAtom)
|
const editMessage = useAtomValue(editMessageAtom)
|
||||||
@ -188,7 +193,11 @@ const SimpleTextMessage: React.FC<Props> = ({ isLatestMessage, msg }) => {
|
|||||||
: 'hidden group-hover:absolute group-hover:right-4 group-hover:top-4 group-hover:flex'
|
: 'hidden group-hover:absolute group-hover:right-4 group-hover:top-4 group-hover:flex'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MessageToolbar message={msg} isLastMessage={isLatestMessage} />
|
<MessageToolbar
|
||||||
|
message={msg}
|
||||||
|
isLastMessage={isLatestMessage}
|
||||||
|
onResendMessage={onResendMessage}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isLatestMessage &&
|
{isLatestMessage &&
|
||||||
(msg.status === 'in_progress' || tokenSpeed > 0) && (
|
(msg.status === 'in_progress' || tokenSpeed > 0) && (
|
||||||
@ -204,7 +213,7 @@ const SimpleTextMessage: React.FC<Props> = ({ isLatestMessage, msg }) => {
|
|||||||
!isUser && !text.includes(' ') && 'break-all'
|
!isUser && !text.includes(' ') && 'break-all'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<>
|
<Fragment>
|
||||||
{msg.content[0]?.type === 'image_file' && (
|
{msg.content[0]?.type === 'image_file' && (
|
||||||
<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">
|
||||||
<div className="left-0 top-0 z-20 h-full w-full group-hover/image:inline-block">
|
<div className="left-0 top-0 z-20 h-full w-full group-hover/image:inline-block">
|
||||||
@ -254,7 +263,7 @@ const SimpleTextMessage: React.FC<Props> = ({ isLatestMessage, msg }) => {
|
|||||||
dangerouslySetInnerHTML={{ __html: parsedText }}
|
dangerouslySetInnerHTML={{ __html: parsedText }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</Fragment>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,20 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import { Accept, useDropzone } from 'react-dropzone'
|
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
|
||||||
|
|
||||||
import { UploadCloudIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
|
||||||
|
|
||||||
import CenterPanelContainer from '@/containers/CenterPanelContainer'
|
import CenterPanelContainer from '@/containers/CenterPanelContainer'
|
||||||
import GenerateResponse from '@/containers/Loader/GenerateResponse'
|
import GenerateResponse from '@/containers/Loader/GenerateResponse'
|
||||||
import ModelStart from '@/containers/Loader/ModelStart'
|
import ModelStart from '@/containers/Loader/ModelStart'
|
||||||
import { fileUploadAtom } from '@/containers/Providers/Jotai'
|
|
||||||
import { snackbar } from '@/containers/Toast'
|
|
||||||
|
|
||||||
import useSendMessage from '@/hooks/useSendMessage'
|
import useSendMessage from '@/hooks/useSendMessage'
|
||||||
|
|
||||||
@ -22,165 +10,26 @@ import ChatBody from '@/screens/Thread/ThreadCenterPanel/ChatBody'
|
|||||||
|
|
||||||
import ChatInput from './ChatInput'
|
import ChatInput from './ChatInput'
|
||||||
|
|
||||||
import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isGeneratingResponseAtom,
|
isGeneratingResponseAtom,
|
||||||
activeThreadAtom,
|
activeThreadAtom,
|
||||||
isLoadingModelAtom,
|
isLoadingModelAtom,
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
const renderError = (code: string) => {
|
|
||||||
switch (code) {
|
|
||||||
case 'multiple-upload':
|
|
||||||
return 'Currently, we only support 1 attachment at the same time'
|
|
||||||
|
|
||||||
case 'retrieval-off':
|
|
||||||
return 'Turn on Retrieval in Assistant Settings to use this feature'
|
|
||||||
|
|
||||||
case 'file-invalid-type':
|
|
||||||
return 'We do not support this file type'
|
|
||||||
|
|
||||||
default:
|
|
||||||
return 'Oops, something error, please try again.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThreadCenterPanel: React.FC = () => {
|
const ThreadCenterPanel: React.FC = () => {
|
||||||
const { sendMessage, stopInference } = useSendMessage()
|
const { sendMessage, stopInference, resendMessage } = useSendMessage()
|
||||||
const [dragRejected, setDragRejected] = useState({ code: '' })
|
|
||||||
const setFileUpload = useSetAtom(fileUploadAtom)
|
|
||||||
const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom)
|
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const isLoadingModel = useAtomValue(isLoadingModelAtom)
|
const isLoadingModel = useAtomValue(isLoadingModelAtom)
|
||||||
const isVisionModel = false // activeThread?.assistants[0].model?.settings.vision_model
|
|
||||||
|
|
||||||
const acceptedFormat: Accept = isVisionModel
|
|
||||||
? {
|
|
||||||
'application/pdf': ['.pdf'],
|
|
||||||
'image/jpeg': ['.jpeg'],
|
|
||||||
'image/png': ['.png'],
|
|
||||||
'image/jpg': ['.jpg'],
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
'application/pdf': ['.pdf'],
|
|
||||||
}
|
|
||||||
|
|
||||||
const { getRootProps, isDragReject } = useDropzone({
|
|
||||||
noClick: true,
|
|
||||||
multiple: false,
|
|
||||||
accept: acceptedFormat,
|
|
||||||
|
|
||||||
onDragOver: (e) => {
|
|
||||||
// Retrieval file drag and drop is experimental feature
|
|
||||||
if (!experimentalFeature) return
|
|
||||||
if (
|
|
||||||
e.dataTransfer.items.length === 1 &&
|
|
||||||
((activeThread?.assistants[0].tools &&
|
|
||||||
activeThread?.assistants[0].tools[0]?.enabled) ||
|
|
||||||
isVisionModel)
|
|
||||||
) {
|
|
||||||
setDragOver(true)
|
|
||||||
} else if (
|
|
||||||
activeThread?.assistants[0].tools &&
|
|
||||||
!activeThread?.assistants[0].tools[0]?.enabled
|
|
||||||
) {
|
|
||||||
setDragRejected({ code: 'retrieval-off' })
|
|
||||||
} else {
|
|
||||||
setDragRejected({ code: 'multiple-upload' })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragLeave: () => setDragOver(false),
|
|
||||||
onDrop: (files, rejectFiles) => {
|
|
||||||
// Retrieval file drag and drop is experimental feature
|
|
||||||
if (!experimentalFeature) return
|
|
||||||
if (
|
|
||||||
!files ||
|
|
||||||
files.length !== 1 ||
|
|
||||||
rejectFiles.length !== 0 ||
|
|
||||||
(activeThread?.assistants[0].tools &&
|
|
||||||
!activeThread?.assistants[0].tools[0]?.enabled &&
|
|
||||||
!isVisionModel)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
const imageType = files[0]?.type.includes('image')
|
|
||||||
setFileUpload([{ file: files[0], type: imageType ? 'image' : 'pdf' }])
|
|
||||||
setDragOver(false)
|
|
||||||
},
|
|
||||||
onDropRejected: (e) => {
|
|
||||||
if (
|
|
||||||
activeThread?.assistants[0].tools &&
|
|
||||||
!activeThread?.assistants[0].tools[0]?.enabled
|
|
||||||
) {
|
|
||||||
setDragRejected({ code: 'retrieval-off' })
|
|
||||||
} else {
|
|
||||||
setDragRejected({ code: e[0].errors[0].code })
|
|
||||||
}
|
|
||||||
setDragOver(false)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (dragRejected.code) {
|
|
||||||
snackbar({
|
|
||||||
description: renderError(dragRejected.code),
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
if (dragRejected.code) {
|
|
||||||
setDragRejected({ code: '' })
|
|
||||||
}
|
|
||||||
}, 2000)
|
|
||||||
}, [dragRejected.code])
|
|
||||||
|
|
||||||
const [dragOver, setDragOver] = useState(false)
|
|
||||||
|
|
||||||
const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom)
|
const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenterPanelContainer>
|
<CenterPanelContainer>
|
||||||
<div
|
<div className="relative flex h-full w-full flex-col outline-none">
|
||||||
className="relative flex h-full w-full flex-col outline-none"
|
|
||||||
{...getRootProps()}
|
|
||||||
>
|
|
||||||
{dragOver && (
|
|
||||||
<div className="absolute z-50 mx-auto h-full w-full p-8 backdrop-blur-lg">
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
'flex h-full w-full items-center justify-center rounded-lg border border-dashed border-[hsla(var(--primary-bg))]',
|
|
||||||
isDragReject && 'border-[hsla(var(--destructive-bg))]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="mx-auto w-1/2 text-center">
|
|
||||||
<div className="mx-auto inline-flex h-12 w-12 items-center justify-center rounded-full">
|
|
||||||
<UploadCloudIcon
|
|
||||||
size={24}
|
|
||||||
className="text-[hsla(var(--primary-bg))]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 text-[hsla(var(--primary-bg))]">
|
|
||||||
<h6 className="font-bold">
|
|
||||||
{isDragReject
|
|
||||||
? `Currently, we only support 1 attachment at the same time with ${
|
|
||||||
isVisionModel ? 'PDF, JPEG, JPG, PNG' : 'PDF'
|
|
||||||
} format`
|
|
||||||
: 'Drop file here'}
|
|
||||||
</h6>
|
|
||||||
{!isDragReject && (
|
|
||||||
<p className="mt-2">
|
|
||||||
{isVisionModel ? 'PDF, JPEG, JPG, PNG' : 'PDF'}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex h-full w-full flex-col justify-between">
|
<div className="flex h-full w-full flex-col justify-between">
|
||||||
{activeThread && (
|
{activeThread && (
|
||||||
<div className="flex h-full w-full overflow-x-hidden">
|
<div className="flex h-full w-full overflow-x-hidden">
|
||||||
<ChatBody />
|
<ChatBody onResendMessage={resendMessage} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { useDebouncedCallback } from 'use-debounce'
|
|||||||
import EngineSetting from '@/containers/EngineSetting'
|
import EngineSetting from '@/containers/EngineSetting'
|
||||||
import ModelSetting from '@/containers/ModelSetting'
|
import ModelSetting from '@/containers/ModelSetting'
|
||||||
|
|
||||||
|
import useModelStop from '@/hooks/useModelStop'
|
||||||
import useModels from '@/hooks/useModels'
|
import useModels from '@/hooks/useModels'
|
||||||
|
|
||||||
import { getConfigurationsData } from '@/utils/componentSettings'
|
import { getConfigurationsData } from '@/utils/componentSettings'
|
||||||
@ -24,7 +25,8 @@ import {
|
|||||||
} from '@/helpers/atoms/Model.atom'
|
} from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
const ModelSettingContainer: React.FC = () => {
|
const ModelSettingContainer: React.FC = () => {
|
||||||
const { stopModel, updateModel } = useModels()
|
const stopModel = useModelStop()
|
||||||
|
const { updateModel } = useModels()
|
||||||
const setSelectedModel = useSetAtom(updateSelectedModelAtom)
|
const setSelectedModel = useSetAtom(updateSelectedModelAtom)
|
||||||
|
|
||||||
const selectedModel = useAtomValue(getSelectedModelAtom)
|
const selectedModel = useAtomValue(getSelectedModelAtom)
|
||||||
@ -33,21 +35,17 @@ const ModelSettingContainer: React.FC = () => {
|
|||||||
if (!selectedModel) return
|
if (!selectedModel) return
|
||||||
// runtime setting
|
// runtime setting
|
||||||
const modelRuntimeParams = toRuntimeParams(selectedModel)
|
const modelRuntimeParams = toRuntimeParams(selectedModel)
|
||||||
const componentDataRuntimeSetting = getConfigurationsData(
|
const componentDataRuntimeSetting =
|
||||||
modelRuntimeParams,
|
getConfigurationsData(modelRuntimeParams)
|
||||||
selectedModel
|
|
||||||
)
|
|
||||||
|
|
||||||
// engine setting
|
// engine setting
|
||||||
const modelEngineParams = toSettingParams(selectedModel)
|
const modelEngineParams = toSettingParams(selectedModel)
|
||||||
const componentDataEngineSetting = getConfigurationsData(
|
const componentDataEngineSetting = getConfigurationsData(
|
||||||
modelEngineParams,
|
modelEngineParams
|
||||||
selectedModel
|
|
||||||
).filter((x) => x.key !== 'prompt_template' && x.key !== 'embedding')
|
).filter((x) => x.key !== 'prompt_template' && x.key !== 'embedding')
|
||||||
|
|
||||||
const promptTemplateSettings = getConfigurationsData(
|
const promptTemplateSettings = getConfigurationsData(
|
||||||
modelEngineParams,
|
modelEngineParams
|
||||||
selectedModel
|
|
||||||
).filter((x) => x.key === 'prompt_template')
|
).filter((x) => x.key === 'prompt_template')
|
||||||
|
|
||||||
// the max value of max token has to follow context length
|
// the max value of max token has to follow context length
|
||||||
@ -57,6 +55,7 @@ const ModelSettingContainer: React.FC = () => {
|
|||||||
const contextLength = componentDataEngineSetting.find(
|
const contextLength = componentDataEngineSetting.find(
|
||||||
(x) => x.key === 'ctx_len'
|
(x) => x.key === 'ctx_len'
|
||||||
)
|
)
|
||||||
|
|
||||||
if (maxTokens && contextLength) {
|
if (maxTokens && contextLength) {
|
||||||
// replace maxToken to componentDataRuntimeSetting
|
// replace maxToken to componentDataRuntimeSetting
|
||||||
const updatedComponentDataRuntimeSetting: SettingComponentProps[] =
|
const updatedComponentDataRuntimeSetting: SettingComponentProps[] =
|
||||||
@ -123,7 +122,7 @@ const ModelSettingContainer: React.FC = () => {
|
|||||||
presetConfiguration[key]?.requireModelReload ?? true
|
presetConfiguration[key]?.requireModelReload ?? true
|
||||||
|
|
||||||
if (shouldStopModel) {
|
if (shouldStopModel) {
|
||||||
stopModel(selectedModel.model)
|
stopModel.mutate(selectedModel.model)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[selectedModel, debounceUpdateModel, stopModel, setSelectedModel]
|
[selectedModel, debounceUpdateModel, stopModel, setSelectedModel]
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { Model, SettingComponentProps } from '@janhq/core'
|
import { SettingComponentProps } from '@janhq/core'
|
||||||
|
|
||||||
import { presetConfiguration } from './predefinedComponent'
|
import { presetConfiguration } from './predefinedComponent'
|
||||||
|
|
||||||
export const getConfigurationsData = (
|
export const getConfigurationsData = (
|
||||||
settings: object,
|
settings: object
|
||||||
selectedModel?: Model
|
|
||||||
): SettingComponentProps[] => {
|
): SettingComponentProps[] => {
|
||||||
const componentData: SettingComponentProps[] = []
|
const componentData: SettingComponentProps[] = []
|
||||||
|
|
||||||
@ -23,15 +22,11 @@ export const getConfigurationsData = (
|
|||||||
switch (key) {
|
switch (key) {
|
||||||
case 'max_tokens':
|
case 'max_tokens':
|
||||||
componentSetting.controllerProps.max =
|
componentSetting.controllerProps.max =
|
||||||
selectedModel?.max_tokens ||
|
componentSetting.controllerProps.max ?? 4096
|
||||||
componentSetting.controllerProps.max ||
|
|
||||||
4096
|
|
||||||
break
|
break
|
||||||
case 'ctx_len':
|
case 'ctx_len':
|
||||||
componentSetting.controllerProps.max =
|
componentSetting.controllerProps.max =
|
||||||
selectedModel?.ctx_len ||
|
componentSetting.controllerProps.max ?? 2048
|
||||||
componentSetting.controllerProps.max ||
|
|
||||||
2048
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user