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

View File

@ -15,7 +15,7 @@ on:
- "!plugins/*/package.json"
jobs:
build:
runs-on: mac-silicon
runs-on: macos-latest
environment: production
outputs:
branch_name: ${{ steps.commit_and_tag.outputs.branch_name }}
@ -27,7 +27,19 @@ jobs:
- name: Install jq
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
run: |
git config --global user.email "service@jan.ai"
@ -76,7 +88,7 @@ jobs:
do
echo $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
npm publish --access public
fi
@ -84,6 +96,8 @@ jobs:
done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
APP_PATH: "."
- name: "Commit new version to main and create 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.
*/
UpdateBot = "updateBot",
/**
* Gets the plugin manifest.
*/
GetPluginManifest = "getPluginManifest",
}
/**

View File

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

View File

@ -1,18 +1,21 @@
---
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
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
[Jan's Hacker House](https://jan.ai) is a 4-day event where we host an open AI Hacker House and invite the local AI community to join us. There is fast wifi, free snacks, drinks and pizza.
[Jan's Hacker House](https://jan.ai) is a 4-day event where we host an open AI Hacker House and invite the local AI community to join us. There is fast wifi, free snacks, drinks and pizza.
We also host a series of talks, workshops and social events at night. We usually start off the week with a "Intro to LLMs" that targets local university students, and then progress to more in-depth technical and research areas.
We also host a series of talks, workshops and social events at night. We usually start off the week with a "Intro to LLMs" that targets local university students, and then progress to more in-depth technical and research areas.
Jan is a fully remote team. We use the money we save from not having an office, to hold Hack Weeks where we meet in a city, eat pizza and work to ship major releases.
Jan is a fully remote team. We use the money we save from not having an office, to hold Hack Weeks where we meet in a city, eat pizza and work to ship major releases.
### Date & Time
@ -20,18 +23,18 @@ Jan is a fully remote team. We use the money we save from not having an office,
### Location
- Thao Dien, District 2, Ho Chi Minh City
- Exact location to be shared later
- Districts 1 & 3, Ho Chi Minh City
- Exact location in Evenbrite (see below)
## Agenda
To help us manage RSVPs, please use the Eventbrite links below to RSVP for each event.
| Day | Eventbrite Link | Signups |
| ------------ | ------------------------------------- | -------------------------------------------------------------------------------------------- |
| 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) |
| Fri (27 Oct) | Jan Launch Party + Build your own LLM | [RSVP here](https://jan-launch-party.eventbrite.sg/) |
| Day | Eventbrite Link | Signups |
| ------------ | ------------------------------------- | ---------------------------------------------------- |
| Mon (23 Oct) | Jan Team & Partners Dinner | Invite-only |
| 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/) |
### 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 | Documentation of Jan Codebase for Plugin Developers |
| 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
![](/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;
}
[class*="docMainContainer_"],
[class*="docSidebarContainer_"] {
table {
a {
@apply text-blue-600 dark:text-blue-400;
}
.theme-doc-markdown {
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 { resolve, join, extname } from "path";
import { rmdir, unlink, createWriteStream } from "fs";
@ -36,12 +43,30 @@ app.on("window-all-closed", () => {
app.quit();
});
ipcMain.handle("setNativeThemeLight", () => {
nativeTheme.themeSource = "light";
});
ipcMain.handle("setNativeThemeDark", () => {
nativeTheme.themeSource = "dark";
});
ipcMain.handle("setNativeThemeSystem", () => {
nativeTheme.themeSource = "system";
});
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
frame: false,
show: false,
backgroundColor: "white",
trafficLightPosition: {
x: 16,
y: 10,
},
titleBarStyle: "hidden",
vibrancy: "sidebar",
webPreferences: {
nodeIntegration: true,
preload: join(__dirname, "preload.js"),
@ -118,11 +143,13 @@ function handleIPCs() {
ipcMain.handle(
"invokePluginFunc",
async (_event, modulePath, method, ...args) => {
const module = require(/* webpackIgnore: true */ join(
app.getPath("userData"),
"plugins",
modulePath
));
const module = require(
/* webpackIgnore: true */ join(
app.getPath("userData"),
"plugins",
modulePath
)
);
requiredModules[modulePath] = module;
if (typeof module[method] === "function") {

View File

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

View File

@ -9,6 +9,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
invokePluginFunc: (plugin: any, method: any, ...args: any[]) =>
ipcRenderer.invoke("invokePluginFunc", plugin, method, ...args),
setNativeThemeLight: () => ipcRenderer.invoke("setNativeThemeLight"),
setNativeThemeDark: () => ipcRenderer.invoke("setNativeThemeDark"),
setNativeThemeSystem: () => ipcRenderer.invoke("setNativeThemeSystem"),
basePlugins: () => ipcRenderer.invoke("basePlugins"),
pluginPath: () => ipcRenderer.invoke("pluginPath"),
@ -23,19 +29,27 @@ contextBridge.exposeInMainWorld("electronAPI", {
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
const welcomeText = await page
.locator(".text-5xl", {
hasText: "Welcome,lets download your first model",
})
.getByTestId("testid-welcome-title")
.first()
.isDisabled();
.isVisible();
expect(welcomeText).toBe(false);
});

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
"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-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:darwin": "yarn build:web && yarn workspace jan build:darwin",
"build:win32": "yarn build:web && yarn workspace jan build:win32",

View File

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

View File

@ -5,4 +5,4 @@
- index.ts: Main entry point for the plugin.
- module.ts: Defines the plugin module which would be executed by the main node process.
- package.json: Defines the plugin metadata.
- tsconfig.json: Defines the typescript configuration.
- tsconfig.json: Defines the typescript configuration.

View File

@ -216,6 +216,9 @@ export function init({ register }: { register: RegisterExtensionPoint }) {
register(DataService.GetBotById, getBotById.name, getBotById);
register(DataService.DeleteBot, deleteBot.name, deleteBot);
register(DataService.UpdateBot, updateBot.name, updateBot);
// for plugin manifest
register(DataService.GetPluginManifest, getPluginManifest.name, getPluginManifest)
}
function getConversations(): Promise<any> {
@ -323,3 +326,20 @@ function getBotById(botId: string): Promise<any> {
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",
"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.",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
"main": "dist/esm/index.js",
@ -12,7 +12,7 @@
],
"scripts": {
"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"
},
"exports": {
@ -40,7 +40,9 @@
"node_modules"
],
"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-node": "^8.0.1"
},

View File

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

View File

@ -19,6 +19,9 @@ module.exports = {
new webpack.DefinePlugin({
PLUGIN_NAME: JSON.stringify(packageJson.name),
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: {

View File

@ -9,13 +9,17 @@ import {
} from "@janhq/core";
import { Observable } from "rxjs";
const initModel = async (product) => invokePluginFunc(MODULE_PATH, "initModel", product);
const initModel = async (product) =>
invokePluginFunc(MODULE_PATH, "initModel", product);
const stopModel = () => {
invokePluginFunc(MODULE_PATH, "killSubprocess");
};
function requestInference(recentMessages: any[], bot?: any): Observable<string> {
function requestInference(
recentMessages: any[],
bot?: any
): Observable<string> {
return new Observable((subscriber) => {
const requestBody = JSON.stringify({
messages: recentMessages,
@ -63,16 +67,21 @@ function requestInference(recentMessages: any[], bot?: any): Observable<string>
}
subscriber.complete();
})
.catch(subscriber.error);
.catch((err) => subscriber.error(err));
});
}
async function retrieveLastTenMessages(conversationId: string, bot?: any) {
// 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
.filter((e) => e.message !== "" && (e.user === "user" || e.user === "assistant"))
.filter(
(e) => e.message !== "" && (e.user === "user" || e.user === "assistant")
)
.slice(-9)
.map((message) => ({
content: message.message.trim(),
@ -81,10 +90,13 @@ async function retrieveLastTenMessages(conversationId: string, bot?: any) {
if (bot && bot.systemPrompt) {
// append bot's system prompt
recentMessages = [{
content: `[INST] ${bot.systemPrompt}`,
role: 'system'
},...recentMessages];
recentMessages = [
{
content: `[INST] ${bot.systemPrompt}`,
role: "system",
},
...recentMessages,
];
}
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) {
const conversation = await store.findOne("conversations", data.conversationId);
const conversation = await store.findOne(
"conversations",
data.conversationId
);
let bot = undefined;
if (conversation.botId != null) {
bot = await store.findOne("bots", conversation.botId);
}
const recentMessages = await retrieveLastTenMessages(data.conversationId, bot);
const recentMessages = await retrieveLastTenMessages(
data.conversationId,
bot
);
const message = {
...data,
message: "",
@ -124,7 +142,9 @@ async function handleMessageRequest(data: NewMessageRequest) {
await store.updateOne("messages", message._id, message);
},
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
await store.updateOne("messages", message._id, message);
},
@ -140,7 +160,10 @@ async function inferenceRequest(data: NewMessageRequest): Promise<any> {
};
return new Promise(async (resolve, reject) => {
const recentMessages = await retrieveLastTenMessages(data.conversationId);
requestInference([...recentMessages, { role: "user", content: data.message }]).subscribe({
requestInference([
...recentMessages,
{ role: "user", content: data.message },
]).subscribe({
next: (content) => {
message.message = content;
},
@ -166,5 +189,9 @@ export function init({ register }) {
register(PluginService.OnStart, PLUGIN_NAME, onStart);
register(InferenceService.InitModel, initModel.name, initModel);
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) {
reject("Model not found, please download again.");
}
if (subprocess) {
console.error("A subprocess is already running. Attempt to kill then reinit.");
killSubprocess();
}
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
.then(() => {
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
let binaryName;
return tcpPortUsed.check(PORT, "127.0.0.1").then((inUse) => {
if (!inUse) {
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
let binaryName;
if (process.platform === "win32") {
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
binaryName = "nitro_windows_amd64_cuda.exe";
} else if (process.platform === "darwin") {
// Mac OS platform
binaryName = process.arch === "arm64" ? "nitro_mac_arm64" : "nitro_mac_intel";
} else {
// Linux
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
binaryName = "nitro_linux_amd64_cuda"; // For other platforms
}
if (process.platform === "win32") {
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
binaryName = "nitro_start_windows.bat";
} else if (process.platform === "darwin") {
// Mac OS platform
binaryName =
process.arch === "arm64"
? "nitro_mac_arm64"
: "nitro_mac_intel";
} 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
subprocess = spawn(binaryPath, { cwd: binaryFolder });
// Execute the binary
subprocess = spawn(binaryPath, { cwd: binaryFolder });
// Handle subprocess output
subprocess.stdout.on("data", (data) => {
console.log(`stdout: ${data}`);
});
// Handle subprocess output
subprocess.stdout.on("data", (data) => {
console.log(`stdout: ${data}`);
});
subprocess.stderr.on("data", (data) => {
console.error(`stderr: ${data}`);
});
subprocess.stderr.on("data", (data) => {
console.error(`stderr: ${data}`);
});
subprocess.on("close", (code) => {
console.log(`child process exited with code ${code}`);
subprocess = null;
subprocess.on("close", (code) => {
console.log(`child process exited with code ${code}`);
subprocess = null;
});
}
});
})
.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",
"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++.",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/command-line.svg",
"main": "dist/index.js",

View File

@ -8,19 +8,24 @@ import {
} from "@janhq/core";
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);
async function getConfiguredModels() {
// Clear cache to get the latest model catalog
delete require.cache[MODEL_CATALOG_URL];
// Import the remote model catalog
const module = require(MODEL_CATALOG_URL);
return module.default.map((e) => {
return parseToModel(e);
});
/**
* Retrieves a list of configured models from the model catalog URL.
* @returns A Promise that resolves to an array of configured models.
*/
async function getConfiguredModels(): Promise<any> {
// Add a timestamp to the URL to prevent caching
return import(
/* 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
*/
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 }) {
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.GetConfiguredModels, getConfiguredModels.name, getConfiguredModels);
register(
ModelManagementService.GetConfiguredModels,
getConfiguredModels.name,
getConfiguredModels
);
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(ModelManagementService.GetModelById, getModelById.name, getModelById);
register(ModelManagementService.GetFinishedDownloadModels, getFinishedDownloadModels.name, getFinishedDownloadModels);
register(
ModelManagementService.DeleteDownloadModel,
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",
"version": "1.0.8",
"version": "1.0.9",
"description": "Model Management Plugin provides model exploration and seamless downloads",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/queue-list.svg",
"main": "dist/index.js",

View File

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

View File

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

View File

@ -7,7 +7,7 @@ type Props = {
const Avatar: React.FC<Props> = ({ allowEdit = false }) => (
<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
className="mx-auto h-full w-full text-gray-300"
fill="currentColor"

View File

@ -3,7 +3,7 @@
import { useSetAtom } from 'jotai'
import { InformationCircleIcon } from '@heroicons/react/24/outline'
import SendButton from '../SendButton'
import { showingAdvancedPromptAtom } from '@/_helpers/atoms/Modal.atom'
import { showingAdvancedPromptAtom } from '@helpers/atoms/Modal.atom'
const BasicPromptAccessories: React.FC = () => {
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom)
@ -11,7 +11,7 @@ const BasicPromptAccessories: React.FC = () => {
const shouldShowAdvancedPrompt = false
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 */}
<div className="flex items-center space-x-5">
<div className="flex items-center">

View File

@ -1,7 +1,7 @@
import React from 'react'
import { useSetAtom } from 'jotai'
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 setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom)

View File

@ -1,11 +1,10 @@
'use client'
import { currentPromptAtom } from '@/_helpers/JotaiWrapper'
import { getActiveConvoIdAtom } from '@/_helpers/atoms/Conversation.atom'
import { selectedModelAtom } from '@/_helpers/atoms/Model.atom'
import useCreateConversation from '@/_hooks/useCreateConversation'
import useInitModel from '@/_hooks/useInitModel'
import useSendChatMessage from '@/_hooks/useSendChatMessage'
import { currentPromptAtom } from '@helpers/JotaiWrapper'
import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom'
import { selectedModelAtom } from '@helpers/atoms/Model.atom'
import useCreateConversation from '@hooks/useCreateConversation'
import useSendChatMessage from '@hooks/useSendChatMessage'
import { useAtom, useAtomValue } from 'jotai'
import { ChangeEvent, useEffect, useRef } from 'react'
@ -16,8 +15,6 @@ const BasicPromptInput: React.FC = () => {
const { sendChatMessage } = useSendChatMessage()
const { requestCreateConvo } = useCreateConversation()
const { initModel } = useInitModel()
const textareaRef = useRef<HTMLTextAreaElement>(null)
const handleKeyDown = async (
@ -35,7 +32,6 @@ const BasicPromptInput: React.FC = () => {
}
await requestCreateConvo(selectedModel)
await initModel(selectedModel)
sendChatMessage()
}
}
@ -68,7 +64,7 @@ const BasicPromptInput: React.FC = () => {
}
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
ref={textareaRef}
onKeyDown={handleKeyDown}
@ -76,7 +72,7 @@ const BasicPromptInput: React.FC = () => {
onChange={handleMessageChange}
name="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 ..."
rows={1}
style={{ overflow: 'auto' }}

View File

@ -1,56 +1,58 @@
import { activeBotAtom } from "@/_helpers/atoms/Bot.atom";
import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import {
MainViewState,
setMainViewStateAtom,
} from "@/_helpers/atoms/MainView.atom";
import useCreateConversation from "@/_hooks/useCreateConversation";
import useDeleteBot from "@/_hooks/useDeleteBot";
import { useAtomValue, useSetAtom } from "jotai";
import React from "react";
import PrimaryButton from "../PrimaryButton";
import ExpandableHeader from "../ExpandableHeader";
} from '@helpers/atoms/MainView.atom'
import useCreateConversation from '@hooks/useCreateConversation'
import useDeleteBot from '@hooks/useDeleteBot'
import { useAtomValue, useSetAtom } from 'jotai'
import React from 'react'
import PrimaryButton from '../PrimaryButton'
import ExpandableHeader from '../ExpandableHeader'
const BotInfo: React.FC = () => {
const { deleteBot } = useDeleteBot();
const { createConvoByBot } = useCreateConversation();
const setMainView = useSetAtom(setMainViewStateAtom);
const botInfo = useAtomValue(activeBotAtom);
if (!botInfo) return null;
const { deleteBot } = useDeleteBot()
const { createConvoByBot } = useCreateConversation()
const setMainView = useSetAtom(setMainViewStateAtom)
const botInfo = useAtomValue(activeBotAtom)
if (!botInfo) return null
const onNewChatClicked = () => {
if (!botInfo) {
alert("No bot selected");
return;
alert('No bot selected')
return
}
createConvoByBot(botInfo);
};
createConvoByBot(botInfo)
}
const onDeleteBotClick = async () => {
// TODO: display confirmation diaglog
const result = await deleteBot(botInfo._id);
if (result === "success") {
setMainView(MainViewState.Welcome);
const result = await deleteBot(botInfo._id)
if (result === 'success') {
setMainView(MainViewState.Welcome)
}
};
}
return (
<div className="flex flex-col gap-2 mx-1 my-1">
<ExpandableHeader title="BOT INFO" expanded={true} onClick={() => {}} />
<div className="mx-1 my-1 flex flex-col gap-2">
<ExpandableHeader title="BOT INFO" />
<div className="flex flex-col">
<label>{botInfo.name}</label>
<PrimaryButton onClick={onNewChatClicked} title="New chat" />
<span>{botInfo.description}</span>
<label className="mb-2">{botInfo.name}</label>
<span className="text-muted-foreground">{botInfo.description}</span>
</div>
<PrimaryButton
title="Delete bot"
onClick={onDeleteBotClick}
className="bg-red-500 hover:bg-red-400"
/>
<div className="flex w-full flex-col space-y-2">
<PrimaryButton onClick={onNewChatClicked} title="New chat" />
<PrimaryButton
title="Delete bot"
onClick={onDeleteBotClick}
className="bg-red-500 hover:bg-red-400"
/>
</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 React from 'react'
import Avatar from '../Avatar'
import PrimaryButton from '../PrimaryButton'
import useCreateConversation from '@/_hooks/useCreateConversation'
import useDeleteBot from '@/_hooks/useDeleteBot'
import useCreateConversation from '@hooks/useCreateConversation'
import useDeleteBot from '@hooks/useDeleteBot'
import {
setMainViewStateAtom,
MainViewState,
} from '@/_helpers/atoms/MainView.atom'
} from '@helpers/atoms/MainView.atom'
const BotInfoContainer: React.FC = () => {
const activeBot = useAtomValue(activeBotAtom)
@ -44,9 +44,7 @@ const BotInfoContainer: React.FC = () => {
<div className="flex h-full w-full pt-4">
<div className="mx-auto flex w-[672px] min-w-max flex-col gap-4">
<Avatar />
<h1 className="text-center text-2xl font-bold">
{activeBot?.name}
</h1>
<h1 className="text-center text-2xl font-bold">{activeBot?.name}</h1>
<div className="flex gap-4">
<PrimaryButton
fullWidth
@ -55,7 +53,7 @@ const BotInfoContainer: React.FC = () => {
/>
<PrimaryButton
fullWidth
className='bg-red-500 hover:bg-red-400'
className="bg-red-500 hover:bg-red-400"
title="Delete bot"
onClick={onDeleteBotClick}
/>

View File

@ -1,14 +1,14 @@
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom'
import { showingBotListModalAtom } from '@/_helpers/atoms/Modal.atom'
import useGetBots from '@/_hooks/useGetBots'
import { Bot } from '@/_models/Bot'
import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
import useGetBots from '@hooks/useGetBots'
import { useAtom, useSetAtom } from 'jotai'
import { rightSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom'
import React, { useEffect, useState } from 'react'
import Avatar from '../Avatar'
import {
MainViewState,
setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom'
} from '@helpers/atoms/MainView.atom'
const BotListContainer: React.FC = () => {
const [open, setOpen] = useAtom(showingBotListModalAtom)
@ -16,6 +16,7 @@ const BotListContainer: React.FC = () => {
const [activeBot, setActiveBot] = useAtom(activeBotAtom)
const [bots, setBots] = useState<Bot[]>([])
const { getAllBots } = useGetBots()
const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom)
useEffect(() => {
if (open) {
@ -23,31 +24,34 @@ const BotListContainer: React.FC = () => {
setBots(res)
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])
const onBotSelected = (bot: Bot) => {
if (bot._id !== activeBot?._id) {
setMainView(MainViewState.BotInfo)
setActiveBot(bot)
setRightSideBarVisibility(true)
}
setOpen(false)
}
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">
{bots.map((bot) => (
{bots.map((bot, i) => (
<li
role="button"
key={bot._id}
className="flex gap-4 p-4 hover:bg-hover-light sm:px-6"
key={i}
className="flex items-center gap-4 p-4 hover:bg-hover-light sm:px-6"
onClick={() => onBotSelected(bot)}
>
<Avatar />
<div className="flex flex-1 flex-col">
<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>
</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 { useAtom } from 'jotai'
import React, { Fragment } from 'react'
@ -19,10 +19,10 @@ const BotListModal: React.FC = () => {
leaveFrom="opacity-100"
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>
<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">
<Transition.Child
as={Fragment}
@ -33,8 +33,8 @@ const BotListModal: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
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">
<h1 className="mb-4 text-lg text-black font-bold">Your bots</h1>
<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 font-bold">Your bots</h1>
<BotListContainer />
</Dialog.Panel>
</Transition.Child>

View File

@ -1,26 +1,26 @@
import Image from "next/image";
import Image from 'next/image'
const BotPreview: React.FC = () => {
return (
<div className="flex pb-2 flex-col border border-gray-400 min-h-[235px] gap-2 overflow-hidden rounded-lg">
<div className="flex items-center justify-center p-2 bg-gray-400">
<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 bg-gray-400 p-2">
<Image
className="rounded-md"
src={
"https://i.pinimg.com/564x/52/b1/6f/52b16f96f52221d48bea716795ccc89a.jpg"
'https://i.pinimg.com/564x/52/b1/6f/52b16f96f52221d48bea716795ccc89a.jpg'
}
width={32}
height={32}
alt=""
/>
</div>
<div className="flex items-center text-xs text-gray-400 gap-1 px-1">
<div className="flex-grow mx-1 border-b border-gray-400"></div>
<div className="flex items-center gap-1 px-1 text-xs text-gray-400">
<div className="mx-1 flex-grow border-b border-gray-400"></div>
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>
);
};
)
}
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 React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import ExpandableHeader from '../ExpandableHeader'
import { useDebouncedCallback } from 'use-debounce'
import useUpdateBot from '@/_hooks/useUpdateBot'
import ProgressSetting from '../ProgressSetting'
import { set } from 'react-hook-form'
import useUpdateBot from '@hooks/useUpdateBot'
import { formatTwoDigits } from '@utils/converter'
const delayBeforeUpdateInMs = 1000
const BotSetting: React.FC = () => {
const activeBot = useAtomValue(activeBotAtom)
const [temperature, setTemperature] = useState(0)
const [maxTokens, setMaxTokens] = useState(0)
const [frequencyPenalty, setFrequencyPenalty] = useState(0)
const [presencePenalty, setPresencePenalty] = useState(0)
const [temperature, setTemperature] = useState(
activeBot?.customTemperature ?? 0
)
useEffect(() => {
if (!activeBot) return
setMaxTokens(activeBot.maxTokens ?? 0)
setTemperature(activeBot.customTemperature ?? 0)
setFrequencyPenalty(activeBot.frequencyPenalty ?? 0)
setPresencePenalty(activeBot.presencePenalty ?? 0)
}, [activeBot?._id])
const [maxTokens, setMaxTokens] = useState(activeBot?.maxTokens ?? 0)
const [frequencyPenalty, setFrequencyPenalty] = useState(
activeBot?.frequencyPenalty ?? 0
)
const [presencePenalty, setPresencePenalty] = useState(
activeBot?.presencePenalty ?? 0
)
const { updateBot } = useUpdateBot()
@ -60,71 +58,109 @@ const BotSetting: React.FC = () => {
return (
<div className="my-3 flex flex-col">
<ExpandableHeader
title="BOT SETTINGS"
expanded={true}
onClick={() => {}}
/>
<ExpandableHeader title="BOT SETTINGS" />
<div className="mx-2 mt-3 flex flex-shrink-0 flex-col gap-4">
{/* System prompt */}
<div>
<label
htmlFor="comment"
className="block text-sm font-medium leading-6 text-gray-900"
>
<label htmlFor="comment" className="block">
System prompt
</label>
<div className="mt-2">
<div className="mt-1">
<textarea
rows={4}
name="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}
onChange={(e) => debouncedSystemPrompt(e.target.value)}
/>
</div>
</div>
<ProgressSetting
title="Max tokens"
min={0}
max={4096}
step={1}
value={maxTokens}
onValueChanged={(value) => debouncedMaxToken(value)}
/>
{/* TODO: clean up this code */}
{/* Max temp */}
<p>Max tokens</p>
<div className="mt-2 flex items-center gap-2">
<input
className="flex-1"
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
min={0}
max={1}
step={0.01}
title="Temperature"
value={temperature}
onValueChanged={(value) => debouncedTemperature(value)}
/>
<p>Frequency penalty</p>
<div className="mt-2 flex items-center gap-2">
<input
className="flex-1"
type="range"
defaultValue={activeBot.frequencyPenalty ?? 0}
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
title="Frequency penalty"
value={frequencyPenalty}
min={0}
max={1}
step={0.01}
onValueChanged={(value) => debouncedFreqPenalty(value)}
/>
<p>Presence penalty</p>
<div className="mt-2 flex items-center gap-2">
<input
className="flex-1"
type="range"
defaultValue={activeBot.maxTokens ?? 0}
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
min={0}
max={1}
step={0.01}
title="Presence penalty"
value={presencePenalty}
onValueChanged={(value) => {
setPresencePenalty(value)
debouncedPresencePenalty(value)
}}
/>
{/* Custom temp */}
<p>Temperature</p>
<div className="mt-2 flex items-center gap-2">
<input
className="flex-1"
type="range"
id="volume"
name="volume"
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>
)

View File

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

View File

@ -1,61 +1,17 @@
'use client'
import React, { useCallback, useRef, useState, useEffect } from 'react'
import React from 'react'
import ChatItem from '../ChatItem'
import { ChatMessage } from '@/_models/ChatMessage'
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'
import useChatMessages from '@hooks/useChatMessages'
const ChatBody: React.FC = () => {
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? ''
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])
const { messages } = useChatMessages()
return (
<div className="scroll flex flex-1 flex-col-reverse overflow-y-auto py-4">
{content}
<div className="flex h-full flex-1 flex-col-reverse overflow-y-auto [&>*:nth-child(odd)]:bg-background">
{messages.map((message) => (
<ChatItem message={message} key={message.id} />
))}
</div>
)
}

View File

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

View File

@ -1,7 +1,6 @@
/* eslint-disable react/display-name */
import React, { forwardRef } from 'react'
import renderChatMessage from '../ChatBody/renderChatMessage'
import { ChatMessage } from '@/_models/ChatMessage'
type Props = {
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 useDeleteConversation from '@/_hooks/useDeleteConversation'
import { showConfirmDeleteConversationModalAtom } from '@helpers/atoms/Modal.atom'
import useDeleteConversation from '@hooks/useDeleteConversation'
import { Dialog, Transition } from '@headlessui/react'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { useAtom } from 'jotai'
@ -31,10 +31,10 @@ const ConfirmDeleteConversationModal: React.FC = () => {
leaveFrom="opacity-100"
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>
<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">
<Transition.Child
as={Fragment}
@ -45,7 +45,7 @@ const ConfirmDeleteConversationModal: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
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="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
@ -54,14 +54,11 @@ const ConfirmDeleteConversationModal: React.FC = () => {
/>
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title
as="h3"
className="text-base font-semibold leading-6 text-gray-900"
>
<Dialog.Title as="h3" className="font-semibold leading-6">
Delete Conversation
</Dialog.Title>
<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
of messages will be permanently removed. This action
cannot be undone.
@ -72,14 +69,14 @@ const ConfirmDeleteConversationModal: React.FC = () => {
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<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()}
>
Delete
</button>
<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)}
ref={cancelButtonRef}
>

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import { AssistantModel } from '@/_models/AssistantModel'
import ConversationalCard from '../ConversationalCard'
import { ChatBubbleBottomCenterTextIcon } from '@heroicons/react/24/outline'
@ -15,7 +14,7 @@ const ConversationalList: React.FC<Props> = ({ models }) => (
</span>
</div>
<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} />
))}
</div>

View File

@ -5,35 +5,39 @@ import DropdownBox from '../DropdownBox'
import PrimaryButton from '../PrimaryButton'
import ToggleSwitch from '../ToggleSwitch'
import CreateBotPromptInput from '../CreateBotPromptInput'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels'
import { Bot } from '@/_models/Bot'
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import { SubmitHandler, useForm } from 'react-hook-form'
import Avatar from '../Avatar'
import { v4 as uuidv4 } from 'uuid'
import DraggableProgressBar from '../DraggableProgressBar'
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 {
MainViewState,
setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom'
import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager'
} from '@helpers/atoms/MainView.atom'
import { DataService } from '@janhq/core'
import { executeSerial } from '@services/pluginService'
const CreateBotContainer: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels()
const setActiveBot = useSetAtom(activeBotAtom)
const setMainViewState = useSetAtom(setMainViewStateAtom)
const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom)
const createBot = async (bot: Bot) => {
try {
await executeSerial(DataService.CreateBot, bot).then(async () => {
setActiveBot(bot)
setMainViewState(MainViewState.BotInfo)
})
await executeSerial(DataService.CreateBot, bot)
} catch (err) {
alert(err)
console.error(err)
} finally {
setMainViewState(MainViewState.BotInfo)
setActiveBot(bot)
setRightSideBarVisibility(true)
}
}
@ -73,9 +77,10 @@ const CreateBotContainer: React.FC = () => {
createBot(bot)
}
let models = downloadedModels.map((model) => {
let models = downloadedModels.map((model: { _id: any }) => {
return model._id
})
models = ['Select a model', ...models]
return (
@ -84,7 +89,7 @@ const CreateBotContainer: React.FC = () => {
onSubmit={handleSubmit(onSubmit)}
>
<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">
<PrimaryButton isSubmit title="Create" />
</div>
@ -108,7 +113,7 @@ const CreateBotContainer: React.FC = () => {
control={control}
/>
<div className="flex flex-col gap-4 pb-2">
<div className="flex flex-col pb-2">
<DropdownBox
id="modelId"
title="Model"
@ -116,30 +121,23 @@ const CreateBotContainer: React.FC = () => {
control={control}
required={true}
/>
</div>
<CreateBotPromptInput
id="systemPrompt"
<CreateBotPromptInput id="systemPrompt" control={control} required />
<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}
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
id="maxTokens"
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 ToggleSwitch from "../ToggleSwitch";
import { useController } from "react-hook-form";
import React, { Fragment, use } from 'react'
import ToggleSwitch from '../ToggleSwitch'
import { useController } from 'react-hook-form'
type Props = {
id: string;
control?: any;
required?: boolean;
};
id: string
control?: any
required?: boolean
}
const CreateBotPromptInput: React.FC<Props> = ({ id, control, required }) => {
const { field } = useController({
name: id,
control: control,
rules: { required: required },
});
})
return (
<Fragment>
<div className="flex flex-col gap-2">
<label
htmlFor="comment"
className="block text-base text-gray-900 font-bold"
>
<label htmlFor="comment" className="block font-bold ">
Prompt
</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
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
@ -32,18 +29,18 @@ const CreateBotPromptInput: React.FC<Props> = ({ id, control, required }) => {
</p>
<ToggleSwitch
id="visibleFromBotProfile"
title={"Prompt visible from bot profile"}
title={'Prompt visible from bot profile'}
control={control}
/>
<textarea
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"
{...field}
/>
</div>
</Fragment>
);
};
)
}
export default CreateBotPromptInput;
export default CreateBotPromptInput

View File

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

View File

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

View File

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

View File

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

View File

@ -1,23 +1,29 @@
import { formatTwoDigits } from "@/_utils/converter";
import React from "react";
import { Controller, useController } from "react-hook-form";
import { formatTwoDigits } from '@utils/converter'
import React from 'react'
import { Controller, useController } from 'react-hook-form'
type Props = {
id: string;
control: any;
min: number;
max: number;
step: number;
};
id: string
control: any
min: number
max: 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({
name: id,
control: control,
});
})
return (
<div className="flex items-center gap-2 mt-2">
<div className="flex items-center gap-2">
<input
{...field}
className="flex-1"
@ -30,13 +36,13 @@ const DraggableProgressBar: React.FC<Props> = ({ id, control, min, max, step })
name={id}
control={control}
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)}
</span>
)}
/>
</div>
);
};
)
}
export default DraggableProgressBar;
export default DraggableProgressBar

View File

@ -24,9 +24,9 @@ const DropdownBox: React.FC<Props> = ({
return (
<Fragment>
<label className="block text-base font-bold text-gray-900">{title}</label>
<label className="block font-bold">{title}</label>
<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}
>
{data.map((option) => (

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
'use client'
import ExploreModelItemHeader from '../ExploreModelItemHeader'
import { Button } from '@uikit'
import ModelVersionList from '../ModelVersionList'
import { Fragment, forwardRef, useEffect, useState } from 'react'
import SimpleTag from '../SimpleTag'
@ -14,10 +15,9 @@ import {
UsecaseTag,
VersionTag,
} from '@/_components/SimpleTag/TagType'
import { displayDate } from '@/_utils/datetime'
import { Product } from '@/_models/Product'
import useGetMostSuitableModelVersion from '@/_hooks/useGetMostSuitableModelVersion'
import { toGigabytes } from '@/_utils/converter'
import { displayDate } from '@utils/datetime'
import useGetMostSuitableModelVersion from '@hooks/useGetMostSuitableModelVersion'
import { toGigabytes } from '@utils/converter'
type Props = {
model: Product
@ -32,6 +32,7 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
useEffect(() => {
getMostSuitableModelVersion(availableVersions)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [availableVersions])
if (!suitableModel) {
@ -43,25 +44,30 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
return (
<div
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
suitableModel={suitableModel}
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 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="text-sm font-medium text-gray-500">
Release Date
</div>
<div className="text-sm font-normal text-gray-900">
<div className="font-semibold">Release Date</div>
<p className="text-muted-foreground mt-1">
{displayDate(model.releaseDate)}
</div>
</p>
</div>
<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">
<SimpleTag
title={model.version}
@ -81,17 +87,14 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
</div>
</div>
</div>
<div className="flex flex-1 flex-col gap-8">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<div className="text-sm font-medium text-gray-500">Author</div>
<div className="text-sm font-normal text-gray-900">
{model.author}
</div>
<div className="font-semibold">Author</div>
<p className="text-muted-foreground mt-1">{model.author}</p>
</div>
<div className="flex flex-col gap-2">
<div className="text-sm font-medium text-gray-500">
Compatibility
</div>
<div className="font-semibold">Compatibility</div>
<div className="flex gap-2">
<SimpleTag
title={usecase}
@ -106,44 +109,42 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
</div>
</div>
</div>
</div>
<div className="mt-[26px] flex flex-col gap-1">
<span className="text-sm font-medium text-gray-500">About</span>
<span className="text-sm font-normal text-gray-500">
{model.longDescription}
</span>
</div>
<div className="mt-5 flex flex-col gap-2">
<span className="text-sm font-medium text-gray-500">Tags</span>
<div className="flex flex-wrap gap-2">
{model.tags.map((tag) => (
<SimpleTag
key={tag}
title={tag}
type={MiscellanousTag.MiscellanousDefault}
clickable={false}
/>
))}
<div className="flex flex-1 flex-col gap-y-4">
<div>
<div className="font-medium">Tags</div>
<div className="mt-1 flex flex-wrap gap-2">
{model.tags.map((tag) => (
<SimpleTag
key={tag}
title={tag}
type={MiscellanousTag.MiscellanousDefault}
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>
{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>
)
})

View File

@ -1,19 +1,18 @@
import SimpleTag from '../SimpleTag'
import PrimaryButton from '../PrimaryButton'
import { formatDownloadPercentage, toGigabytes } from '@/_utils/converter'
import { formatDownloadPercentage, toGigabytes } from '@utils/converter'
import SecondaryButton from '../SecondaryButton'
import { Product } from '@/_models/Product'
import { useCallback, useEffect, useMemo } from 'react'
import { ModelVersion } from '@/_models/ModelVersion'
import useGetPerformanceTag from '@/_hooks/useGetPerformanceTag'
import useDownloadModel from '@/_hooks/useDownloadModel'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels'
import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom'
import useGetPerformanceTag from '@hooks/useGetPerformanceTag'
import useDownloadModel from '@hooks/useDownloadModel'
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
import { atom, useAtomValue, useSetAtom } from 'jotai'
import { Button } from '@uikit'
import {
MainViewState,
setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom'
} from '@helpers/atoms/MainView.atom'
type Props = {
suitableModel: ModelVersion
@ -37,35 +36,36 @@ const ExploreModelItemHeader: React.FC<Props> = ({
useEffect(() => {
getPerformanceForModel(suitableModel)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [suitableModel])
const onDownloadClick = useCallback(() => {
downloadModel(exploreModel, suitableModel)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [exploreModel, suitableModel])
const isDownloaded =
downloadedModels.find((model) => model._id === suitableModel._id) != null
let downloadButton = (
<PrimaryButton
title={
suitableModel.size
? `Download (${toGigabytes(suitableModel.size)})`
: 'Download'
}
onClick={() => onDownloadClick()}
/>
<Button themes="accent" onClick={() => onDownloadClick()}>
{suitableModel.size
? `Download (${toGigabytes(suitableModel.size)})`
: 'Download'}
</Button>
)
if (isDownloaded) {
downloadButton = (
<PrimaryButton
title="View Downloaded Model"
<Button
size="sm"
themes="accent"
onClick={() => {
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 (
<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">
<span>{exploreModel.name}</span>
{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'
import { showingMobilePaneAtom } from '@/_helpers/atoms/Modal.atom'
import { showingMobilePaneAtom } from '@helpers/atoms/Modal.atom'
import { Bars3Icon } from '@heroicons/react/24/outline'
import { useSetAtom } from 'jotai'
import React from 'react'

View File

@ -1,21 +1,18 @@
import React from 'react'
import { useAtomValue, useSetAtom } from 'jotai'
import Image from 'next/image'
import { Conversation } from '@/_models/Conversation'
import { ModelManagementService } from '@janhq/core'
import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager'
import {
getActiveConvoIdAtom,
setActiveConvoIdAtom,
updateConversationErrorAtom,
updateConversationWaitingForResponseAtom,
} from '@/_helpers/atoms/Conversation.atom'
} from '@helpers/atoms/Conversation.atom'
import {
setMainViewStateAtom,
MainViewState,
} from '@/_helpers/atoms/MainView.atom'
import useInitModel from '@/_hooks/useInitModel'
import { displayDate } from '@/_utils/datetime'
} from '@helpers/atoms/MainView.atom'
import { displayDate } from '@utils/datetime'
import { twMerge } from 'tailwind-merge'
import { executeSerial } from '@services/pluginService'
type Props = {
conversation: Conversation
@ -35,14 +32,9 @@ const HistoryItem: React.FC<Props> = ({
const setMainViewState = useSetAtom(setMainViewStateAtom)
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
const updateConvWaiting = useSetAtom(
updateConversationWaitingForResponseAtom
)
const updateConvError = useSetAtom(updateConversationErrorAtom)
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
const isSelected = activeConvoId === conversation._id
const { initModel } = useInitModel()
const onClick = async () => {
const model = await executeSerial(
ModelManagementService.GetModelById,
@ -50,55 +42,38 @@ const HistoryItem: React.FC<Props> = ({
)
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) {
setMainViewState(MainViewState.Conversation)
setActiveConvoId(conversation._id)
}
};
}
const backgroundColor = isSelected
? "bg-gray-100 dark:bg-gray-700"
: "bg-white dark:bg-gray-500"
const description = conversation?.lastMessage ?? "No new message"
const backgroundColor = isSelected ? 'bg-background/80' : 'bg-background/20'
const description = conversation?.lastMessage ?? 'No new message'
return (
<li
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}
>
<div className="h-8 w-8">
<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">
<div className="flex flex-1 flex-col">
{/* title */}
<div className="flex">
<span className="line-clamp-1 flex-1 text-gray-900">
{summary ?? name}
</span>
<span className="line-clamp-1 text-xs leading-5 text-gray-500">
{updatedAt && displayDate(new Date(updatedAt).getTime())}
</span>
</div>
<span className="mb-1 line-clamp-1 leading-5 text-muted-foreground">
{updatedAt && displayDate(new Date(updatedAt).getTime())}
</span>
<span className="line-clamp-1">{summary ?? name}</span>
{/* 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>
</li>
)

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import React, { Fragment } from "react"
import HistoryList from "../HistoryList"
import LeftHeaderAction from "../LeftHeaderAction"
import { leftSideBarExpandStateAtom } from "@/_helpers/atoms/LeftSideBarExpand.atom"
import { useAtomValue } from "jotai"
import { Variants, motion } from "framer-motion"
import React, { Fragment } from 'react'
import HistoryList from '../HistoryList'
import LeftHeaderAction from '../LeftHeaderAction'
import { leftSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom'
import { useAtomValue } from 'jotai'
import { Variants, motion } from 'framer-motion'
const leftSideBarVariants: Variants = {
show: {
@ -13,7 +13,7 @@ const leftSideBarVariants: Variants = {
transition: { duration: 0.3 },
},
hide: {
x: "-100%",
x: '-100%',
width: 0,
opacity: 0,
transition: { duration: 0.3 },
@ -26,13 +26,13 @@ const LeftContainer: React.FC = () => {
return (
<motion.div
initial={false}
animate={isVisible ? "show" : "hide"}
animate={isVisible ? 'show' : 'hide'}
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 && (
<Fragment>
<LeftHeaderAction />
{/* <LeftHeaderAction /> */}
<HistoryList />
</Fragment>
)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,36 +7,9 @@ type Props = {
}
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => (
<Menu as="div" className="relative flex-none">
<Menu.Button className="block text-gray-500 hover:text-gray-900">
<span className="sr-only">Open options</span>
<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>
<button className="text-muted-foreground text-xs" onClick={onDeleteClick}>
Delete
</button>
)
export default ModelActionMenu

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,17 +16,17 @@ export const ModelStatusMapper: Record<ModelStatus, ModelStatusType> = {
[ModelStatus.Installed]: {
title: 'Installed',
textColor: 'text-black',
backgroundColor: 'bg-gray-100',
backgroundColor: 'bg-gray-200 text-gray-600',
},
[ModelStatus.Active]: {
title: 'Active',
textColor: 'text-black',
backgroundColor: 'bg-green-100',
textColor: 'text-green-800',
backgroundColor: 'bg-green-100 dark:bg-green-300 text-green-700',
},
[ModelStatus.RunningInNitro]: {
title: 'Running in Nitro',
textColor: 'text-black',
backgroundColor: 'bg-green-100',
textColor: 'text-green-800',
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]
return (
<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}
</div>

View File

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

View File

@ -7,7 +7,7 @@ type Props = {
const ModelTableHeader: React.FC<Props> = ({ title }) => (
<th
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}
</th>

View File

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

View File

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

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