Merge branch 'main' into docs/add-guides
This commit is contained in:
commit
4f9482263b
23
.github/ISSUE_TEMPLATE/epic-request.md
vendored
23
.github/ISSUE_TEMPLATE/epic-request.md
vendored
@ -7,22 +7,19 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Motivation**
|
## Motivation
|
||||||
-
|
-
|
||||||
|
|
||||||
**Specs & Designs**
|
## Specs
|
||||||
-
|
-
|
||||||
|
|
||||||
**In Scope**
|
## Designs
|
||||||
|
[Figma](link)
|
||||||
|
|
||||||
|
## Tasklist
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Not in Scope
|
||||||
-
|
-
|
||||||
|
|
||||||
**Not in Scope**
|
## Appendix
|
||||||
-
|
|
||||||
|
|
||||||
**Tasklist**
|
|
||||||
> Note: All issues need to share the same `milestone` as this epic
|
|
||||||
-
|
|
||||||
|
|
||||||
**Related Milestones**
|
|
||||||
- Past
|
|
||||||
- Future
|
|
||||||
|
|||||||
@ -70,7 +70,7 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
|
|||||||
<tr style="text-align: center">
|
<tr style="text-align: center">
|
||||||
<td style="text-align:center"><b>Experimental (Nighlty Build)</b></td>
|
<td style="text-align:center"><b>Experimental (Nighlty Build)</b></td>
|
||||||
<td style="text-align:center" colspan="4">
|
<td style="text-align:center" colspan="4">
|
||||||
<a href='https://github.com/janhq/jan/actions/runs/7324039788'>
|
<a href='https://github.com/janhq/jan/actions/runs/7341513351'>
|
||||||
<b>Github action artifactory</b>
|
<b>Github action artifactory</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export enum AppRoute {
|
|||||||
openAppDirectory = 'openAppDirectory',
|
openAppDirectory = 'openAppDirectory',
|
||||||
openFileExplore = 'openFileExplorer',
|
openFileExplore = 'openFileExplorer',
|
||||||
relaunch = 'relaunch',
|
relaunch = 'relaunch',
|
||||||
|
joinPath = 'joinPath'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AppEvent {
|
export enum AppEvent {
|
||||||
|
|||||||
@ -44,6 +44,13 @@ const getUserSpace = (): Promise<string> => global.core.api?.getUserSpace()
|
|||||||
const openFileExplorer: (path: string) => Promise<any> = (path) =>
|
const openFileExplorer: (path: string) => Promise<any> = (path) =>
|
||||||
global.core.api?.openFileExplorer(path)
|
global.core.api?.openFileExplorer(path)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joins multiple paths together.
|
||||||
|
* @param paths - The paths to join.
|
||||||
|
* @returns {Promise<string>} A promise that resolves with the joined path.
|
||||||
|
*/
|
||||||
|
const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.api?.joinPath(paths)
|
||||||
|
|
||||||
const getResourcePath: () => Promise<string> = () => global.core.api?.getResourcePath()
|
const getResourcePath: () => Promise<string> = () => global.core.api?.getResourcePath()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,4 +73,5 @@ export {
|
|||||||
getUserSpace,
|
getUserSpace,
|
||||||
openFileExplorer,
|
openFileExplorer,
|
||||||
getResourcePath,
|
getResourcePath,
|
||||||
|
joinPath,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,13 +67,6 @@ export type Model = {
|
|||||||
*/
|
*/
|
||||||
description: string
|
description: string
|
||||||
|
|
||||||
/**
|
|
||||||
* The model state.
|
|
||||||
* Default: "to_download"
|
|
||||||
* Enum: "to_download" "downloading" "ready" "running"
|
|
||||||
*/
|
|
||||||
state?: ModelState
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The model settings.
|
* The model settings.
|
||||||
*/
|
*/
|
||||||
@ -101,15 +94,6 @@ export type ModelMetadata = {
|
|||||||
cover?: string
|
cover?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The Model transition states.
|
|
||||||
*/
|
|
||||||
export enum ModelState {
|
|
||||||
Downloading = 'downloading',
|
|
||||||
Ready = 'ready',
|
|
||||||
Running = 'running',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The available model settings.
|
* The available model settings.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,24 +1,42 @@
|
|||||||
---
|
---
|
||||||
title: Import Models Manually
|
title: Import Models Manually
|
||||||
|
slug: /guides/using-models/import-manually
|
||||||
|
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
|
||||||
|
keywords:
|
||||||
|
[
|
||||||
|
Jan AI,
|
||||||
|
Jan,
|
||||||
|
ChatGPT alternative,
|
||||||
|
local AI,
|
||||||
|
private AI,
|
||||||
|
conversational AI,
|
||||||
|
no-subscription fee,
|
||||||
|
large language model,
|
||||||
|
import-models-manually,
|
||||||
|
]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
This is currently under development.
|
||||||
|
:::
|
||||||
|
|
||||||
{/* Imports */}
|
{/* Imports */}
|
||||||
import Tabs from "@theme/Tabs";
|
import Tabs from "@theme/Tabs";
|
||||||
import TabItem from "@theme/TabItem";
|
import TabItem from "@theme/TabItem";
|
||||||
|
|
||||||
Jan is compatible with all GGUF models.
|
Jan is compatible with all GGUF models.
|
||||||
|
|
||||||
If you don't see the model you want in the Hub, or if you have a custom model, you can add it to Jan.
|
If you can not find the model you want in the Hub or have a custom model you want to use, you can import it manually.
|
||||||
|
|
||||||
In this guide we will use our latest model, [Trinity](https://huggingface.co/janhq/trinity-v1-GGUF), as an example.
|
In this guide, we will show you how to import a GGUF model from [HuggingFace](https://huggingface.co/), using our lastest model, [Trinity](https://huggingface.co/janhq/trinity-v1-GGUF), as an example.
|
||||||
|
|
||||||
> We are fast shipping a UI to make this easier, but it's a bit manual for now. Apologies.
|
> We are fast shipping a UI to make this easier, but it's a bit manual for now. Apologies.
|
||||||
|
|
||||||
## 1. Create a model folder
|
## Steps to Manually Import a Model
|
||||||
|
|
||||||
Navigate to the `~/jan/models` folder on your computer.
|
### 1. Create a Model Folder
|
||||||
|
|
||||||
In `App Settings`, go to `Advanced`, then `Open App Directory`.
|
Navigate to the `~/jan/models` folder. You can find this folder by going to `App Settings` > `Advanced` > `Open App Directory`.
|
||||||
|
|
||||||
<Tabs groupId="operating-systems">
|
<Tabs groupId="operating-systems">
|
||||||
<TabItem value="mac" label="macOS">
|
<TabItem value="mac" label="macOS">
|
||||||
@ -70,11 +88,11 @@ In the `models` folder, create a folder with the name of the model.
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
## 2. Create a model JSON
|
### 2. Create a Model JSON
|
||||||
|
|
||||||
Jan follows a folder-based, [standard model template](/specs/models) called a `model.json` to persist the model configurations on your local filesystem.
|
Jan follows a folder-based, [standard model template](/docs/engineering/models) called a `model.json` to persist the model configurations on your local filesystem.
|
||||||
|
|
||||||
This means you can easily & transparently reconfigure your models and export and share your preferences.
|
This means that you can easily reconfigure your models, export them, and share your preferences transparently.
|
||||||
|
|
||||||
<Tabs groupId="operating-systems">
|
<Tabs groupId="operating-systems">
|
||||||
<TabItem value="mac" label="macOS">
|
<TabItem value="mac" label="macOS">
|
||||||
@ -89,7 +107,7 @@ This means you can easily & transparently reconfigure your models and export and
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd trinity-v1-7b
|
cd trinity-v1-7b
|
||||||
touch model.json
|
echo {} > model.json
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
@ -103,45 +121,53 @@ This means you can easily & transparently reconfigure your models and export and
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
Copy the following configurations into the `model.json`.
|
Edit `model.json` and include the following configurations:
|
||||||
|
|
||||||
1. Make sure the `id` property is the same as the folder name you created.
|
- Ensure the filename must be `model.json`.
|
||||||
2. Make sure the `source_url` property is the direct binary download link ending in `.gguf`. In HuggingFace, you can find the directl links in `Files and versions` tab.
|
- Ensure the `id` property matches the folder name you created.
|
||||||
3. Ensure you are using the correct `prompt_template`. This is usually provided in the HuggingFace model's description page.
|
- Ensure the GGUF filename should match the `id` property exactly.
|
||||||
|
- Ensure the `source_url` property is the direct binary download link ending in `.gguf`. In HuggingFace, you can find the direct links in `Files and versions` tab.
|
||||||
|
- Ensure you are using the correct `prompt_template`. This is usually provided in the HuggingFace model's description page.
|
||||||
|
- Ensure the `state` property is set to `ready`.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
|
// highlight-start
|
||||||
"source_url": "https://huggingface.co/janhq/trinity-v1-GGUF/resolve/main/trinity-v1.Q4_K_M.gguf",
|
"source_url": "https://huggingface.co/janhq/trinity-v1-GGUF/resolve/main/trinity-v1.Q4_K_M.gguf",
|
||||||
"id": "trinity-v1-7b",
|
"id": "trinity-v1-7b",
|
||||||
|
// highlight-end
|
||||||
"object": "model",
|
"object": "model",
|
||||||
"name": "Trinity 7B Q4",
|
"name": "Trinity-v1 7B Q4",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"description": "Trinity is an experimental model merge of GreenNodeLM & LeoScorpius using the Slerp method. Recommended for daily assistance purposes.",
|
"description": "Trinity is an experimental model merge of GreenNodeLM & LeoScorpius using the Slerp method. Recommended for daily assistance purposes.",
|
||||||
"format": "gguf",
|
"format": "gguf",
|
||||||
"settings": {
|
"settings": {
|
||||||
"ctx_len": 2048,
|
"ctx_len": 4096,
|
||||||
"prompt_template": "<|im_start|>system\n{system_message}<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant"
|
// highlight-next-line
|
||||||
|
"prompt_template": "{system_message}\n### Instruction:\n{prompt}\n### Response:"
|
||||||
},
|
},
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"max_tokens": 2048
|
"max_tokens": 4096
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"author": "Jan",
|
"author": "Jan",
|
||||||
"tags": ["7B", "Merged", "Featured"],
|
"tags": ["7B", "Merged"],
|
||||||
"size": 4370000000
|
"size": 4370000000
|
||||||
},
|
},
|
||||||
|
// highlight-next-line
|
||||||
|
"state": "ready",
|
||||||
"engine": "nitro"
|
"engine": "nitro"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Download your model
|
### 3. Download the Model
|
||||||
|
|
||||||
Restart the Jan application and look for your model in the Hub.
|
Restart Jan and navigate to the Hub. Locate your model and click the `Download` button to download the model binary.
|
||||||
|
|
||||||
Click the green `download` button to download your actual model binary. This pulls from the `source_url` you provided above.
|

|
||||||
|
|
||||||

|
Your model is now ready to use in Jan.
|
||||||
|
|
||||||
There you go! You are ready to use your model.
|
## Assistance and Support
|
||||||
|
|
||||||
If you have any questions or want to request for more preconfigured GGUF models, please message us in [Discord](https://discord.gg/Dt7MxDyNNZ).
|
If you have questions or are looking for more preconfigured GGUF models, please feel free to join our [Discord community](https://discord.gg/Dt7MxDyNNZ) for support, updates, and discussions.
|
||||||
|
|||||||
BIN
docs/docs/guides/04-using-models/assets/download-model.png
Normal file
BIN
docs/docs/guides/04-using-models/assets/download-model.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 378 KiB |
@ -48,6 +48,13 @@ export function handleAppIPCs() {
|
|||||||
shell.openPath(url)
|
shell.openPath(url)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joins multiple paths together, respect to the current OS.
|
||||||
|
*/
|
||||||
|
ipcMain.handle(AppRoute.joinPath, async (_event, paths: string[]) =>
|
||||||
|
join(...paths)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relaunches the app in production - reload window in development.
|
* Relaunches the app in production - reload window in development.
|
||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { DownloadManager } from './../managers/download'
|
|||||||
import { resolve, join } from 'path'
|
import { resolve, join } from 'path'
|
||||||
import { WindowManager } from './../managers/window'
|
import { WindowManager } from './../managers/window'
|
||||||
import request from 'request'
|
import request from 'request'
|
||||||
import { createWriteStream } from 'fs'
|
import { createWriteStream, renameSync } from 'fs'
|
||||||
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
||||||
const progress = require('request-progress')
|
const progress = require('request-progress')
|
||||||
|
|
||||||
@ -48,6 +48,8 @@ export function handleDownloaderIPCs() {
|
|||||||
const userDataPath = join(app.getPath('home'), 'jan')
|
const userDataPath = join(app.getPath('home'), 'jan')
|
||||||
const destination = resolve(userDataPath, fileName)
|
const destination = resolve(userDataPath, fileName)
|
||||||
const rq = request(url)
|
const rq = request(url)
|
||||||
|
// downloading file to a temp file first
|
||||||
|
const downloadingTempFile = `${destination}.download`
|
||||||
|
|
||||||
progress(rq, {})
|
progress(rq, {})
|
||||||
.on('progress', function (state: any) {
|
.on('progress', function (state: any) {
|
||||||
@ -70,6 +72,9 @@ export function handleDownloaderIPCs() {
|
|||||||
})
|
})
|
||||||
.on('end', function () {
|
.on('end', function () {
|
||||||
if (DownloadManager.instance.networkRequests[fileName]) {
|
if (DownloadManager.instance.networkRequests[fileName]) {
|
||||||
|
// Finished downloading, rename temp file to actual file
|
||||||
|
renameSync(downloadingTempFile, destination)
|
||||||
|
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
WindowManager?.instance.currentWindow?.webContents.send(
|
||||||
DownloadEvent.onFileDownloadSuccess,
|
DownloadEvent.onFileDownloadSuccess,
|
||||||
{
|
{
|
||||||
@ -87,7 +92,7 @@ export function handleDownloaderIPCs() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.pipe(createWriteStream(destination))
|
.pipe(createWriteStream(downloadingTempFile))
|
||||||
|
|
||||||
DownloadManager.instance.setRequest(fileName, rq)
|
DownloadManager.instance.setRequest(fileName, rq)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { ExtensionType, fs } from '@janhq/core'
|
import { ExtensionType, fs, joinPath } from '@janhq/core'
|
||||||
import { ConversationalExtension } from '@janhq/core'
|
import { ConversationalExtension } from '@janhq/core'
|
||||||
import { Thread, ThreadMessage } from '@janhq/core'
|
import { Thread, ThreadMessage } from '@janhq/core'
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSONConversationalExtension is a ConversationalExtension implementation that provides
|
* JSONConversationalExtension is a ConversationalExtension implementation that provides
|
||||||
@ -69,14 +68,14 @@ export default class JSONConversationalExtension
|
|||||||
*/
|
*/
|
||||||
async saveThread(thread: Thread): Promise<void> {
|
async saveThread(thread: Thread): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(
|
const threadDirPath = await joinPath([
|
||||||
JSONConversationalExtension._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
thread.id
|
thread.id,
|
||||||
)
|
])
|
||||||
const threadJsonPath = join(
|
const threadJsonPath = await joinPath([
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalExtension._threadInfoFileName
|
JSONConversationalExtension._threadInfoFileName,
|
||||||
)
|
])
|
||||||
await fs.mkdir(threadDirPath)
|
await fs.mkdir(threadDirPath)
|
||||||
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
|
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
@ -89,20 +88,22 @@ export default class JSONConversationalExtension
|
|||||||
* Delete a thread with the specified ID.
|
* Delete a thread with the specified ID.
|
||||||
* @param threadId The ID of the thread to delete.
|
* @param threadId The ID of the thread to delete.
|
||||||
*/
|
*/
|
||||||
deleteThread(threadId: string): Promise<void> {
|
async deleteThread(threadId: string): Promise<void> {
|
||||||
return fs.rmdir(join(JSONConversationalExtension._homeDir, `${threadId}`))
|
return fs.rmdir(
|
||||||
|
await joinPath([JSONConversationalExtension._homeDir, `${threadId}`])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async addNewMessage(message: ThreadMessage): Promise<void> {
|
async addNewMessage(message: ThreadMessage): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(
|
const threadDirPath = await joinPath([
|
||||||
JSONConversationalExtension._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
message.thread_id
|
message.thread_id,
|
||||||
)
|
])
|
||||||
const threadMessagePath = join(
|
const threadMessagePath = await joinPath([
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalExtension._threadMessagesFileName
|
JSONConversationalExtension._threadMessagesFileName,
|
||||||
)
|
])
|
||||||
await fs.mkdir(threadDirPath)
|
await fs.mkdir(threadDirPath)
|
||||||
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
|
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
@ -116,11 +117,14 @@ export default class JSONConversationalExtension
|
|||||||
messages: ThreadMessage[]
|
messages: ThreadMessage[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
|
const threadDirPath = await joinPath([
|
||||||
const threadMessagePath = join(
|
JSONConversationalExtension._homeDir,
|
||||||
|
threadId,
|
||||||
|
])
|
||||||
|
const threadMessagePath = await joinPath([
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalExtension._threadMessagesFileName
|
JSONConversationalExtension._threadMessagesFileName,
|
||||||
)
|
])
|
||||||
await fs.mkdir(threadDirPath)
|
await fs.mkdir(threadDirPath)
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
threadMessagePath,
|
threadMessagePath,
|
||||||
@ -140,11 +144,11 @@ export default class JSONConversationalExtension
|
|||||||
*/
|
*/
|
||||||
private async readThread(threadDirName: string): Promise<any> {
|
private async readThread(threadDirName: string): Promise<any> {
|
||||||
return fs.readFile(
|
return fs.readFile(
|
||||||
join(
|
await joinPath([
|
||||||
JSONConversationalExtension._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
threadDirName,
|
threadDirName,
|
||||||
JSONConversationalExtension._threadInfoFileName
|
JSONConversationalExtension._threadInfoFileName,
|
||||||
)
|
])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,10 +163,10 @@ export default class JSONConversationalExtension
|
|||||||
|
|
||||||
const threadDirs: string[] = []
|
const threadDirs: string[] = []
|
||||||
for (let i = 0; i < fileInsideThread.length; i++) {
|
for (let i = 0; i < fileInsideThread.length; i++) {
|
||||||
const path = join(
|
const path = await joinPath([
|
||||||
JSONConversationalExtension._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
fileInsideThread[i]
|
fileInsideThread[i],
|
||||||
)
|
])
|
||||||
const isDirectory = await fs.isDirectory(path)
|
const isDirectory = await fs.isDirectory(path)
|
||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
console.debug(`Ignore ${path} because it is not a directory`)
|
console.debug(`Ignore ${path} because it is not a directory`)
|
||||||
@ -184,7 +188,10 @@ export default class JSONConversationalExtension
|
|||||||
|
|
||||||
async getAllMessages(threadId: string): Promise<ThreadMessage[]> {
|
async getAllMessages(threadId: string): Promise<ThreadMessage[]> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
|
const threadDirPath = await joinPath([
|
||||||
|
JSONConversationalExtension._homeDir,
|
||||||
|
threadId,
|
||||||
|
])
|
||||||
const isDir = await fs.isDirectory(threadDirPath)
|
const isDir = await fs.isDirectory(threadDirPath)
|
||||||
if (!isDir) {
|
if (!isDir) {
|
||||||
throw Error(`${threadDirPath} is not directory`)
|
throw Error(`${threadDirPath} is not directory`)
|
||||||
@ -197,10 +204,10 @@ export default class JSONConversationalExtension
|
|||||||
throw Error(`${threadDirPath} not contains message file`)
|
throw Error(`${threadDirPath} not contains message file`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageFilePath = join(
|
const messageFilePath = await joinPath([
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalExtension._threadMessagesFileName
|
JSONConversationalExtension._threadMessagesFileName,
|
||||||
)
|
])
|
||||||
|
|
||||||
const result = await fs.readLineByLine(messageFilePath)
|
const result = await fs.readLineByLine(messageFilePath)
|
||||||
|
|
||||||
|
|||||||
@ -28,11 +28,12 @@ export CUDA_VISIBLE_DEVICES=$selectedGpuId
|
|||||||
|
|
||||||
# Attempt to run nitro_linux_amd64_cuda
|
# Attempt to run nitro_linux_amd64_cuda
|
||||||
cd linux-cuda
|
cd linux-cuda
|
||||||
if ./nitro "$@"; then
|
./nitro "$@" > output.log 2>&1 || (
|
||||||
|
echo "Check output log" &&
|
||||||
|
if grep -q "CUDA error" output.log; then
|
||||||
|
echo "CUDA error detected, attempting to run nitro_linux_amd64..."
|
||||||
|
cd ../linux-cpu && ./nitro "$@"
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
exit $?
|
exit $?
|
||||||
else
|
)
|
||||||
echo "nitro_linux_amd64_cuda encountered an error, attempting to run nitro_linux_amd64..."
|
|
||||||
cd ../linux-cpu
|
|
||||||
./nitro "$@"
|
|
||||||
exit $?
|
|
||||||
fi
|
|
||||||
|
|||||||
@ -31,9 +31,10 @@ set CUDA_VISIBLE_DEVICES=!gpuId!
|
|||||||
|
|
||||||
rem Attempt to run nitro_windows_amd64_cuda.exe
|
rem Attempt to run nitro_windows_amd64_cuda.exe
|
||||||
cd win-cuda
|
cd win-cuda
|
||||||
nitro.exe %*
|
|
||||||
if %errorlevel% neq 0 goto RunCpuVersion
|
nitro.exe %* > output.log
|
||||||
goto End
|
type output.log | findstr /C:"CUDA error" >nul
|
||||||
|
if %errorlevel% equ 0 ( goto :RunCpuVersion ) else ( goto :End )
|
||||||
|
|
||||||
:RunCpuVersion
|
:RunCpuVersion
|
||||||
rem Run nitro_windows_amd64.exe...
|
rem Run nitro_windows_amd64.exe...
|
||||||
|
|||||||
@ -111,7 +111,7 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const userSpacePath = await getUserSpace();
|
const userSpacePath = await getUserSpace();
|
||||||
const modelFullPath = join(userSpacePath, "models", model.id, model.id);
|
const modelFullPath = join(userSpacePath, "models", model.id);
|
||||||
|
|
||||||
const nitroInitResult = await executeOnMain(MODULE, "initModel", {
|
const nitroInitResult = await executeOnMain(MODULE, "initModel", {
|
||||||
modelFullPath: modelFullPath,
|
modelFullPath: modelFullPath,
|
||||||
|
|||||||
@ -13,10 +13,11 @@ const NITRO_HTTP_LOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/
|
|||||||
const NITRO_HTTP_UNLOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/unloadModel`;
|
const NITRO_HTTP_UNLOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/unloadModel`;
|
||||||
const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus`;
|
const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus`;
|
||||||
const NITRO_HTTP_KILL_URL = `${NITRO_HTTP_SERVER_URL}/processmanager/destroy`;
|
const NITRO_HTTP_KILL_URL = `${NITRO_HTTP_SERVER_URL}/processmanager/destroy`;
|
||||||
|
const SUPPORTED_MODEL_FORMAT = ".gguf";
|
||||||
|
|
||||||
// The subprocess instance for Nitro
|
// The subprocess instance for Nitro
|
||||||
let subprocess = undefined;
|
let subprocess = undefined;
|
||||||
let currentModelFile = undefined;
|
let currentModelFile: string = undefined;
|
||||||
let currentSettings = undefined;
|
let currentSettings = undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,6 +38,17 @@ function stopModel(): Promise<void> {
|
|||||||
*/
|
*/
|
||||||
async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
||||||
currentModelFile = wrapper.modelFullPath;
|
currentModelFile = wrapper.modelFullPath;
|
||||||
|
const files: string[] = fs.readdirSync(currentModelFile);
|
||||||
|
|
||||||
|
// Look for GGUF model file
|
||||||
|
const ggufBinFile = files.find(
|
||||||
|
(file) =>
|
||||||
|
file === path.basename(currentModelFile) ||
|
||||||
|
file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT)
|
||||||
|
);
|
||||||
|
|
||||||
|
currentModelFile = path.join(currentModelFile, ggufBinFile);
|
||||||
|
|
||||||
if (wrapper.model.engine !== "nitro") {
|
if (wrapper.model.engine !== "nitro") {
|
||||||
return Promise.resolve({ error: "Not a nitro model" });
|
return Promise.resolve({ error: "Not a nitro model" });
|
||||||
} else {
|
} else {
|
||||||
@ -66,15 +78,31 @@ async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
|||||||
async function loadModel(nitroResourceProbe: any | undefined) {
|
async function loadModel(nitroResourceProbe: any | undefined) {
|
||||||
// Gather system information for CPU physical cores and memory
|
// Gather system information for CPU physical cores and memory
|
||||||
if (!nitroResourceProbe) nitroResourceProbe = await getResourcesInfo();
|
if (!nitroResourceProbe) nitroResourceProbe = await getResourcesInfo();
|
||||||
return killSubprocess()
|
return (
|
||||||
.then(() => spawnNitroProcess(nitroResourceProbe))
|
killSubprocess()
|
||||||
.then(() => loadLLMModel(currentSettings))
|
.then(() => tcpPortUsed.waitUntilFree(PORT, 300, 5000))
|
||||||
.then(validateModelStatus)
|
// wait for 500ms to make sure the port is free for windows platform
|
||||||
.catch((err) => {
|
.then(() => {
|
||||||
console.error("error: ", err);
|
if (process.platform === "win32") {
|
||||||
// TODO: Broadcast error so app could display proper error message
|
return sleep(500);
|
||||||
return { error: err, currentModelFile };
|
} else {
|
||||||
});
|
return sleep(0);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => spawnNitroProcess(nitroResourceProbe))
|
||||||
|
.then(() => loadLLMModel(currentSettings))
|
||||||
|
.then(validateModelStatus)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("error: ", err);
|
||||||
|
// TODO: Broadcast error so app could display proper error message
|
||||||
|
return { error: err, currentModelFile };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add function sleep
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
function promptTemplateConverter(promptTemplate) {
|
function promptTemplateConverter(promptTemplate) {
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import {
|
|||||||
abortDownload,
|
abortDownload,
|
||||||
getResourcePath,
|
getResourcePath,
|
||||||
getUserSpace,
|
getUserSpace,
|
||||||
|
InferenceEngine,
|
||||||
|
joinPath,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { ModelExtension, Model, ModelState } from '@janhq/core'
|
import { basename } from 'path'
|
||||||
import { join } from 'path'
|
import { ModelExtension, Model } from '@janhq/core'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A extension for models
|
* A extension for models
|
||||||
@ -15,6 +17,9 @@ import { join } from 'path'
|
|||||||
export default class JanModelExtension implements ModelExtension {
|
export default class JanModelExtension implements ModelExtension {
|
||||||
private static readonly _homeDir = 'models'
|
private static readonly _homeDir = 'models'
|
||||||
private static readonly _modelMetadataFileName = 'model.json'
|
private static readonly _modelMetadataFileName = 'model.json'
|
||||||
|
private static readonly _supportedModelFormat = '.gguf'
|
||||||
|
private static readonly _incompletedModelFileName = '.download'
|
||||||
|
private static readonly _offlineInferenceEngine = InferenceEngine.nitro
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements type from JanExtension.
|
* Implements type from JanExtension.
|
||||||
@ -54,10 +59,10 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
|
|
||||||
// copy models folder from resources to home directory
|
// copy models folder from resources to home directory
|
||||||
const resourePath = await getResourcePath()
|
const resourePath = await getResourcePath()
|
||||||
const srcPath = join(resourePath, 'models')
|
const srcPath = await joinPath([resourePath, 'models'])
|
||||||
|
|
||||||
const userSpace = await getUserSpace()
|
const userSpace = await getUserSpace()
|
||||||
const destPath = join(userSpace, JanModelExtension._homeDir)
|
const destPath = await joinPath([userSpace, JanModelExtension._homeDir])
|
||||||
|
|
||||||
await fs.syncFile(srcPath, destPath)
|
await fs.syncFile(srcPath, destPath)
|
||||||
|
|
||||||
@ -88,11 +93,18 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
*/
|
*/
|
||||||
async downloadModel(model: Model): Promise<void> {
|
async downloadModel(model: Model): Promise<void> {
|
||||||
// create corresponding directory
|
// create corresponding directory
|
||||||
const directoryPath = join(JanModelExtension._homeDir, model.id)
|
const modelDirPath = await joinPath([JanModelExtension._homeDir, model.id])
|
||||||
await fs.mkdir(directoryPath)
|
await fs.mkdir(modelDirPath)
|
||||||
|
|
||||||
// path to model binary
|
// try to retrieve the download file name from the source url
|
||||||
const path = join(directoryPath, model.id)
|
// if it fails, use the model ID as the file name
|
||||||
|
const extractedFileName = basename(model.source_url)
|
||||||
|
const fileName = extractedFileName
|
||||||
|
.toLowerCase()
|
||||||
|
.endsWith(JanModelExtension._supportedModelFormat)
|
||||||
|
? extractedFileName
|
||||||
|
: model.id
|
||||||
|
const path = await joinPath([modelDirPath, fileName])
|
||||||
downloadFile(model.source_url, path)
|
downloadFile(model.source_url, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,10 +115,12 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
*/
|
*/
|
||||||
async cancelModelDownload(modelId: string): Promise<void> {
|
async cancelModelDownload(modelId: string): Promise<void> {
|
||||||
return abortDownload(
|
return abortDownload(
|
||||||
join(JanModelExtension._homeDir, modelId, modelId)
|
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||||
).then(() => {
|
).then(async () =>
|
||||||
fs.deleteFile(join(JanModelExtension._homeDir, modelId, modelId))
|
fs.deleteFile(
|
||||||
})
|
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,27 +130,16 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
*/
|
*/
|
||||||
async deleteModel(modelId: string): Promise<void> {
|
async deleteModel(modelId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const dirPath = join(JanModelExtension._homeDir, modelId)
|
const dirPath = await joinPath([JanModelExtension._homeDir, modelId])
|
||||||
|
|
||||||
// remove all files under dirPath except model.json
|
// remove all files under dirPath except model.json
|
||||||
const files = await fs.listFiles(dirPath)
|
const files = await fs.listFiles(dirPath)
|
||||||
const deletePromises = files.map((fileName: string) => {
|
const deletePromises = files.map(async (fileName: string) => {
|
||||||
if (fileName !== JanModelExtension._modelMetadataFileName) {
|
if (fileName !== JanModelExtension._modelMetadataFileName) {
|
||||||
return fs.deleteFile(join(dirPath, fileName))
|
return fs.deleteFile(await joinPath([dirPath, fileName]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await Promise.allSettled(deletePromises)
|
await Promise.allSettled(deletePromises)
|
||||||
|
|
||||||
// update the state as default
|
|
||||||
const jsonFilePath = join(
|
|
||||||
dirPath,
|
|
||||||
JanModelExtension._modelMetadataFileName
|
|
||||||
)
|
|
||||||
const json = await fs.readFile(jsonFilePath)
|
|
||||||
const model = JSON.parse(json) as Model
|
|
||||||
delete model.state
|
|
||||||
|
|
||||||
await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2))
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -148,24 +151,14 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
* @returns A Promise that resolves when the model is saved.
|
* @returns A Promise that resolves when the model is saved.
|
||||||
*/
|
*/
|
||||||
async saveModel(model: Model): Promise<void> {
|
async saveModel(model: Model): Promise<void> {
|
||||||
const jsonFilePath = join(
|
const jsonFilePath = await joinPath([
|
||||||
JanModelExtension._homeDir,
|
JanModelExtension._homeDir,
|
||||||
model.id,
|
model.id,
|
||||||
JanModelExtension._modelMetadataFileName
|
JanModelExtension._modelMetadataFileName,
|
||||||
)
|
])
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.writeFile(
|
await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2))
|
||||||
jsonFilePath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
...model,
|
|
||||||
state: ModelState.Ready,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -176,11 +169,34 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
* @returns A Promise that resolves with an array of all models.
|
* @returns A Promise that resolves with an array of all models.
|
||||||
*/
|
*/
|
||||||
async getDownloadedModels(): Promise<Model[]> {
|
async getDownloadedModels(): Promise<Model[]> {
|
||||||
const models = await this.getModelsMetadata()
|
return await this.getModelsMetadata(
|
||||||
return models.filter((model) => model.state === ModelState.Ready)
|
async (modelDir: string, model: Model) => {
|
||||||
|
if (model.engine !== JanModelExtension._offlineInferenceEngine) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return await fs
|
||||||
|
.listFiles(await joinPath([JanModelExtension._homeDir, modelDir]))
|
||||||
|
.then((files: string[]) => {
|
||||||
|
// or model binary exists in the directory
|
||||||
|
// model binary name can match model ID or be a .gguf file and not be an incompleted model file
|
||||||
|
return (
|
||||||
|
files.includes(modelDir) ||
|
||||||
|
files.some(
|
||||||
|
(file) =>
|
||||||
|
file
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(JanModelExtension._supportedModelFormat) &&
|
||||||
|
!file.endsWith(JanModelExtension._incompletedModelFileName)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getModelsMetadata(): Promise<Model[]> {
|
private async getModelsMetadata(
|
||||||
|
selector?: (path: string, model: Model) => Promise<boolean>
|
||||||
|
): Promise<Model[]> {
|
||||||
try {
|
try {
|
||||||
const filesUnderJanRoot = await fs.listFiles('')
|
const filesUnderJanRoot = await fs.listFiles('')
|
||||||
if (!filesUnderJanRoot.includes(JanModelExtension._homeDir)) {
|
if (!filesUnderJanRoot.includes(JanModelExtension._homeDir)) {
|
||||||
@ -193,26 +209,35 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
const allDirectories: string[] = []
|
const allDirectories: string[] = []
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const isDirectory = await fs.isDirectory(
|
const isDirectory = await fs.isDirectory(
|
||||||
join(JanModelExtension._homeDir, file)
|
await joinPath([JanModelExtension._homeDir, file])
|
||||||
)
|
)
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
allDirectories.push(file)
|
allDirectories.push(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const readJsonPromises = allDirectories.map((dirName) => {
|
const readJsonPromises = allDirectories.map(async (dirName) => {
|
||||||
const jsonPath = join(
|
// filter out directories that don't match the selector
|
||||||
|
|
||||||
|
// read model.json
|
||||||
|
const jsonPath = await joinPath([
|
||||||
JanModelExtension._homeDir,
|
JanModelExtension._homeDir,
|
||||||
dirName,
|
dirName,
|
||||||
JanModelExtension._modelMetadataFileName
|
JanModelExtension._modelMetadataFileName,
|
||||||
)
|
])
|
||||||
return this.readModelMetadata(jsonPath)
|
let model = await this.readModelMetadata(jsonPath)
|
||||||
|
model = typeof model === 'object' ? model : JSON.parse(model)
|
||||||
|
|
||||||
|
if (selector && !(await selector?.(dirName, model))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return model
|
||||||
})
|
})
|
||||||
const results = await Promise.allSettled(readJsonPromises)
|
const results = await Promise.allSettled(readJsonPromises)
|
||||||
const modelData = results.map((result) => {
|
const modelData = results.map((result) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(result.value) as Model
|
return result.value as Model
|
||||||
} catch {
|
} catch {
|
||||||
console.debug(`Unable to parse model metadata: ${result.value}`)
|
console.debug(`Unable to parse model metadata: ${result.value}`)
|
||||||
return undefined
|
return undefined
|
||||||
@ -230,7 +255,7 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readModelMetadata(path: string) {
|
private readModelMetadata(path: string) {
|
||||||
return fs.readFile(join(path))
|
return fs.readFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -29,6 +29,13 @@ export default function CardSidebar({
|
|||||||
|
|
||||||
useClickOutside(() => setMore(false), null, [menu, toggle])
|
useClickOutside(() => setMore(false), null, [menu, toggle])
|
||||||
|
|
||||||
|
let openFolderTitle: string = 'Open Containing Folder'
|
||||||
|
if (isMac) {
|
||||||
|
openFolderTitle = 'Reveal in Finder'
|
||||||
|
} else if (isWindows) {
|
||||||
|
openFolderTitle = 'Reveal in File Explorer'
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@ -74,7 +81,7 @@ export default function CardSidebar({
|
|||||||
>
|
>
|
||||||
<FolderOpenIcon size={16} className="text-muted-foreground" />
|
<FolderOpenIcon size={16} className="text-muted-foreground" />
|
||||||
<span className="text-bold text-black dark:text-muted-foreground">
|
<span className="text-bold text-black dark:text-muted-foreground">
|
||||||
Reveal in Finder
|
{openFolderTitle}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,48 +1,33 @@
|
|||||||
import { FieldValues, UseFormRegister } from 'react-hook-form'
|
import React from 'react'
|
||||||
|
|
||||||
import { ModelRuntimeParams } from '@janhq/core'
|
|
||||||
import { Switch } from '@janhq/uikit'
|
import { Switch } from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
||||||
|
|
||||||
import {
|
import { getActiveThreadIdAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
getActiveThreadIdAtom,
|
|
||||||
getActiveThreadModelRuntimeParamsAtom,
|
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string
|
name: string
|
||||||
title: string
|
title: string
|
||||||
checked: boolean
|
checked: boolean
|
||||||
register: UseFormRegister<FieldValues>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Checkbox: React.FC<Props> = ({ name, title, checked, register }) => {
|
const Checkbox: React.FC<Props> = ({ name, title, checked }) => {
|
||||||
const { updateModelParameter } = useUpdateModelParameters()
|
const { updateModelParameter } = useUpdateModelParameters()
|
||||||
const threadId = useAtomValue(getActiveThreadIdAtom)
|
const threadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
const activeModelParams = useAtomValue(getActiveThreadModelRuntimeParamsAtom)
|
|
||||||
|
|
||||||
const onCheckedChange = (checked: boolean) => {
|
const onCheckedChange = (checked: boolean) => {
|
||||||
if (!threadId || !activeModelParams) return
|
if (!threadId) return
|
||||||
|
|
||||||
const updatedModelParams: ModelRuntimeParams = {
|
updateModelParameter(threadId, name, checked)
|
||||||
...activeModelParams,
|
|
||||||
[name]: checked,
|
|
||||||
}
|
|
||||||
|
|
||||||
updateModelParameter(threadId, updatedModelParams)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<label>{title}</label>
|
<p className="mb-2 text-sm font-semibold text-gray-600">{title}</p>
|
||||||
<Switch
|
<Switch checked={checked} onCheckedChange={onCheckedChange} />
|
||||||
checked={checked}
|
|
||||||
{...register(name)}
|
|
||||||
onCheckedChange={onCheckedChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,10 +9,6 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
TooltipArrow,
|
|
||||||
Input,
|
Input,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
@ -32,14 +28,22 @@ import useRecommendedModel from '@/hooks/useRecommendedModel'
|
|||||||
|
|
||||||
import { toGigabytes } from '@/utils/converter'
|
import { toGigabytes } from '@/utils/converter'
|
||||||
|
|
||||||
import { activeThreadAtom, threadStatesAtom } from '@/helpers/atoms/Thread.atom'
|
import {
|
||||||
|
activeThreadAtom,
|
||||||
|
getActiveThreadIdAtom,
|
||||||
|
setThreadModelParamsAtom,
|
||||||
|
threadStatesAtom,
|
||||||
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
export const selectedModelAtom = atom<Model | undefined>(undefined)
|
export const selectedModelAtom = atom<Model | undefined>(undefined)
|
||||||
|
|
||||||
export default function DropdownListSidebar() {
|
export default function DropdownListSidebar() {
|
||||||
const setSelectedModel = useSetAtom(selectedModelAtom)
|
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
const threadStates = useAtomValue(threadStatesAtom)
|
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
|
const threadStates = useAtomValue(threadStatesAtom)
|
||||||
|
const setSelectedModel = useSetAtom(selectedModelAtom)
|
||||||
|
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||||
|
|
||||||
const [selected, setSelected] = useState<Model | undefined>()
|
const [selected, setSelected] = useState<Model | undefined>()
|
||||||
const { setMainViewState } = useMainViewState()
|
const { setMainViewState } = useMainViewState()
|
||||||
const [openAISettings, setOpenAISettings] = useState<
|
const [openAISettings, setOpenAISettings] = useState<
|
||||||
@ -58,83 +62,93 @@ export default function DropdownListSidebar() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelected(recommendedModel)
|
setSelected(recommendedModel)
|
||||||
setSelectedModel(recommendedModel)
|
setSelectedModel(recommendedModel)
|
||||||
}, [recommendedModel, setSelectedModel])
|
|
||||||
|
if (activeThread) {
|
||||||
|
const finishInit = threadStates[activeThread.id].isFinishInit ?? true
|
||||||
|
if (finishInit) return
|
||||||
|
const modelParams = {
|
||||||
|
...recommendedModel?.parameters,
|
||||||
|
...recommendedModel?.settings,
|
||||||
|
}
|
||||||
|
setThreadModelParams(activeThread.id, modelParams)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
recommendedModel,
|
||||||
|
activeThread,
|
||||||
|
setSelectedModel,
|
||||||
|
setThreadModelParams,
|
||||||
|
threadStates,
|
||||||
|
])
|
||||||
|
|
||||||
const onValueSelected = useCallback(
|
const onValueSelected = useCallback(
|
||||||
(modelId: string) => {
|
(modelId: string) => {
|
||||||
const model = downloadedModels.find((m) => m.id === modelId)
|
const model = downloadedModels.find((m) => m.id === modelId)
|
||||||
setSelected(model)
|
setSelected(model)
|
||||||
setSelectedModel(model)
|
setSelectedModel(model)
|
||||||
|
|
||||||
|
if (activeThreadId) {
|
||||||
|
const modelParams = {
|
||||||
|
...model?.parameters,
|
||||||
|
...model?.settings,
|
||||||
|
}
|
||||||
|
setThreadModelParams(activeThreadId, modelParams)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[downloadedModels, setSelectedModel]
|
[downloadedModels, activeThreadId, setSelectedModel, setThreadModelParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!activeThread) {
|
if (!activeThread) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const finishInit = threadStates[activeThread.id].isFinishInit ?? true
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<>
|
||||||
<TooltipTrigger className="w-full">
|
<Select value={selected?.id} onValueChange={onValueSelected}>
|
||||||
<Select
|
<SelectTrigger className="w-full">
|
||||||
disabled={finishInit}
|
<SelectValue placeholder="Choose model to start">
|
||||||
value={selected?.id}
|
{downloadedModels.filter((x) => x.id === selected?.id)[0]?.name}
|
||||||
onValueChange={finishInit ? undefined : onValueSelected}
|
</SelectValue>
|
||||||
>
|
</SelectTrigger>
|
||||||
<SelectTrigger className="w-full">
|
<SelectContent className="right-5 block w-full min-w-[300px] pr-0">
|
||||||
<SelectValue placeholder="Choose model to start">
|
<div className="flex w-full items-center space-x-2 px-4 py-2">
|
||||||
{downloadedModels.filter((x) => x.id === selected?.id)[0]?.name}
|
<MonitorIcon size={20} className="text-muted-foreground" />
|
||||||
</SelectValue>
|
<span>Local</span>
|
||||||
</SelectTrigger>
|
</div>
|
||||||
<SelectContent className="right-5 block w-full min-w-[300px] pr-0">
|
<div className="border-b border-border" />
|
||||||
<div className="flex w-full items-center space-x-2 px-4 py-2">
|
{downloadedModels.length === 0 ? (
|
||||||
<MonitorIcon size={20} className="text-muted-foreground" />
|
<div className="px-4 py-2">
|
||||||
<span>Local</span>
|
<p>{`Oops, you don't have a model yet.`}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b border-border" />
|
) : (
|
||||||
{downloadedModels.length === 0 ? (
|
<SelectGroup>
|
||||||
<div className="px-4 py-2">
|
{downloadedModels.map((x, i) => (
|
||||||
<p>{`Oops, you don't have a model yet.`}</p>
|
<SelectItem
|
||||||
</div>
|
key={i}
|
||||||
) : (
|
value={x.id}
|
||||||
<SelectGroup>
|
className={twMerge(x.id === selected?.id && 'bg-secondary')}
|
||||||
{downloadedModels.map((x, i) => (
|
>
|
||||||
<SelectItem
|
<div className="flex w-full justify-between">
|
||||||
key={i}
|
<span className="line-clamp-1 block">{x.name}</span>
|
||||||
value={x.id}
|
<span className="font-bold text-muted-foreground">
|
||||||
className={twMerge(x.id === selected?.id && 'bg-secondary')}
|
{toGigabytes(x.metadata.size)}
|
||||||
>
|
</span>
|
||||||
<div className="flex w-full justify-between">
|
</div>
|
||||||
<span className="line-clamp-1 block">{x.name}</span>
|
</SelectItem>
|
||||||
<span className="font-bold text-muted-foreground">
|
))}
|
||||||
{toGigabytes(x.metadata.size)}
|
</SelectGroup>
|
||||||
</span>
|
)}
|
||||||
</div>
|
<div className="border-b border-border" />
|
||||||
</SelectItem>
|
<div className="w-full px-4 py-2">
|
||||||
))}
|
<Button
|
||||||
</SelectGroup>
|
block
|
||||||
)}
|
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
|
||||||
<div className="border-b border-border" />
|
onClick={() => setMainViewState(MainViewState.Hub)}
|
||||||
<div className="w-full px-4 py-2">
|
>
|
||||||
<Button
|
Explore The Hub
|
||||||
block
|
</Button>
|
||||||
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
|
</div>
|
||||||
onClick={() => setMainViewState(MainViewState.Hub)}
|
</SelectContent>
|
||||||
>
|
</Select>
|
||||||
Explore The Hub
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
{finishInit && (
|
|
||||||
<TooltipContent sideOffset={10}>
|
|
||||||
<span>Start a new thread to change the model</span>
|
|
||||||
<TooltipArrow />
|
|
||||||
</TooltipContent>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selected?.engine === InferenceEngine.openai && (
|
{selected?.engine === InferenceEngine.openai && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
@ -154,6 +168,6 @@ export default function DropdownListSidebar() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
|
|
||||||
import { ExtensionType } from '@janhq/core'
|
|
||||||
import { ModelExtension } from '@janhq/core'
|
|
||||||
import {
|
import {
|
||||||
Progress,
|
Progress,
|
||||||
Modal,
|
Modal,
|
||||||
@ -12,14 +10,19 @@ import {
|
|||||||
ModalTrigger,
|
ModalTrigger,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
import { formatDownloadPercentage } from '@/utils/converter'
|
import { formatDownloadPercentage } from '@/utils/converter'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
export default function DownloadingState() {
|
export default function DownloadingState() {
|
||||||
const { downloadStates } = useDownloadState()
|
const { downloadStates } = useDownloadState()
|
||||||
|
const downloadingModels = useAtomValue(downloadingModelsAtom)
|
||||||
|
const { abortModelDownload } = useDownloadModel()
|
||||||
|
|
||||||
const totalCurrentProgress = downloadStates
|
const totalCurrentProgress = downloadStates
|
||||||
.map((a) => a.size.transferred + a.size.transferred)
|
.map((a) => a.size.transferred + a.size.transferred)
|
||||||
@ -73,9 +76,10 @@ export default function DownloadingState() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item?.modelId) {
|
if (item?.modelId) {
|
||||||
extensionManager
|
const model = downloadingModels.find(
|
||||||
.get<ModelExtension>(ExtensionType.Model)
|
(model) => model.id === item.modelId
|
||||||
?.cancelModelDownload(item.modelId)
|
)
|
||||||
|
if (model) abortModelDownload(model)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import { ModelExtension, ExtensionType } from '@janhq/core'
|
|
||||||
import { Model } from '@janhq/core'
|
import { Model } from '@janhq/core'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -17,11 +16,12 @@ import {
|
|||||||
|
|
||||||
import { atom, useAtomValue } from 'jotai'
|
import { atom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
import { formatDownloadPercentage } from '@/utils/converter'
|
import { formatDownloadPercentage } from '@/utils/converter'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
model: Model
|
model: Model
|
||||||
@ -30,6 +30,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function ModalCancelDownload({ model, isFromList }: Props) {
|
export default function ModalCancelDownload({ model, isFromList }: Props) {
|
||||||
const { modelDownloadStateAtom } = useDownloadState()
|
const { modelDownloadStateAtom } = useDownloadState()
|
||||||
|
const downloadingModels = useAtomValue(downloadingModelsAtom)
|
||||||
const downloadAtom = useMemo(
|
const downloadAtom = useMemo(
|
||||||
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
|
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -37,6 +38,7 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
|
|||||||
)
|
)
|
||||||
const downloadState = useAtomValue(downloadAtom)
|
const downloadState = useAtomValue(downloadAtom)
|
||||||
const cancelText = `Cancel ${formatDownloadPercentage(downloadState.percent)}`
|
const cancelText = `Cancel ${formatDownloadPercentage(downloadState.percent)}`
|
||||||
|
const { abortModelDownload } = useDownloadModel()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal>
|
<Modal>
|
||||||
@ -80,9 +82,10 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
|
|||||||
themes="danger"
|
themes="danger"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (downloadState?.modelId) {
|
if (downloadState?.modelId) {
|
||||||
extensionManager
|
const model = downloadingModels.find(
|
||||||
.get<ModelExtension>(ExtensionType.Model)
|
(model) => model.id === downloadState.modelId
|
||||||
?.cancelModelDownload(downloadState.modelId)
|
)
|
||||||
|
if (model) abortModelDownload(model)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
43
web/containers/ModelConfigInput/index.tsx
Normal file
43
web/containers/ModelConfigInput/index.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Textarea } from '@janhq/uikit'
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
||||||
|
|
||||||
|
import { getActiveThreadIdAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
name: string
|
||||||
|
placeholder: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModelConfigInput: React.FC<Props> = ({
|
||||||
|
title,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
}) => {
|
||||||
|
const { updateModelParameter } = useUpdateModelParameters()
|
||||||
|
const threadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
|
|
||||||
|
const onValueChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (!threadId) return
|
||||||
|
|
||||||
|
updateModelParameter(threadId, name, e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="mb-2 text-sm font-semibold text-gray-600">{title}</p>
|
||||||
|
<Textarea
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={onValueChanged}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModelConfigInput
|
||||||
@ -1,34 +1,35 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { PropsWithChildren, useEffect, useRef } from 'react'
|
import { basename } from 'path'
|
||||||
|
|
||||||
import { ExtensionType } from '@janhq/core'
|
import { PropsWithChildren, useEffect, useRef } from 'react'
|
||||||
import { ModelExtension } from '@janhq/core'
|
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
|
|
||||||
|
import { modelBinFileName } from '@/utils/model'
|
||||||
|
|
||||||
import EventHandler from './EventHandler'
|
import EventHandler from './EventHandler'
|
||||||
|
|
||||||
import { appDownloadProgress } from './Jotai'
|
import { appDownloadProgress } from './Jotai'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
||||||
const setProgress = useSetAtom(appDownloadProgress)
|
const setProgress = useSetAtom(appDownloadProgress)
|
||||||
const models = useAtomValue(downloadingModelsAtom)
|
const models = useAtomValue(downloadingModelsAtom)
|
||||||
const modelsRef = useRef(models)
|
const modelsRef = useRef(models)
|
||||||
useEffect(() => {
|
|
||||||
modelsRef.current = models
|
|
||||||
}, [models])
|
|
||||||
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
||||||
const { setDownloadState, setDownloadStateSuccess, setDownloadStateFailed } =
|
const { setDownloadState, setDownloadStateSuccess, setDownloadStateFailed } =
|
||||||
useDownloadState()
|
useDownloadState()
|
||||||
const downloadedModelRef = useRef(downloadedModels)
|
const downloadedModelRef = useRef(downloadedModels)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
modelsRef.current = models
|
||||||
|
}, [models])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
downloadedModelRef.current = downloadedModels
|
downloadedModelRef.current = downloadedModels
|
||||||
}, [downloadedModels])
|
}, [downloadedModels])
|
||||||
@ -38,40 +39,36 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
|||||||
window.electronAPI.onFileDownloadUpdate(
|
window.electronAPI.onFileDownloadUpdate(
|
||||||
(_event: string, state: any | undefined) => {
|
(_event: string, state: any | undefined) => {
|
||||||
if (!state) return
|
if (!state) return
|
||||||
setDownloadState({
|
const model = modelsRef.current.find(
|
||||||
...state,
|
(model) => modelBinFileName(model) === basename(state.fileName)
|
||||||
modelId: state.fileName.split('/').pop() ?? '',
|
)
|
||||||
})
|
if (model)
|
||||||
|
setDownloadState({
|
||||||
|
...state,
|
||||||
|
modelId: model.id,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
window.electronAPI.onFileDownloadError(
|
window.electronAPI.onFileDownloadError((_event: string, state: any) => {
|
||||||
(_event: string, callback: any) => {
|
console.error('Download error', state)
|
||||||
console.error('Download error', callback)
|
const model = modelsRef.current.find(
|
||||||
const modelId = callback.fileName.split('/').pop() ?? ''
|
(model) => modelBinFileName(model) === basename(state.fileName)
|
||||||
setDownloadStateFailed(modelId)
|
)
|
||||||
}
|
if (model) setDownloadStateFailed(model.id)
|
||||||
)
|
})
|
||||||
|
|
||||||
window.electronAPI.onFileDownloadSuccess(
|
window.electronAPI.onFileDownloadSuccess((_event: string, state: any) => {
|
||||||
(_event: string, callback: any) => {
|
if (state && state.fileName) {
|
||||||
if (callback && callback.fileName) {
|
const model = modelsRef.current.find(
|
||||||
const modelId = callback.fileName.split('/').pop() ?? ''
|
(model) => modelBinFileName(model) === basename(state.fileName)
|
||||||
|
)
|
||||||
const model = modelsRef.current.find((e) => e.id === modelId)
|
if (model) {
|
||||||
|
setDownloadStateSuccess(model.id)
|
||||||
setDownloadStateSuccess(modelId)
|
setDownloadedModels([...downloadedModelRef.current, model])
|
||||||
|
|
||||||
if (model)
|
|
||||||
extensionManager
|
|
||||||
.get<ModelExtension>(ExtensionType.Model)
|
|
||||||
?.saveModel(model)
|
|
||||||
.then(() => {
|
|
||||||
setDownloadedModels([...downloadedModelRef.current, model])
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
window.electronAPI.onAppUpdateDownloadUpdate(
|
window.electronAPI.onAppUpdateDownloadUpdate(
|
||||||
(_event: string, progress: any) => {
|
(_event: string, progress: any) => {
|
||||||
|
|||||||
@ -73,7 +73,7 @@ const Providers = (props: PropsWithChildren) => {
|
|||||||
{setupCore && activated && (
|
{setupCore && activated && (
|
||||||
<FeatureToggleWrapper>
|
<FeatureToggleWrapper>
|
||||||
<EventListenerWrapper>
|
<EventListenerWrapper>
|
||||||
<TooltipProvider>{children}</TooltipProvider>
|
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
|
||||||
</EventListenerWrapper>
|
</EventListenerWrapper>
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-right" />
|
||||||
</FeatureToggleWrapper>
|
</FeatureToggleWrapper>
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
import { FieldValues, UseFormRegister } from 'react-hook-form'
|
import React from 'react'
|
||||||
|
|
||||||
import { ModelRuntimeParams } from '@janhq/core'
|
|
||||||
import { Slider, Input } from '@janhq/uikit'
|
import { Slider, Input } from '@janhq/uikit'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
||||||
|
|
||||||
import {
|
import { getActiveThreadIdAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
getActiveThreadIdAtom,
|
|
||||||
getActiveThreadModelRuntimeParamsAtom,
|
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string
|
name: string
|
||||||
@ -18,7 +14,6 @@ type Props = {
|
|||||||
max: number
|
max: number
|
||||||
step: number
|
step: number
|
||||||
value: number
|
value: number
|
||||||
register: UseFormRegister<FieldValues>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SliderRightPanel: React.FC<Props> = ({
|
const SliderRightPanel: React.FC<Props> = ({
|
||||||
@ -28,21 +23,14 @@ const SliderRightPanel: React.FC<Props> = ({
|
|||||||
max,
|
max,
|
||||||
step,
|
step,
|
||||||
value,
|
value,
|
||||||
register,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { updateModelParameter } = useUpdateModelParameters()
|
const { updateModelParameter } = useUpdateModelParameters()
|
||||||
const threadId = useAtomValue(getActiveThreadIdAtom)
|
const threadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
const activeModelParams = useAtomValue(getActiveThreadModelRuntimeParamsAtom)
|
|
||||||
|
|
||||||
const onValueChanged = (e: number[]) => {
|
const onValueChanged = (e: number[]) => {
|
||||||
if (!threadId || !activeModelParams) return
|
if (!threadId) return
|
||||||
|
|
||||||
const updatedModelParams: ModelRuntimeParams = {
|
updateModelParameter(threadId, name, e[0])
|
||||||
...activeModelParams,
|
|
||||||
[name]: Number(e[0]),
|
|
||||||
}
|
|
||||||
|
|
||||||
updateModelParameter(threadId, updatedModelParams)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -51,9 +39,6 @@ const SliderRightPanel: React.FC<Props> = ({
|
|||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<Slider
|
<Slider
|
||||||
{...register(name, {
|
|
||||||
setValueAs: (v: string) => parseInt(v),
|
|
||||||
})}
|
|
||||||
value={[value]}
|
value={[value]}
|
||||||
onValueChange={onValueChanged}
|
onValueChange={onValueChanged}
|
||||||
min={min}
|
min={min}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ModelRuntimeParams,
|
ModelRuntimeParams,
|
||||||
|
ModelSettingParams,
|
||||||
Thread,
|
Thread,
|
||||||
ThreadContent,
|
ThreadContent,
|
||||||
ThreadState,
|
ThreadState,
|
||||||
@ -110,30 +111,26 @@ export const activeThreadAtom = atom<Thread | undefined>((get) =>
|
|||||||
/**
|
/**
|
||||||
* Store model params at thread level settings
|
* Store model params at thread level settings
|
||||||
*/
|
*/
|
||||||
export const threadModelRuntimeParamsAtom = atom<
|
export const threadModelParamsAtom = atom<Record<string, ModelParams>>({})
|
||||||
Record<string, ModelRuntimeParams>
|
|
||||||
>({})
|
|
||||||
|
|
||||||
export const getActiveThreadModelRuntimeParamsAtom = atom<
|
export type ModelParams = ModelRuntimeParams | ModelSettingParams
|
||||||
ModelRuntimeParams | undefined
|
|
||||||
>((get) => {
|
export const getActiveThreadModelParamsAtom = atom<ModelParams | undefined>(
|
||||||
const threadId = get(activeThreadIdAtom)
|
(get) => {
|
||||||
if (!threadId) {
|
const threadId = get(activeThreadIdAtom)
|
||||||
console.debug('Active thread id is undefined')
|
if (!threadId) {
|
||||||
return undefined
|
console.debug('Active thread id is undefined')
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return get(threadModelParamsAtom)[threadId]
|
||||||
}
|
}
|
||||||
|
|
||||||
return get(threadModelRuntimeParamsAtom)[threadId]
|
|
||||||
})
|
|
||||||
|
|
||||||
export const getThreadModelRuntimeParamsAtom = atom(
|
|
||||||
(get, threadId: string) => get(threadModelRuntimeParamsAtom)[threadId]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export const setThreadModelRuntimeParamsAtom = atom(
|
export const setThreadModelParamsAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, threadId: string, params: ModelRuntimeParams) => {
|
(get, set, threadId: string, params: ModelParams) => {
|
||||||
const currentState = { ...get(threadModelRuntimeParamsAtom) }
|
const currentState = { ...get(threadModelParamsAtom) }
|
||||||
currentState[threadId] = params
|
currentState[threadId] = params
|
||||||
console.debug(
|
console.debug(
|
||||||
`Update model params for thread ${threadId}, ${JSON.stringify(
|
`Update model params for thread ${threadId}, ${JSON.stringify(
|
||||||
@ -142,6 +139,6 @@ export const setThreadModelRuntimeParamsAtom = atom(
|
|||||||
2
|
2
|
||||||
)}`
|
)}`
|
||||||
)
|
)
|
||||||
set(threadModelRuntimeParamsAtom, currentState)
|
set(threadModelParamsAtom, currentState)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import {
|
|||||||
setActiveThreadIdAtom,
|
setActiveThreadIdAtom,
|
||||||
threadStatesAtom,
|
threadStatesAtom,
|
||||||
updateThreadAtom,
|
updateThreadAtom,
|
||||||
setThreadModelRuntimeParamsAtom,
|
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
const createNewThreadAtom = atom(null, (get, set, newThread: Thread) => {
|
const createNewThreadAtom = atom(null, (get, set, newThread: Thread) => {
|
||||||
@ -45,10 +44,6 @@ export const useCreateNewThread = () => {
|
|||||||
const createNewThread = useSetAtom(createNewThreadAtom)
|
const createNewThread = useSetAtom(createNewThreadAtom)
|
||||||
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
||||||
const updateThread = useSetAtom(updateThreadAtom)
|
const updateThread = useSetAtom(updateThreadAtom)
|
||||||
const setThreadModelRuntimeParams = useSetAtom(
|
|
||||||
setThreadModelRuntimeParamsAtom
|
|
||||||
)
|
|
||||||
|
|
||||||
const { deleteThread } = useDeleteThread()
|
const { deleteThread } = useDeleteThread()
|
||||||
|
|
||||||
const requestCreateNewThread = async (
|
const requestCreateNewThread = async (
|
||||||
@ -77,10 +72,7 @@ export const useCreateNewThread = () => {
|
|||||||
model: {
|
model: {
|
||||||
id: modelId,
|
id: modelId,
|
||||||
settings: {},
|
settings: {},
|
||||||
parameters: {
|
parameters: {},
|
||||||
stream: true,
|
|
||||||
max_tokens: 1024,
|
|
||||||
},
|
|
||||||
engine: undefined,
|
engine: undefined,
|
||||||
},
|
},
|
||||||
instructions: assistant.instructions,
|
instructions: assistant.instructions,
|
||||||
@ -94,7 +86,6 @@ export const useCreateNewThread = () => {
|
|||||||
created: createdAt,
|
created: createdAt,
|
||||||
updated: createdAt,
|
updated: createdAt,
|
||||||
}
|
}
|
||||||
setThreadModelRuntimeParams(thread.id, assistantInfo.model.parameters)
|
|
||||||
|
|
||||||
// add the new thread on top of the thread list to the state
|
// add the new thread on top of the thread list to the state
|
||||||
createNewThread(thread)
|
createNewThread(thread)
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
setActiveThreadIdAtom,
|
setActiveThreadIdAtom,
|
||||||
deleteThreadStateAtom,
|
deleteThreadStateAtom,
|
||||||
threadStatesAtom,
|
threadStatesAtom,
|
||||||
|
updateThreadStateLastMessageAtom,
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
export default function useDeleteThread() {
|
export default function useDeleteThread() {
|
||||||
@ -33,21 +34,23 @@ export default function useDeleteThread() {
|
|||||||
const deleteMessages = useSetAtom(deleteChatMessagesAtom)
|
const deleteMessages = useSetAtom(deleteChatMessagesAtom)
|
||||||
const cleanMessages = useSetAtom(cleanChatMessagesAtom)
|
const cleanMessages = useSetAtom(cleanChatMessagesAtom)
|
||||||
const deleteThreadState = useSetAtom(deleteThreadStateAtom)
|
const deleteThreadState = useSetAtom(deleteThreadStateAtom)
|
||||||
|
|
||||||
const threadStates = useAtomValue(threadStatesAtom)
|
const threadStates = useAtomValue(threadStatesAtom)
|
||||||
|
const updateThreadLastMessage = useSetAtom(updateThreadStateLastMessageAtom)
|
||||||
|
|
||||||
const cleanThread = async (threadId: string) => {
|
const cleanThread = async (threadId: string) => {
|
||||||
if (threadId) {
|
if (threadId) {
|
||||||
const thread = threads.filter((c) => c.id === threadId)[0]
|
const thread = threads.filter((c) => c.id === threadId)[0]
|
||||||
cleanMessages(threadId)
|
cleanMessages(threadId)
|
||||||
|
|
||||||
if (thread)
|
if (thread) {
|
||||||
await extensionManager
|
await extensionManager
|
||||||
.get<ConversationalExtension>(ExtensionType.Conversational)
|
.get<ConversationalExtension>(ExtensionType.Conversational)
|
||||||
?.writeMessages(
|
?.writeMessages(
|
||||||
threadId,
|
threadId,
|
||||||
messages.filter((msg) => msg.role === ChatCompletionRole.System)
|
messages.filter((msg) => msg.role === ChatCompletionRole.System)
|
||||||
)
|
)
|
||||||
|
updateThreadLastMessage(threadId, undefined)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
import { Model, ExtensionType, ModelExtension } from '@janhq/core'
|
import {
|
||||||
|
Model,
|
||||||
|
ExtensionType,
|
||||||
|
ModelExtension,
|
||||||
|
abortDownload,
|
||||||
|
joinPath,
|
||||||
|
} from '@janhq/core'
|
||||||
|
|
||||||
import { useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { modelBinFileName } from '@/utils/model'
|
||||||
|
|
||||||
import { useDownloadState } from './useDownloadState'
|
import { useDownloadState } from './useDownloadState'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
@ -33,8 +41,14 @@ export default function useDownloadModel() {
|
|||||||
.get<ModelExtension>(ExtensionType.Model)
|
.get<ModelExtension>(ExtensionType.Model)
|
||||||
?.downloadModel(model)
|
?.downloadModel(model)
|
||||||
}
|
}
|
||||||
|
const abortModelDownload = async (model: Model) => {
|
||||||
|
await abortDownload(
|
||||||
|
await joinPath(['models', model.id, modelBinFileName(model)])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
downloadModel,
|
downloadModel,
|
||||||
|
abortModelDownload,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { join } from 'path'
|
import { fs, joinPath } from '@janhq/core'
|
||||||
|
|
||||||
import { fs } from '@janhq/core'
|
|
||||||
|
|
||||||
export const useEngineSettings = () => {
|
export const useEngineSettings = () => {
|
||||||
const readOpenAISettings = async () => {
|
const readOpenAISettings = async () => {
|
||||||
const settings = await fs.readFile(join('engines', 'openai.json'))
|
const settings = await fs.readFile(
|
||||||
|
await joinPath(['engines', 'openai.json'])
|
||||||
|
)
|
||||||
if (settings) {
|
if (settings) {
|
||||||
return JSON.parse(settings)
|
return JSON.parse(settings)
|
||||||
}
|
}
|
||||||
@ -17,7 +17,10 @@ export const useEngineSettings = () => {
|
|||||||
}) => {
|
}) => {
|
||||||
const settings = await readOpenAISettings()
|
const settings = await readOpenAISettings()
|
||||||
settings.api_key = apiKey
|
settings.api_key = apiKey
|
||||||
await fs.writeFile(join('engines', 'openai.json'), JSON.stringify(settings))
|
await fs.writeFile(
|
||||||
|
await joinPath(['engines', 'openai.json']),
|
||||||
|
JSON.stringify(settings)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return { readOpenAISettings, saveOpenAISettings }
|
return { readOpenAISettings, saveOpenAISettings }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export default function useRecommendedModel() {
|
|||||||
const getRecommendedModel = useCallback(async (): Promise<
|
const getRecommendedModel = useCallback(async (): Promise<
|
||||||
Model | undefined
|
Model | undefined
|
||||||
> => {
|
> => {
|
||||||
|
const models = await getAndSortDownloadedModels()
|
||||||
if (!activeThread) {
|
if (!activeThread) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -49,7 +50,6 @@ export default function useRecommendedModel() {
|
|||||||
const finishInit = threadStates[activeThread.id].isFinishInit ?? true
|
const finishInit = threadStates[activeThread.id].isFinishInit ?? true
|
||||||
if (finishInit) {
|
if (finishInit) {
|
||||||
const modelId = activeThread.assistants[0]?.model.id
|
const modelId = activeThread.assistants[0]?.model.id
|
||||||
const models = await getAndSortDownloadedModels()
|
|
||||||
const model = models.find((model) => model.id === modelId)
|
const model = models.find((model) => model.id === modelId)
|
||||||
|
|
||||||
if (model) {
|
if (model) {
|
||||||
@ -60,7 +60,6 @@ export default function useRecommendedModel() {
|
|||||||
} else {
|
} else {
|
||||||
const modelId = activeThread.assistants[0]?.model.id
|
const modelId = activeThread.assistants[0]?.model.id
|
||||||
if (modelId !== '*') {
|
if (modelId !== '*') {
|
||||||
const models = await getAndSortDownloadedModels()
|
|
||||||
const model = models.find((model) => model.id === modelId)
|
const model = models.find((model) => model.id === modelId)
|
||||||
|
|
||||||
if (model) {
|
if (model) {
|
||||||
@ -78,7 +77,7 @@ export default function useRecommendedModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sort the model, for display purpose
|
// sort the model, for display purpose
|
||||||
const models = await getAndSortDownloadedModels()
|
|
||||||
if (models.length === 0) {
|
if (models.length === 0) {
|
||||||
// if we have no downloaded models, then can't recommend anything
|
// if we have no downloaded models, then can't recommend anything
|
||||||
console.debug("No downloaded models, can't recommend anything")
|
console.debug("No downloaded models, can't recommend anything")
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
|||||||
|
|
||||||
import { toaster } from '@/containers/Toast'
|
import { toaster } from '@/containers/Toast'
|
||||||
|
|
||||||
|
import { toRuntimeParams, toSettingParams } from '@/utils/model_param'
|
||||||
|
|
||||||
import { useActiveModel } from './useActiveModel'
|
import { useActiveModel } from './useActiveModel'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
@ -33,7 +35,7 @@ import {
|
|||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import {
|
import {
|
||||||
activeThreadAtom,
|
activeThreadAtom,
|
||||||
getActiveThreadModelRuntimeParamsAtom,
|
getActiveThreadModelParamsAtom,
|
||||||
threadStatesAtom,
|
threadStatesAtom,
|
||||||
updateThreadAtom,
|
updateThreadAtom,
|
||||||
updateThreadInitSuccessAtom,
|
updateThreadInitSuccessAtom,
|
||||||
@ -56,7 +58,7 @@ export default function useSendChatMessage() {
|
|||||||
const modelRef = useRef<Model | undefined>()
|
const modelRef = useRef<Model | undefined>()
|
||||||
const threadStates = useAtomValue(threadStatesAtom)
|
const threadStates = useAtomValue(threadStatesAtom)
|
||||||
const updateThreadInitSuccess = useSetAtom(updateThreadInitSuccessAtom)
|
const updateThreadInitSuccess = useSetAtom(updateThreadInitSuccessAtom)
|
||||||
const activeModelParams = useAtomValue(getActiveThreadModelRuntimeParamsAtom)
|
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
modelRef.current = activeModel
|
modelRef.current = activeModel
|
||||||
@ -128,17 +130,22 @@ export default function useSendChatMessage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendChatMessage = async () => {
|
const sendChatMessage = async () => {
|
||||||
if (!currentPrompt || currentPrompt.trim().length === 0) {
|
if (!currentPrompt || currentPrompt.trim().length === 0) return
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!activeThread) {
|
if (!activeThread) {
|
||||||
console.error('No active thread')
|
console.error('No active thread')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const activeThreadState = threadStates[activeThread.id]
|
const activeThreadState = threadStates[activeThread.id]
|
||||||
|
|
||||||
|
const runtimeParams = toRuntimeParams(activeModelParams)
|
||||||
|
const settingParams = toSettingParams(activeModelParams)
|
||||||
|
|
||||||
// if the thread is not initialized, we need to initialize it first
|
// if the thread is not initialized, we need to initialize it first
|
||||||
if (!activeThreadState.isFinishInit) {
|
if (
|
||||||
|
!activeThreadState.isFinishInit ||
|
||||||
|
activeThread.assistants[0].model.id !== selectedModel?.id
|
||||||
|
) {
|
||||||
if (!selectedModel) {
|
if (!selectedModel) {
|
||||||
toaster({ title: 'Please select a model' })
|
toaster({ title: 'Please select a model' })
|
||||||
return
|
return
|
||||||
@ -147,11 +154,6 @@ export default function useSendChatMessage() {
|
|||||||
const assistantName = activeThread.assistants[0].assistant_name ?? ''
|
const assistantName = activeThread.assistants[0].assistant_name ?? ''
|
||||||
const instructions = activeThread.assistants[0].instructions ?? ''
|
const instructions = activeThread.assistants[0].instructions ?? ''
|
||||||
|
|
||||||
const modelParams: ModelRuntimeParams = {
|
|
||||||
...selectedModel.parameters,
|
|
||||||
...activeModelParams,
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedThread: Thread = {
|
const updatedThread: Thread = {
|
||||||
...activeThread,
|
...activeThread,
|
||||||
assistants: [
|
assistants: [
|
||||||
@ -161,8 +163,8 @@ export default function useSendChatMessage() {
|
|||||||
instructions: instructions,
|
instructions: instructions,
|
||||||
model: {
|
model: {
|
||||||
id: selectedModel.id,
|
id: selectedModel.id,
|
||||||
settings: selectedModel.settings,
|
settings: settingParams,
|
||||||
parameters: modelParams,
|
parameters: runtimeParams,
|
||||||
engine: selectedModel.engine,
|
engine: selectedModel.engine,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -208,13 +210,17 @@ export default function useSendChatMessage() {
|
|||||||
const msgId = ulid()
|
const msgId = ulid()
|
||||||
|
|
||||||
const modelRequest = selectedModel ?? activeThread.assistants[0].model
|
const modelRequest = selectedModel ?? activeThread.assistants[0].model
|
||||||
|
if (runtimeParams.stream == null) {
|
||||||
|
runtimeParams.stream = true
|
||||||
|
}
|
||||||
const messageRequest: MessageRequest = {
|
const messageRequest: MessageRequest = {
|
||||||
id: msgId,
|
id: msgId,
|
||||||
threadId: activeThread.id,
|
threadId: activeThread.id,
|
||||||
messages,
|
messages,
|
||||||
model: {
|
model: {
|
||||||
...modelRequest,
|
...modelRequest,
|
||||||
...(activeModelParams ? { parameters: activeModelParams } : {}),
|
settings: settingParams,
|
||||||
|
parameters: runtimeParams,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
ExtensionType,
|
ExtensionType,
|
||||||
ModelRuntimeParams,
|
|
||||||
Thread,
|
Thread,
|
||||||
ThreadState,
|
ThreadState,
|
||||||
ConversationalExtension,
|
ConversationalExtension,
|
||||||
@ -12,7 +11,8 @@ import useSetActiveThread from './useSetActiveThread'
|
|||||||
|
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
import {
|
import {
|
||||||
threadModelRuntimeParamsAtom,
|
ModelParams,
|
||||||
|
threadModelParamsAtom,
|
||||||
threadStatesAtom,
|
threadStatesAtom,
|
||||||
threadsAtom,
|
threadsAtom,
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
@ -21,7 +21,7 @@ const useThreads = () => {
|
|||||||
const [threadStates, setThreadStates] = useAtom(threadStatesAtom)
|
const [threadStates, setThreadStates] = useAtom(threadStatesAtom)
|
||||||
const [threads, setThreads] = useAtom(threadsAtom)
|
const [threads, setThreads] = useAtom(threadsAtom)
|
||||||
const [threadModelRuntimeParams, setThreadModelRuntimeParams] = useAtom(
|
const [threadModelRuntimeParams, setThreadModelRuntimeParams] = useAtom(
|
||||||
threadModelRuntimeParamsAtom
|
threadModelParamsAtom
|
||||||
)
|
)
|
||||||
const { setActiveThread } = useSetActiveThread()
|
const { setActiveThread } = useSetActiveThread()
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ const useThreads = () => {
|
|||||||
try {
|
try {
|
||||||
const localThreads = await getLocalThreads()
|
const localThreads = await getLocalThreads()
|
||||||
const localThreadStates: Record<string, ThreadState> = {}
|
const localThreadStates: Record<string, ThreadState> = {}
|
||||||
const threadModelParams: Record<string, ModelRuntimeParams> = {}
|
const threadModelParams: Record<string, ModelParams> = {}
|
||||||
|
|
||||||
localThreads.forEach((thread) => {
|
localThreads.forEach((thread) => {
|
||||||
if (thread.id != null) {
|
if (thread.id != null) {
|
||||||
@ -42,9 +42,12 @@ const useThreads = () => {
|
|||||||
isFinishInit: true,
|
isFinishInit: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// model params
|
|
||||||
const modelParams = thread.assistants?.[0]?.model?.parameters
|
const modelParams = thread.assistants?.[0]?.model?.parameters
|
||||||
threadModelParams[thread.id] = modelParams
|
const engineParams = thread.assistants?.[0]?.model?.settings
|
||||||
|
threadModelParams[thread.id] = {
|
||||||
|
...modelParams,
|
||||||
|
...engineParams,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,31 +1,34 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import {
|
import {
|
||||||
ConversationalExtension,
|
ConversationalExtension,
|
||||||
ExtensionType,
|
ExtensionType,
|
||||||
ModelRuntimeParams,
|
|
||||||
Thread,
|
Thread,
|
||||||
|
ThreadAssistantInfo,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { toRuntimeParams, toSettingParams } from '@/utils/model_param'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { extensionManager } from '@/extension'
|
||||||
import {
|
import {
|
||||||
|
ModelParams,
|
||||||
activeThreadStateAtom,
|
activeThreadStateAtom,
|
||||||
setThreadModelRuntimeParamsAtom,
|
getActiveThreadModelParamsAtom,
|
||||||
|
setThreadModelParamsAtom,
|
||||||
threadsAtom,
|
threadsAtom,
|
||||||
updateThreadAtom,
|
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
export default function useUpdateModelParameters() {
|
export default function useUpdateModelParameters() {
|
||||||
const threads = useAtomValue(threadsAtom)
|
const threads = useAtomValue(threadsAtom)
|
||||||
const updateThread = useSetAtom(updateThreadAtom)
|
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||||
const setThreadModelRuntimeParams = useSetAtom(
|
|
||||||
setThreadModelRuntimeParamsAtom
|
|
||||||
)
|
|
||||||
const activeThreadState = useAtomValue(activeThreadStateAtom)
|
const activeThreadState = useAtomValue(activeThreadStateAtom)
|
||||||
|
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
||||||
|
|
||||||
const updateModelParameter = async (
|
const updateModelParameter = async (
|
||||||
threadId: string,
|
threadId: string,
|
||||||
params: ModelRuntimeParams
|
name: string,
|
||||||
|
value: number | boolean | string
|
||||||
) => {
|
) => {
|
||||||
const thread = threads.find((thread) => thread.id === threadId)
|
const thread = threads.find((thread) => thread.id === threadId)
|
||||||
if (!thread) {
|
if (!thread) {
|
||||||
@ -37,27 +40,37 @@ export default function useUpdateModelParameters() {
|
|||||||
console.error('No active thread')
|
console.error('No active thread')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const updatedModelParams: ModelParams = {
|
||||||
|
...activeModelParams,
|
||||||
|
[name]: value,
|
||||||
|
}
|
||||||
|
|
||||||
// update the state
|
// update the state
|
||||||
setThreadModelRuntimeParams(thread.id, params)
|
setThreadModelParams(thread.id, updatedModelParams)
|
||||||
|
|
||||||
if (!activeThreadState.isFinishInit) {
|
if (!activeThreadState.isFinishInit) {
|
||||||
// if thread is not initialized, we don't need to update thread.json
|
// if thread is not initialized, we don't need to update thread.json
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistants = thread.assistants.map((assistant) => {
|
const assistants = thread.assistants.map(
|
||||||
assistant.model.parameters = params
|
(assistant: ThreadAssistantInfo) => {
|
||||||
return assistant
|
const runtimeParams = toRuntimeParams(updatedModelParams)
|
||||||
})
|
const settingParams = toSettingParams(updatedModelParams)
|
||||||
|
|
||||||
|
assistant.model.parameters = runtimeParams
|
||||||
|
assistant.model.settings = settingParams
|
||||||
|
return assistant
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// update thread
|
// update thread
|
||||||
const updatedThread: Thread = {
|
const updatedThread: Thread = {
|
||||||
...thread,
|
...thread,
|
||||||
assistants,
|
assistants,
|
||||||
}
|
}
|
||||||
updateThread(updatedThread)
|
|
||||||
extensionManager
|
await extensionManager
|
||||||
.get<ConversationalExtension>(ExtensionType.Conversational)
|
.get<ConversationalExtension>(ExtensionType.Conversational)
|
||||||
?.saveThread(updatedThread)
|
?.saveThread(updatedThread)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,9 @@ const nextConfig = {
|
|||||||
JSON.stringify(process.env.ANALYTICS_ID) ?? JSON.stringify('xxx'),
|
JSON.stringify(process.env.ANALYTICS_ID) ?? JSON.stringify('xxx'),
|
||||||
ANALYTICS_HOST:
|
ANALYTICS_HOST:
|
||||||
JSON.stringify(process.env.ANALYTICS_HOST) ?? JSON.stringify('xxx'),
|
JSON.stringify(process.env.ANALYTICS_HOST) ?? JSON.stringify('xxx'),
|
||||||
|
isMac: process.platform === 'darwin',
|
||||||
|
isWindows: process.platform === 'win32',
|
||||||
|
isLinux: process.platform === 'linux',
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
return config
|
return config
|
||||||
|
|||||||
@ -116,7 +116,7 @@ const ChatBody: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<ScrollToBottom className="flex h-full w-full flex-col">
|
<ScrollToBottom className="flex h-full w-full flex-col">
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) => (
|
||||||
<>
|
<div key={message.id}>
|
||||||
<ChatItem {...message} key={message.id} />
|
<ChatItem {...message} key={message.id} />
|
||||||
|
|
||||||
{message.status === MessageStatus.Error &&
|
{message.status === MessageStatus.Error &&
|
||||||
@ -126,8 +126,8 @@ const ChatBody: React.FC = () => {
|
|||||||
className="mt-10 flex flex-col items-center"
|
className="mt-10 flex flex-col items-center"
|
||||||
>
|
>
|
||||||
<span className="mb-3 text-center text-sm font-medium text-gray-500">
|
<span className="mb-3 text-center text-sm font-medium text-gray-500">
|
||||||
Oops! The generation was interrupted. Let's
|
Oops! The generation was interrupted. Let's give it
|
||||||
give it another go!
|
another go!
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
className="w-min"
|
className="w-min"
|
||||||
@ -140,7 +140,7 @@ const ChatBody: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
))}
|
))}
|
||||||
</ScrollToBottom>
|
</ScrollToBottom>
|
||||||
)}
|
)}
|
||||||
|
|||||||
31
web/screens/Chat/EngineSetting/index.tsx
Normal file
31
web/screens/Chat/EngineSetting/index.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { selectedModelAtom } from '@/containers/DropdownListSidebar'
|
||||||
|
|
||||||
|
import { getConfigurationsData } from '@/utils/componentSettings'
|
||||||
|
import { toSettingParams } from '@/utils/model_param'
|
||||||
|
|
||||||
|
import settingComponentBuilder from '../ModelSetting/settingComponentBuilder'
|
||||||
|
|
||||||
|
import { getActiveThreadModelParamsAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
|
const EngineSetting: React.FC = () => {
|
||||||
|
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
||||||
|
const selectedModel = useAtomValue(selectedModelAtom)
|
||||||
|
|
||||||
|
if (!selectedModel || !activeModelParams) return null
|
||||||
|
|
||||||
|
const modelSettingParams = toSettingParams(activeModelParams)
|
||||||
|
|
||||||
|
const componentData = getConfigurationsData(modelSettingParams)
|
||||||
|
|
||||||
|
componentData.sort((a, b) => a.title.localeCompare(b.title))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="flex flex-col">
|
||||||
|
{settingComponentBuilder(componentData)}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EngineSetting
|
||||||
@ -1,47 +1,33 @@
|
|||||||
import { useForm } from 'react-hook-form'
|
import React from 'react'
|
||||||
|
|
||||||
import { ModelRuntimeParams } from '@janhq/core'
|
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import { presetConfiguration } from './predefinedComponent'
|
import { selectedModelAtom } from '@/containers/DropdownListSidebar'
|
||||||
import settingComponentBuilder, {
|
|
||||||
SettingComponentData,
|
|
||||||
} from './settingComponentBuilder'
|
|
||||||
|
|
||||||
import { getActiveThreadModelRuntimeParamsAtom } from '@/helpers/atoms/Thread.atom'
|
import { getConfigurationsData } from '@/utils/componentSettings'
|
||||||
|
import { toRuntimeParams } from '@/utils/model_param'
|
||||||
|
|
||||||
export default function ModelSetting() {
|
import settingComponentBuilder from './settingComponentBuilder'
|
||||||
const { register } = useForm()
|
|
||||||
const activeModelParams = useAtomValue(getActiveThreadModelRuntimeParamsAtom)
|
|
||||||
|
|
||||||
if (!activeModelParams) {
|
import { getActiveThreadModelParamsAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentData: SettingComponentData[] = []
|
const ModelSetting: React.FC = () => {
|
||||||
Object.keys(activeModelParams).forEach((key) => {
|
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
||||||
const componentSetting = presetConfiguration[key]
|
const selectedModel = useAtomValue(selectedModelAtom)
|
||||||
|
|
||||||
if (componentSetting) {
|
if (!selectedModel || !activeModelParams) return null
|
||||||
if ('value' in componentSetting.controllerData) {
|
|
||||||
componentSetting.controllerData.value = Number(
|
|
||||||
activeModelParams[key as keyof ModelRuntimeParams]
|
|
||||||
)
|
|
||||||
} else if ('checked' in componentSetting.controllerData) {
|
|
||||||
const checked = activeModelParams[
|
|
||||||
key as keyof ModelRuntimeParams
|
|
||||||
] as boolean
|
|
||||||
|
|
||||||
componentSetting.controllerData.checked = checked
|
const modelRuntimeParams = toRuntimeParams(activeModelParams)
|
||||||
}
|
|
||||||
componentData.push(componentSetting)
|
const componentData = getConfigurationsData(modelRuntimeParams)
|
||||||
}
|
|
||||||
})
|
componentData.sort((a, b) => a.title.localeCompare(b.title))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="flex flex-col">
|
<form className="flex flex-col">
|
||||||
{settingComponentBuilder(componentData, register)}
|
{settingComponentBuilder(componentData)}
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default React.memo(ModelSetting)
|
||||||
|
|||||||
@ -1,10 +1,43 @@
|
|||||||
import { SettingComponentData } from './settingComponentBuilder'
|
import { SettingComponentData } from './settingComponentBuilder'
|
||||||
|
|
||||||
export const presetConfiguration: Record<string, SettingComponentData> = {
|
export const presetConfiguration: Record<string, SettingComponentData> = {
|
||||||
|
prompt_template: {
|
||||||
|
name: 'prompt_template',
|
||||||
|
title: 'Prompt template',
|
||||||
|
description: 'Prompt template',
|
||||||
|
controllerType: 'input',
|
||||||
|
controllerData: {
|
||||||
|
placeholder: 'Prompt template',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stop: {
|
||||||
|
name: 'stop',
|
||||||
|
title: 'Stop',
|
||||||
|
description: 'Stop',
|
||||||
|
controllerType: 'input',
|
||||||
|
controllerData: {
|
||||||
|
placeholder: 'Stop',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx_len: {
|
||||||
|
name: 'ctx_len',
|
||||||
|
title: 'Context Length',
|
||||||
|
description: 'Context Length',
|
||||||
|
controllerType: 'slider',
|
||||||
|
controllerData: {
|
||||||
|
min: 0,
|
||||||
|
max: 4096,
|
||||||
|
step: 128,
|
||||||
|
value: 1024,
|
||||||
|
},
|
||||||
|
},
|
||||||
max_tokens: {
|
max_tokens: {
|
||||||
name: 'max_tokens',
|
name: 'max_tokens',
|
||||||
title: 'Max Tokens',
|
title: 'Max Tokens',
|
||||||
description: 'Maximum context length the model can handle.',
|
description:
|
||||||
|
'The maximum number of tokens the model will generate in a single response.',
|
||||||
controllerType: 'slider',
|
controllerType: 'slider',
|
||||||
controllerData: {
|
controllerData: {
|
||||||
min: 0,
|
min: 0,
|
||||||
@ -56,4 +89,52 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
|
|||||||
value: 0.7,
|
value: 0.7,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
frequency_penalty: {
|
||||||
|
name: 'frequency_penalty',
|
||||||
|
title: 'Frequency Penalty',
|
||||||
|
description: 'Frequency Penalty',
|
||||||
|
controllerType: 'slider',
|
||||||
|
controllerData: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
value: 0.7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
presence_penalty: {
|
||||||
|
name: 'presence_penalty',
|
||||||
|
title: 'Presence Penalty',
|
||||||
|
description: 'Presence Penalty',
|
||||||
|
controllerType: 'slider',
|
||||||
|
controllerData: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
value: 0.7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
top_p: {
|
||||||
|
name: 'top_p',
|
||||||
|
title: 'Top P',
|
||||||
|
description: 'Top P',
|
||||||
|
controllerType: 'slider',
|
||||||
|
controllerData: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
value: 0.95,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
n_parallel: {
|
||||||
|
name: 'n_parallel',
|
||||||
|
title: 'N Parallel',
|
||||||
|
description: 'N Parallel',
|
||||||
|
controllerType: 'slider',
|
||||||
|
controllerData: {
|
||||||
|
min: 1,
|
||||||
|
max: 4,
|
||||||
|
step: 1,
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,21 @@
|
|||||||
/* eslint-disable no-case-declarations */
|
/* eslint-disable no-case-declarations */
|
||||||
import { FieldValues, UseFormRegister } from 'react-hook-form'
|
|
||||||
|
|
||||||
import Checkbox from '@/containers/Checkbox'
|
import Checkbox from '@/containers/Checkbox'
|
||||||
|
import ModelConfigInput from '@/containers/ModelConfigInput'
|
||||||
import Slider from '@/containers/Slider'
|
import Slider from '@/containers/Slider'
|
||||||
|
|
||||||
export type ControllerType = 'slider' | 'checkbox'
|
export type ControllerType = 'slider' | 'checkbox' | 'input'
|
||||||
|
|
||||||
export type SettingComponentData = {
|
export type SettingComponentData = {
|
||||||
name: string
|
name: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
controllerType: ControllerType
|
controllerType: ControllerType
|
||||||
controllerData: SliderData | CheckboxData
|
controllerData: SliderData | CheckboxData | InputData
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputData = {
|
||||||
|
placeholder: string
|
||||||
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SliderData = {
|
export type SliderData = {
|
||||||
@ -25,10 +29,7 @@ type CheckboxData = {
|
|||||||
checked: boolean
|
checked: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingComponentBuilder = (
|
const settingComponentBuilder = (componentData: SettingComponentData[]) => {
|
||||||
componentData: SettingComponentData[],
|
|
||||||
register: UseFormRegister<FieldValues>
|
|
||||||
) => {
|
|
||||||
const components = componentData.map((data) => {
|
const components = componentData.map((data) => {
|
||||||
switch (data.controllerType) {
|
switch (data.controllerType) {
|
||||||
case 'slider':
|
case 'slider':
|
||||||
@ -42,7 +43,18 @@ const settingComponentBuilder = (
|
|||||||
step={step}
|
step={step}
|
||||||
value={value}
|
value={value}
|
||||||
name={data.name}
|
name={data.name}
|
||||||
register={register}
|
/>
|
||||||
|
)
|
||||||
|
case 'input':
|
||||||
|
const { placeholder, value: textValue } =
|
||||||
|
data.controllerData as InputData
|
||||||
|
return (
|
||||||
|
<ModelConfigInput
|
||||||
|
title={data.title}
|
||||||
|
key={data.name}
|
||||||
|
name={data.name}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={textValue}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
@ -50,7 +62,6 @@ const settingComponentBuilder = (
|
|||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
key={data.name}
|
key={data.name}
|
||||||
register={register}
|
|
||||||
name={data.name}
|
name={data.name}
|
||||||
title={data.title}
|
title={data.title}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { join } from 'path'
|
import React from 'react'
|
||||||
|
|
||||||
import { getUserSpace, openFileExplorer } from '@janhq/core'
|
import { getUserSpace, openFileExplorer, joinPath } from '@janhq/core'
|
||||||
|
|
||||||
import { Input, Textarea } from '@janhq/uikit'
|
import { Input, Textarea } from '@janhq/uikit'
|
||||||
|
|
||||||
@ -16,19 +16,29 @@ import DropdownListSidebar, {
|
|||||||
|
|
||||||
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||||
|
|
||||||
|
import { toSettingParams } from '@/utils/model_param'
|
||||||
|
|
||||||
|
import EngineSetting from '../EngineSetting'
|
||||||
import ModelSetting from '../ModelSetting'
|
import ModelSetting from '../ModelSetting'
|
||||||
|
|
||||||
import { activeThreadAtom, threadStatesAtom } from '@/helpers/atoms/Thread.atom'
|
import {
|
||||||
|
activeThreadAtom,
|
||||||
|
getActiveThreadModelParamsAtom,
|
||||||
|
threadStatesAtom,
|
||||||
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
export const showRightSideBarAtom = atom<boolean>(true)
|
export const showRightSideBarAtom = atom<boolean>(true)
|
||||||
|
|
||||||
export default function Sidebar() {
|
const Sidebar: React.FC = () => {
|
||||||
const showing = useAtomValue(showRightSideBarAtom)
|
const showing = useAtomValue(showRightSideBarAtom)
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const selectedModel = useAtomValue(selectedModelAtom)
|
const selectedModel = useAtomValue(selectedModelAtom)
|
||||||
const { updateThreadMetadata } = useCreateNewThread()
|
const { updateThreadMetadata } = useCreateNewThread()
|
||||||
const threadStates = useAtomValue(threadStatesAtom)
|
const threadStates = useAtomValue(threadStatesAtom)
|
||||||
|
|
||||||
|
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
||||||
|
const modelSettingParams = toSettingParams(activeModelParams)
|
||||||
|
|
||||||
const onReviewInFinderClick = async (type: string) => {
|
const onReviewInFinderClick = async (type: string) => {
|
||||||
if (!activeThread) return
|
if (!activeThread) return
|
||||||
const activeThreadState = threadStates[activeThread.id]
|
const activeThreadState = threadStates[activeThread.id]
|
||||||
@ -42,23 +52,22 @@ export default function Sidebar() {
|
|||||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'Thread':
|
case 'Thread':
|
||||||
filePath = join('threads', activeThread.id)
|
filePath = await joinPath(['threads', activeThread.id])
|
||||||
break
|
break
|
||||||
case 'Model':
|
case 'Model':
|
||||||
if (!selectedModel) return
|
if (!selectedModel) return
|
||||||
filePath = join('models', selectedModel.id)
|
filePath = await joinPath(['models', selectedModel.id])
|
||||||
break
|
break
|
||||||
case 'Assistant':
|
case 'Assistant':
|
||||||
if (!assistantId) return
|
if (!assistantId) return
|
||||||
filePath = join('assistants', assistantId)
|
filePath = await joinPath(['assistants', assistantId])
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filePath) return
|
if (!filePath) return
|
||||||
|
const fullPath = await joinPath([userSpace, filePath])
|
||||||
const fullPath = join(userSpace, filePath)
|
|
||||||
openFileExplorer(fullPath)
|
openFileExplorer(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,23 +84,22 @@ export default function Sidebar() {
|
|||||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'Thread':
|
case 'Thread':
|
||||||
filePath = join('threads', activeThread.id, 'thread.json')
|
filePath = await joinPath(['threads', activeThread.id, 'thread.json'])
|
||||||
break
|
break
|
||||||
case 'Model':
|
case 'Model':
|
||||||
if (!selectedModel) return
|
if (!selectedModel) return
|
||||||
filePath = join('models', selectedModel.id, 'model.json')
|
filePath = await joinPath(['models', selectedModel.id, 'model.json'])
|
||||||
break
|
break
|
||||||
case 'Assistant':
|
case 'Assistant':
|
||||||
if (!assistantId) return
|
if (!assistantId) return
|
||||||
filePath = join('assistants', assistantId, 'assistant.json')
|
filePath = await joinPath(['assistants', assistantId, 'assistant.json'])
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filePath) return
|
if (!filePath) return
|
||||||
|
const fullPath = await joinPath([userSpace, filePath])
|
||||||
const fullPath = join(userSpace, filePath)
|
|
||||||
openFileExplorer(fullPath)
|
openFileExplorer(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,6 +195,17 @@ export default function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardSidebar>
|
</CardSidebar>
|
||||||
|
{Object.keys(modelSettingParams).length ? (
|
||||||
|
<CardSidebar
|
||||||
|
title="Engine"
|
||||||
|
onRevealInFinderClick={onReviewInFinderClick}
|
||||||
|
onViewJsonClick={onViewJsonClick}
|
||||||
|
>
|
||||||
|
<div className="p-2">
|
||||||
|
<EngineSetting />
|
||||||
|
</div>
|
||||||
|
</CardSidebar>
|
||||||
|
) : null}
|
||||||
<CardSidebar
|
<CardSidebar
|
||||||
title="Model"
|
title="Model"
|
||||||
onRevealInFinderClick={onReviewInFinderClick}
|
onRevealInFinderClick={onReviewInFinderClick}
|
||||||
@ -203,3 +222,5 @@ export default function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default React.memo(Sidebar)
|
||||||
|
|||||||
@ -161,7 +161,10 @@ export default function ThreadList() {
|
|||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<ModalTitle>Delete Thread</ModalTitle>
|
<ModalTitle>Delete Thread</ModalTitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>Are you sure you want to delete this thread?</p>
|
<p>
|
||||||
|
Are you sure you want to delete this thread? This action
|
||||||
|
cannot be undone.
|
||||||
|
</p>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
<ModalClose asChild>
|
<ModalClose asChild>
|
||||||
@ -169,6 +172,7 @@ export default function ThreadList() {
|
|||||||
</ModalClose>
|
</ModalClose>
|
||||||
<ModalClose asChild>
|
<ModalClose asChild>
|
||||||
<Button
|
<Button
|
||||||
|
autoFocus
|
||||||
themes="danger"
|
themes="danger"
|
||||||
onClick={() => deleteThread(thread.id)}
|
onClick={() => deleteThread(thread.id)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -58,17 +58,28 @@ const ExploreModelsScreen = () => {
|
|||||||
className="w-full object-cover"
|
className="w-full object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute left-1/2 top-1/2 w-1/3 -translate-x-1/2 -translate-y-1/2">
|
<div className="absolute left-1/2 top-1/2 w-1/3 -translate-x-1/2 -translate-y-1/2">
|
||||||
<SearchIcon
|
<div className="relative">
|
||||||
size={20}
|
<SearchIcon
|
||||||
className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground"
|
size={20}
|
||||||
/>
|
className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||||
<Input
|
/>
|
||||||
placeholder="Search models"
|
<Input
|
||||||
className="bg-white pl-9 dark:bg-background"
|
placeholder="Search models"
|
||||||
onChange={(e) => {
|
className="bg-white pl-9 dark:bg-background"
|
||||||
setsearchValue(e.target.value)
|
onChange={(e) => {
|
||||||
}}
|
setsearchValue(e.target.value)
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-center">
|
||||||
|
<a
|
||||||
|
href="https://jan.ai/guides/using-models/import-manually/"
|
||||||
|
target="_blank"
|
||||||
|
className="font-semibold text-white underline"
|
||||||
|
>
|
||||||
|
How to manually import models
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto w-4/5 py-6">
|
<div className="mx-auto w-4/5 py-6">
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import React, { useState, useEffect, useRef, useContext } from 'react'
|
|||||||
|
|
||||||
import { Button } from '@janhq/uikit'
|
import { Button } from '@janhq/uikit'
|
||||||
|
|
||||||
import Loader from '@/containers/Loader'
|
|
||||||
|
|
||||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||||
|
|
||||||
import { useGetAppVersion } from '@/hooks/useGetAppVersion'
|
import { useGetAppVersion } from '@/hooks/useGetAppVersion'
|
||||||
@ -18,7 +16,6 @@ import { extensionManager } from '@/extension'
|
|||||||
const ExtensionCatalog = () => {
|
const ExtensionCatalog = () => {
|
||||||
const [activeExtensions, setActiveExtensions] = useState<any[]>([])
|
const [activeExtensions, setActiveExtensions] = useState<any[]>([])
|
||||||
const [extensionCatalog, setExtensionCatalog] = useState<any[]>([])
|
const [extensionCatalog, setExtensionCatalog] = useState<any[]>([])
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const { version } = useGetAppVersion()
|
const { version } = useGetAppVersion()
|
||||||
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||||
@ -95,8 +92,6 @@ const ExtensionCatalog = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) return <Loader description="Installing ..." />
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block w-full">
|
<div className="block w-full">
|
||||||
{extensionCatalog
|
{extensionCatalog
|
||||||
|
|||||||
3
web/types/index.d.ts
vendored
3
web/types/index.d.ts
vendored
@ -8,6 +8,9 @@ declare global {
|
|||||||
declare const VERSION: string
|
declare const VERSION: string
|
||||||
declare const ANALYTICS_ID: string
|
declare const ANALYTICS_ID: string
|
||||||
declare const ANALYTICS_HOST: string
|
declare const ANALYTICS_HOST: string
|
||||||
|
declare const isMac: boolean
|
||||||
|
declare const isWindows: boolean
|
||||||
|
declare const isLinux: boolean
|
||||||
interface Core {
|
interface Core {
|
||||||
api: APIFunctions
|
api: APIFunctions
|
||||||
events: EventEmitter
|
events: EventEmitter
|
||||||
|
|||||||
39
web/utils/componentSettings.ts
Normal file
39
web/utils/componentSettings.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { ModelRuntimeParams, ModelSettingParams } from '@janhq/core'
|
||||||
|
|
||||||
|
import { presetConfiguration } from '@/screens/Chat/ModelSetting/predefinedComponent'
|
||||||
|
|
||||||
|
import { SettingComponentData } from '@/screens/Chat/ModelSetting/settingComponentBuilder'
|
||||||
|
|
||||||
|
import { ModelParams } from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
|
export const getConfigurationsData = (
|
||||||
|
settings: ModelSettingParams | ModelRuntimeParams
|
||||||
|
) => {
|
||||||
|
const componentData: SettingComponentData[] = []
|
||||||
|
Object.keys(settings).forEach((key: string) => {
|
||||||
|
const componentSetting = presetConfiguration[key]
|
||||||
|
|
||||||
|
if (!componentSetting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ('slider' === componentSetting.controllerType) {
|
||||||
|
const value = Number(settings[key as keyof ModelParams])
|
||||||
|
if ('value' in componentSetting.controllerData)
|
||||||
|
componentSetting.controllerData.value = value
|
||||||
|
} else if ('input' === componentSetting.controllerType) {
|
||||||
|
const value = settings[key as keyof ModelParams] as string
|
||||||
|
const placeholder = settings[key as keyof ModelParams] as string
|
||||||
|
if ('value' in componentSetting.controllerData)
|
||||||
|
componentSetting.controllerData.value = value
|
||||||
|
if ('placeholder' in componentSetting.controllerData)
|
||||||
|
componentSetting.controllerData.placeholder = placeholder
|
||||||
|
} else if ('checkbox' === componentSetting.controllerType) {
|
||||||
|
const checked = settings[key as keyof ModelParams] as boolean
|
||||||
|
|
||||||
|
if ('checked' in componentSetting.controllerData)
|
||||||
|
componentSetting.controllerData.checked = checked
|
||||||
|
}
|
||||||
|
componentData.push(componentSetting)
|
||||||
|
})
|
||||||
|
return componentData
|
||||||
|
}
|
||||||
12
web/utils/model.ts
Normal file
12
web/utils/model.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { basename } from 'path'
|
||||||
|
|
||||||
|
import { Model } from '@janhq/core'
|
||||||
|
|
||||||
|
export const modelBinFileName = (model: Model) => {
|
||||||
|
const modelFormatExt = '.gguf'
|
||||||
|
const extractedFileName = basename(model.source_url) ?? model.id
|
||||||
|
const fileName = extractedFileName.toLowerCase().endsWith(modelFormatExt)
|
||||||
|
? extractedFileName
|
||||||
|
: model.id
|
||||||
|
return fileName
|
||||||
|
}
|
||||||
53
web/utils/model_param.ts
Normal file
53
web/utils/model_param.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ModelRuntimeParams, ModelSettingParams } from '@janhq/core'
|
||||||
|
|
||||||
|
import { ModelParams } from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
|
export const toRuntimeParams = (
|
||||||
|
modelParams?: ModelParams
|
||||||
|
): ModelRuntimeParams => {
|
||||||
|
if (!modelParams) return {}
|
||||||
|
const defaultModelParams: ModelRuntimeParams = {
|
||||||
|
temperature: undefined,
|
||||||
|
token_limit: undefined,
|
||||||
|
top_k: undefined,
|
||||||
|
top_p: undefined,
|
||||||
|
stream: undefined,
|
||||||
|
max_tokens: undefined,
|
||||||
|
stop: undefined,
|
||||||
|
frequency_penalty: undefined,
|
||||||
|
presence_penalty: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeParams: ModelRuntimeParams = {}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(modelParams)) {
|
||||||
|
if (key in defaultModelParams) {
|
||||||
|
runtimeParams[key as keyof ModelRuntimeParams] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return runtimeParams
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toSettingParams = (
|
||||||
|
modelParams?: ModelParams
|
||||||
|
): ModelSettingParams => {
|
||||||
|
if (!modelParams) return {}
|
||||||
|
const defaultSettingParams: ModelSettingParams = {
|
||||||
|
ctx_len: undefined,
|
||||||
|
ngl: undefined,
|
||||||
|
embedding: undefined,
|
||||||
|
n_parallel: undefined,
|
||||||
|
cpu_threads: undefined,
|
||||||
|
prompt_template: undefined,
|
||||||
|
}
|
||||||
|
const settingParams: ModelSettingParams = {}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(modelParams)) {
|
||||||
|
if (key in defaultSettingParams) {
|
||||||
|
settingParams[key as keyof ModelSettingParams] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingParams
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user