Merge branch 'main' into fix-264
This commit is contained in:
commit
8cf71567c2
17
.github/workflows/build-app.yml
vendored
17
.github/workflows/build-app.yml
vendored
@ -37,17 +37,22 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
VERSION_TAG: ${{ steps.tag.outputs.tag }}
|
VERSION_TAG: ${{ steps.tag.outputs.tag }}
|
||||||
|
|
||||||
- name: Install yarn dependencies
|
|
||||||
run: |
|
|
||||||
yarn install
|
|
||||||
yarn build:plugins
|
|
||||||
|
|
||||||
- name: Get Cer for code signing
|
- name: Get Cer for code signing
|
||||||
run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
|
run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
||||||
|
|
||||||
|
- uses: apple-actions/import-codesign-certs@v2
|
||||||
|
with:
|
||||||
|
p12-file-base64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
||||||
|
p12-password: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Install yarn dependencies
|
||||||
|
run: |
|
||||||
|
yarn install
|
||||||
|
yarn build:plugins-darwin
|
||||||
|
|
||||||
- name: Build and publish app
|
- name: Build and publish app
|
||||||
run: |
|
run: |
|
||||||
yarn build:publish-darwin
|
yarn build:publish-darwin
|
||||||
@ -56,6 +61,8 @@ jobs:
|
|||||||
CSC_LINK: "/tmp/codesign.p12"
|
CSC_LINK: "/tmp/codesign.p12"
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: "true"
|
CSC_IDENTITY_AUTO_DISCOVERY: "true"
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
|
||||||
build-windows-x64:
|
build-windows-x64:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|||||||
88
.github/workflows/linter-and-test.yml
vendored
Normal file
88
.github/workflows/linter-and-test.yml
vendored
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
name: Linter & Test
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'electron/**'
|
||||||
|
- .github/workflows/linter-and-test.yml
|
||||||
|
- 'web/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'node_modules/**'
|
||||||
|
- 'yarn.lock'
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'electron/**'
|
||||||
|
- .github/workflows/linter-and-test.yml
|
||||||
|
- 'web/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'node_modules/**'
|
||||||
|
- 'yarn.lock'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-on-macos:
|
||||||
|
runs-on: [self-hosted, macOS, macos-desktop]
|
||||||
|
steps:
|
||||||
|
- name: Getting the repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Installing node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Linter and test
|
||||||
|
run: |
|
||||||
|
yarn config set network-timeout 300000
|
||||||
|
yarn install
|
||||||
|
yarn lint
|
||||||
|
yarn build:plugins
|
||||||
|
yarn build
|
||||||
|
yarn test
|
||||||
|
env:
|
||||||
|
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||||
|
|
||||||
|
test-on-windows:
|
||||||
|
runs-on: [self-hosted, Windows, windows-desktop]
|
||||||
|
steps:
|
||||||
|
- name: Getting the repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Installing node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Linter and test
|
||||||
|
run: |
|
||||||
|
yarn config set network-timeout 300000
|
||||||
|
yarn install
|
||||||
|
yarn lint
|
||||||
|
yarn build:plugins
|
||||||
|
yarn build:win32
|
||||||
|
yarn test
|
||||||
|
|
||||||
|
test-on-ubuntu:
|
||||||
|
runs-on: [self-hosted, Linux, ubuntu-desktop]
|
||||||
|
steps:
|
||||||
|
- name: Getting the repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Installing node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Linter and test
|
||||||
|
run: |
|
||||||
|
yarn config set network-timeout 300000
|
||||||
|
yarn install
|
||||||
|
yarn lint
|
||||||
|
yarn build:plugins
|
||||||
|
yarn build:linux
|
||||||
|
yarn test
|
||||||
|
env:
|
||||||
|
DISPLAY: ":0"
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,7 +5,6 @@
|
|||||||
models/**
|
models/**
|
||||||
error.log
|
error.log
|
||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
|
||||||
*.tgz
|
*.tgz
|
||||||
yarn.lock
|
yarn.lock
|
||||||
dist
|
dist
|
||||||
|
|||||||
@ -34,5 +34,11 @@ module.exports = {
|
|||||||
{ name: "Link", linkAttribute: "to" },
|
{ name: "Link", linkAttribute: "to" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ignorePatterns: ["renderer/*", "node_modules/*", "core/plugins"],
|
ignorePatterns: [
|
||||||
|
"build",
|
||||||
|
"renderer",
|
||||||
|
"node_modules",
|
||||||
|
"core/plugins",
|
||||||
|
"core/**/*.test.js",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
5
electron/auto-sign.sh
Executable file
5
electron/auto-sign.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/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 {} \;
|
||||||
@ -25,6 +25,7 @@ export async function install(plugins) {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const plgList = await window.pluggableElectronIpc.install(plugins);
|
const plgList = await window.pluggableElectronIpc.install(plugins);
|
||||||
if (plgList.cancelled) return false;
|
if (plgList.cancelled) return false;
|
||||||
return plgList.map((plg) => {
|
return plgList.map((plg) => {
|
||||||
@ -50,6 +51,7 @@ export function uninstall(plugins, reload = true) {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
return window.pluggableElectronIpc.uninstall(plugins, reload);
|
return window.pluggableElectronIpc.uninstall(plugins, reload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +64,7 @@ export async function getActive() {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const plgList = await window.pluggableElectronIpc.getActive();
|
const plgList = await window.pluggableElectronIpc.getActive();
|
||||||
return plgList.map(
|
return plgList.map(
|
||||||
(plugin) =>
|
(plugin) =>
|
||||||
@ -86,6 +89,7 @@ export async function registerActive() {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const plgList = await window.pluggableElectronIpc.getActive();
|
const plgList = await window.pluggableElectronIpc.getActive();
|
||||||
plgList.forEach((plugin) =>
|
plgList.forEach((plugin) =>
|
||||||
register(
|
register(
|
||||||
@ -110,6 +114,7 @@ export async function update(plugins, reload = true) {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const plgList = await window.pluggableElectronIpc.update(plugins, reload);
|
const plgList = await window.pluggableElectronIpc.update(plugins, reload);
|
||||||
return plgList.map(
|
return plgList.map(
|
||||||
(plugin) =>
|
(plugin) =>
|
||||||
@ -132,6 +137,7 @@ export function updatesAvailable(plugin) {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
return window.pluggableElectronIpc.updatesAvailable(plugin);
|
return window.pluggableElectronIpc.updatesAvailable(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,6 +152,7 @@ export async function toggleActive(plugin, active) {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const plg = await window.pluggableElectronIpc.toggleActive(plugin, active);
|
const plg = await window.pluggableElectronIpc.toggleActive(plugin, active);
|
||||||
return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active);
|
return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export * as activationPoints from "./activation-manager.js";
|
|||||||
export * as plugins from "./facade.js";
|
export * as plugins from "./facade.js";
|
||||||
export { default as ExtensionPoint } from "./ExtensionPoint.js";
|
export { default as ExtensionPoint } from "./ExtensionPoint.js";
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
if (typeof window !== "undefined" && !window.pluggableElectronIpc)
|
if (typeof window !== "undefined" && !window.pluggableElectronIpc)
|
||||||
console.warn(
|
console.warn(
|
||||||
"Facade is not registered in preload. Facade functions will throw an error if used."
|
"Facade is not registered in preload. Facade functions will throw an error if used."
|
||||||
|
|||||||
5606
electron/core/plugins/data-plugin/package-lock.json
generated
Normal file
5606
electron/core/plugins/data-plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,8 +11,8 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||||
"build:package": "rimraf ./data-plugin*.tgz && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=arm64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=glibc --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=musl --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_libc=unknown --target_arch=x64 && npm run build && npm pack",
|
"postinstall": "rimraf ./data-plugin*.tgz && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_libc=unknown --target_arch=arm64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=glibc --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_libc=musl --target_arch=x64 && node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_libc=unknown --target_arch=x64 && npm run build",
|
||||||
"build:publish": "npm run build:package && cpx *.tgz ../../pre-install"
|
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
|
|||||||
@ -17,11 +17,16 @@ const dispose = async () =>
|
|||||||
.then((res) => resolve(res));
|
.then((res) => resolve(res));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const inferenceUrl = () => "http://localhost:8080/llama/chat_completion";
|
const inferenceUrl = () => "http://localhost:3928/llama/chat_completion";
|
||||||
|
|
||||||
|
const stopModel = () => {
|
||||||
|
window.electronAPI.invokePluginFunc(MODULE_PATH, "killSubprocess");
|
||||||
|
};
|
||||||
|
|
||||||
// Register all the above functions and objects with the relevant extension points
|
// Register all the above functions and objects with the relevant extension points
|
||||||
export function init({ register }) {
|
export function init({ register }) {
|
||||||
register("initModel", "initModel", initModel);
|
register("initModel", "initModel", initModel);
|
||||||
register("inferenceUrl", "inferenceUrl", inferenceUrl);
|
register("inferenceUrl", "inferenceUrl", inferenceUrl);
|
||||||
register("dispose", "dispose", dispose);
|
register("dispose", "dispose", dispose);
|
||||||
|
register("stopModel", "stopModel", stopModel);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ async function initModel(product) {
|
|||||||
console.error(
|
console.error(
|
||||||
"A subprocess is already running. Attempt to kill then reinit."
|
"A subprocess is already running. Attempt to kill then reinit."
|
||||||
);
|
);
|
||||||
killSubprocess();
|
dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
|
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
|
||||||
@ -51,12 +51,12 @@ async function initModel(product) {
|
|||||||
let binaryName;
|
let binaryName;
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
binaryName = "nitro.exe";
|
binaryName = "nitro_windows_amd64.exe";
|
||||||
} else if (process.platform === "darwin") { // Mac OS platform
|
} else if (process.platform === "darwin") { // Mac OS platform
|
||||||
binaryName = process.arch === "arm64" ? "nitro" : "nitro_mac_intel";
|
binaryName = process.arch === "arm64" ? "nitro_mac_arm64" : "nitro_mac_amd64";
|
||||||
} else {
|
} else {
|
||||||
// Linux
|
// Linux
|
||||||
binaryName = "nitro_linux"; // For other platforms
|
binaryName = "nitro_linux_amd64_cuda"; // For other platforms
|
||||||
}
|
}
|
||||||
|
|
||||||
const binaryPath = path.join(binaryFolder, binaryName);
|
const binaryPath = path.join(binaryFolder, binaryName);
|
||||||
|
|||||||
@ -1 +1,13 @@
|
|||||||
{"custom_config": {"llama_model_path":"","ctx_len":2048,"ngl":100}}
|
{
|
||||||
|
"listeners": [
|
||||||
|
{
|
||||||
|
"address": "0.0.0.0",
|
||||||
|
"port": 3928
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"custom_config": {
|
||||||
|
"llama_model_path": "",
|
||||||
|
"ctx_len": 2048,
|
||||||
|
"ngl": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -24,12 +24,59 @@ typedef struct {
|
|||||||
int8_t qs[QK8_0]; // quants
|
int8_t qs[QK8_0]; // quants
|
||||||
} block_q8_0;
|
} block_q8_0;
|
||||||
|
|
||||||
|
// general-purpose kernel for addition of two tensors
|
||||||
|
// pros: works for non-contiguous tensors, supports broadcast across dims 1, 2 and 3
|
||||||
|
// cons: not very efficient
|
||||||
kernel void kernel_add(
|
kernel void kernel_add(
|
||||||
device const float4 * src0,
|
device const char * src0,
|
||||||
device const float4 * src1,
|
device const char * src1,
|
||||||
device float4 * dst,
|
device char * dst,
|
||||||
uint tpig[[thread_position_in_grid]]) {
|
constant int64_t & ne00,
|
||||||
dst[tpig] = src0[tpig] + src1[tpig];
|
constant int64_t & ne01,
|
||||||
|
constant int64_t & ne02,
|
||||||
|
constant int64_t & ne03,
|
||||||
|
constant int64_t & nb00,
|
||||||
|
constant int64_t & nb01,
|
||||||
|
constant int64_t & nb02,
|
||||||
|
constant int64_t & nb03,
|
||||||
|
constant int64_t & ne10,
|
||||||
|
constant int64_t & ne11,
|
||||||
|
constant int64_t & ne12,
|
||||||
|
constant int64_t & ne13,
|
||||||
|
constant int64_t & nb10,
|
||||||
|
constant int64_t & nb11,
|
||||||
|
constant int64_t & nb12,
|
||||||
|
constant int64_t & nb13,
|
||||||
|
constant int64_t & ne0,
|
||||||
|
constant int64_t & ne1,
|
||||||
|
constant int64_t & ne2,
|
||||||
|
constant int64_t & ne3,
|
||||||
|
constant int64_t & nb0,
|
||||||
|
constant int64_t & nb1,
|
||||||
|
constant int64_t & nb2,
|
||||||
|
constant int64_t & nb3,
|
||||||
|
uint3 tgpig[[threadgroup_position_in_grid]],
|
||||||
|
uint3 tpitg[[thread_position_in_threadgroup]],
|
||||||
|
uint3 ntg[[threads_per_threadgroup]]) {
|
||||||
|
const int64_t i03 = tgpig.z;
|
||||||
|
const int64_t i02 = tgpig.y;
|
||||||
|
const int64_t i01 = tgpig.x;
|
||||||
|
|
||||||
|
const int64_t i13 = i03 % ne13;
|
||||||
|
const int64_t i12 = i02 % ne12;
|
||||||
|
const int64_t i11 = i01 % ne11;
|
||||||
|
|
||||||
|
device const char * src0_ptr = src0 + i03*nb03 + i02*nb02 + i01*nb01 + tpitg.x*nb00;
|
||||||
|
device const char * src1_ptr = src1 + i13*nb13 + i12*nb12 + i11*nb11 + tpitg.x*nb10;
|
||||||
|
device char * dst_ptr = dst + i03*nb3 + i02*nb2 + i01*nb1 + tpitg.x*nb0;
|
||||||
|
|
||||||
|
for (int i0 = tpitg.x; i0 < ne0; i0 += ntg.x) {
|
||||||
|
((device float *)dst_ptr)[0] = ((device float *)src0_ptr)[0] + ((device float *)src1_ptr)[0];
|
||||||
|
|
||||||
|
src0_ptr += ntg.x*nb00;
|
||||||
|
src1_ptr += ntg.x*nb10;
|
||||||
|
dst_ptr += ntg.x*nb0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// assumption: src1 is a row
|
// assumption: src1 is a row
|
||||||
@ -38,7 +85,7 @@ kernel void kernel_add_row(
|
|||||||
device const float4 * src0,
|
device const float4 * src0,
|
||||||
device const float4 * src1,
|
device const float4 * src1,
|
||||||
device float4 * dst,
|
device float4 * dst,
|
||||||
constant int64_t & nb,
|
constant int64_t & nb [[buffer(27)]],
|
||||||
uint tpig[[thread_position_in_grid]]) {
|
uint tpig[[thread_position_in_grid]]) {
|
||||||
dst[tpig] = src0[tpig] + src1[tpig % nb];
|
dst[tpig] = src0[tpig] + src1[tpig % nb];
|
||||||
}
|
}
|
||||||
@ -783,7 +830,9 @@ kernel void kernel_alibi_f32(
|
|||||||
constant uint64_t & nb1,
|
constant uint64_t & nb1,
|
||||||
constant uint64_t & nb2,
|
constant uint64_t & nb2,
|
||||||
constant uint64_t & nb3,
|
constant uint64_t & nb3,
|
||||||
constant float & m0,
|
constant float & m0,
|
||||||
|
constant float & m1,
|
||||||
|
constant int & n_heads_log2_floor,
|
||||||
uint3 tgpig[[threadgroup_position_in_grid]],
|
uint3 tgpig[[threadgroup_position_in_grid]],
|
||||||
uint3 tpitg[[thread_position_in_threadgroup]],
|
uint3 tpitg[[thread_position_in_threadgroup]],
|
||||||
uint3 ntg[[threads_per_threadgroup]]) {
|
uint3 ntg[[threads_per_threadgroup]]) {
|
||||||
@ -799,37 +848,73 @@ kernel void kernel_alibi_f32(
|
|||||||
const int64_t i0 = (n - i3*ne2*ne1*ne0 - i2*ne1*ne0 - i1*ne0);
|
const int64_t i0 = (n - i3*ne2*ne1*ne0 - i2*ne1*ne0 - i1*ne0);
|
||||||
|
|
||||||
device float * dst_data = (device float *) ((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
device float * dst_data = (device float *) ((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||||
float m_k = pow(m0, i2 + 1);
|
float m_k;
|
||||||
|
if (i2 < n_heads_log2_floor) {
|
||||||
|
m_k = pow(m0, i2 + 1);
|
||||||
|
} else {
|
||||||
|
m_k = pow(m1, 2 * (i2 - n_heads_log2_floor) + 1);
|
||||||
|
}
|
||||||
for (int64_t i00 = tpitg.x; i00 < ne00; i00 += ntg.x) {
|
for (int64_t i00 = tpitg.x; i00 < ne00; i00 += ntg.x) {
|
||||||
device const float * src = (device float *)((device char *) src0 + i03*nb03 + i02*nb02 + i01*nb01 + i00*nb00);
|
device const float * src = (device float *)((device char *) src0 + i03*nb03 + i02*nb02 + i01*nb01 + i00*nb00);
|
||||||
dst_data[i00] = src[0] + m_k * (i00 - ne00 + 1);
|
dst_data[i00] = src[0] + m_k * (i00 - ne00 + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef void (rope_t)(
|
||||||
|
device const void * src0,
|
||||||
|
device const int32_t * src1,
|
||||||
|
device float * dst,
|
||||||
|
constant int64_t & ne00,
|
||||||
|
constant int64_t & ne01,
|
||||||
|
constant int64_t & ne02,
|
||||||
|
constant int64_t & ne03,
|
||||||
|
constant uint64_t & nb00,
|
||||||
|
constant uint64_t & nb01,
|
||||||
|
constant uint64_t & nb02,
|
||||||
|
constant uint64_t & nb03,
|
||||||
|
constant int64_t & ne0,
|
||||||
|
constant int64_t & ne1,
|
||||||
|
constant int64_t & ne2,
|
||||||
|
constant int64_t & ne3,
|
||||||
|
constant uint64_t & nb0,
|
||||||
|
constant uint64_t & nb1,
|
||||||
|
constant uint64_t & nb2,
|
||||||
|
constant uint64_t & nb3,
|
||||||
|
constant int & n_past,
|
||||||
|
constant int & n_dims,
|
||||||
|
constant int & mode,
|
||||||
|
constant float & freq_base,
|
||||||
|
constant float & freq_scale,
|
||||||
|
uint tiitg[[thread_index_in_threadgroup]],
|
||||||
|
uint3 tptg[[threads_per_threadgroup]],
|
||||||
|
uint3 tgpig[[threadgroup_position_in_grid]]);
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
kernel void kernel_rope(
|
kernel void kernel_rope(
|
||||||
device const void * src0,
|
device const void * src0,
|
||||||
device float * dst,
|
device const int32_t * src1,
|
||||||
constant int64_t & ne00,
|
device float * dst,
|
||||||
constant int64_t & ne01,
|
constant int64_t & ne00,
|
||||||
constant int64_t & ne02,
|
constant int64_t & ne01,
|
||||||
constant int64_t & ne03,
|
constant int64_t & ne02,
|
||||||
constant uint64_t & nb00,
|
constant int64_t & ne03,
|
||||||
constant uint64_t & nb01,
|
constant uint64_t & nb00,
|
||||||
constant uint64_t & nb02,
|
constant uint64_t & nb01,
|
||||||
constant uint64_t & nb03,
|
constant uint64_t & nb02,
|
||||||
constant int64_t & ne0,
|
constant uint64_t & nb03,
|
||||||
constant int64_t & ne1,
|
constant int64_t & ne0,
|
||||||
constant int64_t & ne2,
|
constant int64_t & ne1,
|
||||||
constant int64_t & ne3,
|
constant int64_t & ne2,
|
||||||
constant uint64_t & nb0,
|
constant int64_t & ne3,
|
||||||
constant uint64_t & nb1,
|
constant uint64_t & nb0,
|
||||||
constant uint64_t & nb2,
|
constant uint64_t & nb1,
|
||||||
constant uint64_t & nb3,
|
constant uint64_t & nb2,
|
||||||
constant int & n_past,
|
constant uint64_t & nb3,
|
||||||
constant int & n_dims,
|
constant int & n_past,
|
||||||
constant int & mode,
|
constant int & n_dims,
|
||||||
constant float & freq_base,
|
constant int & mode,
|
||||||
constant float & freq_scale,
|
constant float & freq_base,
|
||||||
|
constant float & freq_scale,
|
||||||
uint tiitg[[thread_index_in_threadgroup]],
|
uint tiitg[[thread_index_in_threadgroup]],
|
||||||
uint3 tptg[[threads_per_threadgroup]],
|
uint3 tptg[[threads_per_threadgroup]],
|
||||||
uint3 tgpig[[threadgroup_position_in_grid]]) {
|
uint3 tgpig[[threadgroup_position_in_grid]]) {
|
||||||
@ -839,7 +924,9 @@ kernel void kernel_rope(
|
|||||||
|
|
||||||
const bool is_neox = mode & 2;
|
const bool is_neox = mode & 2;
|
||||||
|
|
||||||
const int64_t p = ((mode & 1) == 0 ? n_past + i2 : i2);
|
device const int32_t * pos = src1;
|
||||||
|
|
||||||
|
const int64_t p = pos[i2];
|
||||||
|
|
||||||
const float theta_0 = freq_scale * (float)p;
|
const float theta_0 = freq_scale * (float)p;
|
||||||
const float inv_ndims = -1.f/n_dims;
|
const float inv_ndims = -1.f/n_dims;
|
||||||
@ -851,11 +938,11 @@ kernel void kernel_rope(
|
|||||||
const float cos_theta = cos(theta);
|
const float cos_theta = cos(theta);
|
||||||
const float sin_theta = sin(theta);
|
const float sin_theta = sin(theta);
|
||||||
|
|
||||||
device const float * const src = (device float *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
device const T * const src = (device T *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
||||||
device float * dst_data = (device float *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
device T * dst_data = (device T *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||||
|
|
||||||
const float x0 = src[0];
|
const T x0 = src[0];
|
||||||
const float x1 = src[1];
|
const T x1 = src[1];
|
||||||
|
|
||||||
dst_data[0] = x0*cos_theta - x1*sin_theta;
|
dst_data[0] = x0*cos_theta - x1*sin_theta;
|
||||||
dst_data[1] = x0*sin_theta + x1*cos_theta;
|
dst_data[1] = x0*sin_theta + x1*cos_theta;
|
||||||
@ -870,8 +957,8 @@ kernel void kernel_rope(
|
|||||||
|
|
||||||
const int64_t i0 = ib*n_dims + ic/2;
|
const int64_t i0 = ib*n_dims + ic/2;
|
||||||
|
|
||||||
device const float * const src = (device float *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
device const T * const src = (device T *)((device char *) src0 + i3*nb03 + i2*nb02 + i1*nb01 + i0*nb00);
|
||||||
device float * dst_data = (device float *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
device T * dst_data = (device T *)((device char *) dst + i3*nb3 + i2*nb2 + i1*nb1 + i0*nb0);
|
||||||
|
|
||||||
const float x0 = src[0];
|
const float x0 = src[0];
|
||||||
const float x1 = src[n_dims/2];
|
const float x1 = src[n_dims/2];
|
||||||
@ -883,6 +970,9 @@ kernel void kernel_rope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template [[host_name("kernel_rope_f32")]] kernel rope_t kernel_rope<float>;
|
||||||
|
template [[host_name("kernel_rope_f16")]] kernel rope_t kernel_rope<half>;
|
||||||
|
|
||||||
kernel void kernel_cpy_f16_f16(
|
kernel void kernel_cpy_f16_f16(
|
||||||
device const half * src0,
|
device const half * src0,
|
||||||
device half * dst,
|
device half * dst,
|
||||||
@ -1273,8 +1363,8 @@ kernel void kernel_mul_mat_q3_K_f32(
|
|||||||
|
|
||||||
float yl[32];
|
float yl[32];
|
||||||
|
|
||||||
const uint16_t kmask1 = 0x3030;
|
//const uint16_t kmask1 = 0x3030;
|
||||||
const uint16_t kmask2 = 0x0f0f;
|
//const uint16_t kmask2 = 0x0f0f;
|
||||||
|
|
||||||
const int tid = tiisg/4;
|
const int tid = tiisg/4;
|
||||||
const int ix = tiisg%4;
|
const int ix = tiisg%4;
|
||||||
@ -2350,4 +2440,4 @@ template [[host_name("kernel_mul_mm_q2_K_f32")]] kernel mat_mm_t kernel_mul_mm<b
|
|||||||
template [[host_name("kernel_mul_mm_q3_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q3_K, QK_NL, dequantize_q3_K>;
|
template [[host_name("kernel_mul_mm_q3_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q3_K, QK_NL, dequantize_q3_K>;
|
||||||
template [[host_name("kernel_mul_mm_q4_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q4_K, QK_NL, dequantize_q4_K>;
|
template [[host_name("kernel_mul_mm_q4_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q4_K, QK_NL, dequantize_q4_K>;
|
||||||
template [[host_name("kernel_mul_mm_q5_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q5_K, QK_NL, dequantize_q5_K>;
|
template [[host_name("kernel_mul_mm_q5_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q5_K, QK_NL, dequantize_q5_K>;
|
||||||
template [[host_name("kernel_mul_mm_q6_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q6_K, QK_NL, dequantize_q6_K>;
|
template [[host_name("kernel_mul_mm_q6_K_f32")]] kernel mat_mm_t kernel_mul_mm<block_q6_K, QK_NL, dequantize_q6_K>;
|
||||||
Binary file not shown.
BIN
electron/core/plugins/inference-plugin/nitro/nitro_linux_amd64_cuda
Executable file
BIN
electron/core/plugins/inference-plugin/nitro/nitro_linux_amd64_cuda
Executable file
Binary file not shown.
BIN
electron/core/plugins/inference-plugin/nitro/nitro_mac_amd64
Executable file
BIN
electron/core/plugins/inference-plugin/nitro/nitro_mac_amd64
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3674
electron/core/plugins/inference-plugin/package-lock.json
generated
Normal file
3674
electron/core/plugins/inference-plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,8 +11,8 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\" && npm pack",
|
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
|
||||||
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
@ -25,8 +25,7 @@
|
|||||||
"node-llama-cpp"
|
"node-llama-cpp"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0"
|
||||||
"node-llama-cpp": "^2.4.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
|||||||
@ -38,10 +38,20 @@ const deleteModel = async (path) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const searchModels = async (params) =>
|
||||||
|
new Promise(async (resolve) => {
|
||||||
|
if (window.electronAPI) {
|
||||||
|
window.electronAPI
|
||||||
|
.invokePluginFunc(MODULE_PATH, "searchModels", params)
|
||||||
|
.then((res) => resolve(res));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Register all the above functions and objects with the relevant extension points
|
// Register all the above functions and objects with the relevant extension points
|
||||||
export function init({ register }) {
|
export function init({ register }) {
|
||||||
register("getDownloadedModels", "getDownloadedModels", getDownloadedModels);
|
register("getDownloadedModels", "getDownloadedModels", getDownloadedModels);
|
||||||
register("getAvailableModels", "getAvailableModels", getAvailableModels);
|
register("getAvailableModels", "getAvailableModels", getAvailableModels);
|
||||||
register("downloadModel", "downloadModel", downloadModel);
|
register("downloadModel", "downloadModel", downloadModel);
|
||||||
register("deleteModel", "deleteModel", deleteModel);
|
register("deleteModel", "deleteModel", deleteModel);
|
||||||
|
register("searchModels", "searchModels", searchModels);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { readdirSync, lstatSync } = require("fs");
|
const { readdirSync, lstatSync } = require("fs");
|
||||||
const { app } = require("electron");
|
const { app } = require("electron");
|
||||||
|
const { listModels, listFiles, fileDownloadInfo } = require("@huggingface/hub");
|
||||||
|
|
||||||
|
let modelsIterator = undefined;
|
||||||
|
let currentSearchOwner = undefined;
|
||||||
|
|
||||||
const ALL_MODELS = [
|
const ALL_MODELS = [
|
||||||
{
|
{
|
||||||
@ -87,6 +91,76 @@ function getDownloadedModels() {
|
|||||||
return downloadedModels;
|
return downloadedModels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getNextModels = async (count) => {
|
||||||
|
const models = [];
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
while (models.length < count) {
|
||||||
|
const next = await modelsIterator.next();
|
||||||
|
|
||||||
|
// end if we reached the end
|
||||||
|
if (next.done) {
|
||||||
|
hasMore = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = next.value;
|
||||||
|
const files = await listFilesByName(model.name);
|
||||||
|
|
||||||
|
models.push({
|
||||||
|
...model,
|
||||||
|
files,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
data: models,
|
||||||
|
hasMore,
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchModels = async (params) => {
|
||||||
|
if (currentSearchOwner === params.search.owner && modelsIterator != null) {
|
||||||
|
// paginated search
|
||||||
|
console.debug(`Paginated search owner: ${params.search.owner}`);
|
||||||
|
const models = await getNextModels(params.limit);
|
||||||
|
return models;
|
||||||
|
} else {
|
||||||
|
// new search
|
||||||
|
console.debug(`Init new search owner: ${params.search.owner}`);
|
||||||
|
currentSearchOwner = params.search.owner;
|
||||||
|
modelsIterator = listModels({
|
||||||
|
search: params.search,
|
||||||
|
credentials: params.credentials,
|
||||||
|
});
|
||||||
|
|
||||||
|
const models = await getNextModels(params.limit);
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const listFilesByName = async (modelName) => {
|
||||||
|
const repo = { type: "model", name: modelName };
|
||||||
|
const fileDownloadInfoMap = {};
|
||||||
|
for await (const file of listFiles({
|
||||||
|
repo: repo,
|
||||||
|
})) {
|
||||||
|
if (file.type === "file" && file.path.endsWith(".bin")) {
|
||||||
|
const downloadInfo = await fileDownloadInfo({
|
||||||
|
repo: repo,
|
||||||
|
path: file.path,
|
||||||
|
});
|
||||||
|
fileDownloadInfoMap[file.path] = {
|
||||||
|
...file,
|
||||||
|
...downloadInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileDownloadInfoMap;
|
||||||
|
};
|
||||||
|
|
||||||
function getAvailableModels() {
|
function getAvailableModels() {
|
||||||
const downloadedModelIds = getDownloadedModels().map((model) => model.id);
|
const downloadedModelIds = getDownloadedModels().map((model) => model.id);
|
||||||
return ALL_MODELS.filter((model) => {
|
return ALL_MODELS.filter((model) => {
|
||||||
@ -99,4 +173,5 @@ function getAvailableModels() {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
getDownloadedModels,
|
getDownloadedModels,
|
||||||
getAvailableModels,
|
getAvailableModels,
|
||||||
|
searchModels,
|
||||||
};
|
};
|
||||||
|
|||||||
3607
electron/core/plugins/model-management-plugin/package-lock.json
generated
Normal file
3607
electron/core/plugins/model-management-plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,8 +11,8 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
|
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\"",
|
||||||
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
@ -24,5 +24,11 @@
|
|||||||
"dist/*",
|
"dist/*",
|
||||||
"package.json",
|
"package.json",
|
||||||
"README.md"
|
"README.md"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@huggingface/hub": "^0.8.5"
|
||||||
|
},
|
||||||
|
"bundledDependencies": [
|
||||||
|
"@huggingface/hub"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
1479
electron/core/plugins/monitoring-plugin/package-lock.json
generated
Normal file
1479
electron/core/plugins/monitoring-plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,8 +11,8 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"build:package": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\" && npm pack",
|
"postinstall": "rimraf ./*.tgz && npm run build && cpx \"module.js\" \"dist\"",
|
||||||
"build:publish": "yarn build:package && cpx *.tgz ../../pre-install"
|
"build:publish": "npm pack && cpx *.tgz ../../pre-install"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
|||||||
14
electron/entitlements.mac.plist
Normal file
14
electron/entitlements.mac.plist
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -147,6 +147,18 @@ function handleIPCs() {
|
|||||||
ipcMain.handle("relaunch", async (_event, url) => {
|
ipcMain.handle("relaunch", async (_event, url) => {
|
||||||
dispose(requiredModules);
|
dispose(requiredModules);
|
||||||
app.relaunch();
|
app.relaunch();
|
||||||
|
app.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("reloadPlugins", async (_event, url) => {
|
||||||
|
const userDataPath = app.getPath("userData");
|
||||||
|
const fullPath = join(userDataPath, "plugins");
|
||||||
|
|
||||||
|
rmdir(fullPath, { recursive: true }, function (err) {
|
||||||
|
if (err) console.log(err);
|
||||||
|
app.relaunch();
|
||||||
|
app.exit();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -27,12 +27,18 @@
|
|||||||
],
|
],
|
||||||
"extends": null,
|
"extends": null,
|
||||||
"mac": {
|
"mac": {
|
||||||
"type": "distribution"
|
"type": "distribution",
|
||||||
}
|
"entitlements": "./entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "./entitlements.mac.plist",
|
||||||
|
"notarize": {
|
||||||
|
"teamId": "YT49P7GXG4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"artifactName": "${name}-${os}-${arch}-${version}.${ext}"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||||
"test:e2e": "playwright test --workers=2",
|
"test:e2e": "playwright test --workers=1",
|
||||||
"dev": "tsc -p . && electron .",
|
"dev": "tsc -p . && electron .",
|
||||||
"build": "tsc -p . && electron-builder -p never -m",
|
"build": "tsc -p . && electron-builder -p never -m",
|
||||||
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
|
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
|
||||||
@ -45,13 +51,17 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@npmcli/arborist": "^7.1.0",
|
"@npmcli/arborist": "^7.1.0",
|
||||||
|
"@uiball/loaders": "^1.3.0",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^6.1.4",
|
"electron-updater": "^6.1.4",
|
||||||
"pacote": "^17.0.4",
|
"pacote": "^17.0.4",
|
||||||
|
"react-intersection-observer": "^9.5.2",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-progress": "^3.0.0"
|
"request-progress": "^3.0.0",
|
||||||
|
"use-debounce": "^9.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/notarize": "^2.1.0",
|
||||||
"@playwright/test": "^1.38.1",
|
"@playwright/test": "^1.38.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||||
"@typescript-eslint/parser": "^6.7.3",
|
"@typescript-eslint/parser": "^6.7.3",
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { PlaywrightTestConfig } from "@playwright/test";
|
|||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
testDir: "./tests",
|
testDir: "./tests",
|
||||||
testIgnore: "./core/**",
|
testIgnore: "./core/**",
|
||||||
retries: 0
|
retries: 0,
|
||||||
|
timeout: 120000,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@ -13,6 +13,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
|
|
||||||
pluginPath: () => ipcRenderer.invoke("pluginPath"),
|
pluginPath: () => ipcRenderer.invoke("pluginPath"),
|
||||||
|
|
||||||
|
reloadPlugins: () => ipcRenderer.invoke("reloadPlugins"),
|
||||||
|
|
||||||
appVersion: () => ipcRenderer.invoke("appVersion"),
|
appVersion: () => ipcRenderer.invoke("appVersion"),
|
||||||
|
|
||||||
openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url),
|
openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url),
|
||||||
|
|||||||
@ -19,8 +19,6 @@ test.beforeAll(async () => {
|
|||||||
// parse the packaged Electron app and find paths and other info
|
// parse the packaged Electron app and find paths and other info
|
||||||
const appInfo = parseElectronApp(latestBuild);
|
const appInfo = parseElectronApp(latestBuild);
|
||||||
expect(appInfo).toBeTruthy();
|
expect(appInfo).toBeTruthy();
|
||||||
expect(appInfo.arch).toBeTruthy();
|
|
||||||
expect(appInfo.arch).toBe(process.arch);
|
|
||||||
expect(appInfo.asar).toBe(true);
|
expect(appInfo.asar).toBe(true);
|
||||||
expect(appInfo.executable).toBeTruthy();
|
expect(appInfo.executable).toBeTruthy();
|
||||||
expect(appInfo.main).toBeTruthy();
|
expect(appInfo.main).toBeTruthy();
|
||||||
|
|||||||
@ -52,10 +52,6 @@ test("renders left navigation panel", async () => {
|
|||||||
.getByRole("button", { name: "Explore Models" })
|
.getByRole("button", { name: "Explore Models" })
|
||||||
.first()
|
.first()
|
||||||
.isEnabled();
|
.isEnabled();
|
||||||
const startConversation = await page
|
|
||||||
.getByRole("button", { name: "Start a Conversation" })
|
|
||||||
.first()
|
|
||||||
.isEnabled();
|
|
||||||
const discordBtn = await page
|
const discordBtn = await page
|
||||||
.getByRole("button", { name: "Discord" })
|
.getByRole("button", { name: "Discord" })
|
||||||
.first()
|
.first()
|
||||||
@ -72,7 +68,6 @@ test("renders left navigation panel", async () => {
|
|||||||
[
|
[
|
||||||
newChatBtn,
|
newChatBtn,
|
||||||
exploreBtn,
|
exploreBtn,
|
||||||
startConversation,
|
|
||||||
discordBtn,
|
discordBtn,
|
||||||
myModelsBtn,
|
myModelsBtn,
|
||||||
settingsBtn,
|
settingsBtn,
|
||||||
|
|||||||
1614
node_modules/.yarn-integrity
generated
vendored
1614
node_modules/.yarn-integrity
generated
vendored
File diff suppressed because it is too large
Load Diff
14224
package-lock.json
generated
Normal file
14224
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -21,7 +21,8 @@
|
|||||||
"dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
|
"dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
|
||||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||||
"build:electron": "yarn workspace jan-electron build",
|
"build:electron": "yarn workspace jan-electron build",
|
||||||
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/model-management-plugin && npm install && npm run build:publish\" \"cd ./electron/core/plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm ci\" \"cd ./electron/core/plugins/inference-plugin && npm ci\" \"cd ./electron/core/plugins/model-management-plugin && npm ci\" \"cd ./electron/core/plugins/monitoring-plugin && npm ci\" && concurrently \"cd ./electron/core/plugins/data-plugin && npm run build:publish\" \"cd ./electron/core/plugins/inference-plugin && npm run build:publish\" \"cd ./electron/core/plugins/model-management-plugin && npm run build:publish\" \"cd ./electron/core/plugins/monitoring-plugin && npm run build:publish\"",
|
||||||
|
"build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently \"cd ./electron/core/plugins/data-plugin && npm ci\" \"cd ./electron/core/plugins/inference-plugin && npm ci\" \"cd ./electron/core/plugins/model-management-plugin && npm ci\" \"cd ./electron/core/plugins/monitoring-plugin && npm ci\" && chmod +x ./electron/auto-sign.sh && ./electron/auto-sign.sh && concurrently \"cd ./electron/core/plugins/data-plugin && npm run build:publish\" \"cd ./electron/core/plugins/inference-plugin && npm run build:publish\" \"cd ./electron/core/plugins/model-management-plugin && npm run build:publish\" \"cd ./electron/core/plugins/monitoring-plugin && npm run build:publish\"",
|
||||||
"build": "yarn build:web && yarn build:electron",
|
"build": "yarn build:web && yarn build:electron",
|
||||||
"build:darwin": "yarn build:web && yarn workspace jan-electron build:darwin",
|
"build:darwin": "yarn build:web && yarn workspace jan-electron build:darwin",
|
||||||
"build:win32": "yarn build:web && yarn workspace jan-electron build:win32",
|
"build:win32": "yarn build:web && yarn workspace jan-electron build:win32",
|
||||||
|
|||||||
@ -9,10 +9,10 @@ const ActiveModelTable: React.FC = () => {
|
|||||||
if (!activeModel) return null;
|
if (!activeModel) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<div className="pl-[63px] pr-[89px]">
|
||||||
<h3 className="text-xl leading-[25px] mb-[13px]">Active Model(s)</h3>
|
<h3 className="text-xl leading-[25px] mb-[13px]">Active Model(s)</h3>
|
||||||
<ModelTable models={[activeModel]} />
|
<ModelTable models={[activeModel]} />
|
||||||
</Fragment>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -47,7 +47,7 @@ const AvailableModelCard: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border rounded-lg border-gray-200">
|
<div className="border rounded-lg border-gray-200">
|
||||||
<div className="flex justify-between py-4 px-3 gap-[10px]">
|
<div className="flex justify-between py-4 px-3 gap-2.5">
|
||||||
<DownloadModelContent
|
<DownloadModelContent
|
||||||
required={required}
|
required={required}
|
||||||
author={product.author}
|
author={product.author}
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useCallback, useRef, useState } from "react";
|
import React, { useCallback, useRef, useState, useEffect } from "react";
|
||||||
import ChatItem from "../ChatItem";
|
import ChatItem from "../ChatItem";
|
||||||
import { ChatMessage } from "@/_models/ChatMessage";
|
import { ChatMessage } from "@/_models/ChatMessage";
|
||||||
import useChatMessages from "@/_hooks/useChatMessages";
|
import useChatMessages from "@/_hooks/useChatMessages";
|
||||||
import { showingTyping } from "@/_helpers/JotaiWrapper";
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { selectAtom } from "jotai/utils";
|
import { selectAtom } from "jotai/utils";
|
||||||
import LoadingIndicator from "../LoadingIndicator";
|
|
||||||
import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
|
import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||||
import { chatMessages } from "@/_helpers/atoms/ChatMessage.atom";
|
import { chatMessages } from "@/_helpers/atoms/ChatMessage.atom";
|
||||||
|
|
||||||
@ -16,12 +14,11 @@ const ChatBody: React.FC = () => {
|
|||||||
const messageList = useAtomValue(
|
const messageList = useAtomValue(
|
||||||
selectAtom(
|
selectAtom(
|
||||||
chatMessages,
|
chatMessages,
|
||||||
useCallback((v) => v[activeConversationId], [activeConversationId])
|
useCallback((v) => v[activeConversationId], [activeConversationId]),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const [content, setContent] = useState<React.JSX.Element[]>([]);
|
const [content, setContent] = useState<React.JSX.Element[]>([]);
|
||||||
|
|
||||||
const isTyping = useAtomValue(showingTyping);
|
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
const { loading, hasMore } = useChatMessages(offset);
|
const { loading, hasMore } = useChatMessages(offset);
|
||||||
const intersectObs = useRef<any>(null);
|
const intersectObs = useRef<any>(null);
|
||||||
@ -40,10 +37,10 @@ const ChatBody: React.FC = () => {
|
|||||||
|
|
||||||
if (message) intersectObs.current.observe(message);
|
if (message) intersectObs.current.observe(message);
|
||||||
},
|
},
|
||||||
[loading, hasMore]
|
[loading, hasMore],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
const list = messageList?.map((message, index) => {
|
const list = messageList?.map((message, index) => {
|
||||||
if (messageList?.length === index + 1) {
|
if (messageList?.length === index + 1) {
|
||||||
return (
|
return (
|
||||||
@ -58,11 +55,6 @@ const ChatBody: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col-reverse flex-1 py-4 overflow-y-auto scroll">
|
<div className="flex flex-col-reverse flex-1 py-4 overflow-y-auto scroll">
|
||||||
{isTyping && (
|
|
||||||
<div className="ml-4 mb-2" key="indicator">
|
|
||||||
<LoadingIndicator />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,22 +2,17 @@ import SimpleControlNetMessage from "../SimpleControlNetMessage";
|
|||||||
import SimpleImageMessage from "../SimpleImageMessage";
|
import SimpleImageMessage from "../SimpleImageMessage";
|
||||||
import SimpleTextMessage from "../SimpleTextMessage";
|
import SimpleTextMessage from "../SimpleTextMessage";
|
||||||
import { ChatMessage, MessageType } from "@/_models/ChatMessage";
|
import { ChatMessage, MessageType } from "@/_models/ChatMessage";
|
||||||
import StreamTextMessage from "../StreamTextMessage";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { currentStreamingMessageAtom } from "@/_helpers/atoms/ChatMessage.atom";
|
|
||||||
|
|
||||||
export default function renderChatMessage({
|
export default function renderChatMessage({
|
||||||
id,
|
id,
|
||||||
messageType,
|
messageType,
|
||||||
|
messageSenderType,
|
||||||
senderAvatarUrl,
|
senderAvatarUrl,
|
||||||
senderName,
|
senderName,
|
||||||
createdAt,
|
createdAt,
|
||||||
imageUrls,
|
imageUrls,
|
||||||
htmlText,
|
|
||||||
text,
|
text,
|
||||||
}: ChatMessage): React.ReactNode {
|
}: ChatMessage): React.ReactNode {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const message = useAtomValue(currentStreamingMessageAtom);
|
|
||||||
switch (messageType) {
|
switch (messageType) {
|
||||||
case MessageType.ImageWithText:
|
case MessageType.ImageWithText:
|
||||||
return (
|
return (
|
||||||
@ -42,22 +37,14 @@ export default function renderChatMessage({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case MessageType.Text:
|
case MessageType.Text:
|
||||||
return id !== message?.id ? (
|
return (
|
||||||
<SimpleTextMessage
|
<SimpleTextMessage
|
||||||
key={id}
|
key={id}
|
||||||
avatarUrl={senderAvatarUrl}
|
avatarUrl={senderAvatarUrl}
|
||||||
senderName={senderName}
|
senderName={senderName}
|
||||||
createdAt={createdAt}
|
createdAt={createdAt}
|
||||||
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
|
senderType={messageSenderType}
|
||||||
/>
|
text={text}
|
||||||
) : (
|
|
||||||
<StreamTextMessage
|
|
||||||
key={id}
|
|
||||||
id={id}
|
|
||||||
avatarUrl={senderAvatarUrl}
|
|
||||||
senderName={senderName}
|
|
||||||
createdAt={createdAt}
|
|
||||||
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -63,8 +63,8 @@ const ConfirmDeleteConversationModal: React.FC = () => {
|
|||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Are you sure you want to delete this conversation? All
|
Are you sure you want to delete this conversation? All
|
||||||
of messages will be permanently removed from our servers
|
of messages will be permanently removed. This action
|
||||||
forever. This action cannot be undone.
|
cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const ConversationalCard: React.FC<Props> = ({ product }) => {
|
|||||||
{description}
|
{description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="flex text-xs leading-5 text-gray-500 items-center gap-[2px]">
|
<span className="flex text-xs leading-5 text-gray-500 items-center gap-0.5">
|
||||||
<Image src={"icons/play.svg"} width={16} height={16} alt="" />
|
<Image src={"icons/play.svg"} width={16} height={16} alt="" />
|
||||||
32.2k runs
|
32.2k runs
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -18,19 +18,19 @@ const DownloadModelContent: React.FC<Props> = ({
|
|||||||
type,
|
type,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-4/5 flex flex-col gap-[10px]">
|
<div className="w-4/5 flex flex-col gap-2.5">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<h2 className="font-medium text-xl leading-[25px] tracking-[-0.4px] text-gray-900">
|
<h2 className="font-medium text-xl leading-[25px] tracking-[-0.4px] text-gray-900">
|
||||||
{name}
|
{name}
|
||||||
</h2>
|
</h2>
|
||||||
<DownloadModelTitle title={type} />
|
<DownloadModelTitle title={type} />
|
||||||
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
|
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
|
||||||
<span className="text-xs leading-[18px] font-semibold text-purple-800">
|
<span className="text-xs leading-[18px] font-semibold text-purple-800">
|
||||||
{author}
|
{author}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{required && (
|
{required && (
|
||||||
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
|
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
|
||||||
<span className="text-xs leading-[18px] text-[#11192899]">
|
<span className="text-xs leading-[18px] text-[#11192899]">
|
||||||
Required{" "}
|
Required{" "}
|
||||||
</span>
|
</span>
|
||||||
@ -44,7 +44,7 @@ const DownloadModelContent: React.FC<Props> = ({
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
isRecommend ? "flex" : "hidden"
|
isRecommend ? "flex" : "hidden"
|
||||||
} w-fit justify-center items-center bg-green-50 rounded-full px-[10px] py-[2px] gap-2`}
|
} w-fit justify-center items-center bg-green-50 rounded-full px-2.5 py-0.5 gap-2`}
|
||||||
>
|
>
|
||||||
<div className="w-3 h-3 rounded-full bg-green-400"></div>
|
<div className="w-3 h-3 rounded-full bg-green-400"></div>
|
||||||
<span className="text-green-600 font-medium text-xs leading-18px">
|
<span className="text-green-600 font-medium text-xs leading-18px">
|
||||||
|
|||||||
@ -3,7 +3,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DownloadModelTitle: React.FC<Props> = ({ title }) => (
|
export const DownloadModelTitle: React.FC<Props> = ({ title }) => (
|
||||||
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
|
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
|
||||||
<span className="text-xs leading-[18px] font-medium text-purple-800">
|
<span className="text-xs leading-[18px] font-medium text-purple-800">
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const DownloadedModelCard: React.FC<Props> = ({
|
|||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="border rounded-lg border-gray-200">
|
<div className="border rounded-lg border-gray-200">
|
||||||
<div className="flex justify-between py-4 px-3 gap-[10px]">
|
<div className="flex justify-between py-4 px-3 gap-2.5">
|
||||||
<DownloadModelContent
|
<DownloadModelContent
|
||||||
required={required}
|
required={required}
|
||||||
author={product.author}
|
author={product.author}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { Fragment } from "react";
|
import React from "react";
|
||||||
import SearchBar from "../SearchBar";
|
import SearchBar from "../SearchBar";
|
||||||
import ModelTable from "../ModelTable";
|
import ModelTable from "../ModelTable";
|
||||||
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
||||||
@ -6,14 +6,16 @@ import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
|||||||
const DownloadedModelTable: React.FC = () => {
|
const DownloadedModelTable: React.FC = () => {
|
||||||
const { downloadedModels } = useGetDownloadedModels();
|
const { downloadedModels } = useGetDownloadedModels();
|
||||||
|
|
||||||
|
if (!downloadedModels || downloadedModels.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<div className="pl-[63px] pr-[89px]">
|
||||||
<h3 className="text-xl leading-[25px] mt-[50px]">Downloaded Models</h3>
|
<h3 className="text-xl leading-[25px] mt-[50px]">Downloaded Models</h3>
|
||||||
<div className="py-5 w-[568px]">
|
<div className="py-5 w-[568px]">
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
</div>
|
</div>
|
||||||
<ModelTable models={downloadedModels} />
|
<ModelTable models={downloadedModels} />
|
||||||
</Fragment>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
29
web/app/_components/DownloadingModelTable/index.tsx
Normal file
29
web/app/_components/DownloadingModelTable/index.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React, { Fragment } from "react";
|
||||||
|
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);
|
||||||
|
|
||||||
|
const isAnyModelDownloading = Object.values(modelDownloadState).length > 0;
|
||||||
|
|
||||||
|
if (!isAnyModelDownloading) return null;
|
||||||
|
|
||||||
|
const downloadStates: DownloadState[] = [];
|
||||||
|
for (const [, value] of Object.entries(modelDownloadState)) {
|
||||||
|
downloadStates.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pl-[63px] pr-[89px]">
|
||||||
|
<h3 className="text-xl leading-[25px] mt-[50px] mb-4">
|
||||||
|
Downloading Models
|
||||||
|
</h3>
|
||||||
|
<ModelDownloadingTable downloadStates={downloadStates} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DownloadingModelTable;
|
||||||
@ -8,7 +8,7 @@ type Props = {
|
|||||||
|
|
||||||
const ExpandableHeader: React.FC<Props> = ({ title, expanded, onClick }) => (
|
const ExpandableHeader: React.FC<Props> = ({ title, expanded, onClick }) => (
|
||||||
<button onClick={onClick} className="flex items-center justify-between px-2">
|
<button onClick={onClick} className="flex items-center justify-between px-2">
|
||||||
<h2 className="text-gray-400 font-bold text-[12px] leading-[12px] pl-1">
|
<h2 className="text-gray-400 font-bold text-xs leading-[12px] pl-1">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mr-2">
|
<div className="mr-2">
|
||||||
|
|||||||
@ -1,55 +1,20 @@
|
|||||||
import useGetAvailableModels from "@/_hooks/useGetAvailableModels";
|
|
||||||
import ExploreModelItem from "../ExploreModelItem";
|
|
||||||
import HeaderTitle from "../HeaderTitle";
|
import HeaderTitle from "../HeaderTitle";
|
||||||
import SearchBar from "../SearchBar";
|
import SearchBar, { SearchType } from "../SearchBar";
|
||||||
import SimpleCheckbox from "../SimpleCheckbox";
|
import ExploreModelList from "../ExploreModelList";
|
||||||
import SimpleTag, { TagType } from "../SimpleTag";
|
import ExploreModelFilter from "../ExploreModelFilter";
|
||||||
|
|
||||||
const tags = [
|
const ExploreModelContainer: React.FC = () => (
|
||||||
"Roleplay",
|
<div className="flex flex-col flex-1 px-16 pt-14 overflow-hidden">
|
||||||
"Llama",
|
<HeaderTitle title="Explore Models" />
|
||||||
"Story",
|
<SearchBar
|
||||||
"Casual",
|
type={SearchType.Model}
|
||||||
"Professional",
|
placeholder="Owner name like TheBloke, bhlim etc.."
|
||||||
"CodeLlama",
|
/>
|
||||||
"Coding",
|
<div className="flex flex-1 gap-x-10 mt-9 overflow-hidden">
|
||||||
];
|
<ExploreModelFilter />
|
||||||
const checkboxs = ["GGUF", "TensorRT", "Meow", "JigglyPuff"];
|
<ExploreModelList />
|
||||||
|
|
||||||
const ExploreModelContainer: React.FC = () => {
|
|
||||||
const { allAvailableModels } = useGetAvailableModels();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
|
|
||||||
<HeaderTitle title="Explore Models" />
|
|
||||||
<SearchBar placeholder="Search or HuggingFace URL" />
|
|
||||||
<div className="flex gap-x-14 mt-[38px]">
|
|
||||||
<div className="flex-1 flex-shrink-0">
|
|
||||||
<h2 className="font-semibold text-xs mb-[15px]">Tags</h2>
|
|
||||||
<SearchBar placeholder="Filter by tags" />
|
|
||||||
<div className="flex flex-wrap gap-[9px] mt-[14px]">
|
|
||||||
{tags.map((item) => (
|
|
||||||
<SimpleTag key={item} title={item} type={item as TagType} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<hr className="my-10" />
|
|
||||||
<fieldset>
|
|
||||||
{checkboxs.map((item) => (
|
|
||||||
<SimpleCheckbox key={item} name={item} />
|
|
||||||
))}
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
<div className="flex-[3_3_0%]">
|
|
||||||
<h2 className="font-semibold text-xs mb-[18px]">Results</h2>
|
|
||||||
<div className="flex flex-col gap-[31px]">
|
|
||||||
{allAvailableModels.map((item) => (
|
|
||||||
<ExploreModelItem key={item.id} model={item} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
);
|
||||||
|
|
||||||
export default ExploreModelContainer;
|
export default ExploreModelContainer;
|
||||||
|
|||||||
40
web/app/_components/ExploreModelFilter/index.tsx
Normal file
40
web/app/_components/ExploreModelFilter/index.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from "react";
|
||||||
|
import SearchBar from "../SearchBar";
|
||||||
|
import SimpleCheckbox from "../SimpleCheckbox";
|
||||||
|
import SimpleTag, { TagType } from "../SimpleTag";
|
||||||
|
|
||||||
|
const tags = [
|
||||||
|
"Roleplay",
|
||||||
|
"Llama",
|
||||||
|
"Story",
|
||||||
|
"Casual",
|
||||||
|
"Professional",
|
||||||
|
"CodeLlama",
|
||||||
|
"Coding",
|
||||||
|
];
|
||||||
|
const checkboxs = ["GGUF", "TensorRT", "Meow", "JigglyPuff"];
|
||||||
|
|
||||||
|
const ExploreModelFilter: React.FC = () => {
|
||||||
|
const enabled = false;
|
||||||
|
if (!enabled) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-64">
|
||||||
|
<h2 className="font-semibold text-xs mb-[15px]">Tags</h2>
|
||||||
|
<SearchBar placeholder="Filter by tags" />
|
||||||
|
<div className="flex flex-wrap gap-[9px] mt-[14px]">
|
||||||
|
{tags.map((item) => (
|
||||||
|
<SimpleTag key={item} title={item} type={item as TagType} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<hr className="my-10" />
|
||||||
|
<fieldset>
|
||||||
|
{checkboxs.map((item) => (
|
||||||
|
<SimpleCheckbox key={item} name={item} />
|
||||||
|
))}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExploreModelFilter;
|
||||||
@ -1,36 +1,30 @@
|
|||||||
|
/* eslint-disable react/display-name */
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import ExploreModelItemHeader from "../ExploreModelItemHeader";
|
import ExploreModelItemHeader from "../ExploreModelItemHeader";
|
||||||
import ModelVersionList from "../ModelVersionList";
|
import ModelVersionList from "../ModelVersionList";
|
||||||
import { useMemo, useState } from "react";
|
import { Fragment, forwardRef, useState } from "react";
|
||||||
import { Product } from "@/_models/Product";
|
|
||||||
import SimpleTag, { TagType } from "../SimpleTag";
|
import SimpleTag, { TagType } from "../SimpleTag";
|
||||||
import { displayDate } from "@/_utils/datetime";
|
import { displayDate } from "@/_utils/datetime";
|
||||||
import useDownloadModel from "@/_hooks/useDownloadModel";
|
import { Product } from "@/_models/Product";
|
||||||
import { atom, useAtomValue } from "jotai";
|
|
||||||
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
model: Product;
|
model: Product;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ExploreModelItem: React.FC<Props> = ({ model }) => {
|
const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
||||||
const downloadAtom = useMemo(
|
|
||||||
() => atom((get) => get(modelDownloadStateAtom)[model.fileName ?? ""]),
|
|
||||||
[model.fileName ?? ""]
|
|
||||||
);
|
|
||||||
const downloadState = useAtomValue(downloadAtom);
|
|
||||||
const { downloadModel } = useDownloadModel();
|
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col border border-gray-200 rounded-[5px]">
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="flex flex-col border border-gray-200 rounded-md mb-4"
|
||||||
|
>
|
||||||
<ExploreModelItemHeader
|
<ExploreModelItemHeader
|
||||||
name={model.name}
|
name={model.name}
|
||||||
status={TagType.Recommended}
|
status={TagType.Recommended}
|
||||||
total={model.totalSize}
|
versions={model.availableVersions}
|
||||||
downloadState={downloadState}
|
|
||||||
onDownloadClick={() => downloadModel(model)}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col px-[26px] py-[22px]">
|
<div className="flex flex-col px-[26px] py-[22px]">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
@ -39,7 +33,7 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
|
|||||||
<div className="text-sm font-medium text-gray-500">
|
<div className="text-sm font-medium text-gray-500">
|
||||||
Model Format
|
Model Format
|
||||||
</div>
|
</div>
|
||||||
<div className="px-[10px] py-0.5 bg-gray-100 text-xs text-gray-800 w-fit">
|
<div className="px-2.5 py-0.5 bg-gray-100 text-xs text-gray-800 w-fit">
|
||||||
GGUF
|
GGUF
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -87,15 +81,24 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
|
|||||||
<span className="text-sm font-medium text-gray-500">Tags</span>
|
<span className="text-sm font-medium text-gray-500">Tags</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{show && <ModelVersionList />}
|
{model.availableVersions.length > 0 && (
|
||||||
<button
|
<Fragment>
|
||||||
onClick={() => setShow(!show)}
|
{show && (
|
||||||
className="bg-[#FBFBFB] text-gray-500 text-sm text-left py-2 px-4 border-t border-gray-200"
|
<ModelVersionList
|
||||||
>
|
model={model}
|
||||||
{!show ? "+ Show Available Versions" : "- Collapse"}
|
versions={model.availableVersions}
|
||||||
</button>
|
/>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setShow(!show)}
|
||||||
|
className="bg-[#FBFBFB] text-gray-500 text-sm text-left py-2 px-4 border-t border-gray-200"
|
||||||
|
>
|
||||||
|
{!show ? "+ Show Available Versions" : "- Collapse"}
|
||||||
|
</button>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default ExploreModelItem;
|
export default ExploreModelItem;
|
||||||
|
|||||||
@ -3,11 +3,13 @@ import PrimaryButton from "../PrimaryButton";
|
|||||||
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
|
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
|
||||||
import { DownloadState } from "@/_models/DownloadState";
|
import { DownloadState } from "@/_models/DownloadState";
|
||||||
import SecondaryButton from "../SecondaryButton";
|
import SecondaryButton from "../SecondaryButton";
|
||||||
|
import { ModelVersion } from "@/_models/Product";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string;
|
name: string;
|
||||||
total: number;
|
|
||||||
status: TagType;
|
status: TagType;
|
||||||
|
versions: ModelVersion[];
|
||||||
|
size?: number;
|
||||||
downloadState?: DownloadState;
|
downloadState?: DownloadState;
|
||||||
onDownloadClick?: () => void;
|
onDownloadClick?: () => void;
|
||||||
};
|
};
|
||||||
@ -15,30 +17,41 @@ type Props = {
|
|||||||
const ExploreModelItemHeader: React.FC<Props> = ({
|
const ExploreModelItemHeader: React.FC<Props> = ({
|
||||||
name,
|
name,
|
||||||
status,
|
status,
|
||||||
total,
|
size,
|
||||||
|
versions,
|
||||||
downloadState,
|
downloadState,
|
||||||
onDownloadClick,
|
onDownloadClick,
|
||||||
}) => (
|
}) => {
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
let downloadButton = (
|
||||||
<div className="flex items-center gap-2">
|
<PrimaryButton
|
||||||
<span>{name}</span>
|
title={size ? `Download (${toGigabytes(size)})` : "Download"}
|
||||||
<SimpleTag title={status} type={status} clickable={false} />
|
onClick={() => onDownloadClick?.()}
|
||||||
</div>
|
/>
|
||||||
{downloadState != null ? (
|
);
|
||||||
|
|
||||||
|
if (downloadState != null) {
|
||||||
|
// downloading
|
||||||
|
downloadButton = (
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
disabled
|
disabled
|
||||||
title={`Downloading (${formatDownloadPercentage(
|
title={`Downloading (${formatDownloadPercentage(
|
||||||
downloadState.percent
|
downloadState.percent
|
||||||
)})`}
|
)})`}
|
||||||
onClick={() => {}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
);
|
||||||
<PrimaryButton
|
} else if (versions.length === 0) {
|
||||||
title={total ? `Download (${toGigabytes(total)})` : "Download"}
|
downloadButton = <SecondaryButton disabled title="No files available" />;
|
||||||
onClick={() => onDownloadClick?.()}
|
}
|
||||||
/>
|
|
||||||
)}
|
return (
|
||||||
</div>
|
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||||
);
|
<div className="flex items-center gap-2">
|
||||||
|
<span>{name}</span>
|
||||||
|
<SimpleTag title={status} type={status} clickable={false} />
|
||||||
|
</div>
|
||||||
|
{downloadButton}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ExploreModelItemHeader;
|
export default ExploreModelItemHeader;
|
||||||
|
|||||||
52
web/app/_components/ExploreModelList/index.tsx
Normal file
52
web/app/_components/ExploreModelList/index.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import ExploreModelItem from "../ExploreModelItem";
|
||||||
|
import { modelSearchAtom } from "@/_helpers/JotaiWrapper";
|
||||||
|
import useGetHuggingFaceModel from "@/_hooks/useGetHuggingFaceModel";
|
||||||
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
|
import { useInView } from "react-intersection-observer";
|
||||||
|
import { modelLoadMoreAtom } from "@/_helpers/atoms/ExploreModelLoading.atom";
|
||||||
|
import { Waveform } from "@uiball/loaders";
|
||||||
|
|
||||||
|
const ExploreModelList: React.FC = () => {
|
||||||
|
const [loadMoreInProgress, setLoadMoreInProress] = useAtom(modelLoadMoreAtom);
|
||||||
|
const modelSearch = useAtomValue(modelSearchAtom);
|
||||||
|
const { modelList, getHuggingFaceModel } = useGetHuggingFaceModel();
|
||||||
|
const { ref, inView } = useInView({
|
||||||
|
threshold: 0,
|
||||||
|
triggerOnce: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modelList.length === 0 && modelSearch.length > 0) {
|
||||||
|
setLoadMoreInProress(true);
|
||||||
|
}
|
||||||
|
getHuggingFaceModel(modelSearch);
|
||||||
|
}, [modelSearch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inView) {
|
||||||
|
console.debug("Load more models..");
|
||||||
|
setLoadMoreInProress(true);
|
||||||
|
getHuggingFaceModel(modelSearch);
|
||||||
|
}
|
||||||
|
}, [inView]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col flex-1 overflow-y-auto scroll">
|
||||||
|
{modelList.map((item, index) => (
|
||||||
|
<ExploreModelItem
|
||||||
|
ref={index === modelList.length - 1 ? ref : null}
|
||||||
|
key={item.id}
|
||||||
|
model={item}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{loadMoreInProgress && (
|
||||||
|
<div className="mx-auto mt-2 mb-4">
|
||||||
|
<Waveform size={24} color="#9CA3AF" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExploreModelList;
|
||||||
@ -1,11 +1,14 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderTitle: React.FC<Props> = ({ title }) => (
|
const HeaderTitle: React.FC<Props> = ({ title, className }) => (
|
||||||
<h2 className="my-5 font-semibold text-[34px] tracking-[-0.4px] leading-[41px]">
|
<h2
|
||||||
|
className={`my-5 font-semibold text-[34px] tracking-[-0.4px] leading-[41px] ${className}`}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -66,7 +66,7 @@ const HistoryItem: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`flex flex-row mx-1 items-center gap-[10px] rounded-lg p-2 ${backgroundColor} hover:bg-hover-light`}
|
className={`flex flex-row mx-1 items-center gap-2.5 rounded-lg p-2 ${backgroundColor} hover:bg-hover-light`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@ -79,7 +79,7 @@ const HistoryItem: React.FC<Props> = ({
|
|||||||
<div className="flex flex-col justify-between text-sm leading-[20px] w-full">
|
<div className="flex flex-col justify-between text-sm leading-[20px] w-full">
|
||||||
<div className="flex flex-row items-center justify-between">
|
<div className="flex flex-row items-center justify-between">
|
||||||
<span className="text-gray-900 text-left">{name}</span>
|
<span className="text-gray-900 text-left">{name}</span>
|
||||||
<span className="text-[11px] leading-[13px] tracking-[-0.4px] text-gray-400">
|
<span className="text-xs leading-[13px] tracking-[-0.4px] text-gray-400">
|
||||||
{updatedAt && new Date(updatedAt).toDateString()}
|
{updatedAt && new Date(updatedAt).toDateString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,14 +18,14 @@ const HistoryList: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-grow pt-3 gap-2">
|
<div className="flex flex-col flex-grow pt-3 gap-2 overflow-hidden">
|
||||||
<ExpandableHeader
|
<ExpandableHeader
|
||||||
title="CHAT HISTORY"
|
title="CHAT HISTORY"
|
||||||
expanded={expand}
|
expanded={expand}
|
||||||
onClick={() => setExpand(!expand)}
|
onClick={() => setExpand(!expand)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col gap-1 mt-1 ${!expand ? "hidden " : "block"}`}
|
className={`flex flex-col gap-1 mt-1 overflow-y-auto scroll ${!expand ? "hidden " : "block"}`}
|
||||||
>
|
>
|
||||||
{conversations.length > 0 ? (
|
{conversations.length > 0 ? (
|
||||||
conversations
|
conversations
|
||||||
|
|||||||
@ -4,19 +4,56 @@ import BasicPromptInput from "../BasicPromptInput";
|
|||||||
import BasicPromptAccessories from "../BasicPromptAccessories";
|
import BasicPromptAccessories from "../BasicPromptAccessories";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
|
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
|
import SecondaryButton from "../SecondaryButton";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||||
|
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
|
import { showingTyping } from "@/_helpers/JotaiWrapper";
|
||||||
|
import LoadingIndicator from "../LoadingIndicator";
|
||||||
|
|
||||||
const InputToolbar: React.FC = () => {
|
const InputToolbar: React.FC = () => {
|
||||||
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom);
|
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom);
|
||||||
|
const currentProduct = useAtomValue(currentProductAtom);
|
||||||
|
const { requestCreateConvo } = useCreateConversation();
|
||||||
|
const isTyping = useAtomValue(showingTyping);
|
||||||
|
|
||||||
if (showingAdvancedPrompt) {
|
if (showingAdvancedPrompt) {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: implement regenerate
|
||||||
|
// const onRegenerateClick = () => {};
|
||||||
|
|
||||||
|
const onNewConversationClick = () => {
|
||||||
|
if (currentProduct) {
|
||||||
|
requestCreateConvo(currentProduct);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-3 mb-3 flex-none overflow-hidden shadow-sm ring-1 ring-inset ring-gray-300 rounded-lg dark:bg-gray-800">
|
<Fragment>
|
||||||
<BasicPromptInput />
|
<div className="flex justify-between gap-2 mr-3 my-2">
|
||||||
<BasicPromptAccessories />
|
<div className="h-6">
|
||||||
</div>
|
{isTyping && (
|
||||||
|
<div className="my-2" key="indicator">
|
||||||
|
<LoadingIndicator />
|
||||||
|
</div>
|
||||||
|
)}{" "}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <SecondaryButton title="Regenerate" onClick={onRegenerateClick} /> */}
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={onNewConversationClick}
|
||||||
|
title="New Conversation"
|
||||||
|
icon={<PlusIcon width={16} height={16} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mx-3 mb-3 flex-none overflow-hidden shadow-sm ring-1 ring-inset ring-gray-300 rounded-lg dark:bg-gray-800">
|
||||||
|
<BasicPromptInput />
|
||||||
|
<BasicPromptAccessories />
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const JanLogo: React.FC = () => {
|
|||||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
|
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="p-3 flex gap-[2px] items-center"
|
className="p-3 flex gap-0.5 items-center"
|
||||||
onClick={() => setActiveConvoId(undefined)}
|
onClick={() => setActiveConvoId(undefined)}
|
||||||
>
|
>
|
||||||
<Image src={"icons/app_icon.svg"} width={28} height={28} alt="" />
|
<Image src={"icons/app_icon.svg"} width={28} height={28} alt="" />
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import HistoryList from "../HistoryList";
|
|||||||
import NewChatButton from "../NewChatButton";
|
import NewChatButton from "../NewChatButton";
|
||||||
|
|
||||||
const LeftContainer: React.FC = () => (
|
const LeftContainer: React.FC = () => (
|
||||||
<div className="w-[323px] flex-shrink-0 p-3 h-screen border-r border-gray-200 flex flex-col">
|
<div className="w-[323px] flex-shrink-0 py-3 h-screen border-r border-gray-200 flex flex-col">
|
||||||
<SidebarHeader />
|
<SidebarHeader />
|
||||||
<NewChatButton />
|
<NewChatButton />
|
||||||
<HistoryList />
|
<HistoryList />
|
||||||
|
|||||||
@ -1,12 +1,6 @@
|
|||||||
const LoadingIndicator = () => {
|
const LoadingIndicator = () => {
|
||||||
let circleCommonClasses = "h-1.5 w-1.5 bg-current rounded-full";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <div className="flex">
|
|
||||||
// <div className={`${circleCommonClasses} mr-1 animate-bounce`}></div>
|
|
||||||
// <div className={`${circleCommonClasses} mr-1 animate-bounce200`}></div>
|
|
||||||
// <div className={`${circleCommonClasses} animate-bounce400`}></div>
|
|
||||||
// </div>
|
|
||||||
<div className="typingIndicatorContainer">
|
<div className="typingIndicatorContainer">
|
||||||
<div className="typingIndicatorBubble">
|
<div className="typingIndicatorBubble">
|
||||||
<div className="typingIndicatorBubbleDot"></div>
|
<div className="typingIndicatorBubbleDot"></div>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const LoginButton: React.FC = () => {
|
|||||||
// <button
|
// <button
|
||||||
// onClick={signInWithKeyCloak}
|
// onClick={signInWithKeyCloak}
|
||||||
// type="button"
|
// type="button"
|
||||||
// className="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
// className="rounded-md bg-blue-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||||
// >
|
// >
|
||||||
// Login
|
// Login
|
||||||
// </button>
|
// </button>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { ReactNode } from "react";
|
|
||||||
import Welcome from "../WelcomeContainer";
|
import Welcome from "../WelcomeContainer";
|
||||||
import { Preferences } from "../Preferences";
|
import { Preferences } from "../Preferences";
|
||||||
import MyModelContainer from "../MyModelContainer";
|
import MyModelContainer from "../MyModelContainer";
|
||||||
@ -11,17 +10,14 @@ import {
|
|||||||
getMainViewStateAtom,
|
getMainViewStateAtom,
|
||||||
} from "@/_helpers/atoms/MainView.atom";
|
} from "@/_helpers/atoms/MainView.atom";
|
||||||
import EmptyChatContainer from "../EmptyChatContainer";
|
import EmptyChatContainer from "../EmptyChatContainer";
|
||||||
|
import MainChat from "../MainChat";
|
||||||
|
|
||||||
type Props = {
|
const MainView: React.FC = () => {
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ChatContainer({ children }: Props) {
|
|
||||||
const viewState = useAtomValue(getMainViewStateAtom);
|
const viewState = useAtomValue(getMainViewStateAtom);
|
||||||
|
|
||||||
switch (viewState) {
|
switch (viewState) {
|
||||||
case MainViewState.ConversationEmptyModel:
|
case MainViewState.ConversationEmptyModel:
|
||||||
return <EmptyChatContainer />
|
return <EmptyChatContainer />;
|
||||||
case MainViewState.ExploreModel:
|
case MainViewState.ExploreModel:
|
||||||
return <ExploreModelContainer />;
|
return <ExploreModelContainer />;
|
||||||
case MainViewState.Setting:
|
case MainViewState.Setting:
|
||||||
@ -32,6 +28,8 @@ export default function ChatContainer({ children }: Props) {
|
|||||||
case MainViewState.Welcome:
|
case MainViewState.Welcome:
|
||||||
return <Welcome />;
|
return <Welcome />;
|
||||||
default:
|
default:
|
||||||
return <div className="flex flex-1 overflow-hidden">{children}</div>;
|
return <MainChat />;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default MainView;
|
||||||
@ -38,7 +38,7 @@ const ModelActionButton: React.FC<Props> = ({ type, onActionClick }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<td className="whitespace-nowrap px-6 py-4 text-sm">
|
<td className="whitespace-nowrap px-6 py-4 text-sm">
|
||||||
<PrimaryButton title={styles.title} onClick={onClick} />
|
<PrimaryButton title={styles.title} onClick={onClick} className={styles.backgroundColor} />
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,30 +6,37 @@ type Props = {
|
|||||||
onDeleteClick: () => void;
|
onDeleteClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => {
|
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => (
|
||||||
return (
|
<Menu as="div" className="relative flex-none">
|
||||||
<Menu as="div" className="relative flex-none">
|
<Menu.Button className="block text-gray-500 hover:text-gray-900">
|
||||||
<Menu.Button className="block text-gray-500 hover:text-gray-900">
|
<span className="sr-only">Open options</span>
|
||||||
<span className="sr-only">Open options</span>
|
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
|
</Menu.Button>
|
||||||
</Menu.Button>
|
<Transition
|
||||||
<Transition
|
as={Fragment}
|
||||||
as={Fragment}
|
enter="transition ease-out duration-100"
|
||||||
enter="transition ease-out duration-100"
|
enterFrom="transform opacity-0 scale-95"
|
||||||
enterFrom="transform opacity-0 scale-95"
|
enterTo="transform opacity-100 scale-100"
|
||||||
enterTo="transform opacity-100 scale-100"
|
leave="transition ease-in duration-75"
|
||||||
leave="transition ease-in duration-75"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
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.Items className="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
<Menu.Item>
|
||||||
<Menu.Item>
|
{({ active }) => (
|
||||||
<button onClick={onDeleteClick}>Delete</button>
|
<button
|
||||||
</Menu.Item>
|
className={`${
|
||||||
</Menu.Items>
|
active ? "bg-violet-500 text-white" : "text-gray-900"
|
||||||
</Transition>
|
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
|
||||||
</Menu>
|
onClick={onDeleteClick}
|
||||||
);
|
>
|
||||||
};
|
Delete
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
export default ModelActionMenu;
|
export default ModelActionMenu;
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const ModelDownloadingButton: React.FC<Props> = ({ total, value }) => {
|
|||||||
<button className="py-2 px-3 flex gap-2 border text-xs leading-[18px] border-gray-200 rounded-lg">
|
<button className="py-2 px-3 flex gap-2 border text-xs leading-[18px] border-gray-200 rounded-lg">
|
||||||
Downloading...
|
Downloading...
|
||||||
</button>
|
</button>
|
||||||
<div className="py-[2px] px-[10px] bg-gray-200 rounded">
|
<div className="py-0.5 px-2.5 bg-gray-200 rounded">
|
||||||
<span className="text-xs font-medium text-gray-800">
|
<span className="text-xs font-medium text-gray-800">
|
||||||
{toGigabytes(value)} / {toGigabytes(total)}
|
{toGigabytes(value)} / {toGigabytes(total)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
36
web/app/_components/ModelDownloadingRow/index.tsx
Normal file
36
web/app/_components/ModelDownloadingRow/index.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { DownloadState } from "@/_models/DownloadState";
|
||||||
|
import {
|
||||||
|
formatDownloadPercentage,
|
||||||
|
formatDownloadSpeed,
|
||||||
|
toGigabytes,
|
||||||
|
} from "@/_utils/converter";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
downloadState: DownloadState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModelDownloadingRow: React.FC<Props> = ({ downloadState }) => (
|
||||||
|
<tr
|
||||||
|
className="border-b border-gray-200 last:border-b-0 last:rounded-lg"
|
||||||
|
key={downloadState.fileName}
|
||||||
|
>
|
||||||
|
<td className="flex flex-col whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900">
|
||||||
|
{downloadState.fileName}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||||
|
{toGigabytes(downloadState.size.transferred)}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||||
|
{toGigabytes(downloadState.size.total)}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||||
|
{formatDownloadPercentage(downloadState.percent)}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||||
|
{formatDownloadSpeed(downloadState.speed)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ModelDownloadingRow;
|
||||||
34
web/app/_components/ModelDownloadingTable/index.tsx
Normal file
34
web/app/_components/ModelDownloadingTable/index.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ModelTableHeader from "../ModelTableHeader";
|
||||||
|
import { DownloadState } from "@/_models/DownloadState";
|
||||||
|
import ModelDownloadingRow from "../ModelDownloadingRow";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
downloadStates: DownloadState[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableHeaders = ["MODEL", "TRANSFERRED", "SIZE", "PERCENTAGE", "SPEED"];
|
||||||
|
|
||||||
|
const ModelDownloadingTable: React.FC<Props> = ({ downloadStates }) => (
|
||||||
|
<div className="flow-root border rounded-lg border-gray-200 min-w-full align-middle shadow-lg">
|
||||||
|
<table className="min-w-full">
|
||||||
|
<thead className="bg-gray-50 border-b border-gray-200">
|
||||||
|
<tr className="rounded-t-lg">
|
||||||
|
{tableHeaders.map((item) => (
|
||||||
|
<ModelTableHeader key={item} title={item} />
|
||||||
|
))}
|
||||||
|
<th scope="col" className="relative px-6 py-3 w-fit">
|
||||||
|
<span className="sr-only">Edit</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{downloadStates.map((state) => (
|
||||||
|
<ModelDownloadingRow key={state.fileName} downloadState={state} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default React.memo(ModelDownloadingTable);
|
||||||
@ -1,86 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { searchingModelText } from "@/_helpers/JotaiWrapper";
|
|
||||||
import { Product } from "@/_models/Product";
|
|
||||||
import DownloadedModelCard from "../DownloadedModelCard";
|
|
||||||
import AvailableModelCard from "../AvailableModelCard";
|
|
||||||
import useDeleteModel from "@/_hooks/useDeleteModel";
|
|
||||||
import useGetAvailableModels from "@/_hooks/useGetAvailableModels";
|
|
||||||
import useDownloadModel from "@/_hooks/useDownloadModel";
|
|
||||||
|
|
||||||
const ModelListContainer: React.FC = () => {
|
|
||||||
const searchText = useAtomValue(searchingModelText);
|
|
||||||
const { deleteModel } = useDeleteModel();
|
|
||||||
const { downloadModel } = useDownloadModel();
|
|
||||||
|
|
||||||
const {
|
|
||||||
availableModels,
|
|
||||||
downloadedModels,
|
|
||||||
getAvailableModelExceptDownloaded,
|
|
||||||
} = useGetAvailableModels();
|
|
||||||
|
|
||||||
const onDeleteClick = async (product: Product) => {
|
|
||||||
await deleteModel(product);
|
|
||||||
await getAvailableModelExceptDownloaded();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDownloadClick = async (model: Product) => {
|
|
||||||
await downloadModel(model);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
|
|
||||||
<div className="pb-5 flex flex-col gap-2">
|
|
||||||
<Title title="Downloaded models" />
|
|
||||||
{downloadedModels
|
|
||||||
?.filter(
|
|
||||||
(e) =>
|
|
||||||
searchText.toLowerCase().trim() === "" ||
|
|
||||||
e.name.toLowerCase().includes(searchText.toLowerCase())
|
|
||||||
)
|
|
||||||
.map((item) => (
|
|
||||||
<DownloadedModelCard
|
|
||||||
key={item.id}
|
|
||||||
product={item}
|
|
||||||
onDeleteClick={onDeleteClick}
|
|
||||||
isRecommend={false}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="pb-5 flex flex-col gap-2">
|
|
||||||
<Title title="Browse available models" />
|
|
||||||
{availableModels
|
|
||||||
?.filter(
|
|
||||||
(e) =>
|
|
||||||
searchText.toLowerCase().trim() === "" ||
|
|
||||||
e.name.toLowerCase().includes(searchText.toLowerCase())
|
|
||||||
)
|
|
||||||
.map((item) => (
|
|
||||||
<AvailableModelCard
|
|
||||||
key={item.id}
|
|
||||||
product={item}
|
|
||||||
onDownloadClick={onDownloadClick}
|
|
||||||
isRecommend={false}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Title: React.FC<Props> = ({ title }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex gap-[10px]">
|
|
||||||
<span className="font-semibold text-xl leading-[25px] tracking-[-0.4px]">
|
|
||||||
{title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModelListContainer;
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import HeaderBackButton from "../HeaderBackButton";
|
|
||||||
import HeaderTitle from "../HeaderTitle";
|
|
||||||
import ModelListContainer from "../ModelListContainer";
|
|
||||||
import ModelSearchBar from "../ModelSearchBar";
|
|
||||||
|
|
||||||
export default function ModelManagement() {
|
|
||||||
return (
|
|
||||||
<main className="pt-[30px] pr-[89px] pl-[60px] pb-[70px] flex-1">
|
|
||||||
{/* <HeaderBackButton /> */}
|
|
||||||
<HeaderTitle title="Explore Models" />
|
|
||||||
<ModelSearchBar />
|
|
||||||
<ModelListContainer />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,29 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
|
||||||
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
|
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
|
||||||
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
|
||||||
|
|
||||||
const ModelMenu: React.FC = () => {
|
const ModelMenu: React.FC = () => {
|
||||||
const currentProduct = useAtomValue(currentProductAtom);
|
|
||||||
const { requestCreateConvo } = useCreateConversation();
|
|
||||||
const setShowConfirmDeleteConversationModal = useSetAtom(
|
const setShowConfirmDeleteConversationModal = useSetAtom(
|
||||||
showConfirmDeleteConversationModalAtom
|
showConfirmDeleteConversationModalAtom
|
||||||
);
|
);
|
||||||
|
|
||||||
const onCreateConvoClick = () => {
|
|
||||||
if (currentProduct) {
|
|
||||||
requestCreateConvo(currentProduct);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button onClick={() => onCreateConvoClick()}>
|
|
||||||
<PlusIcon width={24} height={24} color="#9CA3AF" />
|
|
||||||
</button>
|
|
||||||
<button onClick={() => setShowConfirmDeleteConversationModal(true)}>
|
<button onClick={() => setShowConfirmDeleteConversationModal(true)}>
|
||||||
<TrashIcon width={24} height={24} color="#9CA3AF" />
|
<TrashIcon width={24} height={24} color="#9CA3AF" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ModelRow: React.FC<Props> = ({ model }) => {
|
const ModelRow: React.FC<Props> = ({ model }) => {
|
||||||
const { startModel } = useStartStopModel();
|
const { startModel, stopModel } = useStartStopModel();
|
||||||
const activeModel = useAtomValue(currentProductAtom);
|
const activeModel = useAtomValue(currentProductAtom);
|
||||||
const { deleteModel } = useDeleteModel();
|
const { deleteModel } = useDeleteModel();
|
||||||
|
|
||||||
@ -31,6 +31,8 @@ const ModelRow: React.FC<Props> = ({ model }) => {
|
|||||||
const onModelActionClick = (action: ModelActionType) => {
|
const onModelActionClick = (action: ModelActionType) => {
|
||||||
if (action === ModelActionType.Start) {
|
if (action === ModelActionType.Start) {
|
||||||
startModel(model.id);
|
startModel(model.id);
|
||||||
|
} else {
|
||||||
|
stopModel(model.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import { Fragment, useEffect } from "react";
|
import { Fragment, useEffect } from "react";
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
|
||||||
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
|
||||||
import { Product } from "@/_models/Product";
|
import { Product } from "@/_models/Product";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { selectedModelAtom } from "@/_helpers/atoms/Model.atom";
|
import { selectedModelAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
|
import { downloadedModelAtom } from "@/_helpers/atoms/DownloadedModel.atom";
|
||||||
|
|
||||||
function classNames(...classes: any) {
|
function classNames(...classes: any) {
|
||||||
return classes.filter(Boolean).join(" ");
|
return classes.filter(Boolean).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectModels: React.FC = () => {
|
const SelectModels: React.FC = () => {
|
||||||
const { downloadedModels } = useGetDownloadedModels();
|
const downloadedModels = useAtomValue(downloadedModelAtom);
|
||||||
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom);
|
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -36,7 +36,7 @@ const SelectModels: React.FC = () => {
|
|||||||
Select a Model:
|
Select a Model:
|
||||||
</Listbox.Label>
|
</Listbox.Label>
|
||||||
<div className="relative mt-[19px]">
|
<div className="relative mt-[19px]">
|
||||||
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 sm:text-sm sm:leading-6">
|
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-sm sm:leading-6">
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<img
|
<img
|
||||||
src={selectedModel.avatarUrl}
|
src={selectedModel.avatarUrl}
|
||||||
@ -68,8 +68,8 @@ const SelectModels: React.FC = () => {
|
|||||||
key={model.id}
|
key={model.id}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
active ? "bg-indigo-600 text-white" : "text-gray-900",
|
active ? "bg-blue-600 text-white" : "text-gray-900",
|
||||||
"relative cursor-default select-none py-2 pl-3 pr-9"
|
"relative cursor-default select-none py-2 pl-3 pr-9",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
value={model}
|
value={model}
|
||||||
@ -85,7 +85,7 @@ const SelectModels: React.FC = () => {
|
|||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
selected ? "font-semibold" : "font-normal",
|
selected ? "font-semibold" : "font-normal",
|
||||||
"ml-3 block truncate"
|
"ml-3 block truncate",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{model.name}
|
{model.name}
|
||||||
@ -95,8 +95,8 @@ const SelectModels: React.FC = () => {
|
|||||||
{selected ? (
|
{selected ? (
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-indigo-600",
|
active ? "text-white" : "text-blue-600",
|
||||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
"absolute inset-y-0 right-0 flex items-center pr-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export const ModelStatusComponent: React.FC<Props> = ({ status }) => {
|
|||||||
const statusType = ModelStatusMapper[status];
|
const statusType = ModelStatusMapper[status];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`rounded-[10px] py-0.5 px-[10px] w-fit text-xs font-medium ${statusType.backgroundColor}`}
|
className={`rounded-[10px] py-0.5 px-2.5 w-fit text-xs font-medium ${statusType.backgroundColor}`}
|
||||||
>
|
>
|
||||||
{statusType.title}
|
{statusType.title}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ type Props = {
|
|||||||
const tableHeaders = ["MODEL", "FORMAT", "SIZE", "STATUS", "ACTIONS"];
|
const tableHeaders = ["MODEL", "FORMAT", "SIZE", "STATUS", "ACTIONS"];
|
||||||
|
|
||||||
const ModelTable: React.FC<Props> = ({ models }) => (
|
const ModelTable: React.FC<Props> = ({ models }) => (
|
||||||
<div className="flow-root inline-block border rounded-lg border-gray-200 min-w-full align-middle shadow-lg">
|
<div className="flow-root border rounded-lg border-gray-200 min-w-full align-middle shadow-lg">
|
||||||
<table className="min-w-full">
|
<table className="min-w-full">
|
||||||
<thead className="bg-gray-50 border-b border-gray-200">
|
<thead className="bg-gray-50 border-b border-gray-200">
|
||||||
<tr className="rounded-t-lg">
|
<tr className="rounded-t-lg">
|
||||||
|
|||||||
@ -1,25 +1,57 @@
|
|||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import { toGigabytes } from "@/_utils/converter";
|
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { ModelVersion, Product } from "@/_models/Product";
|
||||||
|
import useDownloadModel from "@/_hooks/useDownloadModel";
|
||||||
|
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
|
||||||
|
import { atom, useAtomValue } from "jotai";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
model: Product;
|
||||||
totalSizeInByte: number;
|
modelVersion: ModelVersion;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModelVersionItem: React.FC<Props> = ({ title, totalSizeInByte }) => (
|
const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
|
||||||
<div className="flex justify-between items-center gap-4 pl-[13px] pt-[13px] pr-[17px] pb-3 border-t border-gray-200 first:border-t-0">
|
const { downloadHfModel } = useDownloadModel();
|
||||||
<div className="flex items-center gap-4">
|
const downloadAtom = useMemo(
|
||||||
<Image src={"/icons/app_icon.svg"} width={14} height={20} alt="" />
|
() => atom((get) => get(modelDownloadStateAtom)[modelVersion.path ?? ""]),
|
||||||
<span className="font-sm text-gray-900">{title}</span>
|
[modelVersion.path ?? ""]
|
||||||
</div>
|
);
|
||||||
<div className="flex items-center gap-4">
|
const downloadState = useAtomValue(downloadAtom);
|
||||||
<div className="px-[10px] py-0.5 bg-gray-200 text-xs font-medium rounded">
|
|
||||||
{toGigabytes(totalSizeInByte)}
|
const onDownloadClick = () => {
|
||||||
|
downloadHfModel(model, modelVersion);
|
||||||
|
};
|
||||||
|
|
||||||
|
let downloadButton = (
|
||||||
|
<button
|
||||||
|
className="text-indigo-600 text-sm font-medium"
|
||||||
|
onClick={onDownloadClick}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (downloadState) {
|
||||||
|
downloadButton = (
|
||||||
|
<div>{formatDownloadPercentage(downloadState.percent)}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between items-center gap-4 pl-3 pt-3 pr-4 pb-3 border-t border-gray-200 first:border-t-0">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Image src={"/icons/app_icon.svg"} width={14} height={20} alt="" />
|
||||||
|
<span className="font-sm text-gray-900">{modelVersion.path}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="px-2.5 py-0.5 bg-gray-200 text-xs font-medium rounded">
|
||||||
|
{toGigabytes(modelVersion.size)}
|
||||||
|
</div>
|
||||||
|
{downloadButton}
|
||||||
</div>
|
</div>
|
||||||
<button className="text-indigo-600 text-sm font-medium">Download</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
export default ModelVersionItem;
|
export default ModelVersionItem;
|
||||||
|
|||||||
@ -1,38 +1,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ModelVersionItem from "../ModelVersionItem";
|
import ModelVersionItem from "../ModelVersionItem";
|
||||||
|
import { ModelVersion, Product } from "@/_models/Product";
|
||||||
|
|
||||||
const data = [
|
type Props = {
|
||||||
{
|
model: Product;
|
||||||
name: "Q4_K_M.gguf",
|
versions: ModelVersion[];
|
||||||
total: 5600,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Q4_K_M.gguf",
|
|
||||||
total: 5600,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Q4_K_M.gguf",
|
|
||||||
total: 5600,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ModelVersionList: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div className="px-4 py-5 border-t border-gray-200">
|
|
||||||
<div className="text-sm font-medium text-gray-500">
|
|
||||||
Available Versions
|
|
||||||
</div>
|
|
||||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
|
||||||
{data.map((item, index) => (
|
|
||||||
<ModelVersionItem
|
|
||||||
key={index}
|
|
||||||
title={item.name}
|
|
||||||
totalSizeInByte={item.total}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ModelVersionList: React.FC<Props> = ({ model, versions }) => (
|
||||||
|
<div className="px-4 py-5 border-t border-gray-200">
|
||||||
|
<div className="text-sm font-medium text-gray-500">Available Versions</div>
|
||||||
|
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||||
|
{versions.map((item) => (
|
||||||
|
<ModelVersionItem key={item.path} model={model} modelVersion={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default ModelVersionList;
|
export default ModelVersionList;
|
||||||
|
|||||||
@ -2,63 +2,43 @@ import ProgressBar from "../ProgressBar";
|
|||||||
import SystemItem from "../SystemItem";
|
import SystemItem from "../SystemItem";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { appDownloadProgress } from "@/_helpers/JotaiWrapper";
|
import { appDownloadProgress } from "@/_helpers/JotaiWrapper";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager";
|
|
||||||
import { SystemMonitoringService } from "../../../shared/coreService";
|
|
||||||
import { getSystemBarVisibilityAtom } from "@/_helpers/atoms/SystemBar.atom";
|
|
||||||
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
|
import useGetAppVersion from "@/_hooks/useGetAppVersion";
|
||||||
|
import useGetSystemResources from "@/_hooks/useGetSystemResources";
|
||||||
|
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
|
||||||
|
import { DownloadState } from "@/_models/DownloadState";
|
||||||
|
import { formatDownloadPercentage } from "@/_utils/converter";
|
||||||
|
|
||||||
const MonitorBar: React.FC = () => {
|
const MonitorBar: React.FC = () => {
|
||||||
const show = useAtomValue(getSystemBarVisibilityAtom);
|
|
||||||
const progress = useAtomValue(appDownloadProgress);
|
const progress = useAtomValue(appDownloadProgress);
|
||||||
const activeModel = useAtomValue(currentProductAtom);
|
const activeModel = useAtomValue(currentProductAtom);
|
||||||
const [ram, setRam] = useState<number>(0);
|
const { version } = useGetAppVersion();
|
||||||
const [gpu, setGPU] = useState<number>(0);
|
const { ram, cpu } = useGetSystemResources();
|
||||||
const [cpu, setCPU] = useState<number>(0);
|
const modelDownloadStates = useAtomValue(modelDownloadStateAtom);
|
||||||
const [version, setVersion] = useState<string>("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
const downloadStates: DownloadState[] = [];
|
||||||
const getSystemResources = async () => {
|
for (const [, value] of Object.entries(modelDownloadStates)) {
|
||||||
const resourceInfor = await executeSerial(
|
downloadStates.push(value);
|
||||||
SystemMonitoringService.GET_RESOURCES_INFORMATION
|
}
|
||||||
);
|
|
||||||
const currentLoadInfor = await executeSerial(
|
|
||||||
SystemMonitoringService.GET_CURRENT_LOAD_INFORMATION
|
|
||||||
);
|
|
||||||
const ram =
|
|
||||||
(resourceInfor?.mem?.used ?? 0) / (resourceInfor?.mem?.total ?? 1);
|
|
||||||
setRam(Math.round(ram * 100));
|
|
||||||
setCPU(Math.round(currentLoadInfor?.currentLoad ?? 0));
|
|
||||||
};
|
|
||||||
const getAppVersion = () => {
|
|
||||||
window.electronAPI.appVersion().then((version: string | undefined) => {
|
|
||||||
setVersion(version ?? "");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
getAppVersion();
|
|
||||||
getSystemResources();
|
|
||||||
// Fetch interval - every 3s
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
getSystemResources();
|
|
||||||
}, 3000);
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!show) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center justify-between border-t border-gray-200">
|
<div className="flex flex-row items-center justify-between border-t border-gray-200">
|
||||||
{progress && progress >= 0 ? (
|
{progress && progress >= 0 ? (
|
||||||
<ProgressBar total={100} used={progress} />
|
<ProgressBar total={100} used={progress} />
|
||||||
) : (
|
) : null}
|
||||||
<div className="w-full" />
|
<div className="flex-1 justify-end flex items-center gap-8 px-2">
|
||||||
)}
|
{downloadStates.length > 0 && (
|
||||||
<div className="flex-1 flex items-center gap-8 px-2">
|
<SystemItem
|
||||||
|
name="Downloading"
|
||||||
|
value={`${downloadStates[0].fileName}: ${formatDownloadPercentage(
|
||||||
|
downloadStates[0].percent
|
||||||
|
)}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SystemItem name="CPU" value={`${cpu}%`} />
|
<SystemItem name="CPU" value={`${cpu}%`} />
|
||||||
<SystemItem name="Mem" value={`${ram}%`} />
|
<SystemItem name="Mem" value={`${ram}%`} />
|
||||||
|
|
||||||
{activeModel && (
|
{activeModel && (
|
||||||
<SystemItem name={`Active model: ${activeModel.name}`} value={"1"} />
|
<SystemItem name={`Active model: ${activeModel.name}`} value={""} />
|
||||||
)}
|
)}
|
||||||
<span className="text-gray-900 text-sm">v{version}</span>
|
<span className="text-gray-900 text-sm">v{version}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import HeaderTitle from "../HeaderTitle";
|
import HeaderTitle from "../HeaderTitle";
|
||||||
import DownloadedModelTable from "../DownloadedModelTable";
|
import DownloadedModelTable from "../DownloadedModelTable";
|
||||||
import ActiveModelTable from "../ActiveModelTable";
|
import ActiveModelTable from "../ActiveModelTable";
|
||||||
|
import DownloadingModelTable from "../DownloadingModelTable";
|
||||||
|
|
||||||
const MyModelContainer: React.FC = () => (
|
const MyModelContainer: React.FC = () => (
|
||||||
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px]">
|
<div className="flex flex-col flex-1 pt-[60px]">
|
||||||
<HeaderTitle title="My Models" />
|
<HeaderTitle title="My Models" className="pl-[63px] pr-[89px]" />
|
||||||
<ActiveModelTable />
|
<div className="pb-6 overflow-y-auto scroll">
|
||||||
<DownloadedModelTable />
|
<ActiveModelTable />
|
||||||
|
<DownloadingModelTable />
|
||||||
|
<DownloadedModelTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
|||||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||||
import useInitModel from "@/_hooks/useInitModel";
|
import useInitModel from "@/_hooks/useInitModel";
|
||||||
import { Product } from "@/_models/Product";
|
import { Product } from "@/_models/Product";
|
||||||
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
const NewChatButton: React.FC = () => {
|
const NewChatButton: React.FC = () => {
|
||||||
const activeModel = useAtomValue(currentProductAtom);
|
const activeModel = useAtomValue(currentProductAtom);
|
||||||
@ -32,8 +33,13 @@ const NewChatButton: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SecondaryButton title={"New Chat"} onClick={onClick} className="my-5" />
|
<SecondaryButton
|
||||||
|
title={"New Chat"}
|
||||||
|
onClick={onClick}
|
||||||
|
className="my-5 mx-3"
|
||||||
|
icon={<PlusIcon width={16} height={16} />}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NewChatButton;
|
export default React.memo(NewChatButton);
|
||||||
|
|||||||
@ -172,15 +172,30 @@ export const Preferences = () => {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="flex flex-col space-y-2">
|
||||||
type="submit"
|
<button
|
||||||
className={classNames(
|
type="submit"
|
||||||
"rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
|
className={classNames(
|
||||||
fileName ? "bg-indigo-600 hover:bg-indigo-500" : "bg-gray-500"
|
"rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
|
||||||
)}
|
fileName
|
||||||
>
|
? "bg-blue-500 hover:bg-blue-300"
|
||||||
Install Plugin
|
: "bg-gray-500"
|
||||||
</button>
|
)}
|
||||||
|
>
|
||||||
|
Install Plugin
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
"bg-blue-500 hover:bg-blue-300 rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
window.electronAPI.reloadPlugins();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reload Plugins
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@ -4,17 +4,19 @@ type Props = {
|
|||||||
title: string;
|
title: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PrimaryButton: React.FC<Props> = ({
|
const PrimaryButton: React.FC<Props> = ({
|
||||||
title,
|
title,
|
||||||
onClick,
|
onClick,
|
||||||
fullWidth = false,
|
fullWidth = false,
|
||||||
|
className,
|
||||||
}) => (
|
}) => (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
type="button"
|
type="button"
|
||||||
className={`rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-50 ${
|
className={`rounded-md bg-blue-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-50 line-clamp-1 flex-shrink-0 ${className} ${
|
||||||
fullWidth ? "flex-1 " : ""
|
fullWidth ? "flex-1 " : ""
|
||||||
}}`}
|
}}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -5,24 +5,22 @@ type Props = {
|
|||||||
used: number;
|
used: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProgressBar: React.FC<Props> = ({ used, total }) => {
|
const ProgressBar: React.FC<Props> = ({ used, total }) => (
|
||||||
return (
|
<div className="flex gap-2.5 items-center p-[10px]">
|
||||||
<div className="flex gap-[10px] items-center p-[10px]">
|
<div className="text-xs leading-[18px] gap-0.5 flex items-center">
|
||||||
<div className="text-xs leading-[18px] gap-0.5 flex items-center">
|
<Image src={"icons/app_icon.svg"} width={18} height={18} alt="" />
|
||||||
<Image src={"icons/app_icon.svg"} width={18} height={18} alt="" />
|
Updating
|
||||||
Updating
|
|
||||||
</div>
|
|
||||||
<div className="w-[150px] relative bg-blue-200 h-1 rounded-md flex">
|
|
||||||
<div
|
|
||||||
className="absolute top-0 left-0 h-full rounded-md bg-blue-600"
|
|
||||||
style={{ width: `${((used / total) * 100).toFixed(2)}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs leading-[18px]">
|
|
||||||
{((used / total) * 100).toFixed(0)}%
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="w-[150px] relative bg-blue-200 h-1 rounded-md flex">
|
||||||
};
|
<div
|
||||||
|
className="absolute top-0 left-0 h-full rounded-md bg-blue-600"
|
||||||
|
style={{ width: `${((used / total) * 100).toFixed(2)}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs leading-[18px]">
|
||||||
|
{((used / total) * 100).toFixed(0)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default ProgressBar;
|
export default ProgressBar;
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
import ChatContainer from "../ChatContainer";
|
import MainView from "../MainView";
|
||||||
import MainChat from "../MainChat";
|
|
||||||
import MonitorBar from "../MonitorBar";
|
import MonitorBar from "../MonitorBar";
|
||||||
|
|
||||||
const RightContainer = () => (
|
const RightContainer = () => (
|
||||||
<div className="flex flex-col flex-1 h-screen">
|
<div className="flex flex-col flex-1 h-screen">
|
||||||
<ChatContainer>
|
<MainView />
|
||||||
<MainChat />
|
|
||||||
</ChatContainer>
|
|
||||||
<MonitorBar />
|
<MonitorBar />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,14 +1,25 @@
|
|||||||
import { searchAtom } from "@/_helpers/JotaiWrapper";
|
import { modelSearchAtom } from "@/_helpers/JotaiWrapper";
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
|
||||||
|
export enum SearchType {
|
||||||
|
Model = "model",
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
type?: SearchType;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchBar: React.FC<Props> = ({ placeholder }) => {
|
const SearchBar: React.FC<Props> = ({ type, placeholder }) => {
|
||||||
const setText = useSetAtom(searchAtom);
|
const setModelSearch = useSetAtom(modelSearchAtom);
|
||||||
let placeholderText = placeholder ? placeholder : "Search (⌘K)";
|
let placeholderText = placeholder ? placeholder : "Search (⌘K)";
|
||||||
|
|
||||||
|
const debounced = useDebouncedCallback((value) => {
|
||||||
|
setModelSearch(value);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative mt-3 flex items-center">
|
<div className="relative mt-3 flex items-center">
|
||||||
<div className="absolute top-0 left-2 h-full flex items-center">
|
<div className="absolute top-0 left-2 h-full flex items-center">
|
||||||
@ -24,7 +35,7 @@ const SearchBar: React.FC<Props> = ({ placeholder }) => {
|
|||||||
name="search"
|
name="search"
|
||||||
id="search"
|
id="search"
|
||||||
placeholder={placeholderText}
|
placeholder={placeholderText}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => debounced(e.target.value)}
|
||||||
className="block w-full rounded-md border-0 py-1.5 pl-8 pr-14 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="block w-full rounded-md border-0 py-1.5 pl-8 pr-14 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
onClick: () => void;
|
onClick?: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SecondaryButton: React.FC<Props> = ({
|
const SecondaryButton: React.FC<Props> = ({
|
||||||
@ -10,15 +13,17 @@ const SecondaryButton: React.FC<Props> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
|
icon,
|
||||||
}) => (
|
}) => (
|
||||||
<button
|
<button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`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 ${className}`}
|
className={`flex items-center justify-center gap-1 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 ${className} flex-shrink-0 line-clamp-1`}
|
||||||
>
|
>
|
||||||
|
{icon}
|
||||||
{title}
|
{title}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default SecondaryButton;
|
export default React.memo(SecondaryButton);
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
|
import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
|
||||||
import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
|
import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||||
import useSendChatMessage from "@/_hooks/useSendChatMessage";
|
import useSendChatMessage from "@/_hooks/useSendChatMessage";
|
||||||
|
import { ArrowRightIcon } from "@heroicons/react/24/outline";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
const SendButton: React.FC = () => {
|
const SendButton: React.FC = () => {
|
||||||
const [currentPrompt] = useAtom(currentPromptAtom);
|
const [currentPrompt] = useAtom(currentPromptAtom);
|
||||||
@ -25,9 +25,9 @@ const SendButton: React.FC = () => {
|
|||||||
onClick={sendChatMessage}
|
onClick={sendChatMessage}
|
||||||
style={disabled ? disabledStyle : enabledStyle}
|
style={disabled ? disabledStyle : enabledStyle}
|
||||||
type="submit"
|
type="submit"
|
||||||
className="p-2 gap-[10px] inline-flex items-center rounded-[12px] text-sm font-semibold shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
className="p-2 gap-2.5 inline-flex items-center rounded-xl text-sm font-semibold shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||||
>
|
>
|
||||||
<Image src={"icons/ic_arrowright.svg"} width={24} height={24} alt="" />
|
<ArrowRightIcon width={16} height={16} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import Image from "next/image";
|
|||||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||||
import PrimaryButton from "../PrimaryButton";
|
import PrimaryButton from "../PrimaryButton";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
MainViewState,
|
MainViewState,
|
||||||
@ -11,6 +10,7 @@ import {
|
|||||||
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
|
||||||
import useInitModel from "@/_hooks/useInitModel";
|
import useInitModel from "@/_hooks/useInitModel";
|
||||||
import { Product } from "@/_models/Product";
|
import { Product } from "@/_models/Product";
|
||||||
|
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
|
||||||
|
|
||||||
enum ActionButton {
|
enum ActionButton {
|
||||||
DownloadModel = "Download a Model",
|
DownloadModel = "Download a Model",
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import SecondaryButton from "../SecondaryButton";
|
import SecondaryButton from "../SecondaryButton";
|
||||||
|
|
||||||
const SidebarFooter: React.FC = () => (
|
const SidebarFooter: React.FC = () => (
|
||||||
<div className="flex justify-between items-center gap-2">
|
<div className="flex justify-between items-center gap-2 mx-3">
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
title={"Discord"}
|
title={"Discord"}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -11,7 +11,7 @@ const SidebarFooter: React.FC = () => (
|
|||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
title={"Discord"}
|
title={"Twitter"}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai")
|
window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
const SidebarHeader: React.FC = () => {
|
const SidebarHeader: React.FC = () => (
|
||||||
return (
|
<div className="flex flex-col gap-2.5 px-3">
|
||||||
<div className="flex flex-col gap-[10px]">
|
<Image src={"icons/Jan_AppIcon.svg"} width={68} height={28} alt="" />
|
||||||
<Image src={"icons/Jan_AppIcon.svg"} width={68} height={28} alt="" />
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SidebarHeader;
|
export default React.memo(SidebarHeader);
|
||||||
|
|||||||
@ -21,21 +21,16 @@ const menu = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const SidebarMenu: React.FC = () => (
|
const SidebarMenu: React.FC = () => (
|
||||||
<div className="flex flex-col">
|
<ul role="list" className="mx-1 mt-2 space-y-1 mb-2">
|
||||||
<div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3">
|
{menu.map((item) => (
|
||||||
Your Configurations
|
<SidebarMenuItem
|
||||||
</div>
|
title={item.name}
|
||||||
<ul role="list" className="-mx-2 mt-2 space-y-1 mb-2">
|
viewState={item.state}
|
||||||
{menu.map((item) => (
|
iconName={item.icon}
|
||||||
<SidebarMenuItem
|
key={item.name}
|
||||||
title={item.name}
|
/>
|
||||||
viewState={item.state}
|
))}
|
||||||
iconName={item.icon}
|
</ul>
|
||||||
key={item.name}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default React.memo(SidebarMenu);
|
export default React.memo(SidebarMenu);
|
||||||
|
|||||||
@ -30,15 +30,15 @@ const SimpleControlNetMessage: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex gap-1 justify-start items-baseline">
|
<div className="flex gap-1 justify-start items-baseline">
|
||||||
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px]">
|
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px]">
|
||||||
{senderName}
|
{senderName}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400 ml-2">
|
<div className="text-xs leading-[13.2px] font-medium text-gray-400 ml-2">
|
||||||
{displayDate(createdAt)}
|
{displayDate(createdAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 flex-col">
|
<div className="flex gap-3 flex-col">
|
||||||
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
|
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
|
||||||
{text}
|
{text}
|
||||||
</p>
|
</p>
|
||||||
<JanImage
|
<JanImage
|
||||||
@ -49,7 +49,7 @@ const SimpleControlNetMessage: React.FC<Props> = ({
|
|||||||
<Link
|
<Link
|
||||||
href={imageUrls[0] || "#"}
|
href={imageUrls[0] || "#"}
|
||||||
target="_blank_"
|
target="_blank_"
|
||||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
|
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
|
||||||
>
|
>
|
||||||
<Image src="icons/download.svg" width={16} height={16} alt="" />
|
<Image src="icons/download.svg" width={16} height={16} alt="" />
|
||||||
<span className="leading-[20px] text-[14px] text-[#111928]">
|
<span className="leading-[20px] text-[14px] text-[#111928]">
|
||||||
|
|||||||
@ -30,10 +30,10 @@ const SimpleImageMessage: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex gap-1 justify-start items-baseline">
|
<div className="flex gap-1 justify-start items-baseline">
|
||||||
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px]">
|
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px]">
|
||||||
{senderName}
|
{senderName}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400 ml-2">
|
<div className="text-xs leading-[13.2px] font-medium text-gray-400 ml-2">
|
||||||
{displayDate(createdAt)}
|
{displayDate(createdAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -46,19 +46,19 @@ const SimpleImageMessage: React.FC<Props> = ({
|
|||||||
<Link
|
<Link
|
||||||
href={imageUrls[0] || "#"}
|
href={imageUrls[0] || "#"}
|
||||||
target="_blank_"
|
target="_blank_"
|
||||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
|
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
|
||||||
>
|
>
|
||||||
<Image src="icons/download.svg" width={16} height={16} alt="" />
|
<Image src="icons/download.svg" width={16} height={16} alt="" />
|
||||||
<span className="leading-[20px] text-[14px] text-[#111928]">
|
<span className="leading-[20px] text-sm text-[#111928]">
|
||||||
Download
|
Download
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
|
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
|
||||||
// onClick={() => sendChatMessage()}
|
// onClick={() => sendChatMessage()}
|
||||||
>
|
>
|
||||||
<Image src="icons/refresh.svg" width={16} height={16} alt="" />
|
<Image src="icons/refresh.svg" width={16} height={16} alt="" />
|
||||||
<span className="leading-[20px] text-[14px] text-[#111928]">
|
<span className="leading-[20px] text-sm text-[#111928]">
|
||||||
Re-generate
|
Re-generate
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -69,7 +69,7 @@ const SimpleTag: React.FC<Props> = ({
|
|||||||
if (!clickable) {
|
if (!clickable) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
|
className={`px-2.5 py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
@ -79,7 +79,7 @@ const SimpleTag: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
|
className={`px-2.5 py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
|
||||||
>
|
>
|
||||||
{title} x
|
{title} x
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -3,54 +3,61 @@ import { displayDate } from "@/_utils/datetime";
|
|||||||
import { TextCode } from "../TextCode";
|
import { TextCode } from "../TextCode";
|
||||||
import { getMessageCode } from "@/_utils/message";
|
import { getMessageCode } from "@/_utils/message";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { MessageSenderType } from "@/_models/ChatMessage";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
senderName: string;
|
senderName: string;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
|
senderType: MessageSenderType;
|
||||||
text?: string;
|
text?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SimpleTextMessage: React.FC<Props> = ({
|
const SimpleTextMessage: React.FC<Props> = ({
|
||||||
senderName,
|
senderName,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
senderType,
|
||||||
avatarUrl = "",
|
avatarUrl = "",
|
||||||
text = "",
|
text = "",
|
||||||
}) => (
|
}) => {
|
||||||
<div className="flex items-start gap-2 ml-3">
|
const backgroundColor =
|
||||||
<Image
|
senderType === MessageSenderType.User ? "" : "bg-gray-100";
|
||||||
className="rounded-full"
|
|
||||||
src={avatarUrl}
|
return (
|
||||||
width={32}
|
<div
|
||||||
height={32}
|
className={`flex items-start gap-2 px-[148px] ${backgroundColor} py-5`}
|
||||||
alt=""
|
>
|
||||||
/>
|
<Image
|
||||||
<div className="flex flex-col gap-1 w-full">
|
className="rounded-full"
|
||||||
<div className="flex gap-1 justify-start items-baseline">
|
src={avatarUrl}
|
||||||
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
|
width={32}
|
||||||
{senderName}
|
height={32}
|
||||||
</div>
|
alt=""
|
||||||
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400">
|
/>
|
||||||
{displayDate(createdAt)}
|
<div className="flex flex-col gap-1">
|
||||||
</div>
|
<div className="flex gap-1 justify-start items-baseline">
|
||||||
</div>
|
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
|
||||||
{text.includes("```") ? (
|
{senderName}
|
||||||
getMessageCode(text).map((item, i) => (
|
|
||||||
<div className="flex gap-1 flex-col" key={i}>
|
|
||||||
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
|
|
||||||
{item.text}
|
|
||||||
</p>
|
|
||||||
{item.code.trim().length > 0 && <TextCode text={item.code} />}
|
|
||||||
</div>
|
</div>
|
||||||
))
|
<div className="text-xs leading-[13.2px] font-medium text-gray-400">
|
||||||
) : (
|
{displayDate(createdAt)}
|
||||||
<p
|
</div>
|
||||||
className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]"
|
</div>
|
||||||
dangerouslySetInnerHTML={{ __html: text }}
|
{text.includes("```") ? (
|
||||||
/>
|
getMessageCode(text).map((item, i) => (
|
||||||
)}
|
<div className="flex gap-1 flex-col" key={i}>
|
||||||
|
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
|
{item.code.trim().length > 0 && <TextCode text={item.code} />}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-sm leading-loose font-normal">{text}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
export default React.memo(SimpleTextMessage);
|
export default React.memo(SimpleTextMessage);
|
||||||
|
|||||||
@ -34,17 +34,17 @@ const StreamTextMessage: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-1 w-full">
|
<div className="flex flex-col gap-1 w-full">
|
||||||
<div className="flex gap-1 justify-start items-baseline">
|
<div className="flex gap-1 justify-start items-baseline">
|
||||||
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
|
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
|
||||||
{senderName}
|
{senderName}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400">
|
<div className="text-xs leading-[13.2px] font-medium text-gray-400">
|
||||||
{displayDate(createdAt)}
|
{displayDate(createdAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{message.text.includes("```") ? (
|
{message.text.includes("```") ? (
|
||||||
getMessageCode(message.text).map((item, i) => (
|
getMessageCode(message.text).map((item, i) => (
|
||||||
<div className="flex gap-1 flex-col" key={i}>
|
<div className="flex gap-1 flex-col" key={i}>
|
||||||
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
|
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
|
||||||
{item.text}
|
{item.text}
|
||||||
</p>
|
</p>
|
||||||
{item.code.trim().length > 0 && <TextCode text={item.code} />}
|
{item.code.trim().length > 0 && <TextCode text={item.code} />}
|
||||||
@ -52,7 +52,7 @@ const StreamTextMessage: React.FC<Props> = ({
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p
|
<p
|
||||||
className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]"
|
className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]"
|
||||||
dangerouslySetInnerHTML={{ __html: message.text }}
|
dangerouslySetInnerHTML={{ __html: message.text }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -3,15 +3,13 @@ type Props = {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SystemItem: React.FC<Props> = ({ name, value }) => {
|
const SystemItem: React.FC<Props> = ({ name, value }) => (
|
||||||
return (
|
<div className="flex gap-2 pl-4 my-1">
|
||||||
<div className="flex gap-2 pl-4 my-1">
|
<div className="flex gap-2.5 w-max font-bold text-gray-900 text-sm">
|
||||||
<div className="flex gap-[10px] w-max font-bold text-gray-900 text-sm">
|
{name}
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
<span className="text-gray-900 text-sm">{value}</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<span className="text-gray-900 text-sm">{value}</span>
|
||||||
};
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default SystemItem;
|
export default SystemItem;
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export const TabModelDetail: React.FC<Props> = ({ onTabClick, tab }) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-[2px] rounded p-1 w-full bg-gray-200">
|
<div className="flex gap-0.5 rounded p-1 w-full bg-gray-200">
|
||||||
{btns.map((item, index) => (
|
{btns.map((item, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const UserToolbar: React.FC = () => {
|
|||||||
width={36}
|
width={36}
|
||||||
height={36}
|
height={36}
|
||||||
/>
|
/>
|
||||||
<span className="flex gap-[2px] leading-6 text-base font-semibold">
|
<span className="flex gap-0.5 leading-6 text-base font-semibold">
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user