Merge branch 'main' into feat_adr_008_extensible_docker

This commit is contained in:
namvuong 2023-10-26 10:17:54 +07:00 committed by GitHub
commit 239d2681a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
240 changed files with 4024 additions and 2676 deletions

5
.github/scripts/auto-sign.sh vendored Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
APP_PATH=${APP_PATH}
DEVELOPER_ID=${DEVELOPER_ID}
find $APP_PATH \( -type f -perm +111 -o -name "*.node" \) -exec codesign -s "$DEVELOPER_ID" --options=runtime {} \;

View File

@ -44,6 +44,7 @@ jobs:
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }} CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
- uses: apple-actions/import-codesign-certs@v2 - uses: apple-actions/import-codesign-certs@v2
continue-on-error: true
with: with:
p12-file-base64: ${{ secrets.CODE_SIGN_P12_BASE64 }} p12-file-base64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
p12-password: ${{ secrets.CODE_SIGN_P12_PASSWORD }} p12-password: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
@ -52,6 +53,9 @@ jobs:
run: | run: |
yarn install yarn install
yarn build:plugins-darwin yarn build:plugins-darwin
env:
APP_PATH: "."
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
- name: Build and publish app - name: Build and publish app
run: | run: |

View File

@ -15,7 +15,7 @@ on:
- "!plugins/*/package.json" - "!plugins/*/package.json"
jobs: jobs:
build: build:
runs-on: mac-silicon runs-on: macos-latest
environment: production environment: production
outputs: outputs:
branch_name: ${{ steps.commit_and_tag.outputs.branch_name }} branch_name: ${{ steps.commit_and_tag.outputs.branch_name }}
@ -28,6 +28,18 @@ jobs:
- name: Install jq - name: Install jq
uses: dcarbone/install-jq-action@v2.0.1 uses: dcarbone/install-jq-action@v2.0.1
- name: Get Cer for code signing
run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
shell: bash
env:
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
- uses: apple-actions/import-codesign-certs@v2
continue-on-error: true
with:
p12-file-base64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
p12-password: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
- name: Check Path Change - name: Check Path Change
run: | run: |
git config --global user.email "service@jan.ai" git config --global user.email "service@jan.ai"
@ -76,7 +88,7 @@ jobs:
do do
echo $dir echo $dir
cd $dir cd $dir
npm install && npm run build npm install && npm run postinstall && ../../.github/scripts/auto-sign.sh
if [[ $GITHUB_EVENT_NAME == 'push' && $GITHUB_EVENT_PULL_REQUEST_HEAD_REPO_FULL_NAME != $GITHUB_REPOSITORY ]]; then if [[ $GITHUB_EVENT_NAME == 'push' && $GITHUB_EVENT_PULL_REQUEST_HEAD_REPO_FULL_NAME != $GITHUB_REPOSITORY ]]; then
npm publish --access public npm publish --access public
fi fi
@ -84,6 +96,8 @@ jobs:
done done
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
APP_PATH: "."
- name: "Commit new version to main and create tag" - name: "Commit new version to main and create tag"
id: commit_and_tag id: commit_and_tag

722
LICENSE

File diff suppressed because it is too large Load Diff

View File

@ -131,6 +131,11 @@ export enum DataService {
* Updates a bot matching an ID. * Updates a bot matching an ID.
*/ */
UpdateBot = "updateBot", UpdateBot = "updateBot",
/**
* Gets the plugin manifest.
*/
GetPluginManifest = "getPluginManifest",
} }
/** /**

View File

@ -1,6 +1,6 @@
{ {
"name": "@janhq/core", "name": "@janhq/core",
"version": "0.1.6", "version": "0.1.7",
"description": "Plugin core lib", "description": "Plugin core lib",
"keywords": [ "keywords": [
"jan", "jan",

View File

@ -1,10 +1,13 @@
--- ---
title: "Jan's AI Hacker House (Ho Chi Minh City)" title: "Jan's AI Hacker House (Ho Chi Minh City)"
description: "24-27 Oct 2023, Thao Dien, HCMC. AI-focused talks, workshops and social events. Hosted by Jan.ai" description: "24-27 Oct 2023, District 3, HCMC. AI-focused talks, workshops and social events. Hosted by Jan.ai"
slug: /events/hcmc-oct23 slug: /events/hcmc-oct23
image: /img/hcmc-villa-1.jpeg image: /img/hcmc-launch-party.png
--- ---
![](https://img.evbuc.com/https%3A%2F%2Fcdn.evbuc.com%2Fimages%2F622863119%2F1835665938193%2F1%2Foriginal.20231018-020624?w=940&auto=format%2Ccompress&q=75&sharp=10&rect=0%2C0%2C3240%2C1620&s=2408a9d180947bbc83ccbf0b76e7ab85)
![](/img/hcmc-launch-party.png)
🎉 Join us at our Friday Launch Party for an evening of AI talks from other builders! [(RSVP here)](https://jan-launch-party.eventbrite.sg/) 🎉
## Ho Chi Minh City ## Ho Chi Minh City
@ -20,18 +23,18 @@ Jan is a fully remote team. We use the money we save from not having an office,
### Location ### Location
- Thao Dien, District 2, Ho Chi Minh City - Districts 1 & 3, Ho Chi Minh City
- Exact location to be shared later - Exact location in Evenbrite (see below)
## Agenda ## Agenda
To help us manage RSVPs, please use the Eventbrite links below to RSVP for each event. To help us manage RSVPs, please use the Eventbrite links below to RSVP for each event.
| Day | Eventbrite Link | Signups | | Day | Eventbrite Link | Signups |
| ------------ | ------------------------------------- | -------------------------------------------------------------------------------------------- | | ------------ | ------------------------------------- | ---------------------------------------------------- |
| Mon (23 Oct) | Jan Team & Partners Dinner | Invite-only | | Mon (23 Oct) | Jan Team & Partners Dinner | Invite-only |
| Wed (25 Oct) | Intro to LLMs | [RSVP here](https://www.eventbrite.sg/e/intro-to-large-language-models-tickets-737819045627) | | Thu (26 Oct) | HCMC Startups & VC Night | Invite-only |
| Fri (27 Oct) | Jan Launch Party + Build your own LLM | [RSVP here](https://jan-launch-party.eventbrite.sg/) | | Fri (27 Oct) | Jan Launch Party + Build your own LLM | [RSVP here](https://jan-launch-party.eventbrite.sg/) |
### OKRs ### OKRs
@ -40,9 +43,10 @@ To help us manage RSVPs, please use the Eventbrite links below to RSVP for each
| Key Result | Polished UI with "Create Bot" w/ saved prompts | | Key Result | Polished UI with "Create Bot" w/ saved prompts |
| Key Result | Documentation of Jan Codebase for Plugin Developers | | Key Result | Documentation of Jan Codebase for Plugin Developers |
| Key Result | Roadmap for 4Q 2023 | | Key Result | Roadmap for 4Q 2023 |
| Key Result | *Stretch Goal:* Core Process API for Plugins | | Key Result | _Stretch Goal:_ Core Process API for Plugins |
## Photos ## Photos
![](/img/hcmc-villa-1.jpeg)
![](/img/hcmc-villa-2.jpeg) ![](/img/hcmc-launch-party.png)
🎉 Join us at our Friday Launch Party for an evening of AI talks from other builders! [(RSVP here)](https://jan-launch-party.eventbrite.sg/) 🎉

View File

@ -80,11 +80,8 @@
@apply border-gray-200 dark:border-gray-800; @apply border-gray-200 dark:border-gray-800;
} }
[class*="docMainContainer_"], .theme-doc-markdown {
[class*="docSidebarContainer_"] { a {
table { @apply text-blue-600 dark:text-blue-400;
a {
@apply text-blue-600 dark:text-blue-400;
}
} }
} }

BIN
docs/static/img/hcmc-launch-party.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

View File

@ -1,5 +0,0 @@
#!/bin/bash
DEVELOPER_ID="Developer ID Application: Eigenvector Pte Ltd"
find electron -type f -perm +111 -exec codesign -s "Developer ID Application: Eigenvector Pte Ltd (YT49P7GXG4)" --options=runtime {} \;

View File

@ -1,4 +1,11 @@
import { app, BrowserWindow, ipcMain, dialog, shell } from "electron"; import {
app,
BrowserWindow,
ipcMain,
dialog,
shell,
nativeTheme,
} from "electron";
import { readdirSync, writeFileSync } from "fs"; import { readdirSync, writeFileSync } from "fs";
import { resolve, join, extname } from "path"; import { resolve, join, extname } from "path";
import { rmdir, unlink, createWriteStream } from "fs"; import { rmdir, unlink, createWriteStream } from "fs";
@ -36,12 +43,30 @@ app.on("window-all-closed", () => {
app.quit(); app.quit();
}); });
ipcMain.handle("setNativeThemeLight", () => {
nativeTheme.themeSource = "light";
});
ipcMain.handle("setNativeThemeDark", () => {
nativeTheme.themeSource = "dark";
});
ipcMain.handle("setNativeThemeSystem", () => {
nativeTheme.themeSource = "system";
});
function createMainWindow() { function createMainWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1200, width: 1200,
height: 800, height: 800,
frame: false,
show: false, show: false,
backgroundColor: "white", trafficLightPosition: {
x: 16,
y: 10,
},
titleBarStyle: "hidden",
vibrancy: "sidebar",
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
preload: join(__dirname, "preload.js"), preload: join(__dirname, "preload.js"),
@ -118,11 +143,13 @@ function handleIPCs() {
ipcMain.handle( ipcMain.handle(
"invokePluginFunc", "invokePluginFunc",
async (_event, modulePath, method, ...args) => { async (_event, modulePath, method, ...args) => {
const module = require(/* webpackIgnore: true */ join( const module = require(
app.getPath("userData"), /* webpackIgnore: true */ join(
"plugins", app.getPath("userData"),
modulePath "plugins",
)); modulePath
)
);
requiredModules[modulePath] = module; requiredModules[modulePath] = module;
if (typeof module[method] === "function") { if (typeof module[method] === "function") {

View File

@ -67,7 +67,6 @@
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"electron-updater": "^6.1.4", "electron-updater": "^6.1.4",
"pacote": "^17.0.4", "pacote": "^17.0.4",
"react-intersection-observer": "^9.5.2",
"request": "^2.88.2", "request": "^2.88.2",
"request-progress": "^3.0.0", "request-progress": "^3.0.0",
"use-debounce": "^9.0.4" "use-debounce": "^9.0.4"

View File

@ -9,6 +9,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
invokePluginFunc: (plugin: any, method: any, ...args: any[]) => invokePluginFunc: (plugin: any, method: any, ...args: any[]) =>
ipcRenderer.invoke("invokePluginFunc", plugin, method, ...args), ipcRenderer.invoke("invokePluginFunc", plugin, method, ...args),
setNativeThemeLight: () => ipcRenderer.invoke("setNativeThemeLight"),
setNativeThemeDark: () => ipcRenderer.invoke("setNativeThemeDark"),
setNativeThemeSystem: () => ipcRenderer.invoke("setNativeThemeSystem"),
basePlugins: () => ipcRenderer.invoke("basePlugins"), basePlugins: () => ipcRenderer.invoke("basePlugins"),
pluginPath: () => ipcRenderer.invoke("pluginPath"), pluginPath: () => ipcRenderer.invoke("pluginPath"),
@ -23,19 +29,27 @@ contextBridge.exposeInMainWorld("electronAPI", {
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath), deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
installRemotePlugin: (pluginName: string) => ipcRenderer.invoke("installRemotePlugin", pluginName), installRemotePlugin: (pluginName: string) =>
ipcRenderer.invoke("installRemotePlugin", pluginName),
downloadFile: (url: string, path: string) => ipcRenderer.invoke("downloadFile", url, path), downloadFile: (url: string, path: string) =>
ipcRenderer.invoke("downloadFile", url, path),
onFileDownloadUpdate: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback), onFileDownloadUpdate: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback),
onFileDownloadError: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback), onFileDownloadError: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback),
onFileDownloadSuccess: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback), onFileDownloadSuccess: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback),
onAppUpdateDownloadUpdate: (callback: any) => ipcRenderer.on("APP_UPDATE_PROGRESS", callback), onAppUpdateDownloadUpdate: (callback: any) =>
ipcRenderer.on("APP_UPDATE_PROGRESS", callback),
onAppUpdateDownloadError: (callback: any) => ipcRenderer.on("APP_UPDATE_ERROR", callback), onAppUpdateDownloadError: (callback: any) =>
ipcRenderer.on("APP_UPDATE_ERROR", callback),
onAppUpdateDownloadSuccess: (callback: any) => ipcRenderer.on("APP_UPDATE_COMPLETE", callback), onAppUpdateDownloadSuccess: (callback: any) =>
ipcRenderer.on("APP_UPDATE_COMPLETE", callback),
}); });

View File

@ -48,10 +48,8 @@ test("renders the home page", async () => {
// Welcome text is available // Welcome text is available
const welcomeText = await page const welcomeText = await page
.locator(".text-5xl", { .getByTestId("testid-welcome-title")
hasText: "Welcome,lets download your first model",
})
.first() .first()
.isDisabled(); .isVisible();
expect(welcomeText).toBe(false); expect(welcomeText).toBe(false);
}); });

View File

@ -40,7 +40,7 @@ test("shows my models", async () => {
.getByRole("heading") .getByRole("heading")
.filter({ hasText: "My Models" }) .filter({ hasText: "My Models" })
.first() .first()
.isDisabled(); .isVisible();
expect(header).toBe(false); expect(header).toBe(false);
// More test cases here... // More test cases here...
}); });

View File

@ -35,37 +35,13 @@ test.afterAll(async () => {
}); });
test("renders left navigation panel", async () => { test("renders left navigation panel", async () => {
// Chat History section is available // Chat section should be there
const chatSection = await page const chatSection = await page.getByTestId("Chat").first().isVisible();
.getByRole("heading")
.filter({ hasText: "CHAT HISTORY" })
.first()
.isDisabled();
expect(chatSection).toBe(false); expect(chatSection).toBe(false);
// Home actions // Home actions
const createBotBtn = await page const botBtn = await page.getByTestId("Bot").first().isEnabled();
.getByRole("button", { name: "Create bot" }) const myModelsBtn = await page.getByTestId("My Models").first().isEnabled();
.first() const settingsBtn = await page.getByTestId("Settings").first().isEnabled();
.isEnabled(); expect([botBtn, myModelsBtn, settingsBtn].filter((e) => !e).length).toBe(0);
const exploreBtn = await page
.getByRole("button", { name: "Explore Models" })
.first()
.isEnabled();
const myModelsBtn = await page
.getByTestId("My Models")
.first()
.isEnabled();
const settingsBtn = await page
.getByTestId("Settings")
.first()
.isEnabled();
expect(
[
createBotBtn,
exploreBtn,
myModelsBtn,
settingsBtn,
].filter((e) => !e).length
).toBe(0);
}); });

View File

@ -36,7 +36,5 @@ test.afterAll(async () => {
test("shows settings", async () => { test("shows settings", async () => {
await page.getByTestId("Settings").first().click(); await page.getByTestId("Settings").first().click();
await page.getByTestId("testid-setting-description").isVisible();
const pluginList = await page.getByTestId("plugin-item").count();
expect(pluginList).toBe(4);
}); });

View File

@ -24,7 +24,7 @@
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"", "build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
"build:electron": "yarn workspace jan build", "build:electron": "yarn workspace jan build",
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm install && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-management-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-management-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"", "build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm install && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-management-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-management-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
"build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./plugins/data-plugin && npm install && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-management-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && chmod +x ./electron/auto-sign.sh && ./electron/auto-sign.sh && concurrently \"cd ./plugins/data-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-management-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"", "build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./plugins/data-plugin && npm install && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-management-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && chmod +x ./.github/scripts/auto-sign.sh && ./.github/scripts/auto-sign.sh && concurrently \"cd ./plugins/data-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-management-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
"build": "yarn build:web && yarn build:electron", "build": "yarn build:web && yarn build:electron",
"build:darwin": "yarn build:web && yarn workspace jan build:darwin", "build:darwin": "yarn build:web && yarn workspace jan build:darwin",
"build:win32": "yarn build:web && yarn workspace jan build:win32", "build:win32": "yarn build:web && yarn workspace jan build:win32",

View File

@ -1,2 +1,3 @@
declare const PLUGIN_NAME: string; declare const PLUGIN_NAME: string;
declare const MODULE_PATH: string; declare const MODULE_PATH: string;
declare const PLUGIN_CATALOG: string;

View File

@ -216,6 +216,9 @@ export function init({ register }: { register: RegisterExtensionPoint }) {
register(DataService.GetBotById, getBotById.name, getBotById); register(DataService.GetBotById, getBotById.name, getBotById);
register(DataService.DeleteBot, deleteBot.name, deleteBot); register(DataService.DeleteBot, deleteBot.name, deleteBot);
register(DataService.UpdateBot, updateBot.name, updateBot); register(DataService.UpdateBot, updateBot.name, updateBot);
// for plugin manifest
register(DataService.GetPluginManifest, getPluginManifest.name, getPluginManifest)
} }
function getConversations(): Promise<any> { function getConversations(): Promise<any> {
@ -323,3 +326,20 @@ function getBotById(botId: string): Promise<any> {
return Promise.reject(err); return Promise.reject(err);
}); });
} }
/**
* Retrieves the plugin manifest by importing the remote model catalog and clearing the cache to get the latest version.
* A timestamp is added to the URL to prevent caching.
* @returns A Promise that resolves with the plugin manifest.
*/
function getPluginManifest(): Promise<any> {
// Clear cache to get the latest model catalog
delete require.cache[
require.resolve(/* webpackIgnore: true */ PLUGIN_CATALOG)
];
// Import the remote model catalog
// Add a timestamp to the URL to prevent caching
return import(
/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`
).then((module) => module.default);
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@janhq/data-plugin", "name": "@janhq/data-plugin",
"version": "1.0.7", "version": "1.0.14",
"description": "The Data Connector provides easy access to a data API using the PouchDB engine. It offers accessible data management capabilities.", "description": "The Data Connector provides easy access to a data API using the PouchDB engine. It offers accessible data management capabilities.",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg", "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
"main": "dist/esm/index.js", "main": "dist/esm/index.js",
@ -12,7 +12,7 @@
], ],
"scripts": { "scripts": {
"build": "tsc -b ./config/tsconfig.esm.json && tsc -b ./config/tsconfig.cjs.json && webpack --config webpack.config.js", "build": "tsc -b ./config/tsconfig.esm.json && tsc -b ./config/tsconfig.cjs.json && webpack --config webpack.config.js",
"postinstall": "rimraf *.tgz --glob && npm run build", "postinstall": "electron-rebuild -f -w leveldown@5.6.0 --arch=arm64 -v 26.2.1 && rimraf *.tgz --glob && npm run build",
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install" "build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
}, },
"exports": { "exports": {
@ -40,7 +40,9 @@
"node_modules" "node_modules"
], ],
"dependencies": { "dependencies": {
"@janhq/core": "^0.1.6", "@janhq/core": "^0.1.7",
"electron": "26.2.1",
"electron-rebuild": "^3.2.9",
"pouchdb-find": "^8.0.1", "pouchdb-find": "^8.0.1",
"pouchdb-node": "^8.0.1" "pouchdb-node": "^8.0.1"
}, },

View File

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2016", "target": "es2016",
"module": "ES6", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"outDir": "./dist", "outDir": "./dist",
"esModuleInterop": true, "esModuleInterop": true,

View File

@ -19,6 +19,9 @@ module.exports = {
new webpack.DefinePlugin({ new webpack.DefinePlugin({
PLUGIN_NAME: JSON.stringify(packageJson.name), PLUGIN_NAME: JSON.stringify(packageJson.name),
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`), MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
PLUGIN_CATALOG: JSON.stringify(
"https://cdn.jsdelivr.net/npm/@janhq/plugin-catalog@latest/dist/index.js"
),
}), }),
], ],
output: { output: {

View File

@ -9,13 +9,17 @@ import {
} from "@janhq/core"; } from "@janhq/core";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
const initModel = async (product) => invokePluginFunc(MODULE_PATH, "initModel", product); const initModel = async (product) =>
invokePluginFunc(MODULE_PATH, "initModel", product);
const stopModel = () => { const stopModel = () => {
invokePluginFunc(MODULE_PATH, "killSubprocess"); invokePluginFunc(MODULE_PATH, "killSubprocess");
}; };
function requestInference(recentMessages: any[], bot?: any): Observable<string> { function requestInference(
recentMessages: any[],
bot?: any
): Observable<string> {
return new Observable((subscriber) => { return new Observable((subscriber) => {
const requestBody = JSON.stringify({ const requestBody = JSON.stringify({
messages: recentMessages, messages: recentMessages,
@ -63,16 +67,21 @@ function requestInference(recentMessages: any[], bot?: any): Observable<string>
} }
subscriber.complete(); subscriber.complete();
}) })
.catch(subscriber.error); .catch((err) => subscriber.error(err));
}); });
} }
async function retrieveLastTenMessages(conversationId: string, bot?: any) { async function retrieveLastTenMessages(conversationId: string, bot?: any) {
// TODO: Common collections should be able to access via core functions instead of store // TODO: Common collections should be able to access via core functions instead of store
const messageHistory = (await store.findMany("messages", { conversationId }, [{ createdAt: "asc" }])) ?? []; const messageHistory =
(await store.findMany("messages", { conversationId }, [
{ createdAt: "asc" },
])) ?? [];
let recentMessages = messageHistory let recentMessages = messageHistory
.filter((e) => e.message !== "" && (e.user === "user" || e.user === "assistant")) .filter(
(e) => e.message !== "" && (e.user === "user" || e.user === "assistant")
)
.slice(-9) .slice(-9)
.map((message) => ({ .map((message) => ({
content: message.message.trim(), content: message.message.trim(),
@ -81,10 +90,13 @@ async function retrieveLastTenMessages(conversationId: string, bot?: any) {
if (bot && bot.systemPrompt) { if (bot && bot.systemPrompt) {
// append bot's system prompt // append bot's system prompt
recentMessages = [{ recentMessages = [
content: `[INST] ${bot.systemPrompt}`, {
role: 'system' content: `[INST] ${bot.systemPrompt}`,
},...recentMessages]; role: "system",
},
...recentMessages,
];
} }
console.debug(`Last 10 messages: ${JSON.stringify(recentMessages, null, 2)}`); console.debug(`Last 10 messages: ${JSON.stringify(recentMessages, null, 2)}`);
@ -93,13 +105,19 @@ async function retrieveLastTenMessages(conversationId: string, bot?: any) {
} }
async function handleMessageRequest(data: NewMessageRequest) { async function handleMessageRequest(data: NewMessageRequest) {
const conversation = await store.findOne("conversations", data.conversationId); const conversation = await store.findOne(
"conversations",
data.conversationId
);
let bot = undefined; let bot = undefined;
if (conversation.botId != null) { if (conversation.botId != null) {
bot = await store.findOne("bots", conversation.botId); bot = await store.findOne("bots", conversation.botId);
} }
const recentMessages = await retrieveLastTenMessages(data.conversationId, bot); const recentMessages = await retrieveLastTenMessages(
data.conversationId,
bot
);
const message = { const message = {
...data, ...data,
message: "", message: "",
@ -124,7 +142,9 @@ async function handleMessageRequest(data: NewMessageRequest) {
await store.updateOne("messages", message._id, message); await store.updateOne("messages", message._id, message);
}, },
error: async (err) => { error: async (err) => {
message.message = message.message.trim() + "\n" + "Error occurred: " + err; message.message =
message.message.trim() + "\n" + "Error occurred: " + err.message;
events.emit(EventName.OnMessageResponseUpdate, message);
// TODO: Common collections should be able to access via core functions instead of store // TODO: Common collections should be able to access via core functions instead of store
await store.updateOne("messages", message._id, message); await store.updateOne("messages", message._id, message);
}, },
@ -140,7 +160,10 @@ async function inferenceRequest(data: NewMessageRequest): Promise<any> {
}; };
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const recentMessages = await retrieveLastTenMessages(data.conversationId); const recentMessages = await retrieveLastTenMessages(data.conversationId);
requestInference([...recentMessages, { role: "user", content: data.message }]).subscribe({ requestInference([
...recentMessages,
{ role: "user", content: data.message },
]).subscribe({
next: (content) => { next: (content) => {
message.message = content; message.message = content;
}, },
@ -166,5 +189,9 @@ export function init({ register }) {
register(PluginService.OnStart, PLUGIN_NAME, onStart); register(PluginService.OnStart, PLUGIN_NAME, onStart);
register(InferenceService.InitModel, initModel.name, initModel); register(InferenceService.InitModel, initModel.name, initModel);
register(InferenceService.StopModel, stopModel.name, stopModel); register(InferenceService.StopModel, stopModel.name, stopModel);
register(InferenceService.InferenceRequest, inferenceRequest.name, inferenceRequest); register(
InferenceService.InferenceRequest,
inferenceRequest.name,
inferenceRequest
);
} }

View File

@ -14,53 +14,49 @@ const initModel = (fileName) => {
if (!fileName) { if (!fileName) {
reject("Model not found, please download again."); reject("Model not found, please download again.");
} }
if (subprocess) {
console.error("A subprocess is already running. Attempt to kill then reinit.");
killSubprocess();
}
resolve(fileName); resolve(fileName);
}) })
// Kill port process if it is already in use
.then((fileName) =>
tcpPortUsed
.waitUntilFree(PORT, 200, 3000)
.catch(() => killPortProcess(PORT))
.then(() => fileName)
)
// Spawn Nitro subprocess to load model // Spawn Nitro subprocess to load model
.then(() => { .then(() => {
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default return tcpPortUsed.check(PORT, "127.0.0.1").then((inUse) => {
let binaryName; if (!inUse) {
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
let binaryName;
if (process.platform === "win32") { if (process.platform === "win32") {
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries // Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
binaryName = "nitro_windows_amd64_cuda.exe"; binaryName = "nitro_start_windows.bat";
} else if (process.platform === "darwin") { } else if (process.platform === "darwin") {
// Mac OS platform // Mac OS platform
binaryName = process.arch === "arm64" ? "nitro_mac_arm64" : "nitro_mac_intel"; binaryName =
} else { process.arch === "arm64"
// Linux ? "nitro_mac_arm64"
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries : "nitro_mac_intel";
binaryName = "nitro_linux_amd64_cuda"; // For other platforms } else {
} // Linux
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
binaryName = "nitro_start_linux.sh"; // For other platforms
}
const binaryPath = path.join(binaryFolder, binaryName); const binaryPath = path.join(binaryFolder, binaryName);
// Execute the binary // Execute the binary
subprocess = spawn(binaryPath, { cwd: binaryFolder }); subprocess = spawn(binaryPath, { cwd: binaryFolder });
// Handle subprocess output // Handle subprocess output
subprocess.stdout.on("data", (data) => { subprocess.stdout.on("data", (data) => {
console.log(`stdout: ${data}`); console.log(`stdout: ${data}`);
}); });
subprocess.stderr.on("data", (data) => { subprocess.stderr.on("data", (data) => {
console.error(`stderr: ${data}`); console.error(`stderr: ${data}`);
}); });
subprocess.on("close", (code) => { subprocess.on("close", (code) => {
console.log(`child process exited with code ${code}`); console.log(`child process exited with code ${code}`);
subprocess = null; subprocess = null;
});
}
}); });
}) })
.then(() => tcpPortUsed.waitUntilUsed(PORT, 300, 30000)) .then(() => tcpPortUsed.waitUntilUsed(PORT, 300, 30000))

View File

@ -0,0 +1,6 @@
#!/bin/bash
#!/bin/bash
# Attempt to run the nitro_linux_amd64_cuda file and if it fails, run nitro_linux_amd64
./nitro_linux_amd64_cuda || (echo "nitro_linux_amd64_cuda encountered an error, attempting to run nitro_linux_amd64..." && ./nitro_linux_amd64)

View File

@ -0,0 +1,10 @@
@echo off
rem Attempt to run nitro_windows_amd64_cuda.exe
nitro_windows_amd64_cuda.exe
rem Check the exit code of the previous command
if %errorlevel% neq 0 (
echo nitro_windows_amd64_cuda.exe encountered an error, attempting to run nitro_windows_amd64.exe...
nitro_windows_amd64.exe
)

View File

@ -1,6 +1,6 @@
{ {
"name": "@janhq/inference-plugin", "name": "@janhq/inference-plugin",
"version": "1.0.10", "version": "1.0.14",
"description": "Inference Plugin, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.", "description": "Inference Plugin, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/command-line.svg", "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/command-line.svg",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -8,19 +8,24 @@ import {
} from "@janhq/core"; } from "@janhq/core";
import { parseToModel } from "./helper"; import { parseToModel } from "./helper";
const downloadModel = (product) => downloadFile(product.downloadUrl, product.fileName); const downloadModel = (product) =>
downloadFile(product.downloadUrl, product.fileName);
const deleteModel = (path) => deleteFile(path); const deleteModel = (path) => deleteFile(path);
async function getConfiguredModels() { /**
// Clear cache to get the latest model catalog * Retrieves a list of configured models from the model catalog URL.
delete require.cache[MODEL_CATALOG_URL]; * @returns A Promise that resolves to an array of configured models.
*/
// Import the remote model catalog async function getConfiguredModels(): Promise<any> {
const module = require(MODEL_CATALOG_URL); // Add a timestamp to the URL to prevent caching
return module.default.map((e) => { return import(
return parseToModel(e); /* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}`
}); ).then((module) =>
module.default.map((e) => {
return parseToModel(e);
})
);
} }
/** /**
@ -44,7 +49,11 @@ function storeModel(model: any) {
* @param model Product * @param model Product
*/ */
function updateFinishedDownloadAt(_id: string): Promise<any> { function updateFinishedDownloadAt(_id: string): Promise<any> {
return store.updateMany("models", { _id }, { time: Date.now(), finishDownloadAt: 1 }); return store.updateMany(
"models",
{ _id },
{ time: Date.now(), finishDownloadAt: 1 }
);
} }
/** /**
@ -84,14 +93,38 @@ function onStart() {
export function init({ register }: { register: RegisterExtensionPoint }) { export function init({ register }: { register: RegisterExtensionPoint }) {
register(PluginService.OnStart, PLUGIN_NAME, onStart); register(PluginService.OnStart, PLUGIN_NAME, onStart);
register(ModelManagementService.DownloadModel, downloadModel.name, downloadModel); register(
ModelManagementService.DownloadModel,
downloadModel.name,
downloadModel
);
register(ModelManagementService.DeleteModel, deleteModel.name, deleteModel); register(ModelManagementService.DeleteModel, deleteModel.name, deleteModel);
register(ModelManagementService.GetConfiguredModels, getConfiguredModels.name, getConfiguredModels); register(
ModelManagementService.GetConfiguredModels,
getConfiguredModels.name,
getConfiguredModels
);
register(ModelManagementService.StoreModel, storeModel.name, storeModel); register(ModelManagementService.StoreModel, storeModel.name, storeModel);
register(ModelManagementService.UpdateFinishedDownloadAt, updateFinishedDownloadAt.name, updateFinishedDownloadAt); register(
ModelManagementService.UpdateFinishedDownloadAt,
updateFinishedDownloadAt.name,
updateFinishedDownloadAt
);
register(ModelManagementService.DeleteDownloadModel, deleteDownloadModel.name, deleteDownloadModel); register(
register(ModelManagementService.GetModelById, getModelById.name, getModelById); ModelManagementService.DeleteDownloadModel,
register(ModelManagementService.GetFinishedDownloadModels, getFinishedDownloadModels.name, getFinishedDownloadModels); deleteDownloadModel.name,
deleteDownloadModel
);
register(
ModelManagementService.GetModelById,
getModelById.name,
getModelById
);
register(
ModelManagementService.GetFinishedDownloadModels,
getFinishedDownloadModels.name,
getFinishedDownloadModels
);
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@janhq/model-management-plugin", "name": "@janhq/model-management-plugin",
"version": "1.0.8", "version": "1.0.9",
"description": "Model Management Plugin provides model exploration and seamless downloads", "description": "Model Management Plugin provides model exploration and seamless downloads",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/queue-list.svg", "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/queue-list.svg",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -1,7 +1,7 @@
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import React from 'react' import React from 'react'
import ModelTable from '../ModelTable' import ModelTable from '../ModelTable'
import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom' import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
const ActiveModelTable: React.FC = () => { const ActiveModelTable: React.FC = () => {
const activeModel = useAtomValue(activeAssistantModelAtom) const activeModel = useAtomValue(activeAssistantModelAtom)

View File

@ -2,8 +2,7 @@ import DownloadModelContent from '../DownloadModelContent'
import ModelDownloadButton from '../ModelDownloadButton' import ModelDownloadButton from '../ModelDownloadButton'
import ModelDownloadingButton from '../ModelDownloadingButton' import ModelDownloadingButton from '../ModelDownloadingButton'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
import { AssistantModel } from '@/_models/AssistantModel'
type Props = { type Props = {
model: AssistantModel model: AssistantModel

View File

@ -7,7 +7,7 @@ type Props = {
const Avatar: React.FC<Props> = ({ allowEdit = false }) => ( const Avatar: React.FC<Props> = ({ allowEdit = false }) => (
<div className="mx-auto flex flex-col gap-5"> <div className="mx-auto flex flex-col gap-5">
<span className="mx-auto inline-block h-14 w-14 overflow-hidden rounded-full bg-gray-100"> <span className="mx-auto inline-block h-10 w-10 overflow-hidden rounded-full bg-gray-100">
<svg <svg
className="mx-auto h-full w-full text-gray-300" className="mx-auto h-full w-full text-gray-300"
fill="currentColor" fill="currentColor"

View File

@ -3,7 +3,7 @@
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { InformationCircleIcon } from '@heroicons/react/24/outline' import { InformationCircleIcon } from '@heroicons/react/24/outline'
import SendButton from '../SendButton' import SendButton from '../SendButton'
import { showingAdvancedPromptAtom } from '@/_helpers/atoms/Modal.atom' import { showingAdvancedPromptAtom } from '@helpers/atoms/Modal.atom'
const BasicPromptAccessories: React.FC = () => { const BasicPromptAccessories: React.FC = () => {
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom) const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom)
@ -11,7 +11,7 @@ const BasicPromptAccessories: React.FC = () => {
const shouldShowAdvancedPrompt = false const shouldShowAdvancedPrompt = false
return ( return (
<div className="absolute inset-x-0 bottom-0 flex justify-between py-2 pl-3 pr-2"> <div className="absolute inset-x-0 bottom-0 flex justify-between p-3">
{/* Add future accessories here, e.g upload a file */} {/* Add future accessories here, e.g upload a file */}
<div className="flex items-center space-x-5"> <div className="flex items-center space-x-5">
<div className="flex items-center"> <div className="flex items-center">

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { ChevronLeftIcon } from '@heroicons/react/24/outline' import { ChevronLeftIcon } from '@heroicons/react/24/outline'
import { showingAdvancedPromptAtom } from '@/_helpers/atoms/Modal.atom' import { showingAdvancedPromptAtom } from '@helpers/atoms/Modal.atom'
const BasicPromptButton: React.FC = () => { const BasicPromptButton: React.FC = () => {
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom) const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom)

View File

@ -1,11 +1,10 @@
'use client' 'use client'
import { currentPromptAtom } from '@/_helpers/JotaiWrapper' import { currentPromptAtom } from '@helpers/JotaiWrapper'
import { getActiveConvoIdAtom } from '@/_helpers/atoms/Conversation.atom' import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom'
import { selectedModelAtom } from '@/_helpers/atoms/Model.atom' import { selectedModelAtom } from '@helpers/atoms/Model.atom'
import useCreateConversation from '@/_hooks/useCreateConversation' import useCreateConversation from '@hooks/useCreateConversation'
import useInitModel from '@/_hooks/useInitModel' import useSendChatMessage from '@hooks/useSendChatMessage'
import useSendChatMessage from '@/_hooks/useSendChatMessage'
import { useAtom, useAtomValue } from 'jotai' import { useAtom, useAtomValue } from 'jotai'
import { ChangeEvent, useEffect, useRef } from 'react' import { ChangeEvent, useEffect, useRef } from 'react'
@ -16,8 +15,6 @@ const BasicPromptInput: React.FC = () => {
const { sendChatMessage } = useSendChatMessage() const { sendChatMessage } = useSendChatMessage()
const { requestCreateConvo } = useCreateConversation() const { requestCreateConvo } = useCreateConversation()
const { initModel } = useInitModel()
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
const handleKeyDown = async ( const handleKeyDown = async (
@ -35,7 +32,6 @@ const BasicPromptInput: React.FC = () => {
} }
await requestCreateConvo(selectedModel) await requestCreateConvo(selectedModel)
await initModel(selectedModel)
sendChatMessage() sendChatMessage()
} }
} }
@ -68,7 +64,7 @@ const BasicPromptInput: React.FC = () => {
} }
return ( return (
<div className="overflow-hidden rounded-lg shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-indigo-600"> <div className=" border-border rounded-lg border shadow-sm">
<textarea <textarea
ref={textareaRef} ref={textareaRef}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@ -76,7 +72,7 @@ const BasicPromptInput: React.FC = () => {
onChange={handleMessageChange} onChange={handleMessageChange}
name="comment" name="comment"
id="comment" id="comment"
className="block w-full resize-none border-0 bg-transparent py-1.5 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6" className="text-background-reverse block w-full resize-none border-0 bg-transparent py-1.5 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="Message ..." placeholder="Message ..."
rows={1} rows={1}
style={{ overflow: 'auto' }} style={{ overflow: 'auto' }}

View File

@ -1,56 +1,58 @@
import { activeBotAtom } from "@/_helpers/atoms/Bot.atom"; import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import { import {
MainViewState, MainViewState,
setMainViewStateAtom, setMainViewStateAtom,
} from "@/_helpers/atoms/MainView.atom"; } from '@helpers/atoms/MainView.atom'
import useCreateConversation from "@/_hooks/useCreateConversation"; import useCreateConversation from '@hooks/useCreateConversation'
import useDeleteBot from "@/_hooks/useDeleteBot"; import useDeleteBot from '@hooks/useDeleteBot'
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from 'jotai'
import React from "react"; import React from 'react'
import PrimaryButton from "../PrimaryButton"; import PrimaryButton from '../PrimaryButton'
import ExpandableHeader from "../ExpandableHeader"; import ExpandableHeader from '../ExpandableHeader'
const BotInfo: React.FC = () => { const BotInfo: React.FC = () => {
const { deleteBot } = useDeleteBot(); const { deleteBot } = useDeleteBot()
const { createConvoByBot } = useCreateConversation(); const { createConvoByBot } = useCreateConversation()
const setMainView = useSetAtom(setMainViewStateAtom); const setMainView = useSetAtom(setMainViewStateAtom)
const botInfo = useAtomValue(activeBotAtom); const botInfo = useAtomValue(activeBotAtom)
if (!botInfo) return null; if (!botInfo) return null
const onNewChatClicked = () => { const onNewChatClicked = () => {
if (!botInfo) { if (!botInfo) {
alert("No bot selected"); alert('No bot selected')
return; return
} }
createConvoByBot(botInfo); createConvoByBot(botInfo)
}; }
const onDeleteBotClick = async () => { const onDeleteBotClick = async () => {
// TODO: display confirmation diaglog // TODO: display confirmation diaglog
const result = await deleteBot(botInfo._id); const result = await deleteBot(botInfo._id)
if (result === "success") { if (result === 'success') {
setMainView(MainViewState.Welcome); setMainView(MainViewState.Welcome)
} }
}; }
return ( return (
<div className="flex flex-col gap-2 mx-1 my-1"> <div className="mx-1 my-1 flex flex-col gap-2">
<ExpandableHeader title="BOT INFO" expanded={true} onClick={() => {}} /> <ExpandableHeader title="BOT INFO" />
<div className="flex flex-col"> <div className="flex flex-col">
<label>{botInfo.name}</label> <label className="mb-2">{botInfo.name}</label>
<PrimaryButton onClick={onNewChatClicked} title="New chat" /> <span className="text-muted-foreground">{botInfo.description}</span>
<span>{botInfo.description}</span>
</div> </div>
<PrimaryButton <div className="flex w-full flex-col space-y-2">
title="Delete bot" <PrimaryButton onClick={onNewChatClicked} title="New chat" />
onClick={onDeleteBotClick} <PrimaryButton
className="bg-red-500 hover:bg-red-400" title="Delete bot"
/> onClick={onDeleteBotClick}
className="bg-red-500 hover:bg-red-400"
/>
</div>
</div> </div>
); )
}; }
export default BotInfo; export default BotInfo

View File

@ -1,14 +1,14 @@
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom' import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import React from 'react' import React from 'react'
import Avatar from '../Avatar' import Avatar from '../Avatar'
import PrimaryButton from '../PrimaryButton' import PrimaryButton from '../PrimaryButton'
import useCreateConversation from '@/_hooks/useCreateConversation' import useCreateConversation from '@hooks/useCreateConversation'
import useDeleteBot from '@/_hooks/useDeleteBot' import useDeleteBot from '@hooks/useDeleteBot'
import { import {
setMainViewStateAtom, setMainViewStateAtom,
MainViewState, MainViewState,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
const BotInfoContainer: React.FC = () => { const BotInfoContainer: React.FC = () => {
const activeBot = useAtomValue(activeBotAtom) const activeBot = useAtomValue(activeBotAtom)
@ -44,9 +44,7 @@ const BotInfoContainer: React.FC = () => {
<div className="flex h-full w-full pt-4"> <div className="flex h-full w-full pt-4">
<div className="mx-auto flex w-[672px] min-w-max flex-col gap-4"> <div className="mx-auto flex w-[672px] min-w-max flex-col gap-4">
<Avatar /> <Avatar />
<h1 className="text-center text-2xl font-bold"> <h1 className="text-center text-2xl font-bold">{activeBot?.name}</h1>
{activeBot?.name}
</h1>
<div className="flex gap-4"> <div className="flex gap-4">
<PrimaryButton <PrimaryButton
fullWidth fullWidth
@ -55,7 +53,7 @@ const BotInfoContainer: React.FC = () => {
/> />
<PrimaryButton <PrimaryButton
fullWidth fullWidth
className='bg-red-500 hover:bg-red-400' className="bg-red-500 hover:bg-red-400"
title="Delete bot" title="Delete bot"
onClick={onDeleteBotClick} onClick={onDeleteBotClick}
/> />

View File

@ -1,14 +1,14 @@
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom' import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import { showingBotListModalAtom } from '@/_helpers/atoms/Modal.atom' import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
import useGetBots from '@/_hooks/useGetBots' import useGetBots from '@hooks/useGetBots'
import { Bot } from '@/_models/Bot'
import { useAtom, useSetAtom } from 'jotai' import { useAtom, useSetAtom } from 'jotai'
import { rightSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import Avatar from '../Avatar' import Avatar from '../Avatar'
import { import {
MainViewState, MainViewState,
setMainViewStateAtom, setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
const BotListContainer: React.FC = () => { const BotListContainer: React.FC = () => {
const [open, setOpen] = useAtom(showingBotListModalAtom) const [open, setOpen] = useAtom(showingBotListModalAtom)
@ -16,6 +16,7 @@ const BotListContainer: React.FC = () => {
const [activeBot, setActiveBot] = useAtom(activeBotAtom) const [activeBot, setActiveBot] = useAtom(activeBotAtom)
const [bots, setBots] = useState<Bot[]>([]) const [bots, setBots] = useState<Bot[]>([])
const { getAllBots } = useGetBots() const { getAllBots } = useGetBots()
const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom)
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@ -23,31 +24,34 @@ const BotListContainer: React.FC = () => {
setBots(res) setBots(res)
}) })
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]) }, [open])
const onBotSelected = (bot: Bot) => { const onBotSelected = (bot: Bot) => {
if (bot._id !== activeBot?._id) { if (bot._id !== activeBot?._id) {
setMainView(MainViewState.BotInfo) setMainView(MainViewState.BotInfo)
setActiveBot(bot) setActiveBot(bot)
setRightSideBarVisibility(true)
} }
setOpen(false) setOpen(false)
} }
return ( return (
<div className="overflow-hidden bg-white shadow sm:rounded-md"> <div className="bg-background/50 border-border overflow-hidden border sm:rounded-md">
<ul role="list" className="divide-y divide-gray-200"> <ul role="list" className="divide-y divide-gray-200">
{bots.map((bot) => ( {bots.map((bot, i) => (
<li <li
role="button" role="button"
key={bot._id} key={i}
className="flex gap-4 p-4 hover:bg-hover-light sm:px-6" className="flex items-center gap-4 p-4 hover:bg-hover-light sm:px-6"
onClick={() => onBotSelected(bot)} onClick={() => onBotSelected(bot)}
> >
<Avatar /> <Avatar />
<div className="flex flex-1 flex-col"> <div className="flex flex-1 flex-col">
<p className="line-clamp-1">{bot.name}</p> <p className="line-clamp-1">{bot.name}</p>
<p className="line-clamp-1 text-ellipsis">{bot._id}</p> <p className="text-muted-foreground mt-1 line-clamp-1 text-ellipsis">
{bot._id}
</p>
</div> </div>
</li> </li>
))} ))}

View File

@ -1,4 +1,4 @@
import { showingBotListModalAtom } from '@/_helpers/atoms/Modal.atom' import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
@ -19,10 +19,10 @@ const BotListModal: React.FC = () => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 z-40 h-full bg-gray-950/90 transition-opacity dark:backdrop-blur-sm" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 w-screen overflow-y-auto"> <div className="fixed inset-0 z-50 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
@ -33,8 +33,8 @@ const BotListModal: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6"> <Dialog.Panel className="border-border bg-background/90 relative transform overflow-hidden rounded-lg border px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<h1 className="mb-4 text-lg text-black font-bold">Your bots</h1> <h1 className="mb-4 font-bold">Your bots</h1>
<BotListContainer /> <BotListContainer />
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>

View File

@ -1,26 +1,26 @@
import Image from "next/image"; import Image from 'next/image'
const BotPreview: React.FC = () => { const BotPreview: React.FC = () => {
return ( return (
<div className="flex pb-2 flex-col border border-gray-400 min-h-[235px] gap-2 overflow-hidden rounded-lg"> <div className="flex min-h-[235px] flex-col gap-2 overflow-hidden rounded-lg border border-gray-400 pb-2">
<div className="flex items-center justify-center p-2 bg-gray-400"> <div className="flex items-center justify-center bg-gray-400 p-2">
<Image <Image
className="rounded-md" className="rounded-md"
src={ src={
"https://i.pinimg.com/564x/52/b1/6f/52b16f96f52221d48bea716795ccc89a.jpg" 'https://i.pinimg.com/564x/52/b1/6f/52b16f96f52221d48bea716795ccc89a.jpg'
} }
width={32} width={32}
height={32} height={32}
alt="" alt=""
/> />
</div> </div>
<div className="flex items-center text-xs text-gray-400 gap-1 px-1"> <div className="flex items-center gap-1 px-1 text-xs text-gray-400">
<div className="flex-grow mx-1 border-b border-gray-400"></div> <div className="mx-1 flex-grow border-b border-gray-400"></div>
Context cleared Context cleared
<div className="flex-grow mx-1 border-b border-gray-400"></div> <div className="mx-1 flex-grow border-b border-gray-400"></div>
</div> </div>
</div> </div>
); )
}; }
export default BotPreview; export default BotPreview

View File

@ -1,28 +1,26 @@
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom' import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import React, { useEffect, useState } from 'react' import React, { useState } from 'react'
import ExpandableHeader from '../ExpandableHeader' import ExpandableHeader from '../ExpandableHeader'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
import useUpdateBot from '@/_hooks/useUpdateBot' import useUpdateBot from '@hooks/useUpdateBot'
import ProgressSetting from '../ProgressSetting' import { formatTwoDigits } from '@utils/converter'
import { set } from 'react-hook-form'
const delayBeforeUpdateInMs = 1000 const delayBeforeUpdateInMs = 1000
const BotSetting: React.FC = () => { const BotSetting: React.FC = () => {
const activeBot = useAtomValue(activeBotAtom) const activeBot = useAtomValue(activeBotAtom)
const [temperature, setTemperature] = useState(0) const [temperature, setTemperature] = useState(
const [maxTokens, setMaxTokens] = useState(0) activeBot?.customTemperature ?? 0
const [frequencyPenalty, setFrequencyPenalty] = useState(0) )
const [presencePenalty, setPresencePenalty] = useState(0)
useEffect(() => { const [maxTokens, setMaxTokens] = useState(activeBot?.maxTokens ?? 0)
if (!activeBot) return const [frequencyPenalty, setFrequencyPenalty] = useState(
setMaxTokens(activeBot.maxTokens ?? 0) activeBot?.frequencyPenalty ?? 0
setTemperature(activeBot.customTemperature ?? 0) )
setFrequencyPenalty(activeBot.frequencyPenalty ?? 0) const [presencePenalty, setPresencePenalty] = useState(
setPresencePenalty(activeBot.presencePenalty ?? 0) activeBot?.presencePenalty ?? 0
}, [activeBot?._id]) )
const { updateBot } = useUpdateBot() const { updateBot } = useUpdateBot()
@ -60,71 +58,109 @@ const BotSetting: React.FC = () => {
return ( return (
<div className="my-3 flex flex-col"> <div className="my-3 flex flex-col">
<ExpandableHeader <ExpandableHeader title="BOT SETTINGS" />
title="BOT SETTINGS"
expanded={true}
onClick={() => {}}
/>
<div className="mx-2 mt-3 flex flex-shrink-0 flex-col gap-4"> <div className="mx-2 mt-3 flex flex-shrink-0 flex-col gap-4">
{/* System prompt */} {/* System prompt */}
<div> <div>
<label <label htmlFor="comment" className="block">
htmlFor="comment"
className="block text-sm font-medium leading-6 text-gray-900"
>
System prompt System prompt
</label> </label>
<div className="mt-2"> <div className="mt-1">
<textarea <textarea
rows={4} rows={4}
name="comment" name="comment"
id="comment" id="comment"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" className="bg-background/80 text-background-reverse ring-border placeholder:text-muted-foreground focus:ring-accent/50 block w-full resize-none rounded-md border-0 py-1.5 text-xs leading-relaxed shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset"
defaultValue={activeBot.systemPrompt} defaultValue={activeBot.systemPrompt}
onChange={(e) => debouncedSystemPrompt(e.target.value)} onChange={(e) => debouncedSystemPrompt(e.target.value)}
/> />
</div> </div>
</div> </div>
<ProgressSetting {/* TODO: clean up this code */}
title="Max tokens" {/* Max temp */}
min={0} <p>Max tokens</p>
max={4096} <div className="mt-2 flex items-center gap-2">
step={1} <input
value={maxTokens} className="flex-1"
onValueChanged={(value) => debouncedMaxToken(value)} type="range"
/> defaultValue={activeBot.maxTokens ?? 0}
min={0}
max={4096}
step={1}
onChange={(e) => {
const value = Number(e.target.value)
setMaxTokens(value)
debouncedMaxToken(value)
}}
/>
<span className="border-accent rounded-md border px-2 py-1">
{formatTwoDigits(maxTokens)}
</span>
</div>
<ProgressSetting <p>Frequency penalty</p>
min={0} <div className="mt-2 flex items-center gap-2">
max={1} <input
step={0.01} className="flex-1"
title="Temperature" type="range"
value={temperature} defaultValue={activeBot.frequencyPenalty ?? 0}
onValueChanged={(value) => debouncedTemperature(value)} min={0}
/> max={1}
step={0.01}
onChange={(e) => {
const value = Number(e.target.value)
setFrequencyPenalty(value)
debouncedFreqPenalty(value)
}}
/>
<span className="border-accent rounded-md border px-2 py-1">
{formatTwoDigits(frequencyPenalty)}
</span>
</div>
<ProgressSetting <p>Presence penalty</p>
title="Frequency penalty" <div className="mt-2 flex items-center gap-2">
value={frequencyPenalty} <input
min={0} className="flex-1"
max={1} type="range"
step={0.01} defaultValue={activeBot.maxTokens ?? 0}
onValueChanged={(value) => debouncedFreqPenalty(value)} min={0}
/> max={1}
step={0.01}
onChange={(e) => {
const value = Number(e.target.value)
setPresencePenalty(value)
debouncedPresencePenalty(value)
}}
/>
<span className="border-accent rounded-md border px-2 py-1">
{formatTwoDigits(presencePenalty)}
</span>
</div>
<ProgressSetting {/* Custom temp */}
min={0} <p>Temperature</p>
max={1} <div className="mt-2 flex items-center gap-2">
step={0.01} <input
title="Presence penalty" className="flex-1"
value={presencePenalty} type="range"
onValueChanged={(value) => { id="volume"
setPresencePenalty(value) name="volume"
debouncedPresencePenalty(value) defaultValue={activeBot.customTemperature ?? 0}
}} min="0"
/> max="1"
step="0.01"
onChange={(e) => {
const newTemp = Number(e.target.value)
setTemperature(newTemp)
debouncedTemperature(Number(e.target.value))
}}
/>
<span className="border-accent rounded-md border px-2 py-1">
{formatTwoDigits(temperature)}
</span>
</div>
</div> </div>
</div> </div>
) )

View File

@ -1,12 +1,12 @@
import React from "react"; import React from 'react'
import MainHeader from "../MainHeader"; import MainHeader from '../MainHeader'
import MainView from "../MainView"; import MainView from '../MainView'
const CenterContainer: React.FC = () => ( const CenterContainer: React.FC = () => (
<div className="flex-1 flex flex-col"> <div className="flex flex-1 flex-col dark:bg-gray-950/50">
<MainHeader /> <MainHeader />
<MainView /> <MainView />
</div> </div>
); )
export default React.memo(CenterContainer); export default React.memo(CenterContainer)

View File

@ -1,61 +1,17 @@
'use client' 'use client'
import React, { useCallback, useRef, useState, useEffect } from 'react' import React from 'react'
import ChatItem from '../ChatItem' import ChatItem from '../ChatItem'
import { ChatMessage } from '@/_models/ChatMessage' import useChatMessages from '@hooks/useChatMessages'
import useChatMessages from '@/_hooks/useChatMessages'
import { useAtomValue } from 'jotai'
import { selectAtom } from 'jotai/utils'
import { getActiveConvoIdAtom } from '@/_helpers/atoms/Conversation.atom'
import { chatMessages } from '@/_helpers/atoms/ChatMessage.atom'
const ChatBody: React.FC = () => { const ChatBody: React.FC = () => {
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? '' const { messages } = useChatMessages()
const messageList = useAtomValue(
selectAtom(
chatMessages,
useCallback((v) => v[activeConversationId], [activeConversationId])
)
)
const [content, setContent] = useState<React.JSX.Element[]>([])
const [offset, setOffset] = useState(0)
const { loading, hasMore } = useChatMessages(offset)
const intersectObs = useRef<any>(null)
const lastPostRef = useCallback(
(message: ChatMessage) => {
if (loading) return
if (intersectObs.current) intersectObs.current.disconnect()
intersectObs.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setOffset((prevOffset) => prevOffset + 5)
}
})
if (message) intersectObs.current.observe(message)
},
[loading, hasMore]
)
useEffect(() => {
const list = messageList?.map((message, index) => {
if (messageList?.length === index + 1) {
return (
// @ts-ignore
<ChatItem ref={lastPostRef} message={message} key={message.id} />
)
}
return <ChatItem message={message} key={message.id} />
})
setContent(list)
}, [messageList, lastPostRef])
return ( return (
<div className="scroll flex flex-1 flex-col-reverse overflow-y-auto py-4"> <div className="flex h-full flex-1 flex-col-reverse overflow-y-auto [&>*:nth-child(odd)]:bg-background">
{content} {messages.map((message) => (
<ChatItem message={message} key={message.id} />
))}
</div> </div>
) )
} }

View File

@ -1,7 +1,8 @@
import SimpleControlNetMessage from '../SimpleControlNetMessage' import SimpleControlNetMessage from '../SimpleControlNetMessage'
import SimpleImageMessage from '../SimpleImageMessage' import SimpleImageMessage from '../SimpleImageMessage'
import SimpleTextMessage from '../SimpleTextMessage' import SimpleTextMessage from '../SimpleTextMessage'
import { ChatMessage, MessageType } from '@/_models/ChatMessage' import { ChatMessage, MessageType } from '@models/ChatMessage'
export default function renderChatMessage({ export default function renderChatMessage({
id, id,

View File

@ -1,7 +1,6 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import React, { forwardRef } from 'react' import React, { forwardRef } from 'react'
import renderChatMessage from '../ChatBody/renderChatMessage' import renderChatMessage from '../ChatBody/renderChatMessage'
import { ChatMessage } from '@/_models/ChatMessage'
type Props = { type Props = {
message: ChatMessage message: ChatMessage

View File

@ -1,16 +0,0 @@
import React from 'react'
import JanImage from '../JanImage'
import { useSetAtom } from 'jotai'
import { setActiveConvoIdAtom } from '@/_helpers/atoms/Conversation.atom'
const CompactLogo: React.FC = () => {
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
return (
<button onClick={() => setActiveConvoId(undefined)}>
<JanImage imageUrl="icons/app_icon.svg" width={28} height={28} />
</button>
)
}
export default React.memo(CompactLogo)

View File

@ -1,5 +1,5 @@
import { showConfirmDeleteConversationModalAtom } from '@/_helpers/atoms/Modal.atom' import { showConfirmDeleteConversationModalAtom } from '@helpers/atoms/Modal.atom'
import useDeleteConversation from '@/_hooks/useDeleteConversation' import useDeleteConversation from '@hooks/useDeleteConversation'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline' import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
@ -31,10 +31,10 @@ const ConfirmDeleteConversationModal: React.FC = () => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 z-40 h-full bg-gray-950/90 transition-opacity dark:backdrop-blur-sm" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto"> <div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
@ -45,7 +45,7 @@ const ConfirmDeleteConversationModal: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6"> <Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-border bg-background/90 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div className="sm:flex sm:items-start"> <div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"> <div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationTriangleIcon <ExclamationTriangleIcon
@ -54,14 +54,11 @@ const ConfirmDeleteConversationModal: React.FC = () => {
/> />
</div> </div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left"> <div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title <Dialog.Title as="h3" className="font-semibold leading-6">
as="h3"
className="text-base font-semibold leading-6 text-gray-900"
>
Delete Conversation Delete Conversation
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-muted-foreground">
Are you sure you want to delete this conversation? All Are you sure you want to delete this conversation? All
of messages will be permanently removed. This action of messages will be permanently removed. This action
cannot be undone. cannot be undone.
@ -72,14 +69,14 @@ const ConfirmDeleteConversationModal: React.FC = () => {
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> <div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button <button
type="button" type="button"
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto" className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-xs font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
onClick={() => onConfirmDelete()} onClick={() => onConfirmDelete()}
> >
Delete Delete
</button> </button>
<button <button
type="button" type="button"
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto" className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-xs font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
onClick={() => setShow(false)} onClick={() => setShow(false)}
ref={cancelButtonRef} ref={cancelButtonRef}
> >

View File

@ -2,7 +2,7 @@ import React, { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline' import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { showConfirmDeleteModalAtom } from '@/_helpers/atoms/Modal.atom' import { showConfirmDeleteModalAtom } from '@helpers/atoms/Modal.atom'
const ConfirmDeleteModelModal: React.FC = () => { const ConfirmDeleteModelModal: React.FC = () => {
const [show, setShow] = useAtom(showConfirmDeleteModalAtom) const [show, setShow] = useAtom(showConfirmDeleteModalAtom)

View File

@ -2,8 +2,8 @@ import React, { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline' import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import useSignOut from '@/_hooks/useSignOut' import useSignOut from '@hooks/useSignOut'
import { showConfirmSignOutModalAtom } from '@/_helpers/atoms/Modal.atom' import { showConfirmSignOutModalAtom } from '@helpers/atoms/Modal.atom'
const ConfirmSignOutModal: React.FC = () => { const ConfirmSignOutModal: React.FC = () => {
const [show, setShow] = useAtom(showConfirmSignOutModalAtom) const [show, setShow] = useAtom(showConfirmSignOutModalAtom)

View File

@ -1,8 +1,7 @@
import React from 'react' import React from 'react'
import Image from 'next/image' import Image from 'next/image'
import useCreateConversation from '@/_hooks/useCreateConversation' import useCreateConversation from '@hooks/useCreateConversation'
import { AssistantModel } from '@/_models/AssistantModel' import { PlayIcon } from '@heroicons/react/24/outline'
import { PlayIcon } from "@heroicons/react/24/outline"
type Props = { type Props = {
model: AssistantModel model: AssistantModel

View File

@ -1,4 +1,3 @@
import { AssistantModel } from '@/_models/AssistantModel'
import ConversationalCard from '../ConversationalCard' import ConversationalCard from '../ConversationalCard'
import { ChatBubbleBottomCenterTextIcon } from '@heroicons/react/24/outline' import { ChatBubbleBottomCenterTextIcon } from '@heroicons/react/24/outline'
@ -15,7 +14,7 @@ const ConversationalList: React.FC<Props> = ({ models }) => (
</span> </span>
</div> </div>
<div className="scroll mt-2 flex w-full gap-2 overflow-hidden overflow-x-scroll pl-6"> <div className="scroll mt-2 flex w-full gap-2 overflow-hidden overflow-x-scroll pl-6">
{models.map((item) => ( {models?.map((item) => (
<ConversationalCard key={item._id} model={item} /> <ConversationalCard key={item._id} model={item} />
))} ))}
</div> </div>

View File

@ -5,35 +5,39 @@ import DropdownBox from '../DropdownBox'
import PrimaryButton from '../PrimaryButton' import PrimaryButton from '../PrimaryButton'
import ToggleSwitch from '../ToggleSwitch' import ToggleSwitch from '../ToggleSwitch'
import CreateBotPromptInput from '../CreateBotPromptInput' import CreateBotPromptInput from '../CreateBotPromptInput'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import { Bot } from '@/_models/Bot'
import { SubmitHandler, useForm } from 'react-hook-form' import { SubmitHandler, useForm } from 'react-hook-form'
import Avatar from '../Avatar' import Avatar from '../Avatar'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import DraggableProgressBar from '../DraggableProgressBar' import DraggableProgressBar from '../DraggableProgressBar'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom' import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import {
rightSideBarExpandStateAtom,
} from '@helpers/atoms/SideBarExpand.atom'
import { import {
MainViewState, MainViewState,
setMainViewStateAtom, setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager'
import { DataService } from '@janhq/core' import { DataService } from '@janhq/core'
import { executeSerial } from '@services/pluginService'
const CreateBotContainer: React.FC = () => { const CreateBotContainer: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
const setActiveBot = useSetAtom(activeBotAtom) const setActiveBot = useSetAtom(activeBotAtom)
const setMainViewState = useSetAtom(setMainViewStateAtom) const setMainViewState = useSetAtom(setMainViewStateAtom)
const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom)
const createBot = async (bot: Bot) => { const createBot = async (bot: Bot) => {
try { try {
await executeSerial(DataService.CreateBot, bot).then(async () => { await executeSerial(DataService.CreateBot, bot)
setActiveBot(bot)
setMainViewState(MainViewState.BotInfo)
})
} catch (err) { } catch (err) {
alert(err) alert(err)
console.error(err) console.error(err)
} finally {
setMainViewState(MainViewState.BotInfo)
setActiveBot(bot)
setRightSideBarVisibility(true)
} }
} }
@ -73,9 +77,10 @@ const CreateBotContainer: React.FC = () => {
createBot(bot) createBot(bot)
} }
let models = downloadedModels.map((model) => { let models = downloadedModels.map((model: { _id: any }) => {
return model._id return model._id
}) })
models = ['Select a model', ...models] models = ['Select a model', ...models]
return ( return (
@ -84,7 +89,7 @@ const CreateBotContainer: React.FC = () => {
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
> >
<div className="mx-6 mt-3 flex items-center justify-between gap-3"> <div className="mx-6 mt-3 flex items-center justify-between gap-3">
<span className="text-3xl font-bold text-gray-900">Create Bot</span> <span className="text-lg font-bold">Create Bot</span>
<div className="flex gap-3"> <div className="flex gap-3">
<PrimaryButton isSubmit title="Create" /> <PrimaryButton isSubmit title="Create" />
</div> </div>
@ -108,7 +113,7 @@ const CreateBotContainer: React.FC = () => {
control={control} control={control}
/> />
<div className="flex flex-col gap-4 pb-2"> <div className="flex flex-col pb-2">
<DropdownBox <DropdownBox
id="modelId" id="modelId"
title="Model" title="Model"
@ -116,30 +121,23 @@ const CreateBotContainer: React.FC = () => {
control={control} control={control}
required={true} required={true}
/> />
</div>
<CreateBotPromptInput <CreateBotPromptInput id="systemPrompt" control={control} required />
id="systemPrompt"
<div className="flex flex-col gap-0.5">
<label className="block">Bot access</label>
<span className="mb-4 mt-1 text-muted-foreground">
If this setting is enabled, the bot will be added to your profile
and will be publicly accessible. Turning this off will make the
bot private.
</span>
<ToggleSwitch
id="publiclyAccessible"
title="Bot publicly accessible"
control={control} control={control}
required
/> />
<div className="flex flex-col gap-0.5">
<label className="block text-base font-bold text-gray-900">
Bot access
</label>
<span className="pb-2 text-sm text-[#737d7d]">
If this setting is enabled, the bot will be added to your
profile and will be publicly accessible. Turning this off will
make the bot private.
</span>
<ToggleSwitch
id="publiclyAccessible"
title="Bot publicly accessible"
control={control}
/>
</div>
<p>Max tokens</p>
<DraggableProgressBar <DraggableProgressBar
id="maxTokens" id="maxTokens"
control={control} control={control}

View File

@ -0,0 +1,98 @@
import React, { Fragment, useState } from 'react'
import ToggleSwitch from '../ToggleSwitch'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
import CutomBotTemperature from '../CustomBotTemperature'
import DraggableProgressBar from '../DraggableProgressBar'
type Props = {
control?: any
}
const CreateBotInAdvance: React.FC<Props> = ({ control }) => {
const [showAdvanced, setShowAdvanced] = useState(true)
const handleShowAdvanced = (e: React.MouseEvent<HTMLButtonElement>) => {
setShowAdvanced(!showAdvanced)
}
return (
<div className="flex flex-col gap-2">
<div className="flex items-start">
<button
className="mb-2 flex items-center justify-center font-bold"
onClick={handleShowAdvanced}
>
Advanced
{showAdvanced ? (
<ChevronDownIcon width={16} className="ml-2" />
) : (
<ChevronUpIcon width={16} className="ml-2" />
)}
</button>
</div>
{showAdvanced && (
<>
<div>
<p className="text-bold">Max tokens</p>
<DraggableProgressBar
id="maxTokens"
control={control}
min={0}
max={4096}
step={1}
/>
</div>
<div>
<p className="text-bold">Custom temperature</p>
<DraggableProgressBar
id="customTemperature"
control={control}
min={0}
max={1}
step={0.01}
/>
</div>
<div>
<p className="text-bold">Frequency penalty</p>
<DraggableProgressBar
id="frequencyPenalty"
control={control}
min={0}
max={1}
step={0.01}
/>
</div>
<div>
<p className="text-bold">Presence penalty</p>
<DraggableProgressBar
id="presencePenalty"
control={control}
min={0}
max={1}
step={0.01}
/>
</div>
</>
)}
{/* {showAdvanced && (
<Fragment>
<ToggleSwitch
id="suggestReplies"
title="Suggest replies"
control={control}
/>
<ToggleSwitch
id="renderMarkdownContent"
title="Render markdown content"
control={control}
/>
<CutomBotTemperature control={control} />
</Fragment>
)} */}
</div>
)
}
export default CreateBotInAdvance

View File

@ -1,30 +1,27 @@
import React, { Fragment, use } from "react"; import React, { Fragment, use } from 'react'
import ToggleSwitch from "../ToggleSwitch"; import ToggleSwitch from '../ToggleSwitch'
import { useController } from "react-hook-form"; import { useController } from 'react-hook-form'
type Props = { type Props = {
id: string; id: string
control?: any; control?: any
required?: boolean; required?: boolean
}; }
const CreateBotPromptInput: React.FC<Props> = ({ id, control, required }) => { const CreateBotPromptInput: React.FC<Props> = ({ id, control, required }) => {
const { field } = useController({ const { field } = useController({
name: id, name: id,
control: control, control: control,
rules: { required: required }, rules: { required: required },
}); })
return ( return (
<Fragment> <Fragment>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label <label htmlFor="comment" className="block font-bold ">
htmlFor="comment"
className="block text-base text-gray-900 font-bold"
>
Prompt Prompt
</label> </label>
<p className="text-sm text-gray-400 font-normal"> <p className="mt-1 font-normal text-gray-400">
All conversations with this bot will start with your prompt but it All conversations with this bot will start with your prompt but it
will not be visible to the user in the chat. If you would like the will not be visible to the user in the chat. If you would like the
prompt message to be visible to the user, consider using an intro prompt message to be visible to the user, consider using an intro
@ -32,18 +29,18 @@ const CreateBotPromptInput: React.FC<Props> = ({ id, control, required }) => {
</p> </p>
<ToggleSwitch <ToggleSwitch
id="visibleFromBotProfile" id="visibleFromBotProfile"
title={"Prompt visible from bot profile"} title={'Prompt visible from bot profile'}
control={control} control={control}
/> />
<textarea <textarea
rows={4} rows={4}
className="block w-full resize-none rounded-md border-0 py-1.5 bg-transparent shadow-sm ring-1 ring-inset text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" className="block w-full resize-none rounded-md border-0 bg-background/80 py-1.5 text-xs leading-relaxed text-background-reverse shadow-sm ring-1 ring-inset ring-border placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-accent/50"
placeholder="Talk to me like a pirate" placeholder="Talk to me like a pirate"
{...field} {...field}
/> />
</div> </div>
</Fragment> </Fragment>
); )
}; }
export default CreateBotPromptInput; export default CreateBotPromptInput

View File

@ -1,10 +1,10 @@
import ToggleSwitch from "../ToggleSwitch"; import ToggleSwitch from '../ToggleSwitch'
import DraggableProgressBar from "../DraggableProgressBar"; import DraggableProgressBar from '../DraggableProgressBar'
import { Controller } from "react-hook-form"; import { Controller } from 'react-hook-form'
type Props = { type Props = {
control?: any; control?: any
}; }
const CutomBotTemperature: React.FC<Props> = ({ control }) => ( const CutomBotTemperature: React.FC<Props> = ({ control }) => (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -13,23 +13,29 @@ const CutomBotTemperature: React.FC<Props> = ({ control }) => (
title="Custom temperature" title="Custom temperature"
control={control} control={control}
/> />
<div className="text-gray-500 mt-1 text-[0.8em]"> <div className="mt-1 text-[0.8em] text-gray-500">
Controls the creativity of the bot&apos;s responses. Higher values produce more {`Controls the creativity of the bot's responses. Higher values produce more
varied but unpredictable replies, lower values generate more consistent varied but unpredictable replies, lower values generate more consistent
responses. responses.`}
</div> </div>
<span className="text-gray-900">default: 0.7</span> <span className="text-gray-900">default: 0.7</span>
<Controller <Controller
name="enableCustomTemperature" name="enableCustomTemperature"
control={control} control={control}
render={({ field: { value } }) => { render={({ field: { value } }) => {
if (!value) return <div />; if (!value) return <div />
return ( return (
<DraggableProgressBar id="customTemperature" control={control} min={0} max={1} step={0.01} /> <DraggableProgressBar
); id="customTemperature"
control={control}
min={0}
max={1}
step={0.01}
/>
)
}} }}
/> />
</div> </div>
); )
export default CutomBotTemperature; export default CutomBotTemperature

View File

@ -1,4 +1,3 @@
import { AssistantModel } from '@/_models/AssistantModel'
import DownloadModelContent from '../DownloadModelContent' import DownloadModelContent from '../DownloadModelContent'
type Props = { type Props = {

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import SearchBar from '../SearchBar' import SearchBar from '../SearchBar'
import ModelTable from '../ModelTable' import ModelTable from '../ModelTable'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
const DownloadedModelTable: React.FC = () => { const DownloadedModelTable: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
@ -9,11 +9,11 @@ const DownloadedModelTable: React.FC = () => {
if (!downloadedModels || downloadedModels.length === 0) return null if (!downloadedModels || downloadedModels.length === 0) return null
return ( return (
<div className="pl-[63px] pr-[89px]"> <div className="mt-5">
<h3 className="mt-[50px] text-xl leading-[25px]">Downloaded Models</h3> {/* <h3 className="mt-[50px] text-xl leading-[25px]">Downloaded Models</h3> */}
<div className="w-[568px] py-5"> {/* <div className="w-[568px] py-5">
<SearchBar /> <SearchBar />
</div> </div> */}
<ModelTable models={downloadedModels} /> <ModelTable models={downloadedModels} />
</div> </div>
) )

View File

@ -1,8 +1,7 @@
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import ModelDownloadingTable from '../ModelDownloadingTable' import ModelDownloadingTable from '../ModelDownloadingTable'
import { DownloadState } from '@/_models/DownloadState'
const DownloadingModelTable: React.FC = () => { const DownloadingModelTable: React.FC = () => {
const modelDownloadState = useAtomValue(modelDownloadStateAtom) const modelDownloadState = useAtomValue(modelDownloadStateAtom)

View File

@ -1,23 +1,29 @@
import { formatTwoDigits } from "@/_utils/converter"; import { formatTwoDigits } from '@utils/converter'
import React from "react"; import React from 'react'
import { Controller, useController } from "react-hook-form"; import { Controller, useController } from 'react-hook-form'
type Props = { type Props = {
id: string; id: string
control: any; control: any
min: number; min: number
max: number; max: number
step: number; step: number
}; }
const DraggableProgressBar: React.FC<Props> = ({ id, control, min, max, step }) => { const DraggableProgressBar: React.FC<Props> = ({
id,
control,
min,
max,
step,
}) => {
const { field } = useController({ const { field } = useController({
name: id, name: id,
control: control, control: control,
}); })
return ( return (
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2">
<input <input
{...field} {...field}
className="flex-1" className="flex-1"
@ -30,13 +36,13 @@ const DraggableProgressBar: React.FC<Props> = ({ id, control, min, max, step })
name={id} name={id}
control={control} control={control}
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<span className="border border-[#737d7d] rounded-md py-1 px-2 text-gray-900"> <span className="rounded-md border border-border px-2 py-1 text-accent">
{formatTwoDigits(value)} {formatTwoDigits(value)}
</span> </span>
)} )}
/> />
</div> </div>
); )
}; }
export default DraggableProgressBar; export default DraggableProgressBar

View File

@ -24,9 +24,9 @@ const DropdownBox: React.FC<Props> = ({
return ( return (
<Fragment> <Fragment>
<label className="block text-base font-bold text-gray-900">{title}</label> <label className="block font-bold">{title}</label>
<select <select
className="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" className="bg-background/80 ring-border focus:ring-accent/50 mt-1 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-xs ring-1 ring-inset focus:ring-2 sm:leading-6"
{...field} {...field}
> >
{data.map((option) => ( {data.map((option) => (

View File

@ -2,22 +2,11 @@ import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
type Props = { type Props = {
title: string title: string
expanded: boolean
onClick: () => void
} }
const ExpandableHeader: React.FC<Props> = ({ title, expanded, onClick }) => ( const ExpandableHeader: React.FC<Props> = ({ title }) => (
<button onClick={onClick} className="flex items-center justify-between px-2"> <button className="flex items-center justify-between">
<h2 className="pl-1 text-xs font-bold leading-[12px] text-gray-400"> <h2 className="text-muted-foreground pl-1 font-bold">{title}</h2>
{title}
</h2>
<div className="mr-2">
{expanded ? (
<ChevronDownIcon width={12} height={12} color="#6B7280" />
) : (
<ChevronUpIcon width={12} height={12} color="#6B7280" />
)}
</div>
</button> </button>
) )

View File

@ -1,5 +1,5 @@
import HeaderTitle from '../HeaderTitle' import HeaderTitle from '../HeaderTitle'
import ExploreModelList from '../ExploreModelList' import ExploreModelList from '../../../screens/ExploreModels/ExploreModelList'
import ExploreModelFilter from '../ExploreModelFilter' import ExploreModelFilter from '../ExploreModelFilter'
const ExploreModelContainer: React.FC = () => ( const ExploreModelContainer: React.FC = () => (

View File

@ -16,7 +16,7 @@ const tags = [
const checkboxs = ['GGUF', 'TensorRT', 'Meow', 'JigglyPuff'] const checkboxs = ['GGUF', 'TensorRT', 'Meow', 'JigglyPuff']
const ExploreModelFilter: React.FC = () => { const ExploreModelFilter: React.FC = () => {
const enabled = false const enabled = true
if (!enabled) return null if (!enabled) return null
return ( return (

View File

@ -3,6 +3,7 @@
'use client' 'use client'
import ExploreModelItemHeader from '../ExploreModelItemHeader' import ExploreModelItemHeader from '../ExploreModelItemHeader'
import { Button } from '@uikit'
import ModelVersionList from '../ModelVersionList' import ModelVersionList from '../ModelVersionList'
import { Fragment, forwardRef, useEffect, useState } from 'react' import { Fragment, forwardRef, useEffect, useState } from 'react'
import SimpleTag from '../SimpleTag' import SimpleTag from '../SimpleTag'
@ -14,10 +15,9 @@ import {
UsecaseTag, UsecaseTag,
VersionTag, VersionTag,
} from '@/_components/SimpleTag/TagType' } from '@/_components/SimpleTag/TagType'
import { displayDate } from '@/_utils/datetime' import { displayDate } from '@utils/datetime'
import { Product } from '@/_models/Product' import useGetMostSuitableModelVersion from '@hooks/useGetMostSuitableModelVersion'
import useGetMostSuitableModelVersion from '@/_hooks/useGetMostSuitableModelVersion' import { toGigabytes } from '@utils/converter'
import { toGigabytes } from '@/_utils/converter'
type Props = { type Props = {
model: Product model: Product
@ -32,6 +32,7 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
useEffect(() => { useEffect(() => {
getMostSuitableModelVersion(availableVersions) getMostSuitableModelVersion(availableVersions)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [availableVersions]) }, [availableVersions])
if (!suitableModel) { if (!suitableModel) {
@ -43,25 +44,30 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
return ( return (
<div <div
ref={ref} ref={ref}
className="mb-4 flex flex-col rounded-md border border-gray-200" className="border-border bg-background/60 mb-4 flex flex-col rounded-md border"
> >
<ExploreModelItemHeader <ExploreModelItemHeader
suitableModel={suitableModel} suitableModel={suitableModel}
exploreModel={model} exploreModel={model}
/> />
<div className="flex flex-col px-[26px] py-[22px]"> <div className="flex flex-col p-4">
<div className="mb-4 flex flex-col gap-1">
<span className="font-semibold">About</span>
<span className="text-muted-foreground leading-relaxed">
{model.longDescription}
</span>
</div>
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex flex-1 flex-col gap-8"> <div className="flex flex-1 flex-col gap-y-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="text-sm font-medium text-gray-500"> <div className="font-semibold">Release Date</div>
Release Date <p className="text-muted-foreground mt-1">
</div>
<div className="text-sm font-normal text-gray-900">
{displayDate(model.releaseDate)} {displayDate(model.releaseDate)}
</div> </p>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="text-sm font-medium text-gray-500">Version</div> <div className="font-semibold">Version</div>
<div className="flex gap-2"> <div className="flex gap-2">
<SimpleTag <SimpleTag
title={model.version} title={model.version}
@ -81,17 +87,14 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-1 flex-col gap-8">
<div className="flex flex-1 flex-col gap-y-4">
<div> <div>
<div className="text-sm font-medium text-gray-500">Author</div> <div className="font-semibold">Author</div>
<div className="text-sm font-normal text-gray-900"> <p className="text-muted-foreground mt-1">{model.author}</p>
{model.author}
</div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="text-sm font-medium text-gray-500"> <div className="font-semibold">Compatibility</div>
Compatibility
</div>
<div className="flex gap-2"> <div className="flex gap-2">
<SimpleTag <SimpleTag
title={usecase} title={usecase}
@ -106,44 +109,42 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
</div> </div>
</div> </div>
</div> </div>
</div>
<div className="mt-[26px] flex flex-col gap-1"> <div className="flex flex-1 flex-col gap-y-4">
<span className="text-sm font-medium text-gray-500">About</span> <div>
<span className="text-sm font-normal text-gray-500"> <div className="font-medium">Tags</div>
{model.longDescription} <div className="mt-1 flex flex-wrap gap-2">
</span> {model.tags.map((tag) => (
</div> <SimpleTag
<div className="mt-5 flex flex-col gap-2"> key={tag}
<span className="text-sm font-medium text-gray-500">Tags</span> title={tag}
<div className="flex flex-wrap gap-2"> type={MiscellanousTag.MiscellanousDefault}
{model.tags.map((tag) => ( clickable={false}
<SimpleTag />
key={tag} ))}
title={tag} </div>
type={MiscellanousTag.MiscellanousDefault} </div>
clickable={false}
/>
))}
</div> </div>
</div> </div>
{model.availableVersions?.length > 0 && (
<div className="border-border bg-background mt-5 w-full rounded-md border p-2">
<button onClick={() => setShow(!show)} className="w-full">
{!show
? '+ Show Available Versions'
: '- Collapse Available Versions'}
</button>
{show && (
<ModelVersionList
model={model}
versions={model.availableVersions}
recommendedVersion={suitableModel?._id ?? ''}
/>
)}
</div>
)}
</div> </div>
{model.availableVersions?.length > 0 && (
<Fragment>
{show && (
<ModelVersionList
model={model}
versions={model.availableVersions}
recommendedVersion={suitableModel?._id ?? ''}
/>
)}
<button
onClick={() => setShow(!show)}
className="border-t border-gray-200 bg-[#FBFBFB] px-4 py-2 text-left text-sm text-gray-500"
>
{!show ? '+ Show Available Versions' : '- Collapse'}
</button>
</Fragment>
)}
</div> </div>
) )
}) })

View File

@ -1,19 +1,18 @@
import SimpleTag from '../SimpleTag' import SimpleTag from '../SimpleTag'
import PrimaryButton from '../PrimaryButton' import PrimaryButton from '../PrimaryButton'
import { formatDownloadPercentage, toGigabytes } from '@/_utils/converter' import { formatDownloadPercentage, toGigabytes } from '@utils/converter'
import SecondaryButton from '../SecondaryButton' import SecondaryButton from '../SecondaryButton'
import { Product } from '@/_models/Product'
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo } from 'react'
import { ModelVersion } from '@/_models/ModelVersion' import useGetPerformanceTag from '@hooks/useGetPerformanceTag'
import useGetPerformanceTag from '@/_hooks/useGetPerformanceTag' import useDownloadModel from '@hooks/useDownloadModel'
import useDownloadModel from '@/_hooks/useDownloadModel' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom'
import { atom, useAtomValue, useSetAtom } from 'jotai' import { atom, useAtomValue, useSetAtom } from 'jotai'
import { Button } from '@uikit'
import { import {
MainViewState, MainViewState,
setMainViewStateAtom, setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
type Props = { type Props = {
suitableModel: ModelVersion suitableModel: ModelVersion
@ -37,35 +36,36 @@ const ExploreModelItemHeader: React.FC<Props> = ({
useEffect(() => { useEffect(() => {
getPerformanceForModel(suitableModel) getPerformanceForModel(suitableModel)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [suitableModel]) }, [suitableModel])
const onDownloadClick = useCallback(() => { const onDownloadClick = useCallback(() => {
downloadModel(exploreModel, suitableModel) downloadModel(exploreModel, suitableModel)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [exploreModel, suitableModel]) }, [exploreModel, suitableModel])
const isDownloaded = const isDownloaded =
downloadedModels.find((model) => model._id === suitableModel._id) != null downloadedModels.find((model) => model._id === suitableModel._id) != null
let downloadButton = ( let downloadButton = (
<PrimaryButton <Button themes="accent" onClick={() => onDownloadClick()}>
title={ {suitableModel.size
suitableModel.size ? `Download (${toGigabytes(suitableModel.size)})`
? `Download (${toGigabytes(suitableModel.size)})` : 'Download'}
: 'Download' </Button>
}
onClick={() => onDownloadClick()}
/>
) )
if (isDownloaded) { if (isDownloaded) {
downloadButton = ( downloadButton = (
<PrimaryButton <Button
title="View Downloaded Model" size="sm"
themes="accent"
onClick={() => { onClick={() => {
setMainViewState(MainViewState.MyModel) setMainViewState(MainViewState.MyModel)
}} }}
className="bg-green-500 hover:bg-green-400" >
/> View Downloaded Model
</Button>
) )
} }
@ -82,7 +82,7 @@ const ExploreModelItemHeader: React.FC<Props> = ({
} }
return ( return (
<div className="flex items-center justify-between border-b border-gray-200 p-4"> <div className="border-border bg-background/50 flex items-center justify-between rounded-t-md border-b px-4 py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>{exploreModel.name}</span> <span>{exploreModel.name}</span>
{performanceTag && ( {performanceTag && (

View File

@ -1,28 +0,0 @@
import React, { useEffect } from 'react'
import ExploreModelItem from '../ExploreModelItem'
import { getConfiguredModels } from '@/_hooks/useGetDownloadedModels'
import useGetConfiguredModels from '@/_hooks/useGetConfiguredModels'
import { Waveform } from '@uiball/loaders'
const ExploreModelList: React.FC = () => {
const { loading, models } = useGetConfiguredModels()
useEffect(() => {
getConfiguredModels()
}, [])
return (
<div className="scroll flex flex-1 flex-col overflow-y-auto">
{loading && (
<div className="mx-auto">
<Waveform size={24} color="#CBD5E0" />
</div>
)}
{models.map((item) => (
<ExploreModelItem key={item._id} model={item} />
))}
</div>
)
}
export default ExploreModelList

View File

@ -1,23 +0,0 @@
import Image from 'next/image'
import Link from 'next/link'
// DEPRECATED
export default function Footer() {
return (
<div className="container m-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<Image src={'icons/app_icon.svg'} width={32} height={32} alt="" />
<span>Jan</span>
</div>
<div className="my-6 flex gap-4">
<Link href="/privacy" className="cursor-pointer">
Privacy
</Link>
<span>&#8226;</span>
<Link href="/support" className="cursor-pointer">
Support
</Link>
</div>
</div>
)
}

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { showingMobilePaneAtom } from '@/_helpers/atoms/Modal.atom' import { showingMobilePaneAtom } from '@helpers/atoms/Modal.atom'
import { Bars3Icon } from '@heroicons/react/24/outline' import { Bars3Icon } from '@heroicons/react/24/outline'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import React from 'react' import React from 'react'

View File

@ -1,21 +1,18 @@
import React from 'react' import React from 'react'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import Image from 'next/image'
import { Conversation } from '@/_models/Conversation'
import { ModelManagementService } from '@janhq/core' import { ModelManagementService } from '@janhq/core'
import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager'
import { import {
getActiveConvoIdAtom, getActiveConvoIdAtom,
setActiveConvoIdAtom, setActiveConvoIdAtom,
updateConversationErrorAtom,
updateConversationWaitingForResponseAtom, updateConversationWaitingForResponseAtom,
} from '@/_helpers/atoms/Conversation.atom' } from '@helpers/atoms/Conversation.atom'
import { import {
setMainViewStateAtom, setMainViewStateAtom,
MainViewState, MainViewState,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
import useInitModel from '@/_hooks/useInitModel' import { displayDate } from '@utils/datetime'
import { displayDate } from '@/_utils/datetime' import { twMerge } from 'tailwind-merge'
import { executeSerial } from '@services/pluginService'
type Props = { type Props = {
conversation: Conversation conversation: Conversation
@ -35,14 +32,9 @@ const HistoryItem: React.FC<Props> = ({
const setMainViewState = useSetAtom(setMainViewStateAtom) const setMainViewState = useSetAtom(setMainViewStateAtom)
const activeConvoId = useAtomValue(getActiveConvoIdAtom) const activeConvoId = useAtomValue(getActiveConvoIdAtom)
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom) const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
const updateConvWaiting = useSetAtom( const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
updateConversationWaitingForResponseAtom
)
const updateConvError = useSetAtom(updateConversationErrorAtom)
const isSelected = activeConvoId === conversation._id const isSelected = activeConvoId === conversation._id
const { initModel } = useInitModel()
const onClick = async () => { const onClick = async () => {
const model = await executeSerial( const model = await executeSerial(
ModelManagementService.GetModelById, ModelManagementService.GetModelById,
@ -50,55 +42,38 @@ const HistoryItem: React.FC<Props> = ({
) )
if (conversation._id) updateConvWaiting(conversation._id, true) if (conversation._id) updateConvWaiting(conversation._id, true)
initModel(model).then((res: any) => {
if (conversation._id) updateConvWaiting(conversation._id, false)
if (res?.error && conversation._id) {
updateConvError(conversation._id, res.error)
}
})
if (activeConvoId !== conversation._id) { if (activeConvoId !== conversation._id) {
setMainViewState(MainViewState.Conversation) setMainViewState(MainViewState.Conversation)
setActiveConvoId(conversation._id) setActiveConvoId(conversation._id)
} }
}; }
const backgroundColor = isSelected const backgroundColor = isSelected ? 'bg-background/80' : 'bg-background/20'
? "bg-gray-100 dark:bg-gray-700" const description = conversation?.lastMessage ?? 'No new message'
: "bg-white dark:bg-gray-500"
const description = conversation?.lastMessage ?? "No new message"
return ( return (
<li <li
role="button" role="button"
className={`ml-3 mr-2 flex flex-row rounded p-3 ${backgroundColor} hover:bg-hover-light`} className={twMerge(
'flex flex-row rounded-md border border-border p-3',
backgroundColor
)}
onClick={onClick} onClick={onClick}
> >
<div className="h-8 w-8"> <div className="flex flex-1 flex-col">
<Image
width={32}
height={32}
src={avatarUrl ?? 'icons/app_icon.svg'}
className="aspect-square rounded-full"
alt=""
/>
</div>
<div className="ml-2 flex flex-1 flex-col">
{/* title */} {/* title */}
<div className="flex">
<span className="line-clamp-1 flex-1 text-gray-900"> <span className="mb-1 line-clamp-1 leading-5 text-muted-foreground">
{summary ?? name} {updatedAt && displayDate(new Date(updatedAt).getTime())}
</span> </span>
<span className="line-clamp-1 text-xs leading-5 text-gray-500">
{updatedAt && displayDate(new Date(updatedAt).getTime())} <span className="line-clamp-1">{summary ?? name}</span>
</span>
</div>
{/* description */} {/* description */}
<span className="mt-1 line-clamp-2 text-gray-400">{description}</span> <span className="mt-1 line-clamp-2 text-muted-foreground">
{description}
</span>
</div> </div>
</li> </li>
) )

View File

@ -2,15 +2,15 @@ import HistoryItem from '../HistoryItem'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import ExpandableHeader from '../ExpandableHeader' import ExpandableHeader from '../ExpandableHeader'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { searchAtom } from '@/_helpers/JotaiWrapper' import { searchAtom } from '@helpers/JotaiWrapper'
import useGetUserConversations from '@/_hooks/useGetUserConversations' import useGetUserConversations from '@hooks/useGetUserConversations'
import SidebarEmptyHistory from '../SidebarEmptyHistory' import SidebarEmptyHistory from '../SidebarEmptyHistory'
import { userConversationsAtom } from '@/_helpers/atoms/Conversation.atom' import { userConversationsAtom } from '@helpers/atoms/Conversation.atom'
import { twMerge } from 'tailwind-merge'
const HistoryList: React.FC = () => { const HistoryList: React.FC = () => {
const conversations = useAtomValue(userConversationsAtom) const conversations = useAtomValue(userConversationsAtom)
const searchText = useAtomValue(searchAtom) const searchText = useAtomValue(searchAtom)
const [expand, setExpand] = useState<boolean>(true)
const { getUserConversations } = useGetUserConversations() const { getUserConversations } = useGetUserConversations()
useEffect(() => { useEffect(() => {
@ -18,17 +18,9 @@ const HistoryList: React.FC = () => {
}, []) }, [])
return ( return (
<div className="flex flex-grow flex-col gap-2 overflow-hidden pt-3"> <div className="flex flex-grow flex-col gap-2">
<ExpandableHeader <ExpandableHeader title="CHAT HISTORY" />
title="CHAT HISTORY" <ul className={twMerge('mt-1 flex flex-col gap-y-3 overflow-y-auto')}>
expanded={expand}
onClick={() => setExpand(!expand)}
/>
<ul
className={`scroll mt-1 flex flex-col gap-1 overflow-y-auto ${
!expand ? 'hidden ' : 'block'
}`}
>
{conversations.length > 0 ? ( {conversations.length > 0 ? (
conversations conversations
.filter( .filter(

View File

@ -1,24 +1,23 @@
/* eslint-disable react-hooks/rules-of-hooks */
'use client' 'use client'
import BasicPromptInput from '../BasicPromptInput' import BasicPromptInput from '../BasicPromptInput'
import BasicPromptAccessories from '../BasicPromptAccessories' import BasicPromptAccessories from '../BasicPromptAccessories'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { showingAdvancedPromptAtom } from '@/_helpers/atoms/Modal.atom'
import SecondaryButton from '../SecondaryButton' import SecondaryButton from '../SecondaryButton'
import { Fragment, useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { PlusIcon } from '@heroicons/react/24/outline' import { PlusIcon } from '@heroicons/react/24/outline'
import useCreateConversation from '@/_hooks/useCreateConversation' import useCreateConversation from '@hooks/useCreateConversation'
import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom' import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
import { import {
currentConversationAtom, currentConversationAtom,
currentConvoStateAtom, currentConvoStateAtom,
} from '@/_helpers/atoms/Conversation.atom' } from '@helpers/atoms/Conversation.atom'
import useGetBots from '@/_hooks/useGetBots' import useGetBots from '@hooks/useGetBots'
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom' import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
const InputToolbar: React.FC = () => { const InputToolbar: React.FC = () => {
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom)
const activeModel = useAtomValue(activeAssistantModelAtom) const activeModel = useAtomValue(activeAssistantModelAtom)
const { requestCreateConvo } = useCreateConversation() const { requestCreateConvo } = useCreateConversation()
const currentConvoState = useAtomValue(currentConvoStateAtom) const currentConvoState = useAtomValue(currentConvoStateAtom)
@ -62,6 +61,7 @@ const InputToolbar: React.FC = () => {
} }
} }
getReplyState() getReplyState()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentConvo]) }, [currentConvo])
const onNewConversationClick = () => { const onNewConversationClick = () => {
@ -70,26 +70,19 @@ const InputToolbar: React.FC = () => {
} }
} }
if (showingAdvancedPrompt) { if (inputState === 'loading') return <div>Loading..</div>
return <div />
}
if (inputState === 'loading') {
return <div>Loading..</div>
}
if (inputState === 'disabled') {
// text italic
if (inputState === 'disabled')
return ( return (
<p className="mx-auto my-5 line-clamp-2 text-ellipsis text-center text-sm italic text-gray-600"> <div className="sticky bottom-0 flex items-center justify-center bg-background/90">
{error} <p className="mx-auto my-5 line-clamp-2 text-ellipsis text-center italic text-gray-600">
</p> {error}
</p>
</div>
) )
}
return ( return (
<Fragment> <div className="sticky bottom-0 w-full bg-background/90 px-5 py-0">
{currentConvoState?.error && ( {currentConvoState?.error && (
<div className="flex flex-row justify-center"> <div className="flex flex-row justify-center">
<span className="mx-5 my-2 text-sm text-red-500"> <span className="mx-5 my-2 text-sm text-red-500">
@ -105,13 +98,13 @@ const InputToolbar: React.FC = () => {
/> />
</div> </div>
{/* My text input */} {/* My text input */}
<div className="mx-12 mb-5 flex items-start space-x-4 md:mx-32 2xl:mx-64"> <div className="mb-5 flex items-start space-x-4">
<div className="relative min-w-0 flex-1"> <div className="relative min-w-0 flex-1">
<BasicPromptInput /> <BasicPromptInput />
<BasicPromptAccessories /> <BasicPromptAccessories />
</div> </div>
</div> </div>
</Fragment> </div>
) )
} }

View File

@ -1,9 +1,9 @@
import React, { Fragment } from "react" import React, { Fragment } from 'react'
import HistoryList from "../HistoryList" import HistoryList from '../HistoryList'
import LeftHeaderAction from "../LeftHeaderAction" import LeftHeaderAction from '../LeftHeaderAction'
import { leftSideBarExpandStateAtom } from "@/_helpers/atoms/LeftSideBarExpand.atom" import { leftSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom'
import { useAtomValue } from "jotai" import { useAtomValue } from 'jotai'
import { Variants, motion } from "framer-motion" import { Variants, motion } from 'framer-motion'
const leftSideBarVariants: Variants = { const leftSideBarVariants: Variants = {
show: { show: {
@ -13,7 +13,7 @@ const leftSideBarVariants: Variants = {
transition: { duration: 0.3 }, transition: { duration: 0.3 },
}, },
hide: { hide: {
x: "-100%", x: '-100%',
width: 0, width: 0,
opacity: 0, opacity: 0,
transition: { duration: 0.3 }, transition: { duration: 0.3 },
@ -26,13 +26,13 @@ const LeftContainer: React.FC = () => {
return ( return (
<motion.div <motion.div
initial={false} initial={false}
animate={isVisible ? "show" : "hide"} animate={isVisible ? 'show' : 'hide'}
variants={leftSideBarVariants} variants={leftSideBarVariants}
className="flex flex-col w-80 flex-shrink-0 border-r border-gray-200" className="flex w-80 flex-shrink-0 flex-col dark:bg-gray-950/50"
> >
{isVisible && ( {isVisible && (
<Fragment> <Fragment>
<LeftHeaderAction /> {/* <LeftHeaderAction /> */}
<HistoryList /> <HistoryList />
</Fragment> </Fragment>
)} )}

View File

@ -1,47 +1,47 @@
"use client"; 'use client'
import React from "react"; import React from 'react'
import SecondaryButton from "../SecondaryButton"; import SecondaryButton from '../SecondaryButton'
import { useSetAtom } from "jotai"; import { useSetAtom } from 'jotai'
import { import {
MainViewState, MainViewState,
setMainViewStateAtom, setMainViewStateAtom,
} from "@/_helpers/atoms/MainView.atom"; } from '@helpers/atoms/MainView.atom'
import { MagnifyingGlassIcon, PlusIcon } from "@heroicons/react/24/outline"; import { MagnifyingGlassIcon, PlusIcon } from '@heroicons/react/24/outline'
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels"; import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
const LeftHeaderAction: React.FC = () => { const LeftHeaderAction: React.FC = () => {
const setMainView = useSetAtom(setMainViewStateAtom); const setMainView = useSetAtom(setMainViewStateAtom)
const { downloadedModels } = useGetDownloadedModels(); const { downloadedModels } = useGetDownloadedModels()
const onExploreClick = () => { const onExploreClick = () => {
setMainView(MainViewState.ExploreModel); setMainView(MainViewState.ExploreModel)
}; }
const onCreateBotClicked = () => { const onCreateBotClicked = () => {
if (downloadedModels.length === 0) { if (downloadedModels.length === 0) {
alert("You need to download at least one model to create a bot."); alert('You need to download at least one model to create a bot.')
return; return
} }
setMainView(MainViewState.CreateBot); setMainView(MainViewState.CreateBot)
}; }
return ( return (
<div className="flex flex-row gap-2 mx-3 my-3"> <div className="sticky top-0 mb-4 flex flex-row gap-2">
<SecondaryButton <SecondaryButton
title={"Explore"} title={'Explore'}
onClick={onExploreClick} onClick={onExploreClick}
className="flex-1" className="flex-1"
icon={<MagnifyingGlassIcon width={16} height={16} />} icon={<MagnifyingGlassIcon width={16} height={16} />}
/> />
<SecondaryButton <SecondaryButton
title={"Create bot"} title={'Create bot'}
onClick={onCreateBotClicked} onClick={onCreateBotClicked}
className="flex-1" className="flex-1"
icon={<PlusIcon width={16} height={16} />} icon={<PlusIcon width={16} height={16} />}
/> />
</div> </div>
); )
}; }
export default React.memo(LeftHeaderAction); export default React.memo(LeftHeaderAction)

View File

@ -2,9 +2,9 @@ import {
MainViewState, MainViewState,
getMainViewStateAtom, getMainViewStateAtom,
setMainViewStateAtom, setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
import Image from 'next/image'
import CompactLogo from '../CompactLogo' import CompactLogo from '../../../containers/Logo/CompactLogo'
import { import {
ChatBubbleOvalLeftEllipsisIcon, ChatBubbleOvalLeftEllipsisIcon,
Cog8ToothIcon, Cog8ToothIcon,
@ -13,24 +13,26 @@ import {
Squares2X2Icon, Squares2X2Icon,
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { showingBotListModalAtom } from '@/_helpers/atoms/Modal.atom' import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import useGetBots from '@/_hooks/useGetBots' import useGetBots from '@hooks/useGetBots'
import { Icons, Toggle } from '@uikit'
const menu = [ const menu = [
{ // {
name: 'Explore Models', // name: 'Explore Models',
iconComponent: <CpuChipIcon width={24} height={24} color="#C7D2FE" />, // icon: <CpuChipIcon />,
state: MainViewState.ExploreModel, // state: MainViewState.ExploreModel,
}, // },
{ {
name: 'My Models', name: 'My Models',
iconComponent: <Squares2X2Icon width={24} height={24} color="#C7D2FE" />, icon: <Icons name="layout-grid" />,
state: MainViewState.MyModel, state: MainViewState.MyModel,
}, },
{ {
name: 'Settings', name: 'Settings',
iconComponent: <Cog8ToothIcon width={24} height={24} color="#C7D2FE" />, icon: <Icons name="settings" />,
state: MainViewState.Setting, state: MainViewState.Setting,
}, },
] ]
@ -51,13 +53,13 @@ const LeftRibbonNav: React.FC = () => {
const bgColor = isConversationView ? 'bg-gray-500' : '' const bgColor = isConversationView ? 'bg-gray-500' : ''
const onConversationClick = () => { const onConversationClick = () => {
if (currentState === MainViewState.Conversation) return // if (currentState === MainViewState.Conversation) return
setMainViewState(MainViewState.Conversation) setMainViewState(MainViewState.Conversation)
} }
const onBotListClick = async () => { const onBotListClick = async () => {
const bots = await getAllBots() const bots = await getAllBots()
if (bots.length === 0) { if (bots?.length === 0) {
alert('You have no bot') alert('You have no bot')
return return
} }
@ -71,52 +73,42 @@ const LeftRibbonNav: React.FC = () => {
} }
return ( return (
<nav className="flex h-screen w-20 flex-col bg-gray-900"> <nav className="flex h-screen flex-shrink-0 flex-col items-center pt-10">
<div className='mx-auto mt-4'>
<CompactLogo /> <CompactLogo />
</div> <div className="flex w-full flex-1 flex-col items-center justify-between">
<div className="flex flex-col pt-4">
<div className="flex w-full flex-1 flex-col items-center justify-between px-3 py-6"> <button onClick={onConversationClick}>
<div className="flex flex-1 flex-col gap-2">
<button
onClick={onConversationClick}
className={`rounded-lg p-4 ${bgColor} hover:bg-gray-400`}
>
<ChatBubbleOvalLeftEllipsisIcon <ChatBubbleOvalLeftEllipsisIcon
width={24} width={24}
height={24} height={24}
color="#C7D2FE" color="text-white"
/> />
</button> </button>
<button <button onClick={onBotListClick}>
onClick={onBotListClick} <CubeTransparentIcon />
className={`rounded-lg p-4 hover:bg-gray-400`}
>
<CubeTransparentIcon width={24} height={24} color="#C7D2FE" />
</button> </button>
</div> </div>
<ul className="flex flex-col gap-3"> <ul className="flex flex-col gap-3 py-8">
{menu.map((item) => { {menu.map((item) => {
const bgColor = currentState === item.state ? 'bg-gray-500' : '' const bgColor = currentState === item.state ? 'bg-gray-500' : ''
return ( return (
<li <li
role="button" role="button"
data-testid={item.name}
key={item.name} key={item.name}
className={`rounded-lg p-4 ${bgColor} hover:bg-gray-400`} className="item-center flex gap-x-2"
onClick={() => onMenuClick(item.state)} onClick={() => onMenuClick(item.state)}
> >
{item.iconComponent} {item.icon}
<span className="text-xs">{item.name}</span>
</li> </li>
) )
})} })}
</ul> </ul>
</div> </div>
{/* User avatar */} {/* User avatar */}
<div className="flex items-center justify-center pb-5"> {/* <div className="pb-5 flex items-center justify-center">
<Image src={'/icons/avatar.svg'} width={40} height={40} alt="" /> <Image src={"/icons/avatar.svg"} width={40} height={40} alt="" />
</div> </div> */}
</nav> </nav>
) )
} }

View File

@ -1,5 +1,5 @@
import ChatBody from "../ChatBody"; import ChatBody from '../ChatBody'
import InputToolbar from "../InputToolbar"; import InputToolbar from '../InputToolbar'
const MainChat: React.FC = () => ( const MainChat: React.FC = () => (
<div className="flex h-full w-full flex-col"> <div className="flex h-full w-full flex-col">

View File

@ -1,21 +1,19 @@
import React from "react" import React from 'react'
import LeftContainer from "../LeftContainer" import LeftContainer from '../LeftContainer'
import LeftRibbonNav from "../LeftRibbonNav" import LeftRibbonNav from '../LeftRibbonNav'
import MonitorBar from "../MonitorBar" import MonitorBar from '../MonitorBar'
import RightContainer from "../RightContainer" import RightContainer from '../RightContainer'
import CenterContainer from "../CenterContainer" import CenterContainer from '../CenterContainer'
const MainContainer: React.FC = () => ( const MainContainer: React.FC = () => (
<div className="flex h-screen"> <div className="flex h-screen">
<LeftRibbonNav /> <LeftRibbonNav />
<div className="flex flex-1 flex-col ">
<div className="flex flex-1 flex-col h-full">
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
<LeftContainer /> <LeftContainer />
<CenterContainer /> <CenterContainer />
<RightContainer /> <RightContainer />
</div> </div>
<MonitorBar /> <MonitorBar />
</div> </div>
</div> </div>

View File

@ -1,19 +1,17 @@
import { currentConversationAtom } from '@/_helpers/atoms/Conversation.atom' import { currentConversationAtom } from '@helpers/atoms/Conversation.atom'
import { import {
leftSideBarExpandStateAtom, leftSideBarExpandStateAtom,
rightSideBarExpandStateAtom, rightSideBarExpandStateAtom,
showRightSideBarToggleAtom, } from '@helpers/atoms/SideBarExpand.atom'
} from '@/_helpers/atoms/LeftSideBarExpand.atom' import { showConfirmDeleteConversationModalAtom } from '@helpers/atoms/Modal.atom'
import { TrashIcon } from '@heroicons/react/24/outline' import { ChartPieIcon, TrashIcon } from '@heroicons/react/24/outline'
import { showConfirmDeleteConversationModalAtom } from '@/_helpers/atoms/Modal.atom'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import React from 'react' import React from 'react'
import Image from 'next/image' import { Trash2 } from 'lucide-react'
const MainHeader: React.FC = () => { const MainHeader: React.FC = () => {
const setLeftSideBarVisibility = useSetAtom(leftSideBarExpandStateAtom) const setLeftSideBarVisibility = useSetAtom(leftSideBarExpandStateAtom)
const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom) const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom)
const showRightSideBarToggle = useAtomValue(showRightSideBarToggleAtom)
const setShowConfirmDeleteConversationModal = useSetAtom( const setShowConfirmDeleteConversationModal = useSetAtom(
showConfirmDeleteConversationModalAtom showConfirmDeleteConversationModalAtom
) )
@ -22,43 +20,19 @@ const MainHeader: React.FC = () => {
const currentConvo = useAtomValue(currentConversationAtom) const currentConvo = useAtomValue(currentConversationAtom)
let title = currentConvo?.name ?? '' let title = currentConvo?.name ?? ''
return ( if (!activeConversation) return null
<div className="flex justify-between bg-gray-200 px-2 py-3">
<Image
role="button"
alt=""
src="icons/ic_sidebar_off.svg"
width={24}
onClick={() => setLeftSideBarVisibility((prev) => !prev)}
height={24}
/>
<span className="flex gap-0.5 text-base font-semibold leading-6"> return (
{title} <div className="sticky top-0 border-b border-border bg-background/90 px-4 py-2">
</span> <span className="font-semibold text-muted-foreground">{title}</span>
{/* right most */} {/* right most */}
<div className="flex gap-4"> <div className="absolute right-4 top-2">
{activeConversation != null && ( <Trash2
<TrashIcon role="button"
role="button" size={16}
width={24} onClick={() => setShowConfirmDeleteConversationModal(true)}
height={24} />
color="#9CA3AF"
onClick={() => setShowConfirmDeleteConversationModal(true)}
/>
)}
{showRightSideBarToggle && (
<Image
role="button"
alt=""
src="icons/ic_sidebar_off.svg"
width={24}
onClick={() => setRightSideBarVisibility((prev) => !prev)}
height={24}
/>
)}
</div> </div>
</div> </div>
) )

View File

@ -8,11 +8,10 @@ import ExploreModelContainer from '../ExploreModelContainer'
import { import {
MainViewState, MainViewState,
getMainViewStateAtom, getMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
import EmptyChatContainer from '../EmptyChatContainer' import EmptyChatContainer from '../EmptyChatContainer'
import MainChat from '../MainChat' import MainChat from '../MainChat'
import CreateBotContainer from '../CreateBotContainer' import CreateBotContainer from '../CreateBotContainer'
import BotInfoContainer from '../BotInfoContainer'
const MainView: React.FC = () => { const MainView: React.FC = () => {
const viewState = useAtomValue(getMainViewStateAtom) const viewState = useAtomValue(getMainViewStateAtom)
@ -38,9 +37,6 @@ const MainView: React.FC = () => {
case MainViewState.Welcome: case MainViewState.Welcome:
children = <Welcome /> children = <Welcome />
break break
case MainViewState.BotInfo:
children = <BotInfoContainer />
break
default: default:
children = <MainChat /> children = <MainChat />
break break

View File

@ -3,7 +3,7 @@ import { Popover, Transition } from '@headlessui/react'
import { Fragment } from 'react' import { Fragment } from 'react'
// import useGetCurrentUser from "@/_hooks/useGetCurrentUser"; // import useGetCurrentUser from "@/_hooks/useGetCurrentUser";
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { showConfirmSignOutModalAtom } from '@/_helpers/atoms/Modal.atom' import { showConfirmSignOutModalAtom } from '@helpers/atoms/Modal.atom'
export const MenuHeader: React.FC = () => { export const MenuHeader: React.FC = () => {
const setShowConfirmSignOutModal = useSetAtom(showConfirmSignOutModalAtom) const setShowConfirmSignOutModal = useSetAtom(showConfirmSignOutModalAtom)

View File

@ -3,7 +3,7 @@ import { Dialog } from '@headlessui/react'
import { XMarkIcon } from '@heroicons/react/24/outline' import { XMarkIcon } from '@heroicons/react/24/outline'
import Image from 'next/image' import Image from 'next/image'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { showingMobilePaneAtom } from '@/_helpers/atoms/Modal.atom' import { showingMobilePaneAtom } from '@helpers/atoms/Modal.atom'
const MobileMenuPane: React.FC = () => { const MobileMenuPane: React.FC = () => {
const [show, setShow] = useAtom(showingMobilePaneAtom) const [show, setShow] = useAtom(showingMobilePaneAtom)

View File

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PrimaryButton from '../PrimaryButton' import { Button } from '@uikit'
import ModelActionMenu from '../ModelActionMenu'
export enum ModelActionType { export enum ModelActionType {
Start = 'Start', Start = 'Start',
@ -8,41 +9,51 @@ export enum ModelActionType {
type ModelActionStyle = { type ModelActionStyle = {
title: string title: string
backgroundColor: string
textColor: string
} }
const modelActionMapper: Record<ModelActionType, ModelActionStyle> = { const modelActionMapper: Record<ModelActionType, ModelActionStyle> = {
[ModelActionType.Start]: { [ModelActionType.Start]: {
title: 'Start', title: 'Start',
backgroundColor: 'bg-blue-500 hover:bg-blue-600',
textColor: 'text-white',
}, },
[ModelActionType.Stop]: { [ModelActionType.Stop]: {
title: 'Stop', title: 'Stop',
backgroundColor: 'bg-red-500 hover:bg-red-600',
textColor: 'text-white',
}, },
} }
type Props = { type Props = {
disabled?: boolean
loading?: boolean
type: ModelActionType type: ModelActionType
onActionClick: (type: ModelActionType) => void onActionClick: (type: ModelActionType) => void
onDeleteClick: () => void
} }
const ModelActionButton: React.FC<Props> = ({ type, onActionClick }) => { const ModelActionButton: React.FC<Props> = ({
disabled,
loading,
type,
onActionClick,
onDeleteClick,
}) => {
const styles = modelActionMapper[type] const styles = modelActionMapper[type]
const onClick = () => { const onClick = () => {
onActionClick(type) onActionClick(type)
} }
return ( return (
<td className="whitespace-nowrap px-6 py-4 text-sm"> <td className="whitespace-nowrap px-3 py-2 text-right">
<PrimaryButton <div className="flex items-center justify-end gap-x-4">
title={styles.title} <ModelActionMenu onDeleteClick={onDeleteClick} />
onClick={onClick} <Button
className={styles.backgroundColor} disabled={disabled}
/> size="sm"
themes={styles.title === 'Start' ? 'accent' : 'default'}
onClick={() => onClick()}
loading={loading}
>
{styles.title} Model
</Button>
</div>
</td> </td>
) )
} }

View File

@ -7,36 +7,9 @@ type Props = {
} }
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => ( const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => (
<Menu as="div" className="relative flex-none"> <button className="text-muted-foreground text-xs" onClick={onDeleteClick}>
<Menu.Button className="block text-gray-500 hover:text-gray-900"> Delete
<span className="sr-only">Open options</span> </button>
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
<Menu.Item>
{({ active }) => (
<button
className={`${
active ? 'bg-violet-500 text-white' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={onDeleteClick}
>
Delete
</button>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
) )
export default ModelActionMenu export default ModelActionMenu

View File

@ -1,4 +1,4 @@
import { toGigabytes } from '@/_utils/converter' import { toGigabytes } from '@utils/converter'
type Props = { type Props = {
total: number total: number

View File

@ -1,10 +1,10 @@
import React from 'react' import React from 'react'
import { DownloadState } from '@/_models/DownloadState'
import { import {
formatDownloadPercentage, formatDownloadPercentage,
formatDownloadSpeed, formatDownloadSpeed,
toGigabytes, toGigabytes,
} from '@/_utils/converter' } from '@utils/converter'
type Props = { type Props = {
downloadState: DownloadState downloadState: DownloadState

View File

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import ModelTableHeader from '../ModelTableHeader' import ModelTableHeader from '../ModelTableHeader'
import { DownloadState } from '@/_models/DownloadState'
import ModelDownloadingRow from '../ModelDownloadingRow' import ModelDownloadingRow from '../ModelDownloadingRow'
type Props = { type Props = {

View File

@ -1,13 +1,11 @@
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { ModelStatus, ModelStatusComponent } from '../ModelStatusComponent' import { ModelStatus, ModelStatusComponent } from '../ModelStatusComponent'
import ModelActionMenu from '../ModelActionMenu'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import ModelActionButton, { ModelActionType } from '../ModelActionButton' import ModelActionButton, { ModelActionType } from '../ModelActionButton'
import useStartStopModel from '@/_hooks/useStartStopModel' import useStartStopModel from '@hooks/useStartStopModel'
import useDeleteModel from '@/_hooks/useDeleteModel' import useDeleteModel from '@hooks/useDeleteModel'
import { AssistantModel } from '@/_models/AssistantModel' import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom' import { toGigabytes } from '@utils/converter'
import { toGigabytes } from '@/_utils/converter'
type Props = { type Props = {
model: AssistantModel model: AssistantModel
@ -17,6 +15,7 @@ const ModelRow: React.FC<Props> = ({ model }) => {
const { startModel, stopModel } = useStartStopModel() const { startModel, stopModel } = useStartStopModel()
const activeModel = useAtomValue(activeAssistantModelAtom) const activeModel = useAtomValue(activeAssistantModelAtom)
const { deleteModel } = useDeleteModel() const { deleteModel } = useDeleteModel()
const { loading, model: currentModelState } = useAtomValue(stateModel)
let status = ModelStatus.Installed let status = ModelStatus.Installed
if (activeModel && activeModel._id === model._id) { if (activeModel && activeModel._id === model._id) {
@ -38,32 +37,33 @@ const ModelRow: React.FC<Props> = ({ model }) => {
const onDeleteClick = useCallback(() => { const onDeleteClick = useCallback(() => {
deleteModel(model) deleteModel(model)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [model]) }, [model])
return ( return (
<tr className="border-b border-gray-200 last:rounded-lg last:border-b-0"> <tr className="border-b border-border bg-background/50 last:rounded-lg last:border-b-0">
<td className="flex flex-col whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900"> <td className="whitespace-nowrap px-3 font-semibold text-muted-foreground">
{model.name} {model.name}
<span className="font-normal text-gray-500">{model.version}</span> <span className="ml-2 font-semibold">v{model.version}</span>
</td> </td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500"> <td className="whitespace-nowrap px-3 text-muted-foreground">
<div className="flex flex-col justify-start"> <div className="flex flex-col justify-start">
<span>GGUF</span> <span>GGUF</span>
</div> </div>
</td> </td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500"> <td className="whitespace-nowrap px-3 text-muted-foreground">
{toGigabytes(model.size)} {toGigabytes(model.size)}
</td> </td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500"> <td className="whitespace-nowrap px-3 text-muted-foreground">
<ModelStatusComponent status={status} /> <ModelStatusComponent status={status} />
</td> </td>
<ModelActionButton <ModelActionButton
disabled={loading}
loading={currentModelState === model._id ? loading : false}
type={actionButtonType} type={actionButtonType}
onActionClick={onModelActionClick} onActionClick={onModelActionClick}
onDeleteClick={onDeleteClick}
/> />
<td className="relative w-fit whitespace-nowrap px-6 py-4 text-right text-sm font-medium">
<ModelActionMenu onDeleteClick={onDeleteClick} />
</td>
</tr> </tr>
) )
} }

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { searchingModelText } from '@/_helpers/JotaiWrapper' import { searchingModelText } from '@helpers/JotaiWrapper'
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline' import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'

View File

@ -2,9 +2,8 @@ import { Fragment, useEffect } from 'react'
import { Listbox, Transition } from '@headlessui/react' import { Listbox, Transition } from '@headlessui/react'
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid' import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'
import { useAtom, useAtomValue } from 'jotai' import { useAtom, useAtomValue } from 'jotai'
import { selectedModelAtom } from '@/_helpers/atoms/Model.atom' import { selectedModelAtom } from '@helpers/atoms/Model.atom'
import { downloadedModelAtom } from '@/_helpers/atoms/DownloadedModel.atom' import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom'
import { AssistantModel } from '@/_models/AssistantModel'
function classNames(...classes: any) { function classNames(...classes: any) {
return classes.filter(Boolean).join(' ') return classes.filter(Boolean).join(' ')

View File

@ -16,17 +16,17 @@ export const ModelStatusMapper: Record<ModelStatus, ModelStatusType> = {
[ModelStatus.Installed]: { [ModelStatus.Installed]: {
title: 'Installed', title: 'Installed',
textColor: 'text-black', textColor: 'text-black',
backgroundColor: 'bg-gray-100', backgroundColor: 'bg-gray-200 text-gray-600',
}, },
[ModelStatus.Active]: { [ModelStatus.Active]: {
title: 'Active', title: 'Active',
textColor: 'text-black', textColor: 'text-green-800',
backgroundColor: 'bg-green-100', backgroundColor: 'bg-green-100 dark:bg-green-300 text-green-700',
}, },
[ModelStatus.RunningInNitro]: { [ModelStatus.RunningInNitro]: {
title: 'Running in Nitro', title: 'Running in Nitro',
textColor: 'text-black', textColor: 'text-green-800',
backgroundColor: 'bg-green-100', backgroundColor: 'bg-green-100 dark:bg-green-300 text-green-700',
}, },
} }
@ -38,7 +38,7 @@ export const ModelStatusComponent: React.FC<Props> = ({ status }) => {
const statusType = ModelStatusMapper[status] const statusType = ModelStatusMapper[status]
return ( return (
<div <div
className={`w-fit rounded-[10px] px-2.5 py-0.5 text-xs font-medium ${statusType.backgroundColor}`} className={`w-fit rounded-full px-2.5 py-0.5 text-xs font-medium ${statusType.backgroundColor}`}
> >
{statusType.title} {statusType.title}
</div> </div>

View File

@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import ModelRow from '../ModelRow' import ModelRow from '../ModelRow'
import ModelTableHeader from '../ModelTableHeader' import ModelTableHeader from '../ModelTableHeader'
import { AssistantModel } from '@/_models/AssistantModel'
type Props = { type Props = {
models: AssistantModel[] models: AssistantModel[]
@ -10,25 +9,25 @@ type Props = {
const tableHeaders = ['MODEL', 'FORMAT', 'SIZE', 'STATUS', 'ACTIONS'] const tableHeaders = ['MODEL', 'FORMAT', 'SIZE', 'STATUS', 'ACTIONS']
const ModelTable: React.FC<Props> = ({ models }) => ( const ModelTable: React.FC<Props> = ({ models }) => (
<div className="flow-root min-w-full rounded-lg border border-gray-200 align-middle shadow-lg"> <>
<table className="min-w-full"> <div className="border-border overflow-hidden rounded-lg border align-middle shadow-lg">
<thead className="border-b border-gray-200 bg-gray-50"> <table className="min-w-full">
<tr className="rounded-t-lg"> <thead className="bg-background">
{tableHeaders.map((item) => ( <tr className="rounded-t-lg">
<ModelTableHeader key={item} title={item} /> {tableHeaders.map((item) => (
<ModelTableHeader key={item} title={item} />
))}
</tr>
</thead>
<tbody>
{models?.map((model) => (
<ModelRow key={model._id} model={model} />
))} ))}
<th scope="col" className="relative w-fit px-6 py-3"> </tbody>
<span className="sr-only">Edit</span> </table>
</th> </div>
</tr> <div className="relative"></div>
</thead> </>
<tbody>
{models.map((model) => (
<ModelRow key={model._id} model={model} />
))}
</tbody>
</table>
</div>
) )
export default React.memo(ModelTable) export default React.memo(ModelTable)

View File

@ -7,7 +7,7 @@ type Props = {
const ModelTableHeader: React.FC<Props> = ({ title }) => ( const ModelTableHeader: React.FC<Props> = ({ title }) => (
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500 first:rounded-tl-lg last:rounded-tr-lg" className="text-muted-foreground border-border border-b p-3 text-left text-xs font-semibold uppercase first:rounded-tl-lg last:rounded-tr-lg last:text-right"
> >
{title} {title}
</th> </th>

View File

@ -1,12 +1,10 @@
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { formatDownloadPercentage, toGigabytes } from '@/_utils/converter' import { formatDownloadPercentage, toGigabytes } from '@utils/converter'
import Image from 'next/image' import Image from 'next/image'
import { Product } from '@/_models/Product' import useDownloadModel from '@hooks/useDownloadModel'
import useDownloadModel from '@/_hooks/useDownloadModel' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom'
import { atom, useAtomValue } from 'jotai' import { atom, useAtomValue } from 'jotai'
import { ModelVersion } from '@/_models/ModelVersion' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels'
import SimpleTag from '../SimpleTag' import SimpleTag from '../SimpleTag'
import { RamRequired, UsecaseTag } from '../SimpleTag/TagType' import { RamRequired, UsecaseTag } from '../SimpleTag/TagType'
@ -56,10 +54,9 @@ const ModelVersionItem: React.FC<Props> = ({
const { maxRamRequired, usecase } = modelVersion const { maxRamRequired, usecase } = modelVersion
return ( return (
<div className="flex items-center justify-between gap-4 border-t border-gray-200 pb-3 pl-3 pr-4 pt-3 first:border-t-0"> <div className="border-border flex items-center justify-between gap-4 border-t pb-3 pl-3 pr-4 pt-3 first:border-t-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Image src={'/icons/app_icon.svg'} width={14} height={20} alt="" /> <span className="font-sm text-muted-foreground mb-4 line-clamp-1 flex-1">
<span className="font-sm flex-1 text-gray-900">
{modelVersion.name} {modelVersion.name}
</span> </span>
</div> </div>
@ -75,9 +72,9 @@ const ModelVersionItem: React.FC<Props> = ({
type={RamRequired.RamDefault} type={RamRequired.RamDefault}
clickable={false} clickable={false}
/> />
</div> <div className="bg-background border-border rounded-full border px-2.5 py-0.5 font-medium">
<div className="rounded bg-gray-200 px-2.5 py-0.5 text-xs font-medium"> {toGigabytes(modelVersion.size)}
{toGigabytes(modelVersion.size)} </div>
</div> </div>
{downloadButton} {downloadButton}
</div> </div>

View File

@ -1,7 +1,5 @@
import React from 'react' import React from 'react'
import ModelVersionItem from '../ModelVersionItem' import ModelVersionItem from '../ModelVersionItem'
import { Product } from '@/_models/Product'
import { ModelVersion } from '@/_models/ModelVersion'
type Props = { type Props = {
model: Product model: Product
@ -15,20 +13,15 @@ const ModelVersionList: React.FC<Props> = ({
recommendedVersion, recommendedVersion,
}) => { }) => {
return ( return (
<div className="border-t border-gray-200 px-4 py-5"> <div className="pt-4">
<div className="text-sm font-medium text-gray-500"> {versions.map((item) => (
Available Versions <ModelVersionItem
</div> key={item._id}
<div className="overflow-hidden rounded-lg border border-gray-200"> model={model}
{versions.map((item) => ( modelVersion={item}
<ModelVersionItem isRecommended={item._id === recommendedVersion}
key={item._id} />
model={model} ))}
modelVersion={item}
isRecommended={item._id === recommendedVersion}
/>
))}
</div>
</div> </div>
) )
} }

View File

@ -1,13 +1,12 @@
import ProgressBar from '../ProgressBar' import ProgressBar from '@/_components/ProgressBar'
import SystemItem from '../SystemItem' import SystemItem from '@containers/SystemItem'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { appDownloadProgress } from '@/_helpers/JotaiWrapper' import { appDownloadProgress } from '@helpers/JotaiWrapper'
import useGetAppVersion from '@/_hooks/useGetAppVersion' import useGetAppVersion from '@hooks/useGetAppVersion'
import useGetSystemResources from '@/_hooks/useGetSystemResources' import useGetSystemResources from '@hooks/useGetSystemResources'
import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
import { DownloadState } from '@/_models/DownloadState' import { formatDownloadPercentage } from '@utils/converter'
import { formatDownloadPercentage } from '@/_utils/converter' import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom'
const MonitorBar: React.FC = () => { const MonitorBar: React.FC = () => {
const progress = useAtomValue(appDownloadProgress) const progress = useAtomValue(appDownloadProgress)
@ -22,7 +21,7 @@ const MonitorBar: React.FC = () => {
} }
return ( return (
<div className="flex flex-row items-center justify-between border-t border-gray-200"> <div className="flex flex-row items-center justify-between">
{progress && progress >= 0 ? ( {progress && progress >= 0 ? (
<ProgressBar total={100} used={progress} /> <ProgressBar total={100} used={progress} />
) : null} ) : null}
@ -40,7 +39,7 @@ const MonitorBar: React.FC = () => {
{activeModel && ( {activeModel && (
<SystemItem name={`Active model: ${activeModel.name}`} value={''} /> <SystemItem name={`Active model: ${activeModel.name}`} value={''} />
)} )}
<span className="text-sm text-gray-900">v{version}</span> <span className="text-xs">v{version}</span>
</div> </div>
</div> </div>
) )

Some files were not shown because too many files have changed in this diff Show More